spring mvc源代码
在为非Spring Boot应用程序的执行器编码的过程中,我遇到了一个有趣的问题,即在哪里实际放置一小段通用代码。 这篇文章试图列出所有可用的选项,以及它们在特定情况下的利弊。
作为一个具体示例,让我们使用REST端点返回可通过/jvmprops
子上下文访问的所有JVM属性的映射。 此外,我想提供的选项不仅是搜索单个属性, 例如 /jvmprops/java.vm.vendor
而且还允许过滤属性的子集, 例如 /jvmprops/java.vm.*
。
目前的情况
该代码仅针对Spring应用程序的无聊准则而设计。 上层由控制器组成。 它们使用@RestController
进行注释,并提供REST终结@RestController
,这些终结点可以作为@RequestMapping
注释的方法使用。 这些方法依次调用实现为服务的第二层。
如上所示,过滤器模式本身是最后一个路径段。 通过@PathVariable
批注将其映射到方法参数。
@RestControllerclassJvmPropsController(privatevalservice:JvmPropsService){
@RequestMapping(path=arrayOf("/jvmprops/{filter}"),method=arrayOf(GET))
funreadJvmProps(@PathVariablefilter:String):Map<String,*>=service.getJvmProps()
}
为了有效地执行过滤,路径段允许使用星号字符。 但是,在Java中,字符串匹配是通过正则表达式实现的。 然后,必须将简单的调用模式“转换”为完整的正则表达式。 对于上述示例,不仅需要转义点字符-from .
而是\\.
,但需要相应翻译星号- 到
.
:
valregex=filter.replace(".","\\.").replace("*",".*")
然后,关联的服务返回过滤后的地图,该地图又由控制器返回。 Spring Boot和Jackson负责JSON序列化。
简单的选择
一切都很好,直到需要额外的返回地图的端点(例如,获取环境变量),并且以上代码段最终都被复制粘贴到了每个代码段中。
当然必须有一个更好的解决方案,那么将此代码放在哪里?
在控制器父类中
最简单的方法是为所有控制器创建一个父类,将代码放在那里并显式调用它。
abstractclassArtificialController(){
funtoRegex(filter:String)=filter.replace(".","\\.").replace("*",".*")
}
@RestControllerclassJvmProps(privatevalservice:JvmPropsService):ArtificialController(){
@RequestMapping(path=arrayOf("/jvmprops/{filter}"),method=arrayOf(GET))
funreadJvmProps(@PathVariablefilter:String):Map<String,*>{
valregex=toRegex(filter)
returnservice.getJvmProps(regex)
}
}
这种方法有三个主要缺点:
- 它只是为了共享通用代码而创建了一个人工的父类。
- 其他控制器必须从此父类继承。
- 它需要显式调用,将转换的责任放在客户端代码中。 很有可能没有开发人员,只有创建该方法的开发人员才会使用它。
在服务父类中
可以在服务层中设置代码,而不是在控制器层的共享方法中设置代码。
具有与上述相同的缺点。
在第三方依赖中
代替人工的类层次结构,让我们介绍一个不相关的依赖类。 这将转换为以下代码。
classRegexer{
funtoRegex(filter:String)=filter.replace(".","\\.").replace("*",".*")
}
@RestControllerclassJvmProps(privatevalservice:JvmPropsService,
privatevalregexer:Regexer){
@RequestMapping(path=arrayOf("/jvmprops/{filter}"),method=arrayOf(GET))
funreadJvmProps(@PathVariablefilter:String):Map<String,*>{
valregex=regexer.toRegex(filter)
returnservice.getJvmProps(regex)
}
}
尽管偏向于继承而不是继承 ,但这种方法仍然存在很大的漏洞:需要客户端代码来调用共享的代码。
在Kotlin扩展功能中
如果允许在JVM上使用其他语言,则可能会受益于Kotlin的扩展功能:
interfaceArtificialController
funArtificialController.toRegex(filter:String)=filter.replace(".","\\.").replace("*",".*")
@RestControllerclassJvmProps(privatevalservice:JvmPropsService):ArtificialController{
@RequestMapping(path=arrayOf("/jvmprops/{filter}"),method=arrayOf(GET))
funreadJvmProps(@PathVariablefilter:String):Map<String,*>{
valregex=toRegex(filter)
returnservice.getJvmProps(regex)
}
}
与将代码放入父控制器相比,至少将代码本地化到文件中。 但是同样的缺点仍然存在,因此收益只是微不足道的。
更精致的替代品
上述重构在每种可能的情况下都有效。 以下选项专门适用于(Spring Boot)Web应用程序。
它们都遵循相同的方法:让我们以某种方式将控制器包装在将要执行的单个组件中,而不是显式调用共享代码。
在Servlet过滤器中
在Web应用程序中,必须在servlet筛选器中绑定不同控制器之前/之后执行的代码。
使用Spring MVC,这可以通过过滤器注册bean实现:
@Bean
funfilterBean()=FilterRegistrationBean().apply{
urlPatterns=arrayListOf("/jvmProps/*")
filter=object: Filter{
overridefundestroy(){}
overridefuninit(config:FilterConfig){}
overridefundoFilter(req:ServletRequest,resp:ServletResponse,chain:FilterChain){
chain.doFilter(httpServletReq,resp)
valhttpServletReq=reqasHttpServletRequest
valpaths=request.pathInfo.split("/")
if(paths.size>2){
valsubpaths=paths.subList(2,paths.size)
valfilter=subpaths.joinToString("")
valregex=filter.replace(".","\\.")
.replace("*",".*")
// Change the JSON here...
}
}
}
}
上面代码的好处是不需要控制器显式调用共享代码。 但是,还有一个不太明显的问题:至此,该映射已被序列化为JSON,并已处理为响应。 必须先将初始响应包装在响应包装器中,然后才能继续进行过滤器链并处理JSON(而不是内存中的数据结构)。
这种方式不仅很脆弱,而且对性能有巨大影响。
在Spring MVC拦截器中
不幸的是,将上述代码从Spring MVC拦截器中的过滤器中移出并没有任何改善。
一方面
转换字符串参数和过滤映射的需要是典型的横切关注点。 这是面向方面编程的典型用例。 代码如下所示:
@AspectclassFilterAspect{
@Around("execution(Map ch.frankel.actuator.controller.*.*(..))")
funfilter(joinPoint:ProceedingJoinPoint):Map<String,*>{
valmap=joinPoint.proceed()asMap<String,*>
valfilter=joinPoint.args[0]asString
valregex=filter.replace(".","\\.").replace("*",".*")
returnmap.filter{it.key.matches(regex.toRegex())}
}
}
选择此选项可以按预期方式进行。 另外,方面将自动应用于已配置包中返回映射的所有类的所有方法。
在Spring MVC建议中
Spring MVC中隐藏着一个漂亮的宝石:在控制器返回之后但在返回的值以JSON格式序列化之前 (由于使用@ Dr4K4n作为提示),将执行专门的建议。
该类只需要:
- 实现
ResponseBodyAdvice
接口 - 由
@ControllerAdvice
注释,以供Spring扫描,并控制将其应用于哪个包
@ControllerAdvice("ch.frankel.actuator.controller")
classTransformBodyAdvice():ResponseBodyAdvice<Map<String,Any?>>{
overridefunsupports(returnType:MethodParameter,converterType:Class<outHttpMessageConverter<*>>)=
returnType.method.returnType==Map::class.java
overridefunbeforeBodyWrite(map:Map<String,Any?>,methodParameter:MethodParameter,
mediaType:MediaType,clazz:Class<outHttpMessageConverter<*>>,
serverHttpRequest:ServerHttpRequest,serverHttpResponse:ServerHttpResponse):Map<String,Any?>{
valrequest=(serverHttpRequestasServletServerHttpRequest).servletRequest
valfilterPredicate=getFilterPredicate(request)
returnmap.filter(filterPredicate)
}
privatefungetFilterPredicate(request:HttpServletRequest):(Map.Entry<String,Any?>)->Boolean{
valpaths=request.pathInfo.split("/")
if(paths.size>2){
valsubpaths=paths.subList(2,paths.size)
valfilter=subpaths.joinToString("")
valregex=filter.replace(".","\\.")
.replace("*",".*")
.toRegex()
return{it.key.matches(regex)}
}
return{true}
}
}
不需要显式调用此代码,它将应用于已配置程序包中的所有控制器。 仅当方法的返回类型为Map
类型时才应用此方法(尽管由于类型擦除而没有泛型检查)。
更好的是,它为涉及进一步处理(订购,分页等)的未来开发铺平了道路。
结论
在Spring MVC应用程序中,有几种共享通用代码的方法,每种都有各自的优缺点。 在本文中,对于此特定用例, ResponseBodyAdvice
具有最大的优势。
这里的主要考虑是,围绕工具带的工具越多,最终选择就越好。 去探索一些您尚不了解的工具:今天阅读一些文档呢?
spring mvc源代码