Spring MVC注解Controller源码流程解析--HandlerAdapter执行流程--上
- 引言
- RequestMappingHandlerAdapter
- 方法参数解析器
- 方法参数名解析器
- 类型转换体系
- 简单的使用演示
- 数据绑定器工厂
- 定制化修改DataBinder
- 获取泛型参数
- @ControllerAdvice与@InitBinder注解
- 控制器方法执行流程
- @ControllerAdvice 之 @ModelAttribute
- 返回值处理器
- 小结
引言
前面已经详细介绍过了RequestMappingHandlerMapping是如何在初始化方法中搜集容器中所有标注了@Controller或者@RequestMapping注解的Bean的,然后解析将映射关系保存到映射中心。
在请求到来时,通过request请求对象提供的信息,去注册中心匹配获取到合适的,分别经过精确匹配或者模糊匹配,然后再进行最佳匹配,最终返回一个匹配上的HandlerMethod,交给父类AbstractHandlerMapping包装为HandlerExecutionChain,内部添加合适的拦截器。
无论是URL的精确匹配还是模糊匹配,最终都要交给对应的HandlerMethod持有的RequestMappingInfo,进行条件匹配,如果不满足,则返回null。
如果没有HandlerMethod能够处理当前请求,那么再判断是否部分匹配,即请求路径匹配上了,但是不满足RequestMappingInfo中其他限制条件,如请求头限制等,此时检查不满足条件的请求,抛出对应的异常。如果是URL没有匹配上,则返回给AbstractHandlerMapping的handler结果为null,最终在doDispatch方法中的noHandlerFound方法中抛出404异常。
Spring MVC注解Controller源码流程解析–映射建立
Spring MVC注解Controller源码流程解析–定位HandlerMethod
Spring MVC注解Controller源码流程解析—请求匹配中的容错处理
本文将对RequestMappingHandlerAdapter如何调用执行RequestMappingHandlerMapping返回的handler,即HandlerMethod的过程做出详细分析。
RequestMappingHandlerAdapter
RequestMappingHandlerAdapter调用执行HandlerMethod方法的过程比较复杂,这里先对RequestMappingHandlerAdapter调用执行HandlerMethod方法中涉及到的组件和思路进行讲解,最终再走一遍源码,大家就会非常清晰了。
对于RequestMappingHandlerAdapter来说,它的主要职责有以几个:
- 解析控制器方法的参数列表,并从request请求对象中获取到相关参数值,并保存起来 (这个过程还涉及到参数类型转换问题,需要求助Spirng提供的类型转换模块支持)
- 反射执行控制器方法,将先前准备好的参数值列表传入
- 控制器方法执行完毕后,处理返回结果,并将返回结果统一转换为ModelAndView,供SpringMVC后续视图渲染组件使用
方法参数解析器
对于控制器方法参数解析而言,由于Spring支持多种注解形式来提示从哪里获取参数值,参数key是什么等等,因此如果使用一个参数解析器完成所有注解的解析,那么就成狗屎代码了,因此Spring采用一个参数解析器负责解析一个注解的形式:
spring常见的方法参数解析器有:
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
我们经常在Controller代码中使用的形式有:
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, //cookie中获取参数值
@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 +
'}';
}
}
参数方法解析器单独使用案例:
- 准备controller控制器对象-上面给出的
- 准备mockRequest
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
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();
//多个解析器组合--组合模式体系
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// false 表示必须有 @RequestParam
new RequestParamMethodArgumentResolver(beanFactory, false),
new PathVariableMethodArgumentResolver(),
new RequestHeaderMethodArgumentResolver(beanFactory),
new ServletCookieValueMethodArgumentResolver(beanFactory),
new ExpressionValueMethodArgumentResolver(beanFactory),
new ServletRequestMethodArgumentResolver(),
new ServletModelAttributeMethodProcessor(false), // 必须有 @ModelAttribute
new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true), // 省略了 @ModelAttribute
new RequestParamMethodArgumentResolver(beanFactory, true) // 省略 @RequestParam
);
// 要点4. 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
//获取当前方法参数上的注解名
String annotations = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining());
String str = annotations.length() > 0 ? " @" + annotations + " " : " ";
//设置好spring提供的方法参数名解析器
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
if (composite.supportsParameter(parameter)) {
// 支持此参数
Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), factory);
System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName() + "->" + v);
System.out.println("模型数据为:" + container.getModel());
} else {
System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName());
}
}
}
说明:
又添加了一遍@RequestParam和@ModelAttribute相关的注解解析器,并且设置对省略注解情况的参数解析,同时必须放在参数解析器列表的末尾。
参数解析器列表挑选的原理是找到第一个能够support支持处理当前参数的,然后直接返回该参数处理器进行解析处理。
如果要添加自定义的参数解析器:
自定义参数解析器优先于默认参数解析器被调用。
参数解析器小结:
- 初步了解 RequestMappingHandlerAdapter 的调用过程
- 控制器方法被封装为 HandlerMethod
- 准备对象绑定与类型转换
- 准备 ModelAndViewContainer 用来存储中间 Model 结果
- 解析每个参数值
- 解析参数依赖的就是各种参数解析器,它们都有两个重要方法
- supportsParameter 判断是否支持方法参数
- resolveArgument 解析方法参数,返回参数值
- 常见参数的解析
- @RequestParam
- 省略 @RequestParam
- @RequestParam(defaultValue)
- MultipartFile
- @PathVariable
- @RequestHeader
- @CookieValue
- @Value
- HttpServletRequest 等
- @ModelAttribute
- 省略 @ModelAttribute
- @RequestBody
- 组合模式在 Spring 中的体现
- @RequestParam, @CookieValue 等注解中的参数名、默认值, 都可以写成活的, 即从 ${ } #{ }中获取
方法参数名解析器
正常情况下,java的class文件中是不会保存方法参数名相关信息的,如果要保留有以下两种方式:
- 编译时加上-parameters参数,此时javac编译器在编译时,会在class文件中生成对应的参数表,此时我们通过反射就可以直接拿到参数名
// 1. 反射获取参数名
Method foo = Bean2.class.getMethod("foo", String.class, int.class);
for (Parameter parameter : foo.getParameters()) {
System.out.println(parameter.getName());
}
- 如果编译时添加了 -g 可以生成调试信息, 但分为两种情况
1. 普通类, 会包含局部变量表, 用 asm 可以拿到参数名
2. 接口, 不会包含局部变量表, 无法获得参数名 (这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名)
// 2. 基于 LocalVariableTable 本地变量表--这里借助Spring提供的方法参数名解析器来操作asm解析局部变量表获取参数名
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(foo);
System.out.println(Arrays.toString(parameterNames));
大部分编译器在编译时都会添加-g参数
spring对以上两种方式解析获取参数名都提供了支持:
DefaultParameterNameDiscoverer 也是采用了组合模式,内部组合了多种ParameterNameDiscoverer实现类:
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {
public DefaultParameterNameDiscoverer() {
if (KotlinDetector.isKotlinReflectPresent() && !NativeDetector.inNativeImage()) {
addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
}
//默认添加了对两种参数获取方式的支持
addDiscoverer(new StandardReflectionParameterNameDiscoverer());
addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}
}
DefaultParameterNameDiscoverer内部会依次尝试先根据反射从方法参数表获取参数名,失败了再尝试从局部变量表获取参数名。
类型转换体系
由于历史遗留原因,Spring目前的类型转换体系结构分为了两套架构,一套是基于JDK提供的PropertyEditor接口实现的,一套是Spring自己单独开发的Converters体系。
底层第一套转换接口与实现:
- Printer 把其它类型转为 String
- Parser 把 String 转为其它类型
- Formatter 综合 Printer 与 Parser 功能
- Converter 把类型 S 转为类型 T
- Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合
- FormattingConversionService 内部维护一组Converters集合,用于对外其他类型转换服务
底层第二套转换接口:
- PropertyEditor 把 String 与其它类型相互转换
- PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
- 与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配
高层接口与实现:
- 它们都实现了 TypeConverter 这个高层转换接口,在转换时,会用到 TypeConverter Delegate 委派ConversionService 与 PropertyEditorRegistry 真正执行转换(Facade 门面模式)
- 首先看是否有自定义的propertyEditor类型转换器, @InitBinder 添加的即属于这种 (用了适配器模式把 Formatter 转为需要的 PropertyEditor)
- 再看有没有 ConversionService 转换
- 再利用默认的 PropertyEditor 转换
- 最后有一些特殊处理
- SimpleTypeConverter 仅做类型转换
- BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,走 Property – 调用对象的getter和setter方法完成赋值
- DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,走 Field — 反射调用字段完成赋值
- ServletRequestDataBinder 为 bean 的属性执行绑定,当需要时做类型转换,根据 directFieldAccess 选择走 Property 还是 Field,具备校验与获取校验结果功能 ,还有一点很关键,对象属性值来源于request对象
简单的使用演示
- SimpleTypeConverter: 仅支持简单的类型转换
public class TestSimpleConverter {
public static void main(String[] args) {
// 仅有类型转换的功能
SimpleTypeConverter typeConverter = new SimpleTypeConverter();
Integer number = typeConverter.convertIfNecessary("13", int.class);
Date date = typeConverter.convertIfNecessary("1999/03/04", Date.class);
System.out.println(number);
System.out.println(date);
}
}
- BeanWrapperImpl: 基于getter和setter方法完成属性赋值,如果不提供getter和setter方法,则会抛出异常
public class TestBeanWrapper {
public static void main(String[] args) {
// 利用反射原理, 为 bean 的属性赋值
MyBean target = new MyBean();
BeanWrapperImpl wrapper = new BeanWrapperImpl(target);
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;
//自行提供getter和setter方法
...
}
}
- DirectFieldAccessor: 基于反射获取字段设置值
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;
}
}
- DataBinder: 底层依靠BeanWrapperImpl和DirectFieldAccessor完成属性绑定和类型转换工作 , 默认采用BeanWrapperImpl
public class TestDataBinder {
public static void main(String[] args) {
// 执行数据绑定
MyBean target = new MyBean();
DataBinder dataBinder = new DataBinder(target);
//开启反射获取字段并设置值,默认采用getter或者setter方法
dataBinder.initDirectFieldAccess();
MutablePropertyValues pvs = new MutablePropertyValues();
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;
@Override
public String toString() {
return "MyBean{" +
"a=" + a +
", b='" + b + '\'' +
", c=" + c +
'}';
}
}
}
- ServletRequestDataBinder: 在dataBinder的基础上,增加了通过request对象获取属性值的功能
public class TestServletDataBinder {
public static void main(String[] args) {
// web 环境下数据绑定
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;
...
}
}
数据绑定器工厂
WebDataBinderFactory负责提供对Web环境下DataBinder的创建,即WebDataBinder ,Web环境下的DataBinder,用于绑定属性值的数据来源于Request请求对象中。
使用WebDataBinderFactory创建DataBinder的好处在于,我们可以利用工厂实现类提供的相关扩展回调来给用户提供定制化DataBinder的机会。
具体的扩展定制化DataBinder的方式有@InitBinder注解方式,WebBindingInitializer回调类方式。
为什么需要提供定制化DataBinder的接口,我们看下面这个场景:
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();
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(target);
}
public static class User {
private Date birthday;
private Address address;
//省略getter,setter方法
...
}
public static class Address {
private String name;
//省略getter,setter方法
...
}
}
我们期望的DataBinder在数据绑定过程中可以识别1999|01|02格式的日期字符串,并将其转换为Date类型,但是默认底层的类型转换器是无法识别这种类型的日期格式,因此最终赋值失败。
通过dataBinder可以获取到绑定的结果,绑定结果中会记录下绑定过程中出现的错误:
BindingResult bindingResult = dataBinder.getBindingResult();
System.out.println(bindingResult);
定制化修改DataBinder
要解决上面场景中提供的问题,我们需要使用WebDataBinderFactory给我们提供的定制化DataBinder的两种方式,添加或修改底层某个日期类型转换器,让其支持我们这种格式的日期类型转换:
- 使用@InitBinder注解: RequestMappingHandlerAdapter的初始化方法被调用时,会搜集容器中所有标注了@ControllerAdvice的Bean,然后解析其中标注了@InitBinder注解的方法,然后缓存起来,此种方式属于全局有效的定制化方式
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();
// "2. 用 @InitBinder 转换" PropertyEditorRegistry PropertyEditor
//我们手动封装一个标注了@InitBinder注解的方法
InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), null);
//createBinder方法中,再创建完DataBinder示例后,会分别调用WebBindingInitializer和InvocableHandlerMethod集合
//尝试对DataBinder进行定制化操作
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(target);
}
static class MyController {
@InitBinder
public void aaa(WebDataBinder dataBinder) {
// 扩展 dataBinder 的转换器
dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 方式扩展的"));
}
}
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);
}
}
- 通过提供一个DataBinderFactory内部的WebBindingInitializer实现,完成定制化修改操作
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();
// "3. 用 ConversionService 转换" ConversionService Formatter
FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能"));
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);
}
- 同时提供@InitBinder和WebBindingInitializer: WebBindingInitializer优先被调用,标注了@InitBinder注解的方法后被调用,后者会覆盖前者配置
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();
// "4. 同时加了 @InitBinder 和 ConversionService"
InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), initializer);
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(target);
}
4.通过@DateTimeFormat指定日期格式,使用spring提供的ApplicationConversionService,内置相关日期类型转换器来解析字段上的@DateTimeFormat注解
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();
// "5. 使用默认 ConversionService 转换"
ApplicationConversionService service = new ApplicationConversionService();
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);
}
获取泛型参数
public class TestGenericType {
public static void main(String[] args) {
// 小技巧
// 1. java api
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
Type type = TeacherDao.class.getGenericSuperclass();
System.out.println(type);
if (type instanceof ParameterizedType parameterizedType) {
System.out.println(parameterizedType.getActualTypeArguments()[0]);
}
// 2. spring api 1
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
Class<?> t = GenericTypeResolver.resolveTypeArgument(TeacherDao.class, BaseDao.class);
System.out.println(t);
// 3. spring api 2
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
System.out.println(ResolvableType.forClass(TeacherDao.class).getSuperType().getGeneric().resolve());
}
}
@ControllerAdvice与@InitBinder注解
InitBinderDataBinderFactory数据绑定器工厂中标注了@InitBinder的绑定器定制化方法从哪里获取?
@InitBinder 的来源有两个
-
@ControllerAdvice 中 @InitBinder 标注的方法,由 RequestMappingHandlerAdapter 在初始化时解析并缓存
-
@Controller 中 @InitBinder 标注的方法,由 RequestMappingHandlerAdapter 会在控制器方法首次执行时解析并缓存
我们可以通过RequestMappingHandlerAdapter设置WebBindingInitializer初始化绑定器,该初始化绑定器会应用到ServletRequestDataBinderFactory工厂中:
能应用到当前Controller上的BinderMethod方法搜集工作由RequestMappingHandlerAdapter的getDataBinderFactory负责完成:
RequestMappingHandlerAdapter会在初始化时,获取容器中所有标注了@ControllerAdvice的bean,然后解析获取其中标注了@InitBinder注解的方法,并缓存起来:
所以,如果我们想要在日常开发中注册一些自定义的类型转换器,可以这样做:
//注册的类型转换器,对所有controller生效
@ControllerAdvice
static class MyControllerAdvice {
@InitBinder
public void binder3(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder3 转换器"));
}
}
//注册的类型转换器,只对当前controller生效
@Controller
static class Controller1 {
@InitBinder
public void binder1(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder1 转换器"));
}
public void foo() {
}
}
小结:
- RequestMappingHandlerAdapter 在图中缩写为 HandlerAdapter
- HandlerMethodArgumentResolverComposite 在图中缩写为 ArgumentResolvers
- HandlerMethodReturnValueHandlerComposite 在图中缩写为 ReturnValueHandlers
收获💡:
- RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @InitBinder 方法
- RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @InitBinder 方法
- 以上两种 @InitBinder 的解析结果都会缓存来避免重复解析
- 控制器方法调用时,会综合利用本类的 @InitBinder 方法和 @ControllerAdvice 中的 @InitBinder 方法创建绑定工厂
控制器方法执行流程
HandlerMethod 需要
- bean 即是哪个 Controller
- method 即是 Controller 中的哪个方法
ServletInvocableHandlerMethod 需要
- WebDataBinderFactory 负责对象绑定、类型转换
- ParameterNameDiscoverer 负责参数名解析
- HandlerMethodArgumentResolverComposite 负责解析参数
- HandlerMethodReturnValueHandlerComposite 负责处理返回值
具体源码会在后面的文章讲述源码流程时,带领大家过一遍。
@ControllerAdvice 之 @ModelAttribute
@ModelAttribute注解的作用是向ModelAndViewContainer中添加模型数据,@ModelAttribute可以加在以下位置:
对于标注在全局或者局部位置的@ModelAttribute注解来说,RequestMappingHandlerAdapter搜集这些方法,也是为了调用他们然后将他们的返回结果添加进行ModelAndViewContainer中:
模型工厂主要负责初始化ModelAndViewContainer,也就是搜集全局和局部的标注了@ModelAttribute注解的方法,然后执行这些方法,获取返回结果,添加进ModelAndViewContainer:
搜集标注了@ModelAttribute注解方法的过程,和@InitBinder注解一样,分别从全局和局部两个范围进行搜索:
ModelFactory中的initModel核心方法为:
这里省略掉了很多细节问题,我们后面源码流程中会进行详细分析
实例演示:
- 准备一个配置类
@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("u") User user) {
System.out.println("foo");
return null;
}
}
static class User {
private String name;
//省略getter,setter
...
}
}
- 测试类-- 模拟RequestMappingHandler的handler方法执行流程
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "张三");
/*
现在可以通过 ServletInvocableHandlerMethod 把这些整合在一起, 并完成控制器方法的调用, 如下
*/
ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
new Controller1(), Controller1.class.getMethod("foo", User.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
handlerMethod.setDataBinderFactory(factory);
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
ModelAndViewContainer container = new ModelAndViewContainer();
// 获取模型工厂方法
Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
getModelFactory.setAccessible(true);
ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, factory);
// 初始化模型数据
modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);
//调用控制器方法
handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);
System.out.println(container.getModel());
context.close();
}
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;
}
可以看到ModelAndViewContainer中的数据来源于三个地方: @C
关于@ModelAttribute部分的小结:
准备 @ModelAttribute 在整个 HandlerAdapter 调用过程中所处的位置:
收获💡
- RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @ModelAttribute 方法
- RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @ModelAttribute 方法
- 以上两种 @ModelAttribute 的解析结果都会缓存来避免重复解析
- 控制器方法调用时,会综合利用本类的 @ModelAttribute 方法和 @ControllerAdvice 中的 @ModelAttribute 方法创建模型工厂
返回值处理器
常见的返回值解析处理器有以下几种:
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
这里先给出不同返回值的处理依据:
- ModelAndView,分别获取其模型和视图名,放入 ModelAndViewContainer
- 返回值类型为 String 时,把它当做视图名,放入 ModelAndViewContainer
- 返回值添加了 @ModelAttribute 注解时,将返回值作为模型,放入 ModelAndViewContainer
- 此时需找到默认视图名
- 返回值省略 @ModelAttribute 注解且返回非简单类型时,将返回值作为模型,放入 ModelAndViewContainer
- 此时需找到默认视图名
- 返回值类型为 ResponseEntity 时
- 此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true
- 返回值类型为 HttpHeaders 时
- 会设置 ModelAndViewContainer.requestHandled 为 true
- 返回值添加了 @ResponseBody 注解时
- 此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true
我们来依次演示上面的各种情况,首先准备一个测试控制器:
static class Controller {
private static final Logger log = LoggerFactory.getLogger(Controller.class);
public ModelAndView test1() {
log.debug("test1()");
ModelAndView mav = new ModelAndView("view1");
mav.addObject("name", "张三");
return mav;
}
public String test2() {
log.debug("test2()");
return "view2";
}
@ModelAttribute
// @RequestMapping("/test3")
public User test3() {
log.debug("test3()");
return new User("李四", 20);
}
public User test4() {
log.debug("test4()");
return new User("王五", 30);
}
public HttpEntity<User> test5() {
log.debug("test5()");
return new HttpEntity<>(new User("赵六", 40));
}
public HttpHeaders test6() {
log.debug("test6()");
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "text/html");
return headers;
}
@ResponseBody
public User test7() {
log.debug("test7()");
return new User("钱七", 50);
}
}
// 必须用 public 修饰, 否则 freemarker 渲染其 name, age 属性时失败
public static class User {
private String name;
private int age;
..
}
获取返回值解析器和视图渲染逻辑:
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;
}
@SuppressWarnings("all")
private static void renderView(AnnotationConfigApplicationContext context, ModelAndViewContainer container,
ServletWebRequest webRequest) throws Exception {
log.debug(">>>>>> 渲染视图");
FreeMarkerViewResolver resolver = context.getBean(FreeMarkerViewResolver.class);
String viewName = container.getViewName() != null ? container.getViewName() : new DefaultRequestToViewNameTranslator().getViewName(webRequest.getRequest());
log.debug("没有获取到视图名, 采用默认视图名: {}", 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));
}
DispathcherServlet对于视图渲染也支持多种技术实现,例如: JSP,Freemarker等,每一种技术分别对应一个ViewResolver的实现类,每一次走到视图渲染逻辑时,DispathcherServlet都会遍历ViewResolver集合,传入视图名,判断哪一个ViewResolver能够解析成功当前视图名,如果可以,就交给他生成的View对象去执行视图渲染逻辑:
我们依次来看各种情况的执行结果:
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
// 1. 测试返回值类型为 ModelAndView
test1(context);
}
- 测试返回值类型为 ModelAndView
private static void test1(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test1");
Controller controller = new Controller();
Object returnValue = method.invoke(controller); // 获取返回值
HandlerMethod methodHandle = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
//获取返回值解析器--这里使用的还是组合模式的体现
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
ServletWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
//判断返回值解析器是否支持当前控制器方法返回值类型
if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
//返回值处理器处理返回值
composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
//打印处理完后的ModelAndViewContainer中的视图名和模型数据
System.out.println(container.getModel());
System.out.println(container.getViewName());
renderView(context, container, webRequest); // 渲染视图
}
}
- 测试返回值类型为 String 时, 把它当做视图名
private static void test2(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test2");
Controller controller = new Controller();
Object returnValue = method.invoke(controller); // 获取返回值
HandlerMethod methodHandle = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
ServletWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
renderView(context, container, webRequest); // 渲染视图
}
}
- 测试返回值添加了 @ModelAttribute 注解时, 此时需找到默认视图名,默认视图名为handlerMapping请求解析阶段缓存在request属性集合中的请求路径
private static void test3(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test3");
Controller controller = new Controller();
Object returnValue = method.invoke(controller); // 获取返回值
HandlerMethod methodHandle = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
MockHttpServletRequest request = new MockHttpServletRequest();
//设置请求路径
request.setRequestURI("/test3");
//UrlPathHelper解析请求路径并进行缓存
UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request);
ServletWebRequest webRequest = new ServletWebRequest(request, new MockHttpServletResponse());
if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
renderView(context, container, webRequest); // 渲染视图
}
}
- 测试返回值不加 @ModelAttribute 注解且返回非简单类型时, 此时需找到默认视图名
private static void test4(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test4");
Controller controller = new Controller();
Object returnValue = method.invoke(controller); // 获取返回值
HandlerMethod methodHandle = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/test4");
UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request);
ServletWebRequest webRequest = new ServletWebRequest(request, new MockHttpServletResponse());
if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
renderView(context, container, webRequest); // 渲染视图
}
}
- 测试返回值类型为 ResponseEntity 时, 此时不走视图流程
private static void test5(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test5");
Controller controller = new Controller();
Object returnValue = method.invoke(controller); // 获取返回值
HandlerMethod methodHandle = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest webRequest = new ServletWebRequest(request, response);
if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
//判断请求是否已经在返回值处理器中被处理掉了
if (!container.isRequestHandled()) {
renderView(context, container, webRequest); // 渲染视图
} else {
//如果被处理掉了,那么从response对象中获取数据
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
}
}
- 测试返回值类型为 HttpHeaders 时, 此时不走视图流程 ,此时只返回响应头,不返回响应体
private static void test6(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test6");
Controller controller = new Controller();
Object returnValue = method.invoke(controller); // 获取返回值
HandlerMethod methodHandle = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest webRequest = new ServletWebRequest(request, response);
if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
composite.handleReturnValue(returnValue, methodHandle.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(), StandardCharsets.UTF_8));
}
}
}
- 测试返回值添加了 @ResponseBody 注解时, 此时不走视图流程,相关返回值处理器处理后,会添加一个Content-type响应头表示响应内容格式为JSON,并将方法返回值输出为JSON格式
private static void test7(AnnotationConfigApplicationContext context) throws Exception {
Method method = Controller.class.getMethod("test7");
Controller controller = new Controller();
Object returnValue = method.invoke(controller); // 获取返回值
HandlerMethod methodHandle = new HandlerMethod(controller, method);
ModelAndViewContainer container = new ModelAndViewContainer();
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest webRequest = new ServletWebRequest(request, response);
if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
composite.handleReturnValue(returnValue, methodHandle.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(), StandardCharsets.UTF_8));
}
}
}
小结
由于RequestMappingHandlerAdapter内容过多,所以本文先讲到返回值处理部分,我们下一篇文章再见。