Spring原理MVC
Spring原理 MVC
1 WEB
1.1 RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter
RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter 俩是一对,分别用来
- 处理 @RequestMapping 映射
- 调用控制器方法、并处理方法参数与方法返回值
演示1 - DispatcherServlet 初始化
public class A20 {
private static final Logger log = LoggerFactory.getLogger(A20.class);
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}
}
@Configuration
@ComponentScan //没有加包范围,默认扫描当前类所在的包,以及子包
public class WebConfig {
//内嵌web容器工厂
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(){
return new TomcatServletWebServerFactory();
}
//创建 DispatcherServlet 通过Spring容器创建,通过tomcat初始化
@Bean
public DispatcherServlet dispatcherServlet(){
return new DispatcherServlet();
}
//注册DispatcherServlet ,Spring MVC 的入口
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet){
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");// "/"表示所有请求
registrationBean.setLoadOnStartup(1); //大于1,Tomcat 启动后就初始化
return registrationBean;
}
}
把值写在配置文件中
@Configuration
@ComponentScan //没有加包范围,默认扫描当前类所在的包,以及子包
@PropertySource("classpath:application.properties")
//注入WebMvcProperties,ServerProperties,这两个类.这两个类都是配置文件中的属性值绑定
//WebMvcProperties: web.servlet
//ServerProperties: server
@EnableConfigurationProperties({WebMvcProperties.class, ServerProperties.class})
public class WebConfig {
//内嵌web容器工厂
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(ServerProperties serverProperties){
return new TomcatServletWebServerFactory(serverProperties.getPort());
}
//创建 DispatcherServlet 通过Spring容器创建,通过tomcat初始化
@Bean
public DispatcherServlet dispatcherServlet(){
return new DispatcherServlet();
}
//注册DispatcherServlet ,Spring MVC 的入口
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet
,WebMvcProperties webMvcProperties){
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");// "/"表示所有请求
//获取配置文件的值
int loadOnStartup = webMvcProperties.getServlet().getLoadOnStartup();
registrationBean.setLoadOnStartup(loadOnStartup); //大于1,Tomcat 启动后就初始化
return registrationBean;
}
}
DispatcherServlet 初始化做了什么
RequestMappingHandlerMapping 作用
如果使用默认DispatcherServlet.properties中的RequestMappingHandlerMapping,只会作为成员变量,并不会注入容器中,所以需要自己写
@Controller
public class Controller1 {
private static final Logger log = LoggerFactory.getLogger(Controller1.class);
@GetMapping("/test1")
public ModelAndView test1() throws Exception {
log.debug("test1()");
return null;
}
@PostMapping("/test2")
public ModelAndView test2(@RequestParam("name") String name) {
log.debug("test2({})", name);
return null;
}
@PutMapping("/test3")
public ModelAndView test3(@Token String token) {
log.debug("test3({})", token);
return null;
}
@RequestMapping("/test4")
// @ResponseBody
@Yml
public User test4() {
log.debug("test4");
return new User("张三", 18);
}
public static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
public static void main(String[] args) {
String str = new Yaml().dump(new User("张三", 18));
System.out.println(str);
}
}
public class A20 {
private static final Logger log = LoggerFactory.getLogger(A20.class);
public static void main(String[] args) throws Exception {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
//作用 解析 @RequestMappering 以及派生注解, 生成路径与控制器方法的映射关系,在初始化时就生成
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
//获取映射信息
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
handlerMethods.forEach((k,v)->{
System.out.println(k + "=" + v);
});
/** 打印结果: 请求方式 + 请求路径 = 哪个类控制器以及类中对应的方法信息
* {GET [/test1]}=com.feng.a20webdispatcherservlet.Controller1#test1()
* {PUT [/test3]}=com.feng.a20webdispatcherservlet.Controller1#test3(String)
* {POST [/test2]}=com.feng.a20webdispatcherservlet.Controller1#test2(String)
* { [/test4]}=com.feng.a20webdispatcherservlet.Controller1#test4()
*/
//请求来了,获取控制器的方法 Mock: 模拟请求
//返回处理器执行链对象
HandlerExecutionChain chain = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/test1"));
System.out.println(chain);
/**
* 结果:HandlerExecutionChain with [com.feng.a20webdispatcherservlet.Controller1#test1()] and 0 interceptors
* */
}
}
RequestMappingHandlerAdapter 作用
调用了Controller中的方法
查看参数解析器和返回值解析器
public class A20 {
private static final Logger log = LoggerFactory.getLogger(A20.class);
public static void main(String[] args) throws Exception {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
//作用 解析 @RequestMappering 以及派生注解, 生成路径与控制器方法的映射关系,在初始化时就生成
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
//获取映射信息
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
handlerMethods.forEach((k,v)->{
System.out.println(k + "=" + v);
});
/** 打印结果: 请求方式 + 请求路径 = 哪个类控制器以及类中对应的方法信息
* {GET [/test1]}=com.feng.a20webdispatcherservlet.Controller1#test1()
* {PUT [/test3]}=com.feng.a20webdispatcherservlet.Controller1#test3(String)
* {POST [/test2]}=com.feng.a20webdispatcherservlet.Controller1#test2(String)
* { [/test4]}=com.feng.a20webdispatcherservlet.Controller1#test4()
*/
//请求来了,获取控制器的方法 Mock: 模拟请求
//返回处理器执行链对象
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/test2");
request.setParameter("name", "张三");
MockHttpServletResponse response = new MockHttpServletResponse();
HandlerExecutionChain chain = handlerMapping.getHandler(request);
System.out.println(chain);
/**
* 结果:HandlerExecutionChain with [com.feng.a20webdispatcherservlet.Controller1#test1()] and 0 interceptors
* */
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>");
// HandlerAdapter作用:调用控制器的方法
MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
handlerAdapter.invokeHandlerMethod(request, response, (HandlerMethod) chain.getHandler());
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>> 所有参数解析器");
for (HandlerMethodArgumentResolver resolver : handlerAdapter.getArgumentResolvers()) {
System.out.println(resolver);
}
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>> 所有返回值解析器");
for (HandlerMethodReturnValueHandler handler : handlerAdapter.getReturnValueHandlers()) {
System.out.println(handler);
}
}
}
收获💡
- DispatcherServlet 是在第一次被访问时执行初始化, 也可以通过配置修改为 Tomcat 启动后就初始化
- 在初始化时会从 Spring 容器中找一些 Web 需要的组件, 如 HandlerMapping、HandlerAdapter 等,并逐一调用它们的初始化
- RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map,其中
- key 是 RequestMappingInfo 类型,包括请求路径、请求方法等信息
- value 是 HandlerMethod 类型,包括控制器方法对象、控制器对象
- 有了这个 Map,就可以在请求到达时,快速完成映射,找到 HandlerMethod 并与匹配的拦截器一起返回给 DispatcherServlet
- RequestMappingHandlerAdapter 初始化时,会准备 HandlerMethod 调用时需要的各个组件,如:
- HandlerMethodArgumentResolver 解析控制器方法参数
- HandlerMethodReturnValueHandler 处理控制器方法返回值
演示2 - 自定义参数与返回值处理器
自定义参数解析器
Token
// 例如经常需要用到请求头中的 token 信息, 用下面注解来标注由哪个参数来获取它
// token=令牌
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
}
TokenArgumentResolver
public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
@Override //是否支持某个参数 (查看controller中的参数是否有token注解,如果有返回true,走下一个解析方法,反之false,不走解析方法)
public boolean supportsParameter(MethodParameter parameter) {
Token token = parameter.getParameterAnnotation(Token.class);
return token!=null;
}
@Override //解析参数
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
//获取请求头中的token,然后赋值给 public ModelAndView test3(@Token String token)
return webRequest.getHeader("token");
}
}
WebConfig
@Configuration
@ComponentScan //没有加包范围,默认扫描当前类所在的包,以及子包
@PropertySource("classpath:application.properties")
//注入WebMvcProperties,ServerProperties,这两个类.这两个类都是配置文件中的属性值绑定
//WebMvcProperties: web.servlet
//ServerProperties: server
@EnableConfigurationProperties({WebMvcProperties.class, ServerProperties.class})
public class WebConfig {
//内嵌web容器工厂
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(ServerProperties serverProperties){
return new TomcatServletWebServerFactory(serverProperties.getPort());
}
//创建 DispatcherServlet 通过Spring容器创建,通过tomcat初始化
@Bean
public DispatcherServlet dispatcherServlet(){
return new DispatcherServlet();
}
//注册DispatcherServlet ,Spring MVC 的入口
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet
,WebMvcProperties webMvcProperties){
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");// "/"表示所有请求
//获取配置文件的值
int loadOnStartup = webMvcProperties.getServlet().getLoadOnStartup();
registrationBean.setLoadOnStartup(loadOnStartup); //大于1,Tomcat 启动后就初始化
return registrationBean;
}
// 如果用 DispatcherServlet 初始化时默认添加的组件, 并不会作为 bean, 给测试带来困扰
// ⬅️1. 加入RequestMappingHandlerMapping
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping(){
return new RequestMappingHandlerMapping();
}
// ⬅️2. 继续加入RequestMappingHandlerAdapter, 会替换掉 DispatcherServlet 默认的 4 个 HandlerAdapter
@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter(){
//将自定义的TokenArgumentResolver 加入到RequestMappingHandlerAdapter中
TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
handlerAdapter.setCustomArgumentResolvers(Arrays.asList(tokenArgumentResolver));
return handlerAdapter;
}
}
模拟请求
自定义返回值处理器
Yml
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Yml {
}
YmlReturnValueHandler
public class YmlReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
//获取所在方法上的注解
Yml yml = returnType.getMethodAnnotation(Yml.class);
return yml != null;
}
@Override // 返回值
public void handleReturnValue(Object returnValue , MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
//调用第三方方法,将返回直接转成 yaml 字符串
String str = new Yaml().dump(returnValue);
//将 yaml 字符串写入响应
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
response.setContentType("text/plain;charset=utf-8");
response.getWriter().write(str);
//设置请求已经处理完毕(让springmvc不用后续再进行类似视图解析相关操作)
mavContainer.setRequestHandled(true);
}
}
WebConfig
// ⬅️2. 继续加入RequestMappingHandlerAdapter, 会替换掉 DispatcherServlet 默认的 4 个 HandlerAdapter
@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter(){
//将自定义的TokenArgumentResolver 加入到RequestMappingHandlerAdapter中
TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
YmlReturnValueHandler ymlReturnValueHandler = new YmlReturnValueHandler();
MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
handlerAdapter.setCustomArgumentResolvers(Arrays.asList(tokenArgumentResolver));
handlerAdapter.setCustomReturnValueHandlers(Arrays.asList(ymlReturnValueHandler));
return handlerAdapter;
}
收获💡
- 体会参数解析器的作用
- 体会返回值处理器的作用
1.2 参数解析器
演示 - 常见参数解析器
WebConfig
@Configuration
public class WebConfig {
}
A21
/*
目标: 解析控制器方法的参数值
常见的参数处理器如下:
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver@abbc908
org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver@44afefd5
org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver@9a7a808
org.springframework.web.servlet.mvc.method.annotation.PathVariableMapMethodArgumentResolver@72209d93
org.springframework.web.servlet.mvc.method.annotation.MatrixVariableMethodArgumentResolver@2687f956
org.springframework.web.servlet.mvc.method.annotation.MatrixVariableMapMethodArgumentResolver@1ded7b14
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@29be7749
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor@5f84abe8
org.springframework.web.servlet.mvc.method.annotation.RequestPartMethodArgumentResolver@4650a407
org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver@30135202
org.springframework.web.method.annotation.RequestHeaderMapMethodArgumentResolver@6a4d7f76
org.springframework.web.servlet.mvc.method.annotation.ServletCookieValueMethodArgumentResolver@10ec523c
org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver@53dfacba
org.springframework.web.servlet.mvc.method.annotation.SessionAttributeMethodArgumentResolver@79767781
org.springframework.web.servlet.mvc.method.annotation.RequestAttributeMethodArgumentResolver@78411116
org.springframework.web.servlet.mvc.method.annotation.ServletRequestMethodArgumentResolver@aced190
org.springframework.web.servlet.mvc.method.annotation.ServletResponseMethodArgumentResolver@245a060f
org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor@6edaa77a
org.springframework.web.servlet.mvc.method.annotation.RedirectAttributesMethodArgumentResolver@1e63d216
org.springframework.web.method.annotation.ModelMethodProcessor@62ddd21b
org.springframework.web.method.annotation.MapMethodProcessor@16c3ca31
org.springframework.web.method.annotation.ErrorsMethodArgumentResolver@2d195ee4
org.springframework.web.method.annotation.SessionStatusMethodArgumentResolver@2d6aca33
org.springframework.web.servlet.mvc.method.annotation.UriComponentsBuilderMethodArgumentResolver@21ab988f
org.springframework.web.servlet.mvc.method.annotation.PrincipalMethodArgumentResolver@29314cc9
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver@4e38d975
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@35f8a9d3
*/
public class A21 {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
//提供beanFactory,用于RequestParamMethodArgumentResolver 通过 ${} 解析数据
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
// 准备测试 Request
HttpServletRequest request = mockRequest();
// 要点1. 控制器方法被封装为 HandlerMethod
HandlerMethod handlerMethod = new HandlerMethod(new Controller(), Controller.class.getMethod("test", String.class, String.class, int.class, String.class, MultipartFile.class, int.class, String.class, String.class, String.class, HttpServletRequest.class, User.class, User.class, User.class));
// 要点2. 准备对象绑定与类型转换 (传递数据过程中,都是字符串,需要转成其他类型)
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
// 要点3. 准备 ModelAndViewContainer 用来存储中间 Model 结果
ModelAndViewContainer container = new ModelAndViewContainer();
// 要点4. 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个解析器组合 Composite: 组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
//false表示必须有 @RequestParam
new RequestParamMethodArgumentResolver(beanFactory, false),
new PathVariableMethodArgumentResolver(),
//解析Header
new RequestHeaderMethodArgumentResolver(beanFactory),
new ServletCookieValueMethodArgumentResolver(beanFactory),
//解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
new ServletRequestMethodArgumentResolver(),
//false 表示必须有 @ModelAttribute 注解,true表示可以省略注解
new ServletModelAttributeMethodProcessor(false),
//解析 @RequestBody
//参数是消息转换器,处理json数据
new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())),
// 处理省略注解 ,不能和RequestResponseBodyMethodProcessor调整顺序,避免@RequestBody对应参数被 省略注解解析器解析
new ServletModelAttributeMethodProcessor(true),
new RequestParamMethodArgumentResolver(beanFactory, true) //省略@RequestParam注解
);
String annotations = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining());
String str = annotations.length() > 0 ? " @" + annotations + " " : " ";
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
/**
* composite.supportsParameter(parameter)
* 会从组合的解析器中 挨个找出能够解析参数的解析器
*/
if (composite.supportsParameter(parameter)) {
//支持此参数 (v 代表从请求中获取的结果)
Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), factory);
//System.out.println(v.getClass());
System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName() + " -> " + v);
System.out.println("模型数据为:" + container.getModel());
}else {
System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName() );
}
}
/*
学到了什么
a. 每个参数处理器能干啥
1) 看是否支持某种参数
2) 获取参数的值
b. 组合模式在 Spring 中的体现
c. @RequestParam, @CookieValue 等注解中的参数名、默认值, 都可以写成活的, 即从 ${ } #{ }中获取
*/
}
/**
* 模拟请求
* @return
*/
private static HttpServletRequest mockRequest() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name1", "zhangsan");
request.setParameter("name2", "lisi");
request.addPart(new MockPart("file", "abc", "hello".getBytes(StandardCharsets.UTF_8)));
Map<String, String> map = new AntPathMatcher().extractUriTemplateVariables("/test/{id}", "/test/123");
System.out.println(map);
//将map 集合放入到request作用域中
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, map);
request.setContentType("application/json");
request.setCookies(new Cookie("token", "123456"));
request.setParameter("name", "张三");
request.setParameter("age", "18");
request.setContent("""
{
"name":"李四",
"age":20
}
""".getBytes(StandardCharsets.UTF_8));
return new StandardServletMultipartResolver().resolveMultipart(request);
}
/**
* 控制器方法
*/
static class Controller {
public void test(
@RequestParam("name1") String name1, // name1=张三
String name2, // name2=李四
@RequestParam("age") int age, // age=18
@RequestParam(name = "home", defaultValue = "${JAVA_HOME}") String home1, // spring 获取数据
@RequestParam("file") MultipartFile file, // 上传文件
@PathVariable("id") int id, // /test/124 /test/{id}
@RequestHeader("Content-Type") String header,
@CookieValue("token") String token,
@Value("${JAVA_HOME}") String home2, // spring 获取数据 ${} #{}
HttpServletRequest request, // request, response, session ...
@ModelAttribute("abc") User user1, // name=zhang&age=18
User user2, // name=zhang&age=18
@RequestBody User user3 // json
) {
}
}
static class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
收获💡
- 初步了解 RequestMappingHandlerAdapter 的调用过程
- 控制器方法被封装为 HandlerMethod
- 准备对象绑定与类型转换
- 准备 ModelAndViewContainer 用来存储中间 Model 结果
- 解析每个参数值
- 解析参数依赖的就是各种参数解析器,它们都有两个重要方法
- supportsParameter 判断是否支持方法参数
- resolveArgument 解析方法参数
- 常见参数的解析
- @RequestParam
- 省略 @RequestParam
- @RequestParam(defaultValue)
- MultipartFile
- @PathVariable
- @RequestHeader
- @CookieValue
- @Value
- HttpServletRequest 等
- @ModelAttribute
- 省略 @ModelAttribute
- @RequestBody
- 组合模式在 Spring 中的体现
- @RequestParam, @CookieValue 等注解中的参数名、默认值, 都可以写成活的, 即从 ${ } #{ }中获取
1.3 参数名解析
收获💡
- 如果编译时添加了 -parameters 可以生成参数表, 反射时就可以拿到参数名
- 如果编译时添加了 -g 可以生成调试信息, 但分为两种情况
- 普通类, 会包含局部变量表, 用 asm 可以拿到参数名
- 接口, 不会包含局部变量表, 无法获得参数名
- 这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名
1.4 对象绑定与类型转换
底层第一套转换接口与实现
- Printer 把其它类型转为 String
- Parser 把 String 转为其它类型
- Formatter 综合 Printer 与 Parser 功能
- Converter 把类型 S 转为类型 T
- Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合
- FormattingConversionService 利用其它们实现转换
底层第二套转换接口
- PropertyEditor 把 String 与其它类型相互转换
- PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
- 与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配
高层接口与实现
- 它们都实现了 TypeConverter 这个高层转换接口,在转换时,会用到 TypeConverter Delegate 委派ConversionService 与 PropertyEditorRegistry 真正执行转换(Facade 门面模式)
- 首先看是否有自定义转换器, @InitBinder 添加的即属于这种 (用了适配器模式把 Formatter 转为需要的 PropertyEditor)
- 再看有没有 ConversionService 转换
- 再利用默认的 PropertyEditor 转换
- 最后有一些特殊处理
- SimpleTypeConverter 仅做类型转换
- BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,走 Property
- DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,走 Field
- ServletRequestDataBinder 为 bean 的属性执行绑定,当需要时做类型转换,根据 directFieldAccess 选择走 Property 还是 Field,具备校验与获取校验结果功能
演示1 - 类型转换与数据绑定
TestSimpleConverter
public class TestSimpleConverter {
public static void main(String[] args) {
//仅有类型转换的功能
SimpleTypeConverter typeConverter = new SimpleTypeConverter();
Integer number = typeConverter.convertIfNecessary("13", int.class);
System.out.println(number);
}
}
TestBeanWrapper
public class TestBeanWrapper {
public static void main(String[] args) {
// 利用反射原理,为bean的属性赋值
MyBean target = new MyBean();
BeanWrapperImpl wrapper = new BeanWrapperImpl(target);
/**
* 在赋值(用的是get/set方法赋值)的过程中,如果发现值和定义的变量类型不一致,会自动进行类型转换
*/
wrapper.setPropertyValue("a","10");
wrapper.setPropertyValue("b","hello");
wrapper.setPropertyValue("c","1999/03/04");
System.out.println(target);
}
static class MyBean{
private int a;
private String b;
private Date c;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public String getB() {
return b;
}
public void setB(String b) {
this.b = b;
}
public Date getC() {
return c;
}
public void setC(Date c) {
this.c = c;
}
@Override
public String toString() {
return "MyBean{" +
"a=" + a +
", b='" + b + '\'' +
", c=" + c +
'}';
}
}
}
TestFieldAccessor
public class TestFieldAccessor {
public static void main(String[] args) {
// 利用反射原理,为bean的属性赋值
MyBean target = new MyBean();
DirectFieldAccessor accessor = new DirectFieldAccessor(target);
/**
* 在赋值的过程中(直接用成员变量方法赋值),如果发现值和定义的变量类型不一致,会自动进行类型转换
*/
accessor.setPropertyValue("a","10");
accessor.setPropertyValue("b","hello");
accessor.setPropertyValue("c","1999/03/04");
System.out.println(target);
}
static class MyBean{
private int a;
private String b;
private Date c;
@Override
public String toString() {
return "MyBean{" +
"a=" + a +
", b='" + b + '\'' +
", c=" + c +
'}';
}
}
}
TestDataBinder
public class TestDataBinder {
public static void main(String[] args) {
// 执行数据绑定
MyBean target = new MyBean();
DataBinder dataBinder = new DataBinder(target);
//默认用的是get/set方法赋值, 加了这句代码,用的是成员变量赋值
dataBinder.initDirectFieldAccess();
MutablePropertyValues pvs = new MutablePropertyValues();
/**
* 在赋值(用的是get/set方法赋值)的过程中,如果发现值和定义的变量类型不一致,会自动进行类型转换
*/
pvs.add("a","10");
pvs.add("b","hello");
pvs.add("c","1999/03/04");
dataBinder.bind(pvs);
System.out.println(target);
}
static class MyBean{
private int a;
private String b;
private Date c;
/*public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public String getB() {
return b;
}
public void setB(String b) {
this.b = b;
}
public Date getC() {
return c;
}
public void setC(Date c) {
this.c = c;
}*/
@Override
public String toString() {
return "MyBean{" +
"a=" + a +
", b='" + b + '\'' +
", c=" + c +
'}';
}
}
}
web环境
TestServletDataBinder
public class TestServletDataBinder {
public static void main(String[] args) {
// 执行数据绑定
MyBean target = new MyBean();
ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(target);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("a", "10");
request.setParameter("b", "hello");
request.setParameter("c", "1999/03/04");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(target);
}
static class MyBean{
private int a;
private String b;
private Date c;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public String getB() {
return b;
}
public void setB(String b) {
this.b = b;
}
public Date getC() {
return c;
}
public void setC(Date c) {
this.c = c;
}
@Override
public String toString() {
return "MyBean{" +
"a=" + a +
", b='" + b + '\'' +
", c=" + c +
'}';
}
}
}
TestServletDataBinderFactory
收获💡
基本的类型转换与数据绑定用法
- SimpleTypeConverter
- BeanWrapperImpl
- DirectFieldAccessor
- ServletRequestDataBinder
演示2 - 数据绑定工厂
TestServletDataBinderFactory
public class TestServletDataBinderFactory {
public static void main(String[] args) throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");//这种格式不支持,默认绑定失败
request.setParameter("address.name", "成都");
User target = new User();
// "1. 用工厂, 无转换功能"
//ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
//WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
// "2. 用 @InitBinder 转换" 本质使用的是PropertyEditorRegistry PropertyEditor(查看源码)
// 利用反射,传递一个类对象和一个方法对象,得到一个标注方法信息
InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
//传递方法信息给绑定工厂,创建binder时,回调aaa()方法,进行扩展,最后返回的binder对象包含扩展的方法
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), null);
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
// "3. 用 ConversionService 转换" ConversionService Formatter
// "4. 同时加了 @InitBinder 和 ConversionService"
// "5. 使用默认 ConversionService 转换"
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(target);
}
static class MyController{
@InitBinder
public void aaa(WebDataBinder dataBinder){
//扩展dataBinder的转换器 MyDateFormatter:自定义的转换器,可以进行 1999|01|02 时间格式转换
dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 方式扩展的"));
}
}
public static class User {
private Date birthday;
private Address address;
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"birthday=" + birthday +
", address=" + address +
'}';
}
}
public static class Address {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Address{" +
"name='" + name + '\'' +
'}';
}
}
}
自定义的MyDateFormatter
public class MyDateFormatter implements Formatter<Date> {
private static final Logger log = LoggerFactory.getLogger(MyDateFormatter.class);
private final String desc;
public MyDateFormatter(String desc) {
this.desc = desc;
}
@Override
public String print(Date date, Locale locale) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");
return sdf.format(date);
}
@Override
public Date parse(String text, Locale locale) throws ParseException {
log.debug(">>>>>> 进入了: {}", desc);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");
return sdf.parse(text);
}
}
用 ConversionService 转换" ConversionService Formatter
public class TestServletDataBinderFactory {
public static void main(String[] args) throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");//这种格式不支持,默认绑定失败
request.setParameter("address.name", "成都");
User target = new User();
// "1. 用工厂, 无转换功能"
//ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
//WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
// "2. 用 @InitBinder 转换" 本质使用的是PropertyEditorRegistry PropertyEditor(查看源码)
// 利用反射,传递一个类对象和一个方法对象,得到一个标注方法信息
//InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
//传递方法信息给绑定工厂,创建binder时,回调aaa()方法,进行扩展,最后返回的binder对象包含扩展的方法
//ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), null);
// "3. 用 ConversionService 转换" ConversionService Formatter
/**
* 这是一个Spring提供的服务,用于处理对象与其字符串表示之间的转换。它允许你添加自定义格式化器,以便处理特定类型(如日期、数字等)的转换。
*/
FormattingConversionService service = new FormattingConversionService();
/**
* 这里向FormattingConversionService中添加了一个自定义格式化器MyDateFormatter。这个格式化器负责将字符串转换为日期对象,
* 或将日期对象转换为字符串。在这个例子中,传入的字符串可能用于定义日期格式或描述。
*/
service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展的"));
/**
* 创建一个ConfigurableWebBindingInitializer实例,这个类用于初始化数据绑定的配置。
* 通过setConversionService(service)方法,将之前定义的自定义转换服务与该初始化器关联。
*/
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
/**
* 使用ConfigurableWebBindingInitializer创建一个ServletRequestDataBinderFactory。
* 这个工厂负责创建WebDataBinder,用于将请求参数绑定到目标对象。
*/
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);
/**
* 这里调用createBinder方法生成一个WebDataBinder实例。参数包括:
*
* new ServletWebRequest(request): 这是Spring的一个封装,用于处理HTTP请求。
* target: 绑定的目标对象,通常是控制器中的模型对象。
* "user": 绑定的对象属性名称。
*/
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
// "4. 同时加了 @InitBinder 和 ConversionService"
// "5. 使用默认 ConversionService 转换"
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(target);
}
同时加了 @InitBinder 和 ConversionService"时候,优先使用@InitBinder标注的转换器
使用默认 ConversionService 转换
public class TestServletDataBinderFactory {
public static void main(String[] args) throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");//这种格式不支持,默认绑定失败
request.setParameter("address.name", "成都");
User target = new User();
// "1. 用工厂, 无转换功能"
//ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
//WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
// "2. 用 @InitBinder 转换" 本质使用的是PropertyEditorRegistry PropertyEditor(查看源码)
// 利用反射,传递一个类对象和一个方法对象,得到一个标注方法信息
//InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
//传递方法信息给绑定工厂,创建binder时,回调aaa()方法,进行扩展,最后返回的binder对象包含扩展的方法
//ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), null);
// "3. 用 ConversionService 转换" ConversionService Formatter
/**
* 这是一个Spring提供的服务,用于处理对象与其字符串表示之间的转换。它允许你添加自定义格式化器,以便处理特定类型(如日期、数字等)的转换。
*/
//FormattingConversionService service = new FormattingConversionService();
/**
* 这里向FormattingConversionService中添加了一个自定义格式化器MyDateFormatter。这个格式化器负责将字符串转换为日期对象,
* 或将日期对象转换为字符串。在这个例子中,传入的字符串可能用于定义日期格式或描述。
*/
//service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展的"));
/**
* 创建一个ConfigurableWebBindingInitializer实例,这个类用于初始化数据绑定的配置。
* 通过setConversionService(service)方法,将之前定义的自定义转换服务与该初始化器关联。
*/
//ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
//initializer.setConversionService(service);
/**
* 使用ConfigurableWebBindingInitializer创建一个ServletRequestDataBinderFactory。
* 这个工厂负责创建WebDataBinder,用于将请求参数绑定到目标对象。
*/
//ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);
/**
* 这里调用createBinder方法生成一个WebDataBinder实例。参数包括:
*
* new ServletWebRequest(request): 这是Spring的一个封装,用于处理HTTP请求。
* target: 绑定的目标对象,通常是控制器中的模型对象。
* "user": 绑定的对象属性名称。
*/
//WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
// "4. 同时加了 @InitBinder 和 ConversionService"
// "5. 使用默认 ConversionService 转换"
DefaultFormattingConversionService service = new DefaultFormattingConversionService();
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(target);
}
收获💡
ServletRequestDataBinderFactory 的用法和扩展点
- 可以解析控制器的 @InitBinder 标注方法作为扩展点,添加自定义转换器
- 控制器私有范围
- 可以通过 ConfigurableWebBindingInitializer 配置 ConversionService 作为扩展点,添加自定义转换器
- 公共范围
- 同时加了 @InitBinder 和 ConversionService 的转换优先级
- 优先采用 @InitBinder 的转换器
- 其次使用 ConversionService 的转换器
- 使用默认转换器
- 特殊处理(例如有参构造)
演示3 - 获取泛型参数
收获💡
- java api 获取泛型参数
- spring api 获取泛型参数
1.5 @ControllerAdvice 之 @InitBinder
演示 - 准备 @InitBinder
准备 @InitBinder 在整个 HandlerAdapter 调用过程中所处的位置
- RequestMappingHandlerAdapter 在图中缩写为 HandlerAdapter
- HandlerMethodArgumentResolverComposite 在图中缩写为 ArgumentResolvers
- HandlerMethodReturnValueHandlerComposite 在图中缩写为 ReturnValueHandlers
WebConfig
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice {
@InitBinder //@InitBinder加到@ControllerAdvice中作用于所有的控制器中的类型转换
public void binder3(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder3 转换器"));
}
}
@Controller
static class Controller1 {
@InitBinder //@InitBinder加到@Controller中作用于当前控制器中的类型转换
public void binder1(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder1 转换器"));
}
public void foo() {
}
}
@Controller
static class Controller2 {
@InitBinder
public void binder21(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder21 转换器"));
}
@InitBinder
public void binder22(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder22 转换器"));
}
public void bar() {
}
}
}
public class A24 {
private static final Logger log = LoggerFactory.getLogger(A24.class);
public static void main(String[] args) throws Exception {
/*
@InitBinder 的来源有两个
1. @ControllerAdvice 中 @InitBinder 标注的方法,由 RequestMappingHandlerAdapter 在初始化时解析并记录
2. @Controller 中 @InitBinder 标注的方法,由 RequestMappingHandlerAdapter 会在控制器方法首次执行时解析并记录
*/
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
//RequestMappingHandlerAdapter:执行控制器方法,执行之前完成对@InitBinder的解析
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
handlerAdapter.setApplicationContext(context);
handlerAdapter.afterPropertiesSet();
log.debug("1. 刚开始...");
showBindMethods(handlerAdapter);
Method getDataBinderFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getDataBinderFactory", HandlerMethod.class);
getDataBinderFactory.setAccessible(true);
log.info("2. 模拟调用 Controller1 的 foo 方法时 ...");
//调用Controller1的foo方法,foo方法上没有@InitBinder,但是Controller1中binder1()有,所以会解析binder1()上的@InitBinder
getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo")));
showBindMethods(handlerAdapter);
log.info("3. 模拟调用 Controller2 的 bar 方法时 ...");
getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller2(), WebConfig.Controller2.class.getMethod("bar")));
showBindMethods(handlerAdapter);
context.close();
/*
学到了什么
a. Method 对象的获取利用了缓存来进行加速
b. 绑定器工厂的扩展点(advice 之一), 通过 @InitBinder 扩展类型转换器
*/
}
private static void showBindMethods(RequestMappingHandlerAdapter handlerAdapter) throws NoSuchFieldException, IllegalAccessException {
Field initBinderAdviceCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderAdviceCache");
initBinderAdviceCache.setAccessible(true);
Map<ControllerAdviceBean, Set<Method>> globalMap = (Map<ControllerAdviceBean, Set<Method>>) initBinderAdviceCache.get(handlerAdapter);
log.info("全局的 @InitBinder 方法 {}",
globalMap.values().stream()
.flatMap(ms -> ms.stream().map(m -> m.getName()))
.collect(Collectors.toList())
);
Field initBinderCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderCache");
initBinderCache.setAccessible(true);
Map<Class<?>, Set<Method>> controllerMap = (Map<Class<?>, Set<Method>>) initBinderCache.get(handlerAdapter);
log.info("控制器的 @InitBinder 方法 {}",
controllerMap.entrySet().stream()
.flatMap(e -> e.getValue().stream().map(v -> e.getKey().getSimpleName() + "." + v.getName()))
.collect(Collectors.toList())
);
}
}
收获💡
- RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @InitBinder 方法
- RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @InitBinder 方法
- 以上两种 @InitBinder 的解析结果都会缓存来避免重复解析
- 控制器方法调用时,会综合利用本类的 @InitBinder 方法和 @ControllerAdvice 中的 @InitBinder 方法创建绑定工厂
1.6 控制器方法执行流程
图1
HandlerMethod 需要
- bean 即是哪个 Controller
- method 即是 Controller 中的哪个方法
ServletInvocableHandlerMethod 需要
- WebDataBinderFactory 负责对象绑定、类型转换
- ParameterNameDiscoverer 负责参数名解析
- HandlerMethodArgumentResolverComposite 负责解析参数
- HandlerMethodReturnValueHandlerComposite 负责处理返回值
图2
图3
WebConfig
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice {
@ModelAttribute("a")
public String aa() {
return "aa";
}
}
@Controller
static class Controller1 {
@ModelAttribute("b")
public String aa() {
return "bb";
}
@ResponseStatus(HttpStatus.OK) //加了注解,不用把返回值处理器写完整
public ModelAndView foo(@ModelAttribute User user) {
System.out.println("foo");
return null;
}
}
static class User {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
}
public class A26 {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter("name", "zhangsan");
/**
* 现在可以通过 ServletInvocableHandlerMethod 把这些整合在一起,并完成控制器方法的调用, 如下
* 根据笔记中的图1进行配置:
* ServletInvocableHandlerMethod 需要:
* 1. WebDataBinderFactory 负责对象绑定、类型转换
* 2. ParameterNameDiscoverer 负责参数名解析
* 3. HandlerMethodArgumentResolverComposite 负责解析参数
* 4. HandlerMethodReturnValueHandlerComposite 负责处理返回值
*/
ServletInvocableHandlerMethod handlerMethod =
new ServletInvocableHandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo", WebConfig.User.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
handlerMethod.setDataBinderFactory(factory);//WebDataBinderFactory
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); //ParameterNameDiscoverer
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));//常用的参数解析器组合 HandlerMethodArgumentResolverComposite
// WebConfig中加了@ResponseStatus(HttpStatus.OK) 不用把返回值处理器写完整
/**
* new ServletWebRequest(request): 对原始request进行封装
* ModelAndViewContainer: 过程中的ModelAndView对象放入这个容器中
*/
ModelAndViewContainer container = new ModelAndViewContainer();
handlerMethod.invokeAndHandle(new ServletWebRequest(request),container);
System.out.println("===容器中模型数据==");
System.out.println(container.getModel());
context.close();
/**
* 流程分析:
* ServletInvocableHandlerMethod 调用HandlerMethodArgumentResolverComposite参数解析器组合
* 根据WebConfig.Controller1.foo()方法中的参数User,找到适用的ServletModelAttributeMethodProcessor参数解析器,
* 借助WebDataBinderFactory把request中设置的参数值,赋给foo(user)方法中的User参数对象,
* 并且加入到ModelAndViewContainer容器中(存入名称为模型数据首字母小写:user)
*/
}
/**
* 常用的参数解析器组合
* @param context
* @return
*/
public static HandlerMethodArgumentResolverComposite getArgumentResolvers(AnnotationConfigApplicationContext context) {
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), false),
new PathVariableMethodArgumentResolver(),
new RequestHeaderMethodArgumentResolver(context.getDefaultListableBeanFactory()),
new ServletCookieValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
new ExpressionValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
new ServletRequestMethodArgumentResolver(),
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true),
new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), true)
);
return composite;
}
}
1.7 @ControllerAdvice 之 @ModelAttribute
演示 - 准备 @ModelAttribute
准备 @ModelAttribute 在整个 HandlerAdapter 调用过程中所处的位置
public class A26 {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
/**
* afterPropertiesSet:初始化方法,会找到@ControllerAdvice和@Controller中标注了@ModelAttribute的方法,并记录
*/
adapter.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter("name", "zhangsan");
/**
* 现在可以通过 ServletInvocableHandlerMethod 把这些整合在一起,并完成控制器方法的调用, 如下
* 根据笔记中的图1进行配置:
* ServletInvocableHandlerMethod 需要:
* 1. WebDataBinderFactory 负责对象绑定、类型转换
* 2. ParameterNameDiscoverer 负责参数名解析
* 3. HandlerMethodArgumentResolverComposite 负责解析参数
* 4. HandlerMethodReturnValueHandlerComposite 负责处理返回值
*/
ServletInvocableHandlerMethod handlerMethod =
new ServletInvocableHandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo", WebConfig.User.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
handlerMethod.setDataBinderFactory(factory);//WebDataBinderFactory
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); //ParameterNameDiscoverer
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));//常用的参数解析器组合 HandlerMethodArgumentResolverComposite
// WebConfig中加了@ResponseStatus(HttpStatus.OK) 不用把返回值处理器写完整
/**
* new ServletWebRequest(request): 对原始request进行封装
* ModelAndViewContainer: 过程中的ModelAndView对象放入这个容器中
*/
ModelAndViewContainer container = new ModelAndViewContainer();
//获取模型工厂方法
Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
getModelFactory.setAccessible(true);
ModelFactory invoke = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, factory);
//初始化模型数据(找到记录由@ModelAttribute注解的方法,反射调用方法,将方法的返回值取名,并放入模型数据中)
invoke.initModel(new ServletWebRequest(request),container,handlerMethod);
handlerMethod.invokeAndHandle(new ServletWebRequest(request),container);
System.out.println("===容器中模型数据==");
System.out.println(container.getModel());
context.close();
/**
* 流程分析:
* ServletInvocableHandlerMethod 调用HandlerMethodArgumentResolverComposite参数解析器组合
* 根据WebConfig.Controller1.foo()方法中的参数User,找到适用的ServletModelAttributeMethodProcessor参数解析器,
* 借助WebDataBinderFactory把request中设置的参数值,赋给foo(user)方法中的User参数对象,
* 并且加入到ModelAndViewContainer容器中(存入名称为模型数据首字母小写:user)
*/
}
WebConfig
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice {
//指定了名字用指定的,没有指定用返回值的小写作为名字
@ModelAttribute("a") //通过 RequestMappingHandlerAdapter解析,而不是使用参数解析器进行解析
public String aa() {
return "aa";
}
}
@Controller
static class Controller1 {
@ModelAttribute("b")
public String aa() {
return "bb";
}
@ResponseStatus(HttpStatus.OK) //加了注解,不用把返回值处理器写完整
public ModelAndView foo(@ModelAttribute("u") User user) {
System.out.println("foo");
return null;
}
}
收获💡
- RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @ModelAttribute 方法
- RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @ModelAttribute 方法
- 以上两种 @ModelAttribute 的解析结果都会缓存来避免重复解析
- 控制器方法调用时,会综合利用本类的 @ModelAttribute 方法和 @ControllerAdvice 中的 @ModelAttribute 方法创建模型工厂
1.8 返回值处理器
演示 - 常见返回值处理器
ModelAndViewMethodReturnValueHandler结果处理器
WebConfig
@Configuration
public class WebConfig {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setDefaultEncoding("utf-8");
configurer.setTemplateLoaderPath("classpath:templates"); //找到resource的模版目录
return configurer;
}
@Bean // FreeMarkerView 在借助 Spring 初始化时,会要求 web 环境才会走 setConfiguration, 这里想办法去掉了 web 环境的约束
public FreeMarkerViewResolver viewResolver(FreeMarkerConfigurer configurer) {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver() {
@Override
protected AbstractUrlBasedView instantiateView() {
FreeMarkerView view = new FreeMarkerView() {
@Override
protected boolean isContextRequired() {
return false;
}
};
view.setConfiguration(configurer.getConfiguration());
return view;
}
};
resolver.setContentType("text/html;charset=utf-8");
resolver.setPrefix("/");
resolver.setSuffix(".ftl"); //拼接模版名称:根据上面的/templates目录拼接, /templates/xxx.ftl
resolver.setExposeSpringMacroHelpers(false);
return resolver;
}
}
public class A27 {
/*
目标: 解析控制器方法的返回值
常见的返回值处理器
org.springframework.web.servlet.mvc.method.annotation.ModelAndViewMethodReturnValueHandler@4c9e38
org.springframework.web.method.annotation.ModelMethodProcessor@5d1e09bc
org.springframework.web.servlet.mvc.method.annotation.ViewMethodReturnValueHandler@4bdc8b5d
org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler@3bcd426c
org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBodyReturnValueHandler@5f14a673
org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor@726a17c4
org.springframework.web.servlet.mvc.method.annotation.HttpHeadersReturnValueHandler@5dc3fcb7
org.springframework.web.servlet.mvc.method.annotation.CallableMethodReturnValueHandler@c4c0b41
org.springframework.web.servlet.mvc.method.annotation.DeferredResultMethodReturnValueHandler@76911385
org.springframework.web.servlet.mvc.method.annotation.AsyncTaskMethodReturnValueHandler@5467eea4
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@160396db
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor@7a799159
org.springframework.web.servlet.mvc.method.annotation.ViewNameMethodReturnValueHandler@40ab8a8
org.springframework.web.method.annotation.MapMethodProcessor@6ff37443
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@65cc8228
*/
private static final Logger log = LoggerFactory.getLogger(A27.class);
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
Method method = Controller.class.getMethod("test1");
Controller controller = new Controller();
Object returnValue = method.invoke(controller);//获取返回值
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
//1.测试返回值类型为 ModelAndView
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
//给一个空的请求对象 webRequest
ServletWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest(),new MockHttpServletResponse());
//检查是否支持此类型的返回值
if (composite.supportsReturnType(handlerMethod.getReturnType())) {
//给一个空的请求对象 webRequest
composite.handleReturnValue(returnValue, handlerMethod.getReturnType(), container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
//调用渲染视图方法
renderView(context,container,webRequest);
}
}
/**
* 返回值处理器组合
* @return
*/
public static HandlerMethodReturnValueHandlerComposite getReturnValueHandler() {
HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
composite.addHandler(new ModelAndViewMethodReturnValueHandler());
composite.addHandler(new ViewNameMethodReturnValueHandler());
composite.addHandler(new ServletModelAttributeMethodProcessor(false));
composite.addHandler(new HttpEntityMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())));
composite.addHandler(new HttpHeadersReturnValueHandler());
composite.addHandler(new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())));
composite.addHandler(new ServletModelAttributeMethodProcessor(true));
return composite;
}
private static void renderView(AnnotationConfigApplicationContext context, ModelAndViewContainer container,
ServletWebRequest webRequest) throws Exception {
log.info(">>>>>> 渲染视图");
FreeMarkerViewResolver resolver = context.getBean(FreeMarkerViewResolver.class);
String viewName = container.getViewName() != null ? container.getViewName() : new DefaultRequestToViewNameTranslator().getViewName(webRequest.getRequest());
log.info("没有获取到视图名, 采用默认视图名: {}", viewName);
// 每次渲染时, 会产生新的视图对象, 它并非被 Spring 所管理, 但确实借助了 Spring 容器来执行初始化
View view = resolver.resolveViewName(viewName, Locale.getDefault());
view.render(container.getModel(), webRequest.getRequest(), webRequest.getResponse());
System.out.println(new String(((MockHttpServletResponse) webRequest.getResponse()).getContentAsByteArray(), StandardCharsets.UTF_8));
}
static class Controller {
private static final Logger log = LoggerFactory.getLogger(Controller.class);
public ModelAndView test1() {
log.info("test1()");
ModelAndView mav = new ModelAndView("view1");
mav.addObject("name", "张三");
return mav;
}
public String test2() {
log.info("test2()");
return "view2";
}
@ModelAttribute
// @RequestMapping("/test3")
public User test3() {
log.info("test3()");
return new User("李四", 20);
}
public User test4() {
log.info("test4()");
return new User("王五", 30);
}
public HttpEntity<User> test5() {
log.info("test5()");
return new HttpEntity<>(new User("赵六", 40));
}
public HttpHeaders test6() {
log.info("test6()");
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "text/html");
return headers;
}
@ResponseBody //返回的结果会做为响应体
public User test7() {
log.info("test7()");
return new User("钱七", 50);
}
}
// 必须用 public 修饰, 否则 freemarker 渲染其 name, age 属性时失败
public static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
ViewNameMethodReturnValueHandler:把返回值当做视图名解析
private static final Logger log = LoggerFactory.getLogger(A27.class);
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
//1.测试返回值类型为 ModelAndView
//2.测试返回值类型为String 时候,把它当做视图名
//3.测试返回值添加了@ModelAttribute注解时,此时需找到默认视图名
//4.测试返回值不加@ModelAttribute注解时且返回非简单类型时,此时需找到默认视图名
//5.测试返回值类型为 ResponseEntity 时,此时不走视图流程
//6.测试返回值类型为 HttpHeaders时,此时不走视图流程
//7.测试返回值添加了 @ResponseBody 注解时,此时不走视图流程
test2(context);
}
private static void test2(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test2");
Controller controller = new Controller();
Object returnValue = method.invoke(controller);//获取返回值
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
//给一个空的请求对象 webRequest
ServletWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest(),new MockHttpServletResponse());
//检查是否支持此类型的返回值
if (composite.supportsReturnType(handlerMethod.getReturnType())) {
//给一个空的请求对象 webRequest
composite.handleReturnValue(returnValue, handlerMethod.getReturnType(), container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
//调用渲染视图方法
renderView(context,container,webRequest);
}
}
public String test2() {
log.info("test2()");
return "view2";
}
new ServletModelAttributeMethodProcessor(false):带有@ModelAttribute注解
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
//1.测试返回值类型为 ModelAndView
//2.测试返回值类型为String 时候,把它当做视图名
//3.测试返回值添加了@ModelAttribute注解时,此时需找到默认视图名
//4.测试返回值不加@ModelAttribute注解时且返回非简单类型时,此时需找到默认视图名
//5.测试返回值类型为 ResponseEntity 时,此时不走视图流程
//6.测试返回值类型为 HttpHeaders时,此时不走视图流程
//7.测试返回值添加了 @ResponseBody 注解时,此时不走视图流程
test3(context);
}
private static void test3(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test3");
Controller controller = new Controller();
Object returnValue = method.invoke(controller);//获取返回值
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
//给一个空的请求对象 webRequest
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/test3"); //模拟@RequestMapping("/test3")
//把路径(/test3)存入request域,后续如果没有找到视图名,就把test3当做视图名
UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request);
ServletWebRequest webRequest = new ServletWebRequest(request,new MockHttpServletResponse());
//检查是否支持此类型的返回值
if (composite.supportsReturnType(handlerMethod.getReturnType())) {
//给一个空的请求对象 webRequest
composite.handleReturnValue(returnValue, handlerMethod.getReturnType(), container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
//调用渲染视图方法
renderView(context,container,webRequest);
}
}
@ModelAttribute
//@RequestMapping("/test3") :如果加了这个注解,找不到视图名,就会把路径名当做视图名
public User test3() {
log.info("test3()");
return new User("李四", 20);
}
new ServletModelAttributeMethodProcessor(true):处理不带@ModelAttribute注解
private static final Logger log = LoggerFactory.getLogger(A27.class);
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
//1.测试返回值类型为 ModelAndView
//2.测试返回值类型为String 时候,把它当做视图名
//3.测试返回值添加了@ModelAttribute注解时,此时需找到默认视图名
//4.测试返回值不加@ModelAttribute注解时且返回非简单类型时,此时需找到默认视图名
//5.测试返回值类型为 ResponseEntity 时,此时不走视图流程
//6.测试返回值类型为 HttpHeaders时,此时不走视图流程
//7.测试返回值添加了 @ResponseBody 注解时,此时不走视图流程
test4(context);
}
private static void test4(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test4");
Controller controller = new Controller();
Object returnValue = method.invoke(controller);//获取返回值
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
//给一个空的请求对象 webRequest
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/test4"); //模拟@RequestMapping("/test4")
//把路径(/test4)存入request域,后续如果没有找到视图名,就把test4当做视图名
UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request);
ServletWebRequest webRequest = new ServletWebRequest(request,new MockHttpServletResponse());
//检查是否支持此类型的返回值
if (composite.supportsReturnType(handlerMethod.getReturnType())) {
//给一个空的请求对象 webRequest
composite.handleReturnValue(returnValue, handlerMethod.getReturnType(), container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
//调用渲染视图方法
renderView(context,container,webRequest);
}
}
public User test4() {
log.info("test4()");
return new User("王五", 30);
}
HttpEntityMethodProcessor(List.of(new MappingJackson2HttpMessageConverter()))
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
//1.测试返回值类型为 ModelAndView
//2.测试返回值类型为String 时候,把它当做视图名
//3.测试返回值添加了@ModelAttribute注解时,此时需找到默认视图名
//4.测试返回值不加@ModelAttribute注解时且返回非简单类型时,此时需找到默认视图名
//5.测试返回值类型为 ResponseEntity 时,此时不走视图流程
//6.测试返回值类型为 HttpHeaders时,此时不走视图流程
//7.测试返回值添加了 @ResponseBody 注解时,此时不走视图流程
test5(context);
}
private static void test5(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test5");
Controller controller = new Controller();
Object returnValue = method.invoke(controller);//获取返回值
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
//给一个空的请求对象 webRequest
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest webRequest = new ServletWebRequest(request, response);
//检查是否支持此类型的返回值
if (composite.supportsReturnType(handlerMethod.getReturnType())) {
//给一个空的请求对象 webRequest
composite.handleReturnValue(returnValue, handlerMethod.getReturnType(), container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
if(!container.isRequestHandled()){
//调用渲染视图方法
renderView(context,container,webRequest);
}else {
//通过RequestResponseBodyMethodProcessor(List.of(new
// MappingJackson2HttpMessageConverter()) 把返回值对象转换为json字符串,然后写入到response中
System.out.println(new String(response.getContentAsByteArray(), "UTF-8"));
}
}
}
public HttpEntity<User> test5() {
log.info("test5()");
return new HttpEntity<>(new User("赵六", 40));
}
new HttpHeadersReturnValueHandler()
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
//1.测试返回值类型为 ModelAndView
//2.测试返回值类型为String 时候,把它当做视图名
//3.测试返回值添加了@ModelAttribute注解时,此时需找到默认视图名
//4.测试返回值不加@ModelAttribute注解时且返回非简单类型时,此时需找到默认视图名
//5.测试返回值类型为 ResponseEntity 时,此时不走视图流程
//6.测试返回值类型为 HttpHeaders时,此时不走视图流程
//7.测试返回值添加了 @ResponseBody 注解时,此时不走视图流程
test6(context);
}
private static void test6(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test6");
Controller controller = new Controller();
Object returnValue = method.invoke(controller);//获取返回值
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
//给一个空的请求对象 webRequest
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest webRequest = new ServletWebRequest(request, response);
//检查是否支持此类型的返回值
if (composite.supportsReturnType(handlerMethod.getReturnType())) {
//给一个空的请求对象 webRequest
composite.handleReturnValue(returnValue, handlerMethod.getReturnType(), container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
if(!container.isRequestHandled()){
//调用渲染视图方法
renderView(context,container,webRequest);
}else {
for (String name : response.getHeaderNames()) {
System.out.println(name + ":" + response.getHeader(name));
}
}
}
}
public HttpHeaders test6() {
log.info("test6()");
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "text/html");
return headers;
}
RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter()): 返回值放入响应体,并转成json
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
//1.测试返回值类型为 ModelAndView
//2.测试返回值类型为String 时候,把它当做视图名
//3.测试返回值添加了@ModelAttribute注解时,此时需找到默认视图名
//4.测试返回值不加@ModelAttribute注解时且返回非简单类型时,此时需找到默认视图名
//5.测试返回值类型为 ResponseEntity 时,此时不走视图流程
//6.测试返回值类型为 HttpHeaders时,此时不走视图流程
//7.测试返回值添加了 @ResponseBody 注解时,此时不走视图流程
test7(context);
}
private static void test7(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test7");
Controller controller = new Controller();
Object returnValue = method.invoke(controller);//获取返回值
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
//给一个空的请求对象 webRequest
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest webRequest = new ServletWebRequest(request, response);
//检查是否支持此类型的返回值
if (composite.supportsReturnType(handlerMethod.getReturnType())) {
//给一个空的请求对象 webRequest
composite.handleReturnValue(returnValue, handlerMethod.getReturnType(), container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
if(!container.isRequestHandled()){
//调用渲染视图方法
renderView(context,container,webRequest);
}else {
for (String name : response.getHeaderNames()) {
System.out.println(name + ":" + response.getHeader(name));
}
System.out.println(new String(response.getContentAsByteArray(), "UTF-8"));
}
}
}
@ResponseBody //返回的结果会做为响应体
public User test7() {
log.info("test7()");
return new User("钱七", 50);
}
收获💡
- 常见的返回值处理器
- ModelAndView,分别获取其模型和视图名,放入 ModelAndViewContainer
- 返回值类型为 String 时,把它当做视图名,放入 ModelAndViewContainer
- 返回值添加了 @ModelAttribute 注解时,将返回值作为模型,放入 ModelAndViewContainer
- 此时需找到默认视图名
- 返回值省略 @ModelAttribute 注解且返回非简单类型时,将返回值作为模型,放入 ModelAndViewContainer
- 此时需找到默认视图名
- 返回值类型为 ResponseEntity 时
- 此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true
- 返回值类型为 HttpHeaders 时
- 会设置 ModelAndViewContainer.requestHandled 为 true
- 返回值添加了 @ResponseBody 注解时
- 此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true
- 组合模式在 Spring 中的体现 + 1
1.9 MessageConverter
演示 - MessageConverter 的作用
public class A28 {
public static void main(String[] args) throws Exception {
test1();
}
public static void test1() throws Exception {
MockHttpOutputMessage message = new MockHttpOutputMessage();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
if (converter.canWrite(User.class, MediaType.APPLICATION_JSON)) {//判断是否支持转换json格式
converter.write(new User("zhangsan", 18), MediaType.APPLICATION_JSON, message);//最后转换的结果放入message中
System.out.println(message.getBodyAsString());
}
}
public static class User {
private String name;
private int age;
@JsonCreator
public User(@JsonProperty("name") String name, @JsonProperty("age") int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
public static void main(String[] args) throws Exception {
test2();
}
public static void test2() throws Exception {
MockHttpOutputMessage message = new MockHttpOutputMessage();
MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter();
if (converter.canWrite(User.class, MediaType.APPLICATION_XML)) {
converter.write(new User("lisi", 20), MediaType.APPLICATION_XML, message);//最后转换的结果放入message中
System.out.println(message.getBodyAsString());
}
}
public static void main(String[] args) throws Exception {
test3();
}
private static void test3() throws Exception {
MockHttpInputMessage message = new MockHttpInputMessage("""
{"name":"李四","age":20}""".getBytes(StandardCharsets.UTF_8));
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
if (converter.canRead(User.class, MediaType.APPLICATION_JSON)) {
Object read = converter.read(User.class, message);
System.out.println(read);
}
}
public static void main(String[] args) throws Exception {
test4();
}
private static void test4() throws NoSuchMethodException, HttpMediaTypeNotAcceptableException, IOException {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest webRequest = new ServletWebRequest(request, response);
/**
* 优先级
* 第1:response.setContentType("application/json");
* 第2:request.addHeader("Accept","application/xml");
* 第3:根据RequestResponseBodyMethodProcessor(List.of 的添加顺序,依次从高到低
*/
request.addHeader("Accept","application/xml");
response.setContentType("application/json");
/**
* @ResponseBody是由RequestResponseBodyMethodProcessor解析的
*/
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(List.of(
new MappingJackson2HttpMessageConverter(), new MappingJackson2XmlHttpMessageConverter()
));
processor.handleReturnValue(new User("zhangsan", 18),
new MethodParameter(A28.class.getMethod("user"),-1),
new ModelAndViewContainer(),
webRequest);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
@ResponseBody
//produces 控制响应的内容类型 == response.setContentType("application/json");
@RequestMapping(produces = "application/json")
public User user(){
return null;
}
收获💡
- MessageConverter 的作用
- @ResponseBody 是返回值处理器解析的
- 但具体转换工作是 MessageConverter 做的
- 如何选择 MediaType
- 首先看 @RequestMapping 上有没有指定
- 其次看 request 的 Accept 头有没有指定
- 最后按 MessageConverter 的顺序, 谁能谁先转换
1.10 @ControllerAdvice 之 ResponseBodyAdvice
演示 - ResponseBodyAdvice 增强
ResponseBodyAdvice 增强 在整个 HandlerAdapter 调用过程中所处的位置
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result {
private int code;
private String msg;
private Object data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
@JsonCreator
private Result(@JsonProperty("code") int code, @JsonProperty("data") Object data) {
this.code = code;
this.data = data;
}
private Result(int code, String msg) {
this.code = code;
this.msg = msg;
}
public static Result ok() {
return new Result(200, null);
}
public static Result ok(Object data) {
return new Result(200, data);
}
public static Result error(String msg) {
return new Result(500, "服务器内部错误:" + msg);
}
}
A29
public class A29 {
// {"name":"王五","age":18}
// {"code":xx,"msg":xx,data: {"name":"王五","age":18}}
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
context.getBean(WebConfig.MyController.class),
WebConfig.MyController.class.getMethod("user")
);
handlerMethod.setDataBinderFactory(new ServletRequestDataBinderFactory(Collections.emptyList(), null));
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
handlerMethod.setHandlerMethodReturnValueHandlers(getReturnValueHandlers(context));
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ModelAndViewContainer container = new ModelAndViewContainer();
handlerMethod.invokeAndHandle(new ServletWebRequest(request, response), container);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
/*
学到了什么
a. advice 之三, ResponseBodyAdvice 返回响应体前包装
*/
}
public static HandlerMethodArgumentResolverComposite getArgumentResolvers(AnnotationConfigApplicationContext context) {
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), false),
new PathVariableMethodArgumentResolver(),
new RequestHeaderMethodArgumentResolver(context.getDefaultListableBeanFactory()),
new ServletCookieValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
new ExpressionValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
new ServletRequestMethodArgumentResolver(),
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true),
new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), true)
);
return composite;
}
public static HandlerMethodReturnValueHandlerComposite getReturnValueHandlers(AnnotationConfigApplicationContext context) {
// 添加 advice
List<ControllerAdviceBean> annotatedBeans = ControllerAdviceBean.findAnnotatedBeans(context);
List<Object> collect = annotatedBeans.stream().filter(b -> ResponseBodyAdvice.class.isAssignableFrom(b.getBeanType()))
.collect(Collectors.toList());
HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
composite.addHandler(new ModelAndViewMethodReturnValueHandler());
composite.addHandler(new ViewNameMethodReturnValueHandler());
composite.addHandler(new ServletModelAttributeMethodProcessor(false));
composite.addHandler(new HttpEntityMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())));
composite.addHandler(new HttpHeadersReturnValueHandler());
composite.addHandler(new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter()), collect));
composite.addHandler(new ServletModelAttributeMethodProcessor(true));
return composite;
}
}
WebConfig
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice implements ResponseBodyAdvice<Object> {
//满足条件才转换
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if (returnType.getMethodAnnotation(ResponseBody.class) != null ||
//这个方法可以找到外层以及向内层查找
AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null ){
//returnType.getContainingClass().isAnnotationPresent(ResponseBody.class)) {//所在类上是否有注解(只能找到外层)
return true;
}
return false;
}
// 将 User 或其他类型统一为 Result 类型
/**
* Object body: 将返回结果User对象放入
* MethodParameter returnType: 返回值类型,所有方法,方法上的注解信息
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.ok(body);
}
}
/**
* @RequestBody + @ResponseBody = @RestController
* @RestController 中包含了 @ResponseBody,但是直接使用
* returnType.getContainingClass().isAnnotationPresent(ResponseBody.class)) 只能找到外层的,必须使用
* spring提供的方法
*/
//@Controller
//@ResponseBody 加到类上表示类中所有方法返回值都会加入到响应体中
@RestController
public static class MyController {
//@ResponseBody
public User user() {
return new User("王五", 18);
}
}
public static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
}
收获💡
- ResponseBodyAdvice 返回响应体前包装
1.11 异常解析器
演示 - ExceptionHandlerExceptionResolver
测试json
public class A30 {
public static void main(String[] args) throws NoSuchMethodException, UnsupportedEncodingException {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
resolver.afterPropertiesSet();
//测试json
/**
* 执行流程:
* 根据handlerMethod找到对应的Controller1类,知道异常是在哪个类中出现的。如果进一步找到Controller1类中是否有
* 对应的@ExceptionHandler标注的方法,如果找到把方法对应的参数类型ArithmeticException和实际的异常类型对比,如果能够对应上
* 则表示这个方法可以处理此异常。接着resolver.resolveException会反射调用Map<String, Object> handle(ArithmeticException e)方法
* 调用的时候把传入的参数用参数解析器解析,把返回的结果根据例如@ResponseBody进行解析,得到最后的结果(这个地方用的是@ResponseBody注解,
* 同时resolver设置了MappingJackson2HttpMessageConverter,所以最后的结果是json)
*/
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
Exception exception = new ArithmeticException("被零除");
resolver.resolveException(request, response, handlerMethod, exception);
//验证结果
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
static class Controller1 {
public void foo() {
}
@ExceptionHandler
@ResponseBody
public Map<String, Object> handle(ArithmeticException e) {
return Map.of("error", e.getMessage());
}
}
测试mav
public static void main(String[] args) throws NoSuchMethodException, UnsupportedEncodingException {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
resolver.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
//测试 mav
HandlerMethod handlerMethod = new HandlerMethod(new Controller2(), Controller2.class.getMethod("foo"));
Exception exception = new ArithmeticException("被零除");
ModelAndView mav = resolver.resolveException(request, response, handlerMethod, exception);
System.out.println(mav.getModel());
System.out.println(mav.getViewName());
}
static class Controller2 {
public void foo() {
}
@ExceptionHandler
public ModelAndView handle(ArithmeticException e) {
return new ModelAndView("test2", Map.of("error", e.getMessage()));
}
}
测试嵌套异常
//3.测试嵌套异常
HandlerMethod handlerMethod = new HandlerMethod(new Controller3(), Controller3.class.getMethod("foo"));
Exception e = new Exception("e1", new RuntimeException("e2", new IOException("e3")));
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
static class Controller3 {
public void foo() {
}
@ExceptionHandler
@ResponseBody
//把所有嵌套的异常依次放到集合中,然后进行匹配
public Map<String, Object> handle(IOException e3) {
return Map.of("error", e3.getMessage());
}
}
测试异常处理方法参数解析
//4.测试异常处理方法参数解析
HandlerMethod handlerMethod = new HandlerMethod(new Controller4(), Controller4.class.getMethod("foo"));
Exception e = new Exception("e1");
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
/**
* 学到了什么
* a. ExceptionHandlerExceptionResolver 能够重用参数解析器、返回值处理器,实现组件重用
* b. 能够支持嵌套异常
*/
static class Controller4 {
public void foo() {}
@ExceptionHandler
@ResponseBody
public Map<String, Object> handler(Exception e, HttpServletRequest request) {
System.out.println(request);
return Map.of("error", e.getMessage());
}
}
收获💡
- 它能够重用参数解析器、返回值处理器,实现组件重用
- 它能够支持嵌套异常
1.12 @ControllerAdvice 之 @ExceptionHandler
演示 - 准备 @ExceptionHandler
WebConfig
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice {
@ExceptionHandler
@ResponseBody
public Map<String, Object> handle(Exception e) {
return Map.of("error", e.getMessage());
}
}
@Bean
public ExceptionHandlerExceptionResolver resolver() {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
return resolver;
}
}
public class A31 {
public static void main(String[] args) throws NoSuchMethodException {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
/*ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
resolver.afterPropertiesSet();*/
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
ExceptionHandlerExceptionResolver resolver = context.getBean(ExceptionHandlerExceptionResolver.class);
/**
* 执行流程:
* 优先从Controller5中找到@ExceptionHandler标注的方法,如果没有,再从所有的@ControllerAdvice标注的类中
* 去找,@ExceptionHandler标注的方法
* ExceptionHandlerExceptionResolver寻找@ControllerAdvice标注@ExceptionHandler标注的方法时机:截图
*/
HandlerMethod handlerMethod = new HandlerMethod(new Controller5(), Controller5.class.getMethod("foo"));
Exception e = new Exception("e1");
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
static class Controller5 {
public void foo() {
}
}
}
收获💡
- ExceptionHandlerExceptionResolver 初始化时会解析 @ControllerAdvice 中的 @ExceptionHandler 方法
- ExceptionHandlerExceptionResolver 会以类为单位,在该类首次处理异常时,解析此类的 @ExceptionHandler 方法
- 以上两种 @ExceptionHandler 的解析结果都会缓存来避免重复解析
1.13 Tomcat 异常处理
-
我们知道 @ExceptionHandler 只能处理发生在 mvc 流程中的异常,例如控制器内、拦截器内,那么如果是 Filter 出现了异常,如何进行处理呢?
-
在 Spring Boot 中,是这么实现的:
- 因为内嵌了 Tomcat 容器,因此可以配置 Tomcat 的错误页面,Filter 与 错误页面之间是通过请求转发跳转的,可以在这里做手脚
- 先通过 ErrorPageRegistrarBeanPostProcessor 这个后处理器配置错误页面地址,默认为
/error
也可以通过${server.error.path}
进行配置 - 当 Filter 发生异常时,不会走 Spring 流程,但会走 Tomcat 的错误处理,于是就希望转发至
/error
这个地址- 当然,如果没有 @ExceptionHandler,那么最终也会走到 Tomcat 的错误处理
- Spring Boot 又提供了一个 BasicErrorController,它就是一个标准 @Controller,@RequestMapping 配置为
/error
,所以处理异常的职责就又回到了 Spring - 异常信息由于会被 Tomcat 放入 request 作用域,因此 BasicErrorController 里也能获取到
- 具体异常信息会由 DefaultErrorAttributes 封装好
- BasicErrorController 通过 Accept 头判断需要生成哪种 MediaType 的响应
- 如果要的不是 text/html,走 MessageConverter 流程
- 如果需要 text/html,走 mvc 流程,此时又分两种情况
- 配置了 ErrorViewResolver,根据状态码去找 View
- 没配置或没找到,用 BeanNameViewResolver 根据一个固定为 error 的名字找到 View,即所谓的 WhitelabelErrorView
评价
- 一个错误处理搞得这么复杂,就问恶心不?
如果没有全局异常处理,默认tomcat处理web异常
WebConfig
@Configuration
public class WebConfig {
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
@Bean // @RequestMapping
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new RequestMappingHandlerMapping();
}
@Bean // 注意默认的 RequestMappingHandlerAdapter 不会带 jackson 转换器
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
handlerAdapter.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
return handlerAdapter;
}
@Controller
public static class MyController {
@RequestMapping("test")
public ModelAndView test() {
int i = 1 / 0;
return null;
}
}
}
public class A32 {
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
handlerMapping.getHandlerMethods().forEach((RequestMappingInfo k, HandlerMethod v) ->{
System.out.println("映射路径: "+ k + " 方法: "+ v);
});
}
}
演示1 - 错误页处理
WebConfig
@Configuration
public class WebConfig {
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
@Bean // @RequestMapping
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new RequestMappingHandlerMapping();
}
@Bean // 注意默认的 RequestMappingHandlerAdapter 不会带 jackson 转换器
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
handlerAdapter.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
return handlerAdapter;
}
@Bean //改动tomcat 默认错误地址
public ErrorPageRegistrar errorPageRegistrar(){
return new ErrorPageRegistrar() {
@Override
public void registerErrorPages(ErrorPageRegistry webServerFactory) {
//当tomcat出现500错误时,跳转到/error页面或者是error路径
//出现错误,会使用请求转发 forward 跳转到 error 地址
webServerFactory.addErrorPages(new ErrorPage("/error"));
}
};
}
//因为errorPageRegistrar不会主动增加错误页面
//方法作用:再创建好WebServerFactory对象之后,初始化之前,执行该方法,
// 到容器中找到所有的ErrorPageRegistrar,给webServerFactory对象添加所有的ErrorPage
@Bean
public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor(){
return new ErrorPageRegistrarBeanPostProcessor();
}
@Controller
public static class MyController {
@RequestMapping("test")
public ModelAndView test() {
int i = 1 / 0;
return null;
}
@RequestMapping("/error")
@ResponseBody //以json格式返回
public Map<String,Object> error(HttpServletRequest request) { //tomcat发生异常,会把异常信息放入request作用域中
Throwable e = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
return Map.of("error", e.getMessage());
}
}
}
收获💡
- Tomcat 的错误页处理手段
演示2 - BasicErrorController
WebConfig
@Controller
public static class MyController {
@RequestMapping("test")
public ModelAndView test() {
int i = 1 / 0;
return null;
}
/*@RequestMapping("/error")
@ResponseBody //以json格式返回
public Map<String,Object> error(HttpServletRequest request) { //tomcat发生异常,会把异常信息放入request作用域中
Throwable e = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
return Map.of("error", e.getMessage());
}*/
}
@Bean
public BasicErrorController basicErrorController(){
return new BasicErrorController(new DefaultErrorAttributes(),new ErrorProperties());
}
@Bean
public View error(){
return new View() {
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println(model);
response.setContentType("text/html;charset=utf-8");
response.getWriter().print("""
<h1>服务器内部错误</h1>
""");
}
};
}
//需要按照error名称找到对应Bean的View对象,需要一个View的解析器
@Bean
public ViewResolver viewResolver(){
//只要你的类型是View类型,根据bean的名字error,对应到我们需要的视图名上,没有这个解析器,找不到error对应的view
//这个ViewResolver将来会交给DispatcherServlet,DispatcherServlet会调用解析器,根据视图名找到视图对象,然后渲染
return new BeanNameViewResolver();
}
}
如果是用浏览器访问test,需要额外处理
收获💡
- Spring Boot 中 BasicErrorController 如何工作
1.14 BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter
演示 - 本组映射器和适配器
WebConfig
@Configuration
public class WebConfig {
@Bean // ⬅️内嵌 web 容器工厂
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}
@Bean // ⬅️创建 DispatcherServlet
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
// /c1(请求) 找到--> /c1(Bean的名字带 /)
// /c2 --> /c2
@Bean //做路径的请求
public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {
return new BeanNameUrlHandlerMapping();
}
@Bean //调用请求(调用handleRequest方法,返回结果)
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter();
}
@Component("/c1")
public static class Controller1 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("this is c1");
return null;
}
}
@Component("/c2")
public static class Controller2 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("this is c2");
return null;
}
}
@Bean("/c3")
public Controller controller3() {
return (request, response) -> {
response.getWriter().print("this is c3");
return null;
};
}
}
public class A33 {
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}
}
自定义HandlerMapping和HandleAdapter
WebConfig
@Configuration
public class WebConfig {
@Bean // ⬅️内嵌 web 容器工厂
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}
@Bean // ⬅️创建 DispatcherServlet
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
// /c1(请求) 找到--> /c1(Bean的名字带 /)
// /c2 --> /c2
@Component
static class MyHandlerMapping implements HandlerMapping{
@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
String key = request.getRequestURI(); //只返回路径部分(URI)
Controller controller = collect.get(key);
if (controller == null) {
return null; //返回null, DispatcherServlet会抛出404
}
return new HandlerExecutionChain(controller);
}
@Autowired //依赖注入在前
private ApplicationContext context;
private Map<String ,Controller> collect;
@PostConstruct //初始化在后
public void init(){
//都实现了Controller接口
Map<String, Controller> map = context.getBeansOfType(Controller.class);
//⬇️过滤出Bean名字带 / 的
collect = map.entrySet().stream()
.filter(entry -> entry.getKey().startsWith("/"))
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
}
}
@Component
static class MyHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof Controller; //只能处理实现了Controller接口的对象
}
@Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//handler 是 Controller接口的实现类,直接转换成 controller
if (handler instanceof Controller controller) {
controller.handleRequest(request, response);
}
return null; //返回null, 不会走ModelAndView视图渲染流程
}
@Override
public long getLastModified(HttpServletRequest request, Object handler) {
return -1;
}
}
@Component("/c1")
public static class Controller1 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("this is c1");
return null;
}
}
@Component("/c2")
public static class Controller2 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("this is c2");
return null;
}
}
@Bean("/c3")
public Controller controller3() {
return (request, response) -> {
response.getWriter().print("this is c3");
return null;
};
}
}
public class A33 {
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}
}
收获💡
- BeanNameUrlHandlerMapping,以 / 开头的 bean 的名字会被当作映射路径
- 这些 bean 本身当作 handler,要求实现 Controller 接口
- SimpleControllerHandlerAdapter,调用 handler
- 模拟实现这组映射器和适配器
1.15 RouterFunctionMapping 与 HandlerFunctionAdapter
演示 - 本组映射器和适配器
public class A34 {
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context
= new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
/*
学到了什么
函数式控制器
a. RouterFunctionMapping, 通过 RequestPredicate 映射
b. handler 要实现 HandlerFunction 接口
c. HandlerFunctionAdapter, 调用 handler
对比
a. RequestMappingHandlerMapping, 以 @RequestMapping 作为映射路径
b. 控制器的具体方法会被当作 handler
c. RequestMappingHandlerAdapter, 调用 handler
*/
}
}
WebConfig
@Configuration
public class WebConfig {
@Bean // ⬅️内嵌 web 容器工厂
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}
@Bean // ⬅️创建 DispatcherServlet
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
@Bean
public RouterFunctionMapping routerFunctionMapping() {
/**RouterFunctionMapping作用:
* 初始化时候,在容器中找到所有的RouterFunction,然后记录对应的信息
* route(GET("/r1"), request -> ok().body("this is r1")); route(GET("/r2"), request -> ok().body("this is r2"));
* 浏览器发起请求GET("/r1"),根据记录的信息,找到处理器对象request -> ok().body("this is r1")),把处理器对象
* 交给HandlerFunctionAdapter进行调用。HandlerFunctionAdapter把请求对象request准备好,执行ok().body("this is r1")),
* 得到一个响应,最后HandlerFunctionAdapter把这个响应返回给浏览器
*
*/
return new RouterFunctionMapping();
}
@Bean
public HandlerFunctionAdapter handlerFunctionAdapter() {
return new HandlerFunctionAdapter();
}
@Bean
public RouterFunction<ServerResponse> r1() {
//r1(请求)的GET请求 --> 执行后面的的方法 ---> 返回ServerResponse(状态是:ok 200,响应体是"this is r1")
return route(GET("/r1"), request -> ok().body("this is r1"));
}
@Bean
public RouterFunction<ServerResponse> r2() {
return route(GET("/r2"), request -> ok().body("this is r2"));
}
}
收获💡
- RouterFunctionMapping, 通过 RequestPredicate 条件映射
- handler 要实现 HandlerFunction 接口
- HandlerFunctionAdapter, 调用 handler
1.16 SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter
演示1 - 本组映射器和适配器
public class A35 {
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context
= new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}
}
WebConfig
@Configuration
public class WebConfig {
@Bean // ⬅️内嵌 web 容器工厂
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}
@Bean // ⬅️创建 DispatcherServlet
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ApplicationContext context) {
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
/**
* SimpleUrlHandlerMapping没有收集映射(offAndSet),需要手动处理
*/
//map -> k: bean的名字/**,/img/** v -> 对应的处理器方法
Map<String, ResourceHttpRequestHandler> map = context.getBeansOfType(ResourceHttpRequestHandler.class);
handlerMapping.setUrlMap(map);
System.out.println(map);
//将来请求来了,就到map集合中去找映射关系 例如请求 /index.html --> /** /img/1.jpg --> /img/**
//然后根据处理器方法找到类路径下的静态资源
return handlerMapping;
}
@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
//根据找到的映射路径找到对应的handler1,HttpRequestHandlerAdapter调用这个handler1,找到静态资源,然后返回给浏览器
return new HttpRequestHandlerAdapter();
}
/**
* /index.html
* /r1.html ---> /**
* /r2.html
*/
@Bean("/**")
public ResourceHttpRequestHandler handler1(){
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("static/")));
return handler;
}
/**
* /img/1.jpg
* /img/2.jpg ---> /img/**
* /img/3.jpg
*/
@Bean("/img/**")
public ResourceHttpRequestHandler handler2(){
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("images/")));
return handler;
}
}
收获💡
- SimpleUrlHandlerMapping 不会在初始化时收集映射信息,需要手动收集
- SimpleUrlHandlerMapping 映射路径
- ResourceHttpRequestHandler 作为静态资源 handler
- HttpRequestHandlerAdapter, 调用此 handler
演示2 - 静态资源解析优化
收获💡
- 责任链模式体现
- 压缩文件需要手动生成
演示3 - 欢迎页
A35
public class A35 {
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context
= new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}
}
WebConfig
@Configuration
public class WebConfig {
@Bean // ⬅️内嵌 web 容器工厂
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}
@Bean // ⬅️创建 DispatcherServlet
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ApplicationContext context) {
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
/**
* SimpleUrlHandlerMapping没有收集映射(offAndSet),需要手动处理
*/
//map -> k: bean的名字/**,/img/** v -> 对应的处理器方法
Map<String, ResourceHttpRequestHandler> map = context.getBeansOfType(ResourceHttpRequestHandler.class);
handlerMapping.setUrlMap(map);
System.out.println(map);
//将来请求来了,就到map集合中去找映射关系 例如请求 /index.html --> /** /img/1.jpg --> /img/**
//然后根据处理器方法找到类路径下的静态资源
return handlerMapping;
}
@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
//根据找到的映射路径找到对应的handler1,HttpRequestHandlerAdapter调用这个handler1,找到静态资源,然后返回给浏览器
return new HttpRequestHandlerAdapter();
}
/**
* /index.html
* /r1.html ---> /**
* /r2.html
*/
@Bean("/**")
public ResourceHttpRequestHandler handler1(){
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("static/")));
handler.setResourceResolvers(List.of(
//缓存资源,如果没有通过PathResourceResolver找到资源,就会缓存起来,下次从缓存中读取
new CachingResourceResolver(new ConcurrentMapCache("cache1")),
new EncodedResourceResolver(),//读压缩资源
new PathResourceResolver() //基本的资源解析器,到磁盘上去读资源
));
return handler;
}
/**
* /img/1.jpg
* /img/2.jpg ---> /img/**
* /img/3.jpg
*/
@Bean("/img/**")
public ResourceHttpRequestHandler handler2(){
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("images/")));
return handler;
}
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context) {
Resource resource = context.getResource("classpath:static/index.html");
/**
* 参数1:和动态欢迎页相关,不需要
*/
return new WelcomePageHandlerMapping(null, context, resource, "/**");
// Controller 接口
}
/**
* 适配器和其他的映射器配置使用
* @return
*/
@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter(){
return new SimpleControllerHandlerAdapter();
}
}
收获💡
- 欢迎页支持静态欢迎页与动态欢迎页
- WelcomePageHandlerMapping 映射欢迎页(即只映射 '/')
- 它内置的 handler ParameterizableViewController 作用是不执行逻辑,仅根据视图名找视图
- 视图名固定为 forward:index.html
- SimpleControllerHandlerAdapter, 调用 handler
- 转发至 /index.html
- 处理 /index.html 又会走上面的静态资源处理流程
映射器与适配器小结
- HandlerMapping 负责建立请求与控制器之间的映射关系
- RequestMappingHandlerMapping (与 @RequestMapping 匹配)
- WelcomePageHandlerMapping (/)
- BeanNameUrlHandlerMapping (与 bean 的名字匹配 以 / 开头)
- RouterFunctionMapping (函数式 RequestPredicate, HandlerFunction)
- SimpleUrlHandlerMapping (静态资源 通配符 /** /img/**)
- 之间也会有顺序问题, boot 中默认顺序如上
- HandlerAdapter 负责实现对各种各样的 handler 的适配调用
- RequestMappingHandlerAdapter 处理:@RequestMapping 方法
- 参数解析器、返回值处理器体现了组合模式
- SimpleControllerHandlerAdapter 处理:Controller 接口
- HandlerFunctionAdapter 处理:HandlerFunction 函数式接口
- HttpRequestHandlerAdapter 处理:HttpRequestHandler 接口 (静态资源处理)
- 这也是典型适配器模式体现
- RequestMappingHandlerAdapter 处理:@RequestMapping 方法
1.17 mvc 处理流程
当浏览器发送一个请求 http://localhost:8080/hello
后,请求到达服务器,其处理流程是:
-
服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术
- 路径:默认映射路径为
/
,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器- jsp 不会匹配到 DispatcherServlet
- 其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet
- 创建:在 Boot 中,由 DispatcherServletAutoConfiguration 这个自动配置类提供 DispatcherServlet 的 bean
- 初始化:DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量
- HandlerMapping,初始化时记录映射关系
- HandlerAdapter,初始化时准备参数解析器、返回值处理器、消息转换器
- HandlerExceptionResolver,初始化时准备参数解析器、返回值处理器、消息转换器
- ViewResolver
- 路径:默认映射路径为
-
DispatcherServlet 会利用 RequestMappingHandlerMapping 查找控制器方法
-
例如根据 /hello 路径找到 @RequestMapping("/hello") 对应的控制器方法
-
控制器方法会被封装为 HandlerMethod 对象,并结合匹配到的拦截器一起返回给 DispatcherServlet
-
HandlerMethod 和拦截器合在一起称为 HandlerExecutionChain(调用链)对象
-
-
DispatcherServlet 接下来会:
- 调用拦截器的 preHandle 方法
- RequestMappingHandlerAdapter 调用 handle 方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod
- @ControllerAdvice 全局增强点1️⃣:补充模型数据
- @ControllerAdvice 全局增强点2️⃣:补充自定义类型转换器
- 使用 HandlerMethodArgumentResolver 准备参数
- @ControllerAdvice 全局增强点3️⃣:RequestBody 增强
- 调用 ServletInvocableHandlerMethod
- 使用 HandlerMethodReturnValueHandler 处理返回值
- @ControllerAdvice 全局增强点4️⃣:ResponseBody 增强
- 根据 ModelAndViewContainer 获取 ModelAndView
- 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程
- 例如,有的返回值处理器调用了 HttpMessageConverter 来将结果转换为 JSON,这时 ModelAndView 就为 null
- 如果返回的 ModelAndView 不为 null,会在第 4 步走视图解析及渲染流程
- 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程
- 调用拦截器的 postHandle 方法
- 处理异常或视图渲染
- 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
- @ControllerAdvice 全局增强点5️⃣:@ExceptionHandler 异常处理
- 正常,走视图解析及渲染流程
- 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
- 调用拦截器的 afterCompletion 方法