掌握@ControllerAdvice配合RequestBodyAdvice/ResponseBodyAdvice使用,讓你的選擇不僅僅只有攔截器【享學Spring MVC】
- 2019 年 10 月 10 日
- 筆記
版權聲明:本文為部落客原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接:https://blog.csdn.net/f641385712/article/details/101396307
前言
要麼出眾,要麼出局(stand out or get out)。
前言
我們在實際的項目開發中,肯定會有這樣的需求:請求時記錄請求日誌,返回時記錄返回日誌;對所有的入參解密,對所有的返回值加密…。這些都是與業務沒關係的花邊但又不可缺少的功能,若你全都寫在Controller
的方法內部,那將造成大量的程式碼重複且嚴重干擾了業務程式碼的可讀性。 怎麼破?可能你第一反應想到的是使用Spring MVC
的HandlerInterceptor
攔截器來做,沒毛病,相信大部分公司的同學也都是這麼來乾的。那麼本文就介紹一種更為優雅、更為簡便的實現方案:使用@ControllerAdvice
+ RequestBodyAdvice/ResponseBodyAdvice
不僅僅只有攔截器一種。
@ControllerAdvice / @RestControllerAdvice
對於這個註解你可能即熟悉,卻又覺得陌生。熟悉是因為你看到很多項目都使用了@ControllerAdvice + @ExceptionHandler
來實現全局異常捕獲;陌生在於你除了copy程式碼時看到過外,自己似乎從來沒有真正使用過它。 在前面關於@ModelAttribute和@InitBinder 的相關文章中其實和這個註解是打過照面的:在此註解標註的類上使用@InitBinder
等註解可以使得它對"全局"生效實現統一的控制。本文將把@ControllerAdvice
此註解作為重點進一步的去了解它的使用以及工作機制。
此類的命名是很有資訊量的:Controller
的Advice
通知。關於Advice
的含義,熟悉AOP
相關概念的同學就不會陌生了,因此可以看到它整體上還是個AOP
的設計思想,只是實現方式不太一樣而已。
@ControllerAdvice
使用AOP
思想可以這麼理解:此註解對目標Controller
的通知是個環繞通知,織入的方式是註解方式,增強器是註解標註的方法。如此就很好理解@ControllerAdvice
搭配@InitBinder/@ModelAttribute/@ExceptionHandler
起到的效果嘍~
使用示例
最簡單的示例前文有過,這裡摘抄出一小段:
@RestControllerAdvice public class MyControllerAdvice { @InitBinder public void initBinder(WebDataBinder binder) { //binder.setDisallowedFields("name"); binder.registerCustomEditor(String.class, new StringTrimmerEditor()); } }
這樣我們的@InitBinder
標註的方法對所有的Controller
都是生效的。(@InitBinder
寫在Controller
內部只對當前處理器生效)
原理分析
接下來就看看這個註解到底是怎麼work的,做到知其然,知其所以然。
// @since 3.2 @Target(ElementType.TYPE) // 只能標註在類上 @Retention(RetentionPolicy.RUNTIME) @Documented @Component // 派生有組件註解的功能 public @interface ControllerAdvice { @AliasFor("basePackages") String[] value() default {}; @AliasFor("value") String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; Class<?>[] assignableTypes() default {}; Class<? extends Annotation>[] annotations() default {}; }
官方doc說它可以和如上我指出的三個註解的一起使用。關於它的使用我總結有如下注意事項:
@ControllerAdvice
只需要標註上即可,Spring MVC
會在容器里自動探測到它(請確保能被掃描到,否則無效哦~)- 若有多個
@ControllerAdvice
可以使用@Order
或者Ordered
介面來控制順序 basePackageClasses
屬性最終也是轉換為了basePackages
拿去匹配的,相關程式碼如下:
HandlerTypePredicate: // 這是packages屬性本文:有一個判空的過濾器 public Builder basePackage(String... packages) { Arrays.stream(packages).filter(StringUtils::hasText).forEach(this::addBasePackage); return this; } // packageClasses最終都是轉換為了addBasePackage // 只是它的pachage值是:ClassUtils.getPackageName(clazz) // 說明:ClassUtils.getPackageName(String.class) --> java.lang public Builder basePackageClass(Class<?>... packageClasses) { Arrays.stream(packageClasses).forEach(clazz -> addBasePackage(ClassUtils.getPackageName(clazz))); return this; } private void addBasePackage(String basePackage) { this.basePackages.add(basePackage.endsWith(".") ? basePackage : basePackage + "."); }
- 它的
basePackages
掃包不支援佔位符Ant
形式的匹配。對於其他幾個屬性的匹配可參照下面這段匹配程式碼(我配上了文字說明):
HandlerTypePredicate: @Override public boolean test(Class<?> controllerType) { // 1、若所有屬性一個都沒有指定,那就是default情況-->作用於所有的Controller if (!hasSelectors()) { return true; } else if (controllerType != null) { // 2、注意此處的basePackage只是簡單的startsWith前綴匹配而已~~~ // 說明:basePackageClasses屬性最終都是轉為它來匹配的, // 如果寫了一個Controller類匹配上了,那它所在的包下所有的都是匹配的(因為同包嘛) for (String basePackage : this.basePackages) { if (controllerType.getName().startsWith(basePackage)) { return true; } } // 3、指定具體的Class類型,只會匹配數組裡面的這些類型,精確匹配。 for (Class<?> clazz : this.assignableTypes) { if (ClassUtils.isAssignable(clazz, controllerType)) { return true; } } // 4、根據類上的註解類型來匹配(若你想個性化靈活配置,可以使用這種方式) for (Class<? extends Annotation> annotationClass : this.annotations) { if (AnnotationUtils.findAnnotation(controllerType, annotationClass) != null) { return true; } } } return false; }
說明一點:若註解的多個屬性都給值,它們是取並集的關係。
針對於@RestControllerAdvice
,它就類似於@RestController和@Controller
之間的區別,在@ControllerAdvice
的基礎上帶有@ResponseBody
的效果。
@ControllerAdvice
在容器初始化的時候被解析,偽程式碼如下:
所有的被標註有此註解的Bean最終都變成一個
org.springframework.web.method.ControllerAdviceBean
,它內部持有Bean本身,以及判斷邏輯器(HandlerTypePredicate
)的引用
RequestMappingHandlerAdapter: @Override public void afterPropertiesSet() { // Do this first, it may add ResponseBody advice beans initControllerAdviceCache(); ... } private void initControllerAdviceCache() { // 因為它需要通過它去容器內找到所有標註有@ControllerAdvice註解的Bean們 if (getApplicationContext() == null) { return; } // 關鍵就是在findAnnotatedBeans方法里:傳入了容器上下文 List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); // 注意此處是有個排序的~~~~ AnnotationAwareOrderComparator.sort(adviceBeans); ... // 注意:找到這些標註有@ControllerAdvice後並不需要保存下來。 // 而是一個一個的找它們裡面的@InitBinder/@ModelAttribute 以及 RequestBodyAdvice和ResponseBodyAdvice // 說明:異常註解不在這裡解析,而是在`ExceptionHandlerMethodResolver`里~~~ for (ControllerAdviceBean adviceBean : adviceBeans) { ... } } ControllerAdviceBean: // 找到容器內(包括父容器)所有的標註有@ControllerAdvice的Bean們~~~ public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) { return Arrays.stream(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) .filter(name -> context.findAnnotationOnBean(name, ControllerAdvice.class) != null) .map(name -> new ControllerAdviceBean(name, context)) .collect(Collectors.toList()); }
這就是@ControllerAdvice
被解析、初始化的原理。它提供一個書寫Advice增強器的平台,在初始化的時候根據此類完成解析各種註解作用於各個功能上,從而在運行期直接運行即可。
RequestBodyAdvice/ResponseBodyAdvice
顧名思義,它們和@RequestBody
和@ResponseBody
有關,ResponseBodyAdvice
是Spring4.1
推出的,另外一個是4.2
後才有。它哥倆和@ControllerAdvice
一起使用會有很好的化學反應
說明:這哥倆是介面不是註解,實現類需要自己提供實現
RequestBodyAdvice
官方解釋為:允許body體轉換為對象之前進行自定義訂製;也允許該對象作為實參傳入方法之前對其處理。
public interface RequestBodyAdvice { // 第一個調用的。判斷當前的攔截器(advice是否支援) // 注意它的入參有:方法參數、目標類型、所使用的消息轉換器等等 boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); // 如果body體木有內容就執行這個方法(後面的就不會再執行嘍) Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); // 重點:它在body被read讀/轉換**之前**進行調用的 HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException; // 它在body體已經轉換為Object後執行。so此時都不拋出IOException了嘛~ Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); }
它的內置實現有這些:
RequestResponseBodyAdviceChain
比較特殊,放在後面重點說明。RequestBodyAdviceAdapter
沒啥說的,因此主要看看JsonViewRequestBodyAdvice
這個實現。
JsonViewRequestBodyAdvice
Spring MVC
的內置實現,它支援的是Jackson的com.fasterxml.jackson.annotation.@JsonView
這個註解,@JsonView
一般用於標註在HttpEntity/@RequestBody
上,來決定處理入參的哪些key。 該註解指定的反序列視圖將傳遞給MappingJackson2HttpMessageConverter
,然後用它來反序列化請求體(從而做對應的過濾)。
// @since 4.2 public class JsonViewRequestBodyAdvice extends RequestBodyAdviceAdapter { // 處理使用的消息轉換器是AbstractJackson2HttpMessageConverter類型 // 並且入參上標註有@JsonView註解的 @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return (AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType) && methodParameter.getParameterAnnotation(JsonView.class) != null); } // 顯然這裡實現的beforeBodyRead這個方法: // 它把body最終交給了MappingJacksonInputMessage來反序列處理消息體 // 注意:@JsonView能處理這個註解。也就是說能指定把消息體轉換成指定的類型,還是比較實用的 // 可以看到當標註有@jsonView註解後 targetType就沒啥卵用了 @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> selectedConverterType) throws IOException { JsonView ann = methodParameter.getParameterAnnotation(JsonView.class); Assert.state(ann != null, "No JsonView annotation"); Class<?>[] classes = ann.value(); // 必須指定class類型,並且有且只能指定一個類型 if (classes.length != 1) { throw new IllegalArgumentException("@JsonView only supported for request body advice with exactly 1 class argument: " + methodParameter); } // 它是一個InputMessage的實現 return new MappingJacksonInputMessage(inputMessage.getBody(), inputMessage.getHeaders(), classes[0]); } }
說明:這個類只要你導入了jackson的jar,默認就會被添加進去,so註解@JsonView
屬於天生就支援的。偽程式碼如下:
WebMvcConfigurationSupport: @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { ... if (jackson2Present) { adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice())); adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice())); } ... }
使用示例
@Getter @Setter @ToString public static class User { @JsonView({Simple.class, Complex.class}) private Long id; @JsonView({Simple.class, Complex.class}) private String name; @JsonView({Complex.class}) private Integer age; } // 準備兩個view類型(使用介面、類均可) interface Simple {} interface Complex {}
至於我為何這麼準備示例,有興趣的同學可以了解下@JsonView
註解的用法和使用場景,你便會有所收穫。
繼續準備一個控制器,使用@JsonView
來指定視圖類型:
@ResponseBody @PostMapping("/test/requestbody") public String testRequestBodyAdvice(@JsonView(Simple.class) @RequestBody User user) { System.out.println(user); return "hello world"; }
這時候請求(發送的body里有age這個key哦):
控制台輸出:
HelloController.User(id=1, name=fsx, age=null)
可以看到即使body體里有age這個key,服務端也是不會給與接收的(age仍然為null),就因為我要的是Simple類型的JsonView。這個時候若換成@JsonView(Complex.class)
那最終的結果就為:
HelloController.User(id=1, name=fsx, age=18)
使用時需要注意如下幾點:
- 若不標註
@JsonView
註解,默認是接收所有(這是我們絕大部分的使用場景) @JsonView
的value有且只能寫一個類型(必須寫)- 若
@JsonView
指定的類型,在POJO的所有屬性(或者set方法)里都沒有@JsonView
對應的指定,那最終一個值都不會接收(因為一個都匹配不上)。
@JsonView
執行原理簡述
簡單說說@JsonView
在生效的原理。它主要是在AbstractJackson2HttpMessageConverter
的這個方法里(這就是為何JsonViewRequestBodyAdvice
只會處理這種消息轉轉器的原因):
AbstractJackson2HttpMessageConverter(實際為MappingJackson2HttpMessageConverter): @Override public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { JavaType javaType = getJavaType(type, contextClass); // 把body內的東西轉換為java對象 return readJavaType(javaType, inputMessage); } private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException { if (inputMessage instanceof MappingJacksonInputMessage) { Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView(); if (deserializationView != null) { return this.objectMapper.readerWithView(deserializationView).forType(javaType).readValue(inputMessage.getBody()); } } return this.objectMapper.readValue(inputMessage.getBody(), javaType); }
因為標註了@JsonView
註解就使用的是它MappingJacksonInputMessage
。so可見最底層的原理就是readerWithView
和readValue
的區別。
ResponseBodyAdvice
它允許在@ResponseBody/ResponseEntity
標註的處理方法上在用HttpMessageConverter
在寫數據之前做些什麼。
// @since 4.1 泛型T:body類型 public interface ResponseBodyAdvice<T> { boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType); @Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response); }
它的內置實現類們:
AbstractMappingJacksonResponseBodyAdvice
它做出了限定:body使用的消息轉換器必須是AbstractJackson2HttpMessageConverter
才會生效。
public abstract class AbstractMappingJacksonResponseBodyAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType); } // 最終使用MappingJacksonValue來序列化body體 @Override @Nullable public final Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType contentType, Class<? extends HttpMessageConverter<?>> converterType, ServerHttpRequest request, ServerHttpResponse response) { if (body == null) { return null; } MappingJacksonValue container = getOrCreateContainer(body); beforeBodyWriteInternal(container, contentType, returnType, request, response); return container; } }
JsonViewResponseBodyAdvice
繼承自父類,用法幾乎同上面的@JsonView
,只是它是標註在方法返回值上的。
它的源碼此處忽略,沒什麼特別的需要說明的
使用示例
準備一個控制器如下(其它的同上):
@ResponseBody @GetMapping("/test/responsebody") @JsonView(Simple.class) public User testResponseBodyAdvice() { User user = new User(); user.setId(1L); user.setName("fsx"); user.setAge(18); return user; }
請求結果如下:
它的使用注意事項同上,基本原理同上(writerWithView/writer的區別
)。
RequestResponseBodyAdviceChain
它是代理模式的實現,用於執行指定的RequestBodyAdvice/ResponseBodyAdvice
們,實現方式基本同前面講過多次的xxxComposite
模式。
需要注意的是,兩個advice的
support()
方法都只只只在這裡被調用。所以很容易相想到Spring調用advice增強時最終調用的都是它,它就是一個門面。
// @since 4.2 請注意:它的訪問許可權是default哦 class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice<Object> { //它持有所有的,記住是所有的advice們 private final List<Object> requestBodyAdvice = new ArrayList<>(4); private final List<Object> responseBodyAdvice = new ArrayList<>(4); // 可以看到這是個通用的方法。內來進行區分存儲的 getAdviceByType這個區分方法可以看一下 // 兼容到了ControllerAdviceBean以及beanType本身 public RequestResponseBodyAdviceChain(@Nullable List<Object> requestResponseBodyAdvice) { this.requestBodyAdvice.addAll(getAdviceByType(requestResponseBodyAdvice, RequestBodyAdvice.class)); this.responseBodyAdvice.addAll(getAdviceByType(requestResponseBodyAdvice, ResponseBodyAdvice.class)); } @Override public boolean supports(MethodParameter param, Type type, Class<? extends HttpMessageConverter<?>> converterType) { throw new UnsupportedOperationException("Not implemented"); } @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { throw new UnsupportedOperationException("Not implemented"); } // 可以看到最終都是委託給具體的Advice去執行的(supports方法) // 特點:符合條件的所有的`Advice`都會順序的、依次的執行 @Override public HttpInputMessage beforeBodyRead(HttpInputMessage request, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) { if (advice.supports(parameter, targetType, converterType)) { request = advice.beforeBodyRead(request, parameter, targetType, converterType); } } return request; } ... // 其餘方法略。處理邏輯同上順序執行。 // 最重要的是如下這個getMatchingAdvice()匹配方法 private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) { // 簡單的說你想要的是Request的還是Response的List呢? List<Object> availableAdvice = getAdvice(adviceType); if (CollectionUtils.isEmpty(availableAdvice)) { return Collections.emptyList(); } List<A> result = new ArrayList<>(availableAdvice.size()); for (Object advice : availableAdvice) { if (advice instanceof ControllerAdviceBean) { ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice; // 這裡面會調用beanTypePredicate.test(beanType)方法 // 也就是根據basePackages等等判斷此advice是否是否要作用在本類上 if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) { continue; } advice = adviceBean.resolveBean(); } // 當前的advice若是滿足類型要求的,那就添加進去 最終執行切面操作 if (adviceType.isAssignableFrom(advice.getClass())) { result.add((A) advice); } } return result; } }
這是批量代理模式的典型實現,Spring
框架中不乏這種實現方式,對使用者非常友好,也很容易控制為鏈式執行或者短路執行。
初始化解析流程分析
我們知道所有的xxxBodyAdvice
最終都是通過暴露的RequestResponseBodyAdviceChain
來使用的,它內部持有容器內所有的Advice的引用。由於RequestResponseBodyAdviceChain
的訪問許可權是default
,所以這套機制完全由Spring
內部控制。 他唯一設值處是:AbstractMessageConverterMethodArgumentResolver
。
AbstractMessageConverterMethodArgumentResolver(一般實際為RequestResponseBodyMethodProcessor): // 唯一構造函數,指定所有的advices public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters, @Nullable List<Object> requestResponseBodyAdvice) { Assert.notEmpty(converters, "'messageConverters' must not be empty"); this.messageConverters = converters; this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters); this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice); }
此構造函數在new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)
時候調用,傳進來的requestResponseBodyAdvice
就剛好是在初始化RequestMappingHandlerAdapter
的時候全局掃描進來的所有的增強器們。
使用場景
本文介紹了@ControllerAdvice
的使用以及它的解析原理,最重要的是結合RequestBodyAdvice/ResponseBodyAdvice
來實現類似攔截器的效果。在現在前後端分離的開發模式下,大部分的情況下的請求是json格式,因此此種方式會有很大的用武之地,我舉例幾個經典使用場景供以參考:
- 列印請求、響應日誌
- 對參數解密、對響應加密
- 對請求傳入的非法字元做過濾/檢測
總結
本文旨在介紹@ControllerAdvice
和RequestBodyAdvice/ResponseBodyAdvice
的作用,為你解決在解決一些攔截問題時提供一個新的思路,希望能夠對你的眼界、程式碼結構上的把控能有所幫助。 同時也著重介紹了@JsonView
的使用:它可以放入參時接收指定的欄位;也可以讓返回值中敏感欄位(如密碼、鹽值等)不予返回,可做到非常靈活的配置和管理,實現一套程式碼多處使用的目的,提高集成程度。
咀咒,需要注意的是:xxxBodyAdvice
雖然使用方便,但是它的普適性還是沒有HandlerInterceptor
那麼強的,下面我列出使用它的幾點局限/限制:
xxxAdvice
必須被@ControllerAdvice
註解標註了才會生效,起到攔截的效果- 它只能作用於基於消息轉換器的請求/響應(參考註解
@RequestBody/@ResponseBody
) - 當然,只能作用於
@RequestMapping
模式下的處理器模型上