Spring 中,用于加载和启动 Web Server 的容器类型是:
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(Config.class);
new CountDownLatch(1).await();
其配置类需要至少三个 Bean:
static class Config {
/**
* Web Server,这里使用 Tomcat
* @return
*/
public ServletWebServerFactory servletContainer() {
return new TomcatServletWebServerFactory();
}
/**
* 作为入口的 Servlet
* @return
*/
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
/**
* 将 Servlet 注册到 Web Server
* @return
*/
public ServletRegistrationBean<DispatcherServlet> servletRegistrationBean() {
return new ServletRegistrationBean<>(dispatcherServlet(), "/");
}
}
在将作为入口的DispatcherServlet
绑定到 Web Server 的时候需要指定路径,通常是/
或/*
,它们有一些微妙区别:
-
/
,作为备选的路径路由选项,如果匹配不到其它的 Servlet,就使用这个 Servlet。 -
/*
,任何路径请求都会匹配到这个 Servlet。
DispatcherServlet
容器启动后并不会立即对DispatcherServlet
进行初始化。只有在有请求进入时(比如访问 http://localhost:8080/)才会初始化,此时日志中会出现信息:
web.servlet.DispatcherServlet -- Context initialization failed
要让容器创建启动 Tomcat 后立即对 DispatcherServlet 初始化,可以:
public ServletRegistrationBean<DispatcherServlet> servletRegistrationBean() {
ServletRegistrationBean<DispatcherServlet> registrationBean = new ServletRegistrationBean<>(dispatcherServlet(), "/");
// 立即初始化
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
只要通过setLoadOnStartup
指定一个大于0的值即可。这个值决定了启动 Tomcat 时 Servlet 的加载(初始化)顺序(值越小越优先)。
当然,这些信息最好从配置文件中获取,比如:
server.port=8081
spring.mvc.servlet.load-on-startup=-1
通过配置类读取:
"classpath:application.properties")
(ServerProperties.class, WebMvcProperties.class})
({static class Config {
public ServletWebServerFactory servletContainer(ServerProperties serverProperties) {
return new TomcatServletWebServerFactory(serverProperties.getPort());
}
// ...
public ServletRegistrationBean<DispatcherServlet> servletRegistrationBean(WebMvcProperties mvcProperties) {
ServletRegistrationBean<DispatcherServlet> registrationBean = new ServletRegistrationBean<>(dispatcherServlet(), "/");
// 立即初始化
registrationBean.setLoadOnStartup(mvcProperties.getServlet().getLoadOnStartup());
return registrationBean;
}
}
initStrategies
Tomcat 初始化 DispatchServlet
时会调用其onRefresh
方法:
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
这个方法实际调用了initStrategies
:
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
DispatcherServlet
内部使用一组策略对象(Strategy Objects)决定具体如何处理请求。这些 init 方法对应初始化相应的策略对象,比如:
-
initMultipartResolver
,初始化文件上传的策略 -
initLocaleResolver
,初始化本地化的策略 -
initHandlerMappings
,初始化路径映射的策略 -
initHandlerAdapters
,初始化适配器策略 -
initHandlerExceptionResolvers
,初始化异常处理策略
这些策略会优先从容器中获取:
this.detectAllHandlerMappings
如果为true
,会先在本容器查找,如果查找不到,继续从父容器查找,直到根容器。
如果容器中没有,会提供默认的实现:
这些默认实现通过配置文件加载:
RequestMappingHandlerMapping
Spring 中RequestMappingHandlerMapping
负责解析控制器中的@RequestMapping
注解,并生成请求到处理器方法的映射:
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
// 获取请求-方法映射
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
handlerMethods.forEach((k,v)->{
System.out.println("请求:%s,处理器:%s".formatted(k,v));
});
容器中的控制器:
"/hello")
(public class HelloController {
public String hello() {
return "hello";
}
"/say")
(
public String say( String name) {
return "say " + name;
}
"/bye")
(
public String bye( String name) {
return "bye " + name;
}
}
打印结果:
请求:{POST [/hello/say]},处理器:com.example.demo.controller.HelloController#say(String) 请求:{GET [/hello]},处理器:com.example.demo.controller.HelloController#hello() 请求:{ [/hello/bye]},处理器:com.example.demo.controller.HelloController#bye(String)
当一个 HTTP 请求产生,Spring 会通过RequestMappingHandlerMapping
决定负责处理请求的处理器方法:
HttpServletRequest httpRequest = new MockHttpServletRequest("GET", "/hello");
HandlerExecutionChain handler = handlerMapping.getHandler(httpRequest);
这里通过
MockHttpServletRequest
构造一个 HTTP 请求的假数据。
返回的结果HandlerExecutionChain
不是一个单一的处理器方法,而是包含了处理器方法的一个处理器执行链(Handler Execution Chain),里边包含要执行的处理器方法以及相应的拦截器。
在这个示例中,输出:
HandlerExecutionChain with [com.example.demo.controller.HelloController#hello()] and 2 interceptors
包含两个拦截器(Interceptor)。
RequestMappingHandlerAdapter
Spring 中负责调用处理器方法的是HandlerAdapter
,其用于处理@RequestMapping
注解定义处理器的实现为RequestMappingHandlerAdapter
。
具体调用处理器的方法为invokeHandlerMethod
,但其访问修饰符为protected
,为了能够模拟调用,需要自定义一个子类以暴露该方法:
public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
return super.invokeHandlerMethod(request, response, handlerMethod);
}
}
通过 Bean 方法添加该类:
public MyRequestMappingHandlerAdapter handlerAdapter() {
return new MyRequestMappingHandlerAdapter();
}
使用其调用处理器:
MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
HttpServletResponse response = new MockHttpServletResponse();
handlerAdapter.invokeHandlerMethod(httpRequest, response, (HandlerMethod) executionChain.getHandler());
类似的,也可以调用带参数的 POST 方法,只需要为假数据添加相应参数:
MockHttpServletRequest httpRequest = new MockHttpServletRequest("POST", "/hello/say");
httpRequest.setParameter("name", "Tom");
// ...
handlerAdapter.invokeHandlerMethod(httpRequest, response, (HandlerMethod) executionChain.getHandler());
参数和返回值解析器
HandlerAdapter
内使用参数解析器(Argument Resolver)来解析和处理控制器方法中的参数注解,比如@RequestParam
、@RequestBody
等。
List<HandlerMethodArgumentResolver> argumentResolvers = handlerAdapter.getArgumentResolvers();
for (HandlerMethodArgumentResolver argumentResolver : argumentResolvers) {
System.out.println(argumentResolver);
}
打印:
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver@5b733ef7 org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver@43a4a9e5 org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver@764fffa0 ...
同样的,由不同注解标记的返回值也有做不同处理,这些由返回值解析器(Return Value Resolver)完成:
List<HandlerMethodReturnValueHandler> returnValueHandlers = handlerAdapter.getReturnValueHandlers();
for (HandlerMethodReturnValueHandler returnValueHandler : returnValueHandlers) {
System.out.println(returnValueHandler);
}
打印:
org.springframework.web.servlet.mvc.method.annotation.ModelAndViewMethodReturnValueHandler@73bb1337 org.springframework.web.method.annotation.ModelMethodProcessor@685f5d0d org.springframework.web.servlet.mvc.method.annotation.ViewMethodReturnValueHandler@3830b06c ...
自定义参数解析器
添加一个自定义注解:
ElementType.PARAMETER)
(RetentionPolicy.RUNTIME)
(public @interface Token {
}
用于标记处理器参数,用其标记的参数表示由前端传入的 token:
public String hello( String token) {
log.info("hello() is called.");
log.info("token:%s".formatted(token));
return "hello";
}
要让这个注解能正常解析,需要添加一个处理这个注解的参数解析器:
public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter parameter) {
Token parameterAnnotation = parameter.getParameterAnnotation(Token.class);
return parameterAnnotation != null;
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String token = webRequest.getHeader("token");
if (token != null) {
return token;
}
// 头信息中没有,尝试通过查询参数获取
token = webRequest.getParameter("token");
return token;
}
}
supportsParameter
方法用于表示能否处理指定参数,resolveArgument
将处理参数并返回。
在配置类中添加这个参数处理器:
public MyRequestMappingHandlerAdapter handlerAdapter() {
MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
// 添加自定义参数解析器
handlerAdapter.setCustomArgumentResolvers(List.of(new TokenArgumentResolver()));
return handlerAdapter;
}
测试:
// ...
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hello2");
request.addHeader("token", "abc123");
HandlerExecutionChain chain = handlerMapping.getHandler(request);
HandlerMethod handlerMethod = (HandlerMethod) chain.getHandler();
HttpServletResponse response = new MockHttpServletResponse();
handlerAdapter.invokeHandlerMethod(request, response, handlerMethod);
自定义返回值解析器
通常我们需要将控制器方法的输出包装为统一的标准输出格式提供给前端,有两种做法:
-
在控制器方法中手动封装然后返回。
-
使用控制器拦截器统一处理封装后返回。
前者太过麻烦,而后者不够灵活。可以通过使用自定义返回值解析器来实现第三种方式——基于注解的控制器方法返回值封装。
添加一个自定义注解:
ElementType.METHOD)
(RetentionPolicy.RUNTIME)
(public @interface ResultOk {
}
对需要封装返回值的控制器方法添加这个注解:
"/hello2")
(
public class HelloController2 {
//...
"/info")
(
public UserInfo info() {
return new UserInfo("张三", 20);
}
}
添加处理这个注解的返回值解析器:
public class ResultOkReturnValueResolver implements HandlerMethodReturnValueHandler {
public boolean supportsReturnType(MethodParameter returnType) {
// 有 @ResultOk 注解才处理
boolean updated = returnType.hasMethodAnnotation(ResultOk.class);
return updated;
}
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// 将返回值包装成标准返回
StandardResult<?> standardResult = null;
if (returnValue == null) {
standardResult = StandardResult.success();
} else {
standardResult = StandardResult.success(returnValue);
}
// JSON 后写入返回报文的输出流
ObjectMapper mapper = new ObjectMapper();
String resultStr = mapper.writeValueAsString(standardResult);
HttpServletResponse httpServletResponse = webRequest.getNativeResponse(HttpServletResponse.class);
httpServletResponse.setContentType("application/json");
httpServletResponse.getWriter().write(resultStr);
// 告诉 mvc 请求已经处理完毕,无需再处理
mavContainer.setRequestHandled(true);
}
}
注意,在将处理后的返回值写入返回报文体后,需要通过mavContainer.setRequestHandled(true)
方法告诉 ModelAndView 容器这个请求已经处理完,不需要再处理。
ModelAndView 在 MVC 中负责将控制器返回的数据结合页面模版由模版引擎生成最终的页面视图,在这个示例以及目前流行的前后端分离开发中,不再需要 MAV 处理页面。
在配置类中将返回值解析器添加到HandlerAdapter
使其生效:
public MyRequestMappingHandlerAdapter handlerAdapter() {
MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
// 添加自定义返回值解析器
handlerAdapter.setCustomReturnValueHandlers(List.of(new ResultOkReturnValueResolver()));
return handlerAdapter;
}
测试用例:
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hello2/info");
HandlerExecutionChain chain = handlerMapping.getHandler(request);
HandlerMethod handlerMethod = (HandlerMethod) chain.getHandler();
MockHttpServletResponse response = new MockHttpServletResponse();
handlerAdapter.invokeHandlerMethod(request, response, handlerMethod);
String content = response.getContentAsString();
System.out.println(content);
本文的完整示例可以从获取。
The End.
文章评论