Spring Web MVC

M : Model

V : View

C : Controller -> DispatcherServlet

Front Controller = DispatcherServlet

Application Controller = @Controller or xx implements Controller

  • Context
    • ServletContextListener -> ContextLoaderListener -> Root WebApplicationContext
    • DispatcherServlet -> Servlet WebApplicationContext
  • Services
    • @Services
  • Repositories
    • @Repositories

映射处理

  • Servlet匹配规则
    • 精确匹配
      • /IndexServlet
    • 模糊匹配
      • /*.jsp
      • 当前目录下的所有目录
    • /匹配
      • /
      • 当前目录
  • Servlet请求映射
    • Servlet URL Pattern
    • Filter URL Pattern
  • Spring Web MVC
    • DispatcherServlet
    • HandlerMapping

@RestController=@Controller+@ResponseBody

org.springframework.web.bind.annotation.RestController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.springframework.web.bind.annotation;
//ignore
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
//关键点
@Controller
@ResponseBody
public @interface RestController {

/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
* @since 4.0.1
*/
@AliasFor(annotation = Controller.class)
String value() default "";

}

DispatcherServlet < FrameworkServlet < HttpServletBean < HttpServlet

  • 新建项目,依赖Web
  • 新建packet->controller -> 新建Controller -> RestDemoController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.bai.springwebmvc.controller;


import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* Rest Demo Controller
*/
@RestController
public class RestDemoController {
@GetMapping
public String index(){
return "Hello World";
}
}

浏览器访问:localhost:8080,页面显示“Hello World”。

问题及回答

Q : 为什么Controller没有映射地址却能启动起来

A : 自动装配,详情参见下面源码:org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration。此处的ServletContext path =”” or “/“,此二者等价。

Request URL = ServletContext path + @RequestMapping(“”) 或者 @GetMapping()

当前例子中,Request URL = “”+“”=“” -> RestDemoController#index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package org.springframework.boot.autoconfigure.web.servlet;

// ignore import

@AutoConfigureOrder(-2147483648)
@Configuration
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({DispatcherServlet.class})
@AutoConfigureAfter({ServletWebServerFactoryAutoConfiguration.class})
@EnableConfigurationProperties({ServerProperties.class})
public class DispatcherServletAutoConfiguration {

/*
* The bean name for a DispatcherServlet that will be mapped to the root URL "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";

/*
* The bean name for a ServletRegistrationBean for the DispatcherServlet "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";

@Configuration
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
//关键点
@EnableConfigurationProperties(WebMvcProperties.class)
protected static class DispatcherServletConfiguration {
//ignore
}
}

Q : 为什么访问localhost:8080可以调用到RestDemoController#index?

A : Spring MVC的处理方式是HandlerMapping自动寻找Request URL,匹配的Handler,Handler是处理的方法,当前这是一种实例。Request -> Handler -> 执行结果 -> 返回(Rest) -> 普通的文本。

HandlerMapping -> RequestMappingHandlerMapping -> @RequestMapping + Handler Mapping

补充


Spring Web MVC 的配置 Bean :org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties

Spring Boot允许通过application.properties去定义配置,配置外部化。

WebMvcProperties配置前缀:spring.mvc


@PostMapping Post请求 @RequestMapping(method = RequestMethod.POST) Create(C)

@GetMapping Get请求 @RequestMapping(method = RequestMethod.GET) Read(R)

@PutMapping Put请求 @RequestMapping(method = RequestMethod.PUT) Update(R)

@DeleteMapping Delete请求 @RequestMapping(method = RequestMethod.DELETE) Delete(D)

无HeadMapping


拦截器:HandlerInterceptor ,可以理解handler到底是什么

如何装配

在启动类(xxxApplication,@SpringBootApplication) 继承WebMvcConfigurerAdapter(此类已经逐渐被淘汰),重写addInterceptors方法,通过registry.addInterceptor方法将自定义拦截器装配。

处理顺序

preHandle(true) -> HandlerMethod(因为采用了@GetMapping,其它场景可能会有不同)执行(Method#invoke) -> postHandle -> afterCompletion

异常处理

  • Servlet标准
  • Spring Web MVC
  • Spring Boot

理解web.xml错误页面

传统的Servlet web.xml错误页面

Servlet -> web.xml(schema -> .xsd) -> 错误页面

处理逻辑:

  • 处理状态码
  • 处理异常类型
  • 处理服务
  • 优点
    • 统一处理,业界标准
  • 不足
    • 灵活度不够,只能定义在web.xml文件或annotation里面
Spring Web MVC 异常处理
  • @ExceptionHandler
  • @RestControllerAdvice=@ControllerAdvice+@ResponseBody
  • @ControllerAdvice专门拦截@Controller
Spring Boot错误处理页面
  • 实现ErrorPageRegistrar接口
  • 注册ErrorPage对象
  • 实现ErrorPage对象中的Path路径Web服务
  • 不足
    • 页面处理的路径必须笃定
  • 优点
    • 状态码
      • 比较通用,不需要理解SpringWebMVC异常体系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.bai.springwebmvc;

import com.bai.springwebmvc.interceptor.DefaultHandlerInterceptor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@SpringBootApplication
public class SpringWebmvcApplication extends WebMvcConfigurerAdapter implements ErrorPageRegistrar{

public static void main(String[] args) {
SpringApplication.run(SpringWebmvcApplication.class, args);
}

@Override
public void registerErrorPages(ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,"/404.html"));
}
}

视图技术

  • View
  • ViewResolver
    • ContentNegotiatingViewResolver
  • 实战
    • Thymeleaf

View

org.springframework.web.servlet.View#render

1
2
void render(@Nullable Map<String, ?> model,
HttpServletRequest request, HttpServletResponse response) throws Exception;
  • 处理页面渲染的逻辑
    • Velocity
    • JSP
    • Thymeleaf

ViewResolver

页面+解析器(resolve)

org.springframework.web.servlet.ViewResolver#resolveViewName

1
2
3
4
5
6
7
8
9
10
/**
* viewName : view的名称
* locale : 多语言(国际化)
* 寻找对应的View对象
* requestURI -> RequestMappingHandlerMaping -> HanleMethod ->
* return "viewName" -> ViewResolver -> View -> render -> HTML
* 完整的页面名称 = prefix + "viewName" + suffix
*/
@Nullable
View resolveViewName(String viewName, Locale locale) throws Exception;

prefix 和 suffix 在org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.View的属性内

1
2
3
4
5
6
7
8
9
10
11
12
public static class View {
/**
* Spring MVC view prefix.
*/
private String prefix;

/**
* Spring MVC view suffix.
*/
private String suffix;
//ignore
}
Spring Boot接续完整的页面路径

spring.view.prefix + hanlerMethod return + spring.view.suffix

自动装配类org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration

进行自动装配,html不需要在web.xml里配置

配置项类org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties

配置项前缀:spring.thymeleaf

模板寻找前缀:spring.thymeleaf.prefix

模板寻找后缀:spring.thymeleaf.suffix

1
2
3
4
5
6
7
8
9
10
11
package org.springframework.boot.autoconfigure.thymeleaf;
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {

private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8");

public static final String DEFAULT_PREFIX = "classpath:/templates/";

public static final String DEFAULT_SUFFIX = ".html";
//ignore
}

ContentNegotiatingViewResolver

org.springframework.web.servlet.view.ContentNegotiatingViewResolver

  • 用于处理多个ViewResolver
    • JSP
    • Velocity
    • Thymeleaf
  • 当所有的ViewResover配置完成时,它们的order默认值时一样的,所以先来先服务(List)
  • 当它们定义自己的order,通过order来倒序排列
    • ViewResolver有优先级,排序在#getCandidateViews内
  • 流程
    • 得到CandidateViews(List)
    • 得到最匹配的View
    • render

代码示例

  • 添加maven依赖
1
2
3
4
5
<!--Thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • 修改默认配置
1
2
3
<!--resources/application.properties-->
spring.thymeleaf.prefix = classpath:/thymeleaf/
spring.thymeleaf.suffix = .htm
  • 添加页面
    • resources下添加directory:thymeleaf
    • thymeleaf下添加file:index.htm
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>首页</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<!--/*@thymesVar id="name" type="java.lang.String"*/-->
<p th:text="#{home.welcome}"></p>
</body>
</html>
  • 添加controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.bai.springwebmvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Controller
public class indexController {

@RequestMapping
public String index(){
return "index";
}
}
  • 浏览器访问localhost:8080,页面显示??home.welcome_zh_CN??

国际化(i18N)

  • Locale/LocaleContext
  • LocaleContextHolder
    • Spring内的对象
    • 缓存context
  • LocaleResolver/LocaleContextResolver

org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

代码示例

  • 修改application.properties
1
spring.messages.basename=META-INF/locale/messages
  • 在上面的文件夹下添加文件
1
2
#文件路径META-INF/locale/messages/messages.properties
home.welcome = welcome
1
2
#文件路径META-INF/locale/messages/messages_zh_cn.properties
home.welcome = 欢迎
  • 访问localhost:8080,页面会根据浏览器的设置返回对应语言的文字

问题及回答

Q:遇到新问题如何查文档

A:在能定位错误范围的前提下,尽量不Google,找规范文档。解决问题后,读相关资料,不要为了解决问题而解决,而是要尽量扩充知识面,积累知识。

Q:怎么处理异常比较优雅

A:采用ExceptionHandler,代码层面易于理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.bai.springwebmvc.controller;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class RestControllerAdvicer {
//多个Exception中间用","分隔
@ExceptionHandler({NoHandlerFoundException.class,IllegalAccessException.class})
public Object pageNotFound(HttpStatus status,HttpServletRequest request, Throwable throwable){
Map<String,Object> errors =new HashMap<>();
errors.put("statusCode",request.getAttribute("javax.servlet.error.status_code"));
errors.put("requestUri",request.getAttribute("javax.servlet.error.request_uri"));
return errors;
}

@ExceptionHandler(NullPointerException.class)
public Object handleNPE( Throwable throwable){

return throwable.getMessage();
}

}