Spring Trick - 重載方法參數繼承注解
在基于 Spring 的后端技術棧中,經常會將 Feign Client 與 Spring MVC 結合使用,Feign Client 負責維持 API 接口約定,Spring MVC 的 Controller 則負責實現 API。當一個 Controller 實現 Feign Client 接口時,方法上的注解可以不用再寫,但是參數的注解則需要再寫一遍,本文會介紹通過歪門邪道的方式,讓 Spring 支持讀取重載方法的參數繼承的注解。
關于 Spring Trick
本系列將主要介紹一些關于 Spring 的歪門邪道用法與技巧,我也不知道會有幾期,可能也就這么一期吧。由于都是些歪門邪道的做法,我這里為了實現效果,可能會不擇手段,如果不是十分了解其中的工作原理,請絕對不要模仿!另外,本文所有代碼皆為 Kotlin,珍愛生命,遠離 Java。
Feign Client 與 Controller
如果一個后端程序是基于 JVM 的,那么基本上 90% 都是基于 Spring 了,這個時候為了多個服務的 API 接口之間調用方便,一般會把項目分為兩個工程。一個是 Feign Client,只是定義一些 Feign Client,從而定義整個 API 的接口與調用約定。另一個則是真正的 Spring 項目,這里會從之前的 Feign Client 接口創建 Controller,從而實現接口實現與調用的約定。
大概就是這樣的感覺:
// Feign Client 定義
@FeignClient(name = "Api")
interface ApiFeignClient {
@RequestMapping(value = "/api/test", method = arrayOf(RequestMethod.GET))
fun getTestResult(): ResultModel
}
// Controller 實現,通過 Feign Client 保證調用約定
@RestController
class ApiController : ApiFeignClient {
override fun getTestResult(): ResultModel {
// Business code here
}
}
但是如果當一個 API 接口具有多種類型的參數,這種看上去非常優雅的寫法,可能就變得不那么優雅了。
@FeignClient(name = "Editor")
interface ApiFeignClient {
@RequestMapping(value = "/api/test/{id}", method = arrayOf(RequestMethod.GET))
fun getTestResult(@PathVariable("id") id: String, @RequestParam(value = "paging", required = false) paging:String?): ResultModel;
}
@RestController
public class ApiController implements ApiFeignClient {
@Override
fun getTestResult(@PathVariable("id") id: String, @RequestParam(value = "paging", required = false) paging:String?): ResultModel{
// Business code here
}
}
雖然方法上面的 RequestMapping 注解不需要重新寫一遍,但是像參數上面的注解還是需要再重新寫一遍,而且需要保證兩邊一致,這樣讓 Feign Client 在接口調用約定上少了一截作用。還是有一些調用約定是基于代碼上一致,在開發過程中非常容易遺漏這一點,導致一些 BUG 的出現。
核心原理
為什么接口參數上的注解,在實現其接口的類上就沒有了呢?這是由于編譯時重寫方法相當于創建了一個全新的方法,兩個方法不是同一個 Method 反射對象,自然也就是不能獲取到了。
Java 或者 Kotlin 的本身的反射是無法直接通過類方法獲取到其接口方法上的參數注解的,所以這里我們需要為其寫一些工具方法和拓展方法。
object ClassUtils {
fun getInheritedParamAnnotations(method: Method, index: Int): List<Annotation> {
val type = method.declaringClass
return getAnnotationsComplete(type, method, index)
}
inline fun <reified T> getInheritedParamAnnotation(method: Method, index: Int): T?{
return getAnnotationsComplete(method, index).firstOrNull{ it is T } as T?
}
private fun getInheritedParamAnnotations(clazz: Class<*>?, method: Method, index: Int): List<Annotation> {
clazz ?: return emptyList()
if (index > method.parameterAnnotations.size) {
throw IllegalArgumentException("Index out of bound")
}
val set = try {
clazz.getDeclaredMethod(method.name, *method.parameterTypes).let {
it.parameterAnnotations[index].toMutableSet()
}
}catch (exception: NoSuchMethodException){
hashSetOf<Annotation>()
}
set += clazz.interfaces.map { getAnnotationsComplete(it, method, index) }.flatten()
set += getAnnotationsComplete(clazz.superclass, method, index)
return set.toList()
}
}
原理很簡單,大概分為這么幾步:
- 先從當前方法獲取到方法所在的類,通過反射先獲取到這個方法參數的注解
- 然后獲取到當前這個類實現接口,通過反射嘗試獲取簽名相同的方法,然后獲取參數的注解
- 最后再拿到基類,一直遞歸下去即可
- 最后的最后,所有的結果去重,就是我們要的注解了
這個的排序是當前類方法最優先,其次是實現的接口方法,然后是父類方法
不修改 Spring 代碼實現
上面介紹的方法需要修改 Spring 的代碼,在現在官方沒有實現的情況下,自己編譯一整套 Spring 是很麻煩的事情,而且還有各種依賴管理,所以接下來就開始我們的歪門邪道了。
通過繼承 Spring 本身的 MethodArgumentResolver 實現
經過查看 Spring 的源碼,簡單的了解 Spring 的 PathVariable 注解的實現與原理,其實就是有一個 MethodArgumentResolver 會在請求到來的時候,來將 HTTP 請求上的各種參數解釋成參數,并傳給 Controller,這個 MethodArgumentResolver 會讀取注解來判斷應該從什么地方拿參數,例如 Query String,Request Body 與 請求 URL 的占位符之類的。
這里我們先從最簡單的 PathVariableMethodArgumentResolver 開始,這個類就是用于解析 PathVariable 注解并提供參數給 Controller。
class SuperClassPathVariableMethodArgumentResolver: PathVariableMethodArgumentResolver() {
override fun supportsParameter(parameter: MethodParameter): Boolean {
val superResult = super.supportsParameter(parameter)
if(superResult){
return false
}
return parameter.getInheritedParamAnnotation<PathVariable>() != null
}
override fun createNamedValueInfo(parameter: MethodParameter): AbstractNamedValueMethodArgumentResolver.NamedValueInfo {
val annotation = parameter.getInheritedParamAnnotation<PathVariable>() ?: throw Exception()
return PathVariableNamedValueInfo(annotation)
}
override fun contributeMethodArgument(parameter: MethodParameter, value: Any?,
builder: UriComponentsBuilder, uriVariables: MutableMap<String, Any>, conversionService: ConversionService) {
val annotation = parameter.getInheritedParamAnnotation<PathVariable>()
val name = annotation?.name ?: parameter.parameterName
uriVariables.put(name, formatUriValue(conversionService, TypeDescriptor(parameter.nestedIfOptional()), value))
}
private class PathVariableNamedValueInfo(annotation: PathVariable) : AbstractNamedValueMethodArgumentResolver.NamedValueInfo(annotation.name, annotation.required, ValueConstants.DEFAULT_NONE)
}
通過繼承 PathVariableMethodArgumentResolver 并重載了幾個關鍵方法,將原來的直接從參數上面拿注解改成了通過之前我們寫的能獲取繼承的注解的方法,就可以了。
對于 RequestParamMethodArgumentResolver 也是如法炮制。
class SuperClassRequestParamMethodArgumentResolver: RequestParamMethodArgumentResolver {
private val useDefaultResolution: Boolean
constructor(useDefaultResolution: Boolean): super(useDefaultResolution) {
this.useDefaultResolution = useDefaultResolution
}
constructor(beanFactory: ConfigurableBeanFactory, useDefaultResolution: Boolean): super(beanFactory, useDefaultResolution) {
this.useDefaultResolution = useDefaultResolution
}
override fun supportsParameter(parameter: MethodParameter): Boolean {
val superResult = super.supportsParameter(parameter)
if(superResult){
return false
}
return parameter.getInheritedParamAnnotation<RequestParam>() != null
}
override fun createNamedValueInfo(parameter: MethodParameter): AbstractNamedValueMethodArgumentResolver.NamedValueInfo {
val annotation = parameter.getInheritedParamAnnotation<RequestParam>()
return annotation?.let { RequestParamNamedValueInfo(it) } ?: RequestParamNamedValueInfo()
}
override fun contributeMethodArgument(parameter: MethodParameter, value: Any?,
builder: UriComponentsBuilder, uriVariables: Map<String, Any>, conversionService: ConversionService) {
val annotation = parameter.getInheritedParamAnnotation<RequestParam>()
val name = annotation?.name ?: parameter.parameterName
when (value) {
null -> {
annotation?.let {
if (!it.required || it.defaultValue != ValueConstants.DEFAULT_NONE) {
return
}
}
builder.queryParam(name)
}
is Collection<*> -> for (element in (value as Collection<*>?)!!) {
builder.queryParam(name, formatUriValue(conversionService, TypeDescriptor.nested(parameter, 1), element))
}
else -> builder.queryParam(name, formatUriValue(conversionService, TypeDescriptor(parameter), value))
}
}
private class RequestParamNamedValueInfo : AbstractNamedValueMethodArgumentResolver.NamedValueInfo {
constructor() : super("", false, ValueConstants.DEFAULT_NONE)
constructor(annotation: RequestParam) : super(annotation.name, annotation.required, annotation.defaultValue)
}
}
為了簡單起見,我在這里做了些許功能的簡化,例如不支持 Map 的 Request Param 之類的。
最后是 RequestResponseBodyMethodProcessor 這個東西有些復雜,我們放到寫一個章節再說。
Bean 的循環依賴
RequestResponseBodyMethodProcessor 的構造函數是需要有 Message Converter 與 Request Response Body Advice 的,這些東西我們在構造時基本上是不可能有的。
由于 Spring 的注入機制問題,我們自己寫的 MethodArgumentResolver 通過 WebMvcConfigurerAdapter 注入時,總是會在 Spring 默認的 MethodArgumentResolver 前面。
這個時候 Message Converter 與 Request Response Body Advice 基本上也沒有創建好,這個東西又存放于一個類型為 RequestMappingHandlerAdapter 的 Bean 里面。
那么,我們就遇到了 Bean 的循環依賴的問題,要想加入我們自己定義的 MethodArgumentResolver 必須要先有 RequestMappingHandlerAdapter ,而 RequestMappingHandlerAdapter 是在我們自定義的配置創建完成之后才會注入。
RequestMappingHandlerAdapter 等著我們完成注入,我們需要等 RequestMappingHandlerAdapter 完成注入,直接在 WebMvcConfigurerAdapter 中加上自動填裝的 RequestMappingHandlerAdapter 就會導致整個 Spring 應用根本跑起不來。
那么,如何解決這個問題呢?要想完成整個注入過程,那么勢必需要有一方讓步, RequestMappingHandlerAdapter 是不可能讓步的,那就只能我們讓步了。我這里采取了惰性構建,與套殼注入的原理,讓我們的 MethodArgumentResolver 能夠稍晚一步創建。
class SuperClassRequestBodyMethodArgumentResolver(val context: ApplicationContext) : HandlerMethodArgumentResolver {
var requestBodyMethodProcessor: SuperClassRequestBodyMethodProcessor? = null
override fun resolveArgument(parameter: MethodParameter, mavContainer: ModelAndViewContainer, webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory): Any {
createProcessor()
return requestBodyMethodProcessor?.resolveArgument(parameter, mavContainer, webRequest, binderFactory) ?: throw Exception("Create 'SuperClassRequestBodyMethodProcessor' failed")
}
override fun supportsParameter(parameter: MethodParameter): Boolean {
createProcessor()
return requestBodyMethodProcessor?.supportsParameter(parameter) ?: throw Exception("Create 'SuperClassRequestBodyMethodProcessor' failed")
}
private fun createProcessor(){
if(requestBodyMethodProcessor == null){
val adapter = context.getBean(RequestMappingHandlerAdapter::class.java)
requestBodyMethodProcessor = SuperClassRequestBodyMethodProcessor(adapter.messageConverters, adapter.getPrivateField<List<Any>>("requestResponseBodyAdvice") ?: throw Exception("Can't get 'requestResponseBodyAdvice'"))
}
}
class SuperClassRequestBodyMethodProcessor : RequestResponseBodyMethodProcessor {
constructor(converters: List<HttpMessageConverter<*>>) : super(converters) {
}
constructor(converters: List<HttpMessageConverter<*>>,
manager: ContentNegotiationManager) : super(converters, manager) {
}
constructor(converters: List<HttpMessageConverter<*>>,
requestResponseBodyAdvice: List<Any>) : super(converters, null, requestResponseBodyAdvice) {
}
constructor(converters: List<HttpMessageConverter<*>>,
manager: ContentNegotiationManager, requestResponseBodyAdvice: List<Any>) : super(converters, manager, requestResponseBodyAdvice) {
}
override fun supportsParameter(parameter: MethodParameter): Boolean {
return parameter.getInheritedParamAnnotation<RequestBody>() != null
}
override fun checkRequired(parameter: MethodParameter): Boolean {
return (parameter.getInheritedParamAnnotation<RequestBody>()?.required ?: false) && !parameter.isOptional
}
}
}
在上述代碼中,有一個 SuperClassRequestBodyMethodArgumentResolver 類,它只是一個殼,讓 Spring 在早期的時候注入進去,其實它什么也不會干,它的構造參數有一個當前應用的上下文,通過這個上下文,我們在之后可以拿到注入好的 RequestMappingHandlerAdapter 。
SuperClassRequestBodyMethodProcessor 類,才是真正干活的類,但是他的構造參數所需要的條件在注入的時候沒有,所以我們在這個時候通過其套殼的類 SuperClassRequestBodyMethodArgumentResolver ,先把實例注入進去,在第一次調用,也就是第一個請求到來的時候,再構建 SuperClassRequestBodyMethodProcessor 這個時候肯定能夠通過應用上下文獲取到 RequestMappingHandlerAdapter 。完美的回避了循環依賴的問題。
私有字段?!
RequestMappingHandlerAdapter 中可以直接獲取到 Message Converter,但是 Request Response Body Advice 卻只有 setter,無法直接獲取到,這個時候就需要祭出大殺器--反射了。
通過反射,我們可以直接獲取到一個對象的私有字段的值,雖然,并不推薦這么做。反正我們這次是歪門邪道用法,也沒辦法了。
object ClassUtils {
fun <T> getPrivateField(any: Any, name: String): T?{
return any.javaClass.getDeclaredField(name).apply { isAccessible = true }.get(any) as T?
}
}
直接拿到反射出來的 Field 對象,然后將其可訪問性改成 true,這樣所就能通過這個獲取私有字段的值。
實現了這些之后我們就可以獲得一個十分清爽的 Controller 了,再也不用擔心忘記加注解了。
@FeignClient(name = "Editor")
interface ApiFeignClient {
@RequestMapping(value = "/api/test/{id}", method = arrayOf(RequestMethod.GET))
fun getTestResult(@PathVariable("id") id: String, @RequestParam(value = "paging", required = false) paging:String?): ResultModel;
}
@RestController
public class ApiController implements ApiFeignClient {
@Override
fun getTestResult(id: String, paging:String?): ResultModel{
// Business code here
}
}
修改 Spring 代碼的實現方案
其實終極的解決方案應該是讓 Spring 本身支持獲取繼承參數注解,本文就不詳細說明了,Java 版的代碼與如何修改 Spring 的代碼實現該功能,可以參考這個 SPR-16216
來自:http://blog.higan.me/spring-trick-1/