文章目录
- DispatcherServlet
- RequestMappingHandlerMapping
- RequestMappingHandlerAdapter
- 自定义参数处理器
- 自定义返回值处理器
- 参数解析器
- 获取参数名
- 对象绑定与类型转换
- 底层第一套转换接口与实现
- 底层第二套转换接口与实现
- 高层转换接口与实现
- 自定义转换器
- @ControllerAdvice 之 @InitBinder
- 附:说说MVC
- ModelAndView
- ModelAndViewContainer
DispatcherServlet
既然我们讨论SpringMVC那么就必然绕不开一个东西叫做DispatcherServlet。
DispatcherServlet是SpringMVC的核心Servlet,也叫做前端控制器。它的主要作用是调度请求并将请求分发给相应的处理器。
我们要注意:
DispatcherServlet由Servlet容器创建,并且它的生命周期也是Servlet那套体系由Servlet容器进行控制
DispatcherServlet 是在第一次被访问时执行初始化, 也可以通过配置修改为 Tomcat 启动后就初始化:
那么DispatcherServlet 在初始化的时候都做了什么呢?
DispatcherServlet 初始化的时候会调用onRefresh方法,而这个方法中又会调用initStrategies方法:
DispatcherServlet的initStrategies()方法用于初始化DispatcherServlet所需的各种策略(strategy):
主要包括:
HandlerMapping
:路径映射器,用于根据请求URL找到对应的Handler(Controller的方法)HandlerAdapter
:处理器配置器,用于执行Handler,将请求参数绑定到Handler入参,创建返回的ModelAndView。ViewResolver
:用于根据逻辑视图名解析成真正的视图View。LocaleResolver
:本地化信息解析器,用于获取客户端的地域信息,国际化用。ThemeResolver
:用于提供主题信息,一般用不太多。MultipartResolver
:文件上传解析器,用于上传文件用,当有文件上传需求时使用。HandlerExceptionResolvers
:控制器异常解析器
默认的DispatcherServlet会对这些策略进行自动检测和设置。我们也可以自定义这些策略。
其中最常自定义的就是HandlerMapping、ViewResolver和MultipartResolver。
所以这个方法主要是初始化一些DispatchServlet执行请求所需要的策略和组件。这些组件大多来自Spring容器,所以DispatcherServlet在初始化阶段首先要创建Spring容器,然后再从容器中获取这些策略的实现。有了这些策略和组件的支持,DispatchServlet才有能力完成从接收请求到产生响应的整个流程。
RequestMappingHandlerMapping
RequestMappingHandlerMapping是一个HandlerMapping实现,它的作用是根据RequestMapping注解将请求映射到对应的Handler(Controller的方法)。
它会解析类及方法上的@RequestMapping注解,并根据注解中的信息注册Handler。当请求过来时,会根据URL查找对应的Handler进行执行。
主要功能如下:
-
解析@RequestMapping注解,获取URL、method等信息。
-
根据URL、method等条件查找对应的Handler。支持ANT风格的URL。
-
支持组合注解。即一个类或方法上有多个@RequestMapping注解的情况。会将这些信息组合起来一并解析。
-
支持派生注解。如@GetMapping、@PostMapping等也可以使用。
-
支持定制的HandlerMapping通过实现HandlerMapping接口。
主要的解析规则是:
- 方法上的@RequestMapping优先级最高
- 然后是类上的@RequestMapping
- URL可以使用ANT风格的通配符
- 当一个Handler同时匹配类和方法的@RequestMapping时,方法的映射规则优先
RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map,其中
- key 是 RequestMappingInfo 类型,包括请求路径、请求方法等信息
- value 是 HandlerMethod 类型,包括控制器方法对象、控制器对象
- 有了这个 Map,就可以在请求到达时,快速完成映射,找到 HandlerMethod 并与匹配的拦截器一起返回给 DispatcherServlet
接下来我们使用代码模拟一下过程:
配置类:
这个地方我们如果不主动注入,DispatcherServlet 初始化时默认会添加RequestMappingHandlerMapping组件,但是并不会作为 bean,而是会当作DispatcherServlet 的属性。
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);
// 作用 解析 @RequestMapping 以及派生注解,生成路径与控制器方法的映射关系, 在初始化时就生成
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
// 获取映射结果
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
handlerMethods.forEach((k, v) -> {
System.out.println(k + "=" + v);
});
// 请求来了,获取控制器方法 返回处理器执行链对象
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test4");
HandlerExecutionChain chain = handlerMapping.getHandler(request);
System.out.println(chain);
}
}
结果:
注意:
- 路径与控制器方法的映射关系, 在初始化时就生成了
HandlerExecutionChain
是一个HandlerMethod
执行链,它包含一个HandlerMethod
和多个HandlerInterceptor
。当一个请求匹配到一个HandlerMethod时,会创建一个HandlerExecutionChain,然后顺序执行链中的所有拦截器和最后一个HandlerMethod。- 它的主要属性有:
- HandlerMethod:要执行的HandlerMethod
- HandlerInterceptorList:要执行的拦截器列表
- 它的主要属性有:
RequestMappingHandlerAdapter
RequestMappingHandlerAdapter是一个HandlerAdapter实现,它支持处理基于注解的Controller,即使用@RequestMapping映射请求的Controller。
它主要功能是:
- 绑定请求参数到Controller方法的参数上。支持@RequestParam、@RequestBody等注解。
- 执行HandlerMethod,为方法提供一个绑定了请求参数的可执行的方法参数数组。
- 处理返回值并设置到ModelAndViewContainer中,包括:
- 返回String则当成逻辑视图名,交给视图解析器解析。
- 返回void则当作逻辑视图名为空。
- 返回ModelAndView对象则直接使用。
- 返回其他对象则当作模型数据添加到Model中。
RequestMappingHandlerAdapter 初始化时,会准备 HandlerMethod 调用时需要的各个组件(这两个组件都是RequestMappingHandlerAdapter的属性),如:
- HandlerMethodArgumentResolver 解析控制器方法参数
- HandlerMethodReturnValueHandler 处理控制器方法返回值
我们使用代码模拟一下:
这里我们使用的MyRequestMappingHandlerAdapter是因为invokeHandlerMethod方法是Protected的,我们继承一下才能用:
自定义参数处理器
例如我们经常需要用到请求头中的 token 信息, 用下面注解来标注由哪个参数来获取它:
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
}
Controller:
@PutMapping("/test3")
public ModelAndView test3(@Token String token) {
log.debug("test3({})", token);
return null;
}
然后我们自定义一个参数处理器:
public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
@Override
// 是否支持某个参数
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 {
return webRequest.getHeader("token");
}
}
注意:
HandlerMethodArgumentResolver接口中定义了两个方法:
- supportsParameter():该方法用于判断当前的参数解析器是否支持解析该方法参数。如果返回true,则会调用resolveArgument()进行实际解析。
- resolveArgument():该方法会根据请求信息(webRequest)解析请求参数,并将解析后的参数绑定到方法入参上。
- 这两个方法需要结合使用,DispatcherServlet会先调用supportsParameter判断当前解析器是否支持该参数,如果支持再调用resolveArgument()进行实际解析。
我们自定义完一个参数解析器之后,还要讲我们的参数解析器加入到我们的RequestMappingHandlerAdapter 中去:
// ⬅️2. 继续加入RequestMappingHandlerAdapter, 会替换掉 DispatcherServlet 默认的 4 个 HandlerAdapter
@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter() {
TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
handlerAdapter.setCustomArgumentResolvers(List.of(tokenArgumentResolver));
return handlerAdapter;
}
这里使用setCustomArgumentResolvers()就可以添加我们自定义的参数解析器了。
自定义返回值处理器
与前面的自定义参数处理器差不多:
HandlerMethodReturnValueHandler接口用于处理HandlerMethod的返回值。它定义了两个方法:
- supportsReturnType():该方法用于判断当前的返回值处理器是否支持处理该返回值类型。如果返回true,则会调用handleReturnValue()方法进行实际处理。
- handleReturnValue():该方法会对返回值进行处理,主要做了以下工作:
- 根据返回值添加模型数据到ModelAndViewContainer中。
- 设置逻辑视图名到ModelAndViewContainer。
- 对特殊的返回值类型(如ResponseEntity)进行处理。
- 处理异常,添加到ModelAndViewContainer中。
接下来我们看一个例子:
我们自定义了一个注解@Yml,接下来我们通过识别这个注解来达到返回值处理的效果:
自定义返回值处理器:
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 {
// 1. 转换返回结果为 yaml 字符串
String str = new Yaml().dump(returnValue);
// 2. 将 yaml 字符串写入响应
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
response.setContentType("text/plain;charset=utf-8");
response.getWriter().print(str);
// 3. 设置请求已经处理完毕
mavContainer.setRequestHandled(true);
}
}
最后将自定义的返回值处理器加入到RequestMappingHandlerAdapter 中去:
参数解析器
绑定请求参数到Controller方法的参数上,这一重要的步骤在SpringMVC框架中就是参数解析器帮助我们完成的。
解析参数依赖的就是各种参数解析器,它们都有两个重要方法
supportsParameter
判断是否支持方法参数resolveArgument
解析方法参数
在RequestMappingHandlerAdapter 中自带了以下几种HandlerMethodArgumentResolver(参数解析器):
这里我们看几个常用的参数解析器,测试代码如下:
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
) {
}
}
这里我们首先来看RequestParamMethodArgumentResolver参数解析器,它对应的注解就是@RequestParam。也就对应了我们测试代码中这五个示例:
都一种是标准使用方式,对比第一种:
- 第二种测试不显示使用@RequestParam的情况
- 第三种测试涉及类型转换的情况
- 第四种测试从环境变量中获取值的情况
- 第五种测试获取文件的情况
测试代码如下:
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
// 准备测试 Request对象,这里使用了mock进行模拟,可以让我们脱离web环境进行测试
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()) {
//false 表示必须有 @RequestParam
RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory, false),
String annotations = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining());
String str = annotations.length() > 0 ? " @" + annotations + " " : " ";
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
if (composite.supportsParameter(parameter)) {
// 支持此参数
Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), factory);
// System.out.println(v.getClass());
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());
}
}
}
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);
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");
return new StandardServletMultipartResolver().resolveMultipart(request);
}
结果:
注意:
-
测试代码主体分为以下几部分
- 控制器方法被封装为 HandlerMethod:这一部分的工作一般是由HandlerMapping来做的,他们在做路径映射的时候就会把方法封装成HandlerMethod,这里我们没有使用。这里我们没有使用路径映射,所以要自己单独封装
- 准备对象绑定与类型转换
- 准备 ModelAndViewContainer 用来存储中间 Model 结果
- 解析每个参数值
-
RequestParamMethodArgumentResolver有两种工作模式,对应着构造器中的第二个参数,true表示可以没有@RequestParam也能解析,false表示必须有@RequestParam才能解析
-
ServletRequestDataBinderFactory用于对象的绑定与类型转换,如果没有它那么我们这个时候解析出来的age其实是string类型的,而我们想要的是int类型。对象的绑定就是说方法接收到的是一个对象,该组件就可以将对象的属性与方法的参数进行绑定
-
如果涉及到
${ } #{ }
的解析,前文我们提到过ApplicationContext 容器继承了EnvironmentCapable接口具有读取环境变量、配置文件的能力,也就是说这方面的功能我们是交给容器去实现的。这也就是说我们为什么在构造RequestParamMethodArgumentResolver的时候要传入一个容器对象
可以看到我们解析到第五个之后就报错了,这是因为此时我们只有1个解析器,我们可以多添加几种解析器,一个解析失败就换另一个尝试。Spring在底层使用了一种组合模式:
我们也使用这种组合模式:
这里的HandlerMethodArgumentResolverComposite就是一个参数解析器的复合,有了它之后我们可以更加方便。以后我们只需要调用复合解析器的supportsParameter、resolveArgument方法,而不需要关心其中包含有什么解析器。
然后我们加入PathVariableMethodArgumentResolver,这个解析器用来处理@PathVariable。
其原理如下:
- HandlerMapping会将路径中的一组对应关系放在map集合里,存在request域中
- 举个例子:请求路径:/test/124 ,注解中的值:/test/{id}
- 于是map中就会把id --> 124 进行对应
- 当执行到此解析器的时候,他就会根据@PathVariable中的name到map集合中去找
- 找到之后就和我们方法的参数值进行绑定
@Value对应的解析器是ExpressionValueMethodArgumentResolver
而我们的第九种:
它使用的解析器是ServletRequestMethodArgumentResolver,它是根据参数的类型进行解析,事实上这个解析器他不光可以解析HttpServletRequest这一个类型,还有一些其他的类型,我们看看它的supportsParameter 方法:
再来说说@ModelAttribute,它由ServletModelAttributeMethodProcessor进行解析,它可以将名字等于值的参数与我们的java对象进行绑定,参数名对应着java对象中的属性。并且它还会把我们参数解析器处理得到的结果作为模型数据存储到ModelAndViewContainer中。
在Spring底层添加了两次ServletModelAttributeMethodProcessor:
分别处理@ModelAttribute标识和省略@ModelAttribute的情况。
我们还有一个注意点:我们参数解析器的顺序也是有讲究的:
我们省略的情况要统一放到最后判断,加入这里我将倒数第二和倒数第三换个位置,就会出现下面的结果:
我们使用@RequestBody解析的参数,会被误认为省略@ModelAttribute的情况而被ServletModelAttributeMethodProcessor先解析。
最后还要注意:
@RequestParam, @CookieValue 等注解中的参数名、默认值, 都可以写成活的, 即从 ${ } #{ }
中获取
获取参数名
首先我们要知道我们的java文件在编译的时候,是不会保留参数名的,例如:
Java文件:
编译之后:
所以说这个参数名的获取也不是我们想象中的那么简单。
获取参数名的方法如下:
-
如果编译时添加了 -parameters 可以生成参数表, 反射时就可以拿到参数名(通过ASM拿不到)
-
如果编译时添加了 -g 可以生成调试信息, 但分为两种情况
- 普通类, 会包含局部变量表, 用 asm 可以拿到参数名(局部变量表使用反射是拿不到的)
- 接口, 不会包含局部变量表, 无法获得参数名
- 这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名
- spring boot 在编译时会加 -parameters
- 大部分 IDE 编译时都会加 -g
在Spring中,其底层使用了LocalVariableTableParameterNameDiscoverer来通过ASM去获得参数名。
还记得我们在前面写过的代码吗?
我们在获得参数名称的时候直接使用MethodParameter.getParameterName()得到的参数名是null,我们还需要一句话:
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
而这个DefaultParameterNameDiscoverer中我们就可以发现LocalVariableTableParameterNameDiscoverer:
上面那个StandardReflectionParameterNameDiscoverer就是用来解决第一种情况。也就是说Spring对两种情况都进行了处理并进行了封装。
对象绑定与类型转换
Spring中的类型转换体系非常的复杂,涉及到:
- 底层第一套转换接口与实现
- 底层第二套转换接口与实现
- 高层接口与实现
接下来我们一个个来看:
底层第一套转换接口与实现
- Printer 把其它类型转为 String
- Parser 把 String 转为其它类型
- Formatter 综合 Printer 与 Parser 功能
- Converter 把类型 S 转为类型 T
- Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合
- FormattingConversionService 利用它们实现转换
底层第二套转换接口与实现
这一套由JDK提供,并不是Spring中的
- PropertyEditor 把 String 与其它类型相互转换
- PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
- 与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配
问:为什么Spring要同时使用这两套类型转换接口?
答:历史遗留问题,最早的Spring直至JDK的这套转换接口,随着发展这套转换接口的功能不够全面,例如:不支持任意两个类型的转换。为了拓展于是Spring引入了一套新的转换体系。不抛弃旧的是因为涉及到版本兼容原因。所以呈现出了两套转换接口并存的情况。
高层转换接口与实现
- 它们都实现了 TypeConverter 这个高层转换接口,在转换时,会用到 TypeConverter Delegate 委派ConversionService 与 PropertyEditorRegistry 真正执行转换(Facade 门面模式)
- 处理逻辑:
- 首先看PropertyEditorRegistry中是否有自定义转换器, @InitBinder 添加的即属于这种 (用了适配器模式把 Formatter 转为需要的 PropertyEditor)
- 再看有没有 ConversionService 转换
- 再利用默认的 PropertyEditor 转换
- 最后有一些特殊处理
- SimpleTypeConverter 仅做类型转换
- BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,走 Property
- DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,走 Field
- ServletRequestDataBinder 为 bean 的属性执行绑定,当需要时做类型转换,根据 directFieldAccess 选择走 Property 还是 Field,同时还具备校验与获取校验结果功能
接下来我们详细的说说这四个组件,他帮我们实现了基本的类型转换与数据绑定:
- SimpleTypeConverter
- BeanWrapperImpl
- DirectFieldAccessor
- ServletRequestDataBinder
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
BeanWrapperImpl是Spring框架中一个重要的类,它的主要作用是:
将JavaBean属性的读取、写入操作统一进行处理。它可以设置和获取JavaBean的属性(本质是通过getter/setter方法),并通过PropertyEditor支持数据类型转换。
它简化了我们直接操作Bean的复杂度,提供了一套完备的机制用于属性的访问。在Spring框架中有广泛的使用,如:
- DataBinder使用BeanWrapper进行数据绑定
- BeanFactory使用BeanWrapper进行Bean属性的依赖注入
- Expression解析器使用BeanWrapper获取或设置值
- 等等
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;
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 +
'}';
}
}
}
DirectFieldAccessor
与传统的通过getter/setter方法来读写属性相比,DirectFieldAccessor的优点是:
- 性能更高。直接访问field避免了方法调用的开销。
- 可以访问private字段。
与传统的BeanWrapperImpl相比,DirectFieldAccessor的优点在于性能更高,可以访问private字段。但是由于直接访问Field的方式破坏了封装性,也带来了一定隐患:
- 子类继承的字段也会变成可访问,这可能不是设计意图。
- 修改字段值会直接作用在原始对象上,违反JavaBean的设计模式。这可能会引起既有代码的问题。
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 +
'}';
}
}
}
DataBinder
public class TestDataBinder {
public static void main(String[] args) {
// 执行数据绑定
MyBean target = new MyBean();
DataBinder dataBinder = new DataBinder(target);
//根据 directFieldAccess 选择走 Property 还是 Field
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 +
'}';
}
}
}
web环境下对应着其子类ServletRequestDataBinder:
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;
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 +
'}';
}
}
}
我们前面所说的ServletModelAttributeMethodProcessor参数解析器中的功能就是它提供的。
ServletRequestParameterPropertyValues是Spring框架中一个实现了PropertyValues接口的类。
它的主要作用是:将ServletRequest中的参数转为PropertyValues,以便用于数据绑定。
当我们要将ServletRequest的参数绑定到JavaBean时,需要先将这些参数转为PropertyValues格式,才可以使用DataBinder进行绑定。
ServletRequestParameterPropertyValues常与WebDataBinder、RequestMappingHandlerAdapter等配合使用,完成高效的请求参数到Bean的绑定,
自定义转换器
接下来我们将上面的代码稍作修改:
可以看到这两个绑定上面这一种肯定是不能成功的,日期准换不支持这种格式,而下面这一种可以成功:
如果我们非要这样绑定呢?所以这里我们就引出一个新的问题:添加自定义转换器
这里我们有两种思路:
- ConversionService + Formatter
- PropertyEditorRegistry + PropertyEditor
也就是对应着我们前面说的底层两套转换接口。接下来我们开始实现:
PropertyEditorRegistry + PropertyEditor
这里我们就不自己创建ServletRequestDataBinder,而是改用ServletRequestDataBinderFactory帮我们创建ServletRequestDataBinder。因为使用这个工厂创建时可以添加各种选项,比如说想基于JDK转换接口进行拓展还是Spring转换接口进行拓展自己的转换器。
当然直接使用工厂还是没有转换功能的,这里我们可以借助@InitBinder注解帮助我们进行转换器的拓展。
@InitBinder是SpringMVC中一个非常重要的注解,它的主要作用是:
初始化WebDataBinder,为Web请求参数到JavaBean的绑定提供定制化的功能。
这里的WebDataBinder是DataBinder和ServletRequestDataBinder的中间类型:
例如,我们可以通过@InitBinder在控制器中添加:
- Custom editors 自定义属性编辑器
- Custom converters 自定义类型转换器
- Custom validators 自定义校验器
来增强WebDataBinder的功能,以完成更加丰富的数据绑定。
使用方式:
在@Controller的方法上标注@InitBinder,传入WebDataBinder类型的参数:
@Controller
public class MyController {
@InitBinder
public void initBinder(WebDataBinder binder) {
// 添加编辑器、转换器、校验器
binder.addCustomEditor(...);
binder.addConverter(...);
binder.addValidator(...);
}
}
这样,为@InitBinder方法传入的WebDataBinder对象添加的定制化功能会应用到该Controller的所有@RequestMapping方法中。
接下来我们就开始修改我们的代码:
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();
//将控制器中的方法包装成回调方法
InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
//创建工厂的时候将回调方法的传入,在创建DataBinder的时候会进行回调
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), null);
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 方式扩展的"));
}
}
自定义转换器:
这个地方要注意我们使用InvocableHandlerMethod将@InitBinder标注的方法包了一层。
InvocableHandlerMethod是Spring MVC中一个重要的类,它的主要作用是:
代表一个可调用的控制器方法,通过它我们可以调用指定的控制器方法。
在创建ServletRequestDataBinderFactory的时候,我们传入了一个列表里面就装着这些控制器回调方法,如此该工厂在创建WebDataBinder就可以回调这些方法,对WebDataBinder进行类型转换的拓展。
我们点开addCustomFormatter方法,发现它使用的就是PropertyEditorRegistry + PropertyEditor:
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();
//创建类型转换服务
FormattingConversionService service = new FormattingConversionService();
//将自定义的转化器添加到服务中
service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能"));
//创建一个WebDataBinder的初始化器
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);
}
这里我们使用到了ConfigurableWebBindingInitializer,其主要作用就是初始化WebDataBinder。这次我们在创建ServletRequestDataBinderFactory时候没有传入回调函数的列表而是传入了这个WebDataBinder的初始化器。
如果我们同时使用这两种方法:
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), initializer);
@InitBinder的优先级更高
还有一种方法:使用默认 ConversionService 转换
要配合@DateTimeFormat注解进行使用。
也就是说:
@DateTimeFormat注解是DefaultFormattingConversionService负责解析的。
最后我们总结一下:
ServletRequestDataBinderFactory 的用法和扩展点
- 可以解析控制器的 @InitBinder 标注方法作为扩展点,添加自定义转换器
- 控制器私有范围
- 可以通过 ConfigurableWebBindingInitializer 配置 ConversionService 作为扩展点,添加自定义转换器
- 公共范围
- 同时加了 @InitBinder 和 ConversionService 的转换优先级
- 优先采用 @InitBinder 的转换器
- 其次使用 ConversionService 的转换器
- 使用默认转换器
- 特殊处理(例如有参构造)
@ControllerAdvice 之 @InitBinder
@ControllerAdvice是一个注解,它的主要作用是:
对一组Controller进行全局配置,比如:
- 异常处理
- 数据绑定
- 拦截器绑定
- 等等
使用@ControllerAdvice,我们可以将一些共享的代码抽取出来,应用到一组Controller上。比如:
// 定义一个统一异常处理类
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
public String exception(Exception e) {
// 统一异常处理逻辑
}
}
然后,GlobalExceptionHandler中的方法会应用到所有@Controller上,实现全局异常处理。
我们也可以将@ControllerAdvice的属性使用更加具体:
@ControllerAdvice(assignableTypes = {Controller1.class, Controller2.class})
public class ExampleAdvice {}
上述@ControllerAdvice只应用于Controller1和Controller2类型的Controller,实现更加细粒度的配置。
@ControllerAdvice还支持像@ModelAttribute这样的注解,我们可以整合多个Controller的@ModelAttribute方法:
@ControllerAdvice
public class ExampleAdvice {
@ModelAttribute("commonAttribute")
public void commonAttribute() { ... }
}
这样,所有的Controller在其@RequestMapping方法被调用前,首先会执行@ModelAttribute(“commonAttribute”)方法。
除此之外,@ControllerAdvice还支持:
- @InitBinder: 实现多个Controller的参数绑定定制
- @Resource和@Autowired: 向一组Controller提供共享的bean
- Interceptor: 向一组Controller添加拦截器
- 等等
接下来我们详细的说说@InitBinder:
它是由RequestMappingHandlerAdapter进行解析的。其解析后的结果存放在RequestMappingHandlerAdapter的两个属性值中:
- initBinderCache:用来存放每个控制器类的initBinder,map中的key是控制器类型,值是@InitBinder标注的方法
- initBinderAdviceCache:用来存储全局的initBinder
接下来我们看看这两种@InitBinder解析的时机:
@InitBinder 在整个 HandlerAdapter 调用过程中所处的位置
- RequestMappingHandlerAdapter 在图中缩写为 HandlerAdapter
- HandlerMethodArgumentResolverComposite 在图中缩写为 ArgumentResolvers
- HandlerMethodReturnValueHandlerComposite 在图中缩写为 ReturnValueHandlers
重点💡:
- RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @InitBinder 方法
- RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @InitBinder 方法
- 以上两种 @InitBinder 的解析结果都会缓存来避免重复解析
- 控制器方法调用时,会综合利用本类的 @InitBinder 方法和 @ControllerAdvice 中的 @InitBinder 方法创建绑定工厂
附:说说MVC
MVC可以说是三层架构的一种实现类
MVC框架是一种设计模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。MVC框架通过这三个组件的协同工作,实现了应用程序的解耦、可维护性和可扩展性。
具体来说,MVC框架中的:
- 模型(Model)表示应用程序的数据和业务逻辑
- 视图(View)表示数据的展示方式
- 控制器(Controller)则负责协调模型和视图之间的交互。
当用户请求一个页面时,控制器接收到请求后会调用模型来获取数据,然后将数据传递给视图进行展示。用户可以通过视图来操作数据,控制器则负责将这些操作反映到模型中。
我们举个例子:
假设我们正在开发一个简单的图书管理系统,其中包含以下功能:
- 用户可以浏览图书列表,并查看每本书的详细信息;
- 用户可以添加、编辑和删除图书。
在这个系统中,我们可以将:
-
图书作为模型(Model),它包含图书的各种属性(如书名、作者、出版社、出版日期等)以及对图书的各种操作(如添加、编辑和删除等)。
-
视图(View)则是用来展示图书信息的界面,它可以是一个HTML页面、一个JSP页面或者一个JSON数据格式等。例如,我们可以创建一个图书列表页面,用来展示所有图书的基本信息,以及一个图书详情页面,用来展示某本书的详细信息。
-
控制器(Controller)则负责协调模型和视图之间的交互。例如,当用户请求图书列表页面时,控制器会调用模型来获取所有图书的信息,然后将这些信息传递给视图进行展示。当用户点击某本书的链接时,控制器会根据图书的ID来调用模型,获取该书的详细信息,然后将这些信息传递给视图展示。
ModelAndView
ModelAndView是SpringMVC中重要的对象之一,它封装了模型数据和视图信息,用来从Controller向视图传递 数据和视图信息。
它包含两个部分:
Model
:模型数据,用来携带Controller处理后需要显示给用户的数据。可以是任意的POJO对象。View
:视图信息,指定了视图的名称或实例,告诉Spring要使用哪个视图渲染模型数据。可以是一个逻辑名称,也可以是View实例。
ModelAndView的创建方式有三种:
-
指定模型数据和逻辑视图名:
ModelAndView mv = new ModelAndView("viewName", "modelAttr", modelObj);
-
指定模型数据和View实例:
View view = ... ModelAndView mv = new ModelAndView(view, "modelAttr", modelObj);
-
不指定任何参数,后续再添加模型数据和视图信息:
ModelAndView mv = new ModelAndView(); mv.addObject("modelAttr", modelObj); mv.setViewName("viewName");
使用方式:
Controller方法可以返回ModelAndView,并在方法中创建并返回:
@Controller
public class MyController {
@RequestMapping("/someUrl")
public ModelAndView handleRequest() {
ModelAndView mv = new ModelAndView("viewName");
mv.addObject("modelAttr", modelObj);
return mv;
}
}
这样DispatcherServlet在调用完Controller后会获取到返回的ModelAndView,并使用其中的视图信息进行视图解析,得到View。然后利用ModelAndView中的模型数据渲染View,得到最终响应。
所以ModelAndView的主要作用是在Controller和View之间传递数据和视图信息。Controller将需要显示的数据和视图放入ModelAndView,然后DispatcherServlet使用这些信息进行视图解析和渲染,生成最终响应。
综上,ModelAndView是SpringMVC中非常重要且常用的对象,它承载了Controller处理请求后需要显示给用户的模型数据和指定的视图,起到了在Controller和View之间传递信息的作用。虽然现在有更加灵活的返回值支持,但是ModelAndView仍然是比较经典和简单的选择。
ModelAndViewContainer
ModelAndViewContainer用来存储 HandlerMethod的处理结果,它包含模型数据、逻辑视图名、异常信息等。
它主要有以下作用:
- 存储从HandlerMethod中返回的模型数据。无论HandlerMethod的返回值是Model、ModelMap、Map等,都可以存储为模型数据。
- 存储HandlerMethod返回值中的逻辑视图名。
- 存储在HandlerMethod执行过程中出现的异常信息。
- 此外,也可以存储一些标志位,如是否跳转至登录页等信息。
- ModelAndViewContainer会在HandlerAdapter处理完HandlerMethod后,将结果传递给DispatcherServlet使用。DispatcherServlet会使用视图解析器解析逻辑视图名,得到View,并使用ModelAndViewContainer中的模型数据渲染View。
所以ModelAndViewContainer是连接HandlerAdapter和DispatcherServlet的重要载体,它承载处理HandlerMethod的结果,并传递给DispatcherServlet使用。
它简化了HandlerMethod的返回值,方法可以返回各种类型,而不需要局限于ModelAndView。因为无论返回什么类型,都可以通过HandlerMethodReturnValueHandler处理后存储到ModelAndViewContainer中,然后再由DispatcherServlet使用。
使用方式:
我们无需自己创建ModelAndViewContainer,它是由HandlerAdapter创建并使用的。我们主要是从HandlerMethod的返回值中添加信息到ModelAndViewContainer。
举个例子,在HandlerMethodReturnValueHandler的handleReturnValue()方法中,我们可以这样做:
// 处理返回Model类型
modelMap.addAttribute("attr", returnValue);
mavContainer.addAllAttributes(modelMap);
// 处理返回String,作为逻辑视图名
mavContainer.setViewName(returnValue);
// 标记请求已处理
mavContainer.setRequestHandled(true);