1、@ControllerAdvice、@InitBinder
@ControllerAdvice注解是 Spring MVC 中的一个注解,它的作用是全局性地统一处理控制器(Controller)的异常,以及对控制器的全局配置。
全局异常处理:可以在 @ControllerAdvice 注解标记的类中定义异常处理方法,用于捕获和处理所有控制器中抛出的异常。这些方法可以处理特定类型的异常,提供自定义的异常处理逻辑,并返回友好的错误信息或错误页面。
全局数据绑定:可以在 @ControllerAdvice注解标记的类中定义全局的数据绑定规则,例如注册全局的属性编辑器、格式化器、验证器等,以应用于所有控制器中的数据绑定操作。
全局模型数据:可以在 @ControllerAdvice注解标记的类中定义方法,用于在所有控制器中共享的模型数据,这些数据会自动添加到每个控制器方法的模型中,从而在视图中可用。
上一篇中提到,@InitBinder 注解的作用是标记一个用于初始化DataBinder对象,自定义数据绑定行为的方法,它会在控制器处理请求之前被调用。
如果@InitBinder 注解加在被@ControllerAdvice 注解标记的控制器类的方法中时,其作用范围是全局的,并且是由RequestMappingHandlerAdapter 在初始化时解析并记录。
而@InitBinder 注解加在被@Controller 标记的控制器中的方法上时,会在控制器方法首次执行时解析并记录。
定义一个Config类,类中有三个静态内部类,MyControllerAdvice类是被@ControllerAdvice
控制的。
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice {
@InitBinder
public void binder3(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder3 转换器"));
}
}
@Controller
static class Controller1 {
@InitBinder
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() {
}
}
}
测试类中,首先创建一个 RequestMappingHandlerAdapter对象,并将其配置为使用指定的 ApplicationContext上下文。然后调用 afterPropertiesSet()方法来完成对象的初始化。
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
handlerAdapter.setApplicationContext(context);
handlerAdapter.afterPropertiesSet();
然后定义一个方法,获取RequestMappingHandlerAdapter中的两个成员变量,这里利用了缓存的思想(不知是否可被归类为享元模式):
- private final Map<ControllerAdviceBean, Set<Method>> initBinderAdviceCache:用于存储被@ControllerAdvice标记的控制器中 @InitBinder 标注的方法。
- private final Map<Class<?>, Set<Method>> initBinderCache:用于存储@Controller标记的控制器中 @InitBinder 标注的方法。
RequestMappingHandlerAdapter是HandlerAdapter的一个实现类:
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中的getDataBinderFactory方法(用于模拟调用Controller1和Controller2的方法):
log.debug("1. 刚开始...");
showBindMethods(handlerAdapter);
Method getDataBinderFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getDataBinderFactory", HandlerMethod.class);
getDataBinderFactory.setAccessible(true);
log.debug("2. 模拟调用 Controller1 的 foo 方法时 ...");
getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo")));
showBindMethods(handlerAdapter);
log.debug("3. 模拟调用 Controller2 的 bar 方法时 ...");
getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller2(), WebConfig.Controller2.class.getMethod("bar")));
showBindMethods(handlerAdapter);
context.close();
输出结果:
2、控制器方法执行流程
复习一下,DispatcherServlet 的工作流程中,首先是客户端发送请求到 DispatcherServlet然后DispatcherServlet 根据请求的 URL 找到对应的 HandlerMapping(处理器映射器),然后将请求分发给相应的 Controller(处理器)。
RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map,Map的K为路径,V就是下图中的HandlerMethod。HandlerMethod又是交给HandlerAdapters执行的。
HandlerMethod需要:
bean:控制器类
method :控制器类中的方法。
但是只有控制器和方法不够,因为方法上还有可能存在参数或者自定义的注解,而参数有可能还需要进行类型转换。所以需要用此前提到过的参数解析器,返回值解析器,它们共同组合成了ServletInvocableHandlerMethod:
WebDataBinderFactory 负责对象绑定、类型转换
ParameterNameDiscoverer 负责参数名解析
HandlerMethodArgumentResolverComposite 负责解析参数
HandlerMethodReturnValueHandlerComposite 负责处理返回值(统一成ModelAndView类型)
首先HandlerAdapters 会创建WebDataBinderFactory工厂,可以执行被@InitBinder 注解修饰的自定义的解析参数方法。然后会创建ModelFactory,会把标注了@ModelAttribute的数据放入ModelAndViewContainer容器中。
紧接着就会获取并解析参数,通过反射调用方法,处理返回值。其中在解析参数和处理返回值时,还会涉及到RequestBodyAdvice和ResponseBodyAdvice,并且有可能会生成模型数据。最后返回ModelAndViewContainer。
用代码演示一下图一与图二:
首先创建一个Config类,方法foo() 的参数是一个User对象。(没有显式使用@ModelAttribute)
@Configuration
public class WebConfig {
@Controller
static class Controller1 {
@ResponseStatus(HttpStatus.OK)
public ModelAndView foo(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 + '\'' +
'}';
}
}
}
在测试类中创建ApplicationContext,发送模拟请求,将来name会绑定到User类的name字段上:
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "张三");
创建ServletInvocableHandlerMethod对象,并且给WebDataBinderFactory,ParameterNameDiscoverer ,HandlerMethodArgumentResolverComposite赋值:
ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo", WebConfig.User.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
//负责对象绑定、类型转换
handlerMethod.setDataBinderFactory(factory);
//负责解析参数名
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
//解析参数
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
ModelAndViewContainer container = new ModelAndViewContainer();
handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);
System.out.println(container.getModel());
context.close();
参数解析器组合类:(方法foo() 的参数User会被ServletModelAttributeMethodProcessor解析。)
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(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true),
new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), true)
);
return composite;
}
@ModelAttribute 注解,不仅能加在参数上,还能加在类上和方法上:
区别在于,加在参数上,是由参数解析器进行解析的,加在类上或方法上,是由HandlerAdapter解析的,我们在Config类中加入一个静态内部类:
@ControllerAdvice
static class MyControllerAdvice {
@ModelAttribute("a")
public String aa() {
return "aa";
}
}
在测试方法上,创建RequestMappingHandlerAdapter并初始化,在初始化时,会找到加了 @ControllerAdvice类中加了 @ModelAttribute的方法并记录下来
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();
在 handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);前还需要利用模型的工厂方法中的.initModel() 初始化模型数据:
// 获取模型工厂方法
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);
最终模型名是注解中的value,模型值为aa()方法的返回值。(即将aa()方法的返回值放入ModelAndViewContainer容器中)
3、返回值处理
返回值处理和参数解析一样,同样可以使用组合的方式:
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(Collections.singletonList(new MappingJackson2HttpMessageConverter())));
composite.addHandler(new HttpHeadersReturnValueHandler());
composite.addHandler(new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())));
composite.addHandler(new ServletModelAttributeMethodProcessor(true));
return composite;
}
MappingJackson2HttpMessageConverter是一种消息转换器,可以把请求、响应的消息转换成JSON或xml格式。
3.1、解析ModelAndView返回值类型
在控制器类中有如下的方法,返回值是ModelAndView类型:
public ModelAndView test1() {
log.debug("test1()");
ModelAndView mav = new ModelAndView("view1");
mav.addObject("name", "张三");
return mav;
}
对其进行解析,经历的步骤:
- 准备容器,封装HandlerMethod,创建模拟请求响应
- 得到返回值处理器的组合,并进行匹配(ModelAndViewMethodReturnValueHandler)
- 调用匹配的返回值处理器,将ModelAndView的视图、模型提取出来,放到container容器中
- 根据视图找到模板页面,然后用container容器中的视图模型渲染页面
private static void test1(AnnotationConfigApplicationContext context) throws Exception {
// 通过反射调用Controller中的test1方法 获取返回值
Method method = Controller.class.getMethod("test1");
Controller controller = new Controller();
Object returnValue = method.invoke(controller);
//封装HandlerMethod
HandlerMethod methodHandle = new HandlerMethod(controller, method);
//准备容器
ModelAndViewContainer container = new ModelAndViewContainer();
//得到返回值处理器组合
HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
//创建模拟请求响应
ServletWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
// 检查是否支持此类型的返回值 用的处理器是ModelAndViewMethodReturnValueHandler()
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);
}
}
3.2、解析String返回值类型
在控制器类中有如下的方法,返回值是String类型:
public String test2() {
log.debug("test2()");
return "view2";
}
其解析过程和3.1类似,但是用到的返回值解析器是ViewNameMethodReturnValueHandler
最后是根据test2()的返回值view2找到对应名称的视图,因为没有定义ModelAndView的视图、模型,所以是用view2中默认的数据渲染视图。
3.3、解析对象返回值类型
在控制器类中有如下的方法,返回值是一个对象,不同的是,test3() 方法显式地加上了@ModelAttribute注解,但无论是否显式声明,最终的结果都是将返回值加入modelAndView。(是否显式声明,都是通过ServletModelAttributeMethodProcessor处理器)
/**
* 将返回值加入modelAndView
* 默认是按照@RequestMapping("/test3")找视图名
* @return
*/
@ModelAttribute
// @RequestMapping("/test3")
public User test3() {
log.debug("test3()");
return new User("李四", 20);
}
public User test4() {
log.debug("test4()");
return new User("王五", 30);
}
默认是按照@RequestMapping() 中的路径找视图名,如果没有加@RequestMapping() 注解,则需要自定义视图名:
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.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); // 渲染视图
}
}
3.4、解析HttpEntity、HttpHeaders、加上了@ResponseBody 注解的对象返回值类型
在控制器中有如下三个方法:
- HttpEntity代表返回响应体信息并转换成JSON,被HttpEntityMethodProcessor处理
- HttpHeaders代表返回响应头信息,被HttpHeadersReturnValueHandler处理
- 加上了@ResponseBody 注解的方法代表返回响应头和响应体,并且会转换成JSON格式,被RequestResponseBodyMethodProcessor 处理
这三种返回类型的共同点在于,无需走视图解析:
用于标记请求已经被处理,无需继续渲染视图。
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);
}
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));
}
}
}
System.out.println(container.getModel());
System.out.println(container.getViewName());获取到的都是null
小结:
ModelAndView、String、未被@ResponseBody 注解控制的对象类型返回值(无论是否显式声明了@ModelAttribute),都要经历视图渲染的过程。
ModelAndView找视图,是根据ModelAndView构造中的viewName寻找同名的视图,还会使用.addObject() 方法中指定的数据对视图进行渲染 。
如果没有指定视图名,则会:
- 根据请求路径推断视图名: 如果在处理器方法中没有显式指定视图名,Spring MVC 会根据请求路径来推断视图名。
- 根据返回值类型推断视图名: 如果处理器方法的返回值类型是String类型,并且没有使用 @ResponseBody 注解,Spring MVC 会将返回的字符串作为视图名处理。
- 默认视图名: 如果以上两种方式都没有找到视图名,Spring MVC 会使用默认的视图名。默认的视图名通常是处理器方法所在的类名转换而来,再加上适当的前后缀。
String找视图,是根据返回值的名称去找同名的视图。
未被@ResponseBody 注解控制的对象类型返回值,找视图时,如果方法上使用 @RequestMapping("/") 及其派生注解声明了路径,则按照路径的值去匹配视图。如果没有,则需要手动指定路径。
HttpEntity、HttpHeaders、加上了@ResponseBody 注解的对象返回值类型,因为对应解析器的handleReturnValue 方法中标记了请求已经被处理,无需继续渲染视图,所以不走渲染视图流程。其区别在于返回的响应头和响应体的完整性。
4、ResponseBodyAdvice
ResponseBodyAdvice是@ControllerAdvice三大增强中的一种,前面提到过@InitBinder和@ModelAttribute。
ResponseBodyAdvice可用于返回值的统一处理,例如在项目开发中,通常会把返回信息封装成一个统一返回类:
@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);
}
}
但是有些返回信息封装成了Result返回类,有一些是直接返回的,我们可以通过自定义一个类实现ResponseBodyAdvice接口做统一处理:
实现了ResponseBodyAdvice接口需要重写其中两个方法
- supports():判断支持转换的条件
- beforeBodyWrite():增强的逻辑
@ControllerAdvice
static class MyResponseAdvice implements ResponseBodyAdvice<Object> {
/**
* 判断支持转换的条件
*
* @param returnType 返回值类型
* @param converterType 转换类型
* @return
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
//如果方法上加了ResponseBody注解,或者类上加了ResponseBody/RestController注解,才进行转换
if (returnType.getMethodAnnotation(ResponseBody.class) != null
|| AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null) {
return true;
}
return false;
}
/**
* 增强的逻辑
*
* @param body 返回值
* @param returnType 返回类型
* @param selectedContentType 所选的响应内容类型。
* @param selectedConverterType 所选的消息转换器类型。
* @param request 当前的请求对象。
* @param response 当前的响应对象。
* @return
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//如果返回值就是Result类型则直接返回
if (body instanceof Result) {
return body;
}
//否则包装成Result类型返回
return Result.ok(body);
}
}
和自定义参数解析器,返回值解析器类似。