SpringMVC扩展点和案例说明
RequestMappingHandlerMapping 扩展点
重写这个类的扩展点如下:
public class CustRequeMappingHandlerMapping extends RequestMappingHandlerMapping {
//判断是否是处理的方法:基本上不需要重写
@Override
protected boolean isHandler(Class<?> beanType) {
return super.isHandler(beanType);
}
//获取了处理器的信息,封装到了RequestMappingInfo中,用于注册处理器,此时可以修改定义信息
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
//调用原有的逻辑,拿到处理器封装后的信息
RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
//增加自定义处理逻辑
return info;
}
//添加自定义的查询(类上的)匹配条件(请求过来是查询处理器使用)
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
return super.getCustomTypeCondition(handlerType);
}
//添加自定义的查询(方法上的)匹配条件(请求过来是查询处理器使用)
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
return super.getCustomMethodCondition(method);
}
//注册处理器,可以修改注册信息统一处理,然后再注册
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping){
super.registerMapping(mapping, handler, method);
}
//所有处理器都注册完成后回调的方法(暂时没发现场景需求)
@Override
protected void handlerMethodsInitialized(Map<RequestMappingInfo, HandlerMethod> handlerMethods) {
super.handlerMethodsInitialized(handlerMethods);
}
}
注册让自定义生效:
@Component
public class WebMvcRegistrationsImpl implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new CustRequeMappingHandlerMapping();
}
}
场景一:为URI统一加版本控制
需求1:目前项目中的一些Controller需要对url增加统一的版本控制,为了规范版本记号出现在url最开头,使用统一从处理方式
- 定义注解如下:默认是V1版本
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface APIVersion {
String version() default "V1";
}
- 在类中使用注解
@APIVersion
@RestController
@RequestMapping(value = "/demo")
public class DemoController {
@PostMapping("/user")
public User getUser(@ValidCust @RequestBody Grade grade, BindingResult result, HttpServletRequest request){
return new User("张三",15,grade);
}
}
- 重写处理器映射器getMappingForMethod方法
public class CustRequeMappingHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
//调用原有的逻辑,拿到处理器封装后的信息
RequestMappingInfo mapping = super.getMappingForMethod(method, handlerType);
//获取类上的注解
APIVersion annotation = AnnotationUtils.findAnnotation(handlerType, APIVersion.class);
if (annotation != null){
String version = annotation.version();
if (!version.startsWith("/")){
version = "/"+version;
}
//构建匹配器(路径)
String[] partten = new String[]{version};
PatternsRequestCondition newPartten = new PatternsRequestCondition(partten);
//将新的路径合并到旧路径
PatternsRequestCondition combine = newPartten.combine(mapping.getPatternsCondition());
RequestMappingInfo requestMappingInfo = new RequestMappingInfo(mapping.getName(), combine,
mapping.getMethodsCondition(),mapping.getParamsCondition(),mapping.getHeadersCondition(),
mapping.getConsumesCondition(),mapping.getProducesCondition(),mapping.getCustomCondition());
return requestMappingInfo;
}
//增加自定义处理逻辑
return mapping;
}
}
以上逻辑也可以在registerMapping方法中实现,查看结果如下:
因为我这里没有配置context-path根路径,所以V1就是路径开头,访问路径就是 http://localhost:15851/V1/demo/user
需求2:由于接口升级,新接口不能兼容旧接口,但是考虑到新旧接口都是同一个功能,原来的uri应该保持不变,修改路径版本即可,使得新旧接口可用。
- 升级之前的版本注解,使得可以使用在方法上
- 在方法使用注解如下:
- 修改映射器实现
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
//调用原有的逻辑,拿到处理器封装后的信息
RequestMappingInfo mapping = super.getMappingForMethod(method, handlerType);
//获取类上的注解
APIVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, APIVersion.class);
//获取方法上的注解
APIVersion methodAnnotation = AnnotationUtils.findAnnotation(method, APIVersion.class);
String version = "";
if (methodAnnotation != null) {
version =getVersion(methodAnnotation);
}else if (typeAnnotation != null){
version =getVersion(typeAnnotation);
}
if (version != null && version.trim() != ""){
String[] partten = new String[]{version};
PatternsRequestCondition newPartten = new PatternsRequestCondition(partten);
PatternsRequestCondition combine = newPartten.combine(mapping.getPatternsCondition());
RequestMappingInfo requestMappingInfo = new RequestMappingInfo(mapping.getName(), combine,
mapping.getMethodsCondition(),mapping.getParamsCondition(),mapping.getHeadersCondition(),
mapping.getConsumesCondition(),mapping.getProducesCondition(),mapping.getCustomCondition());
return requestMappingInfo;
}
//增加自定义处理逻辑
return mapping;
}
查看结果:
新接口访问http://127.0.0.1:15851/V2/demo/user即可。
这里如果不进行重写,启动会直接报错,因为两个方法的映射路径一样。
需求3:由于系统升级要求,需要所有系统都调用新接口,旧接口需要在指定日期后废弃使用。为了防止过了指定日期还有调用旧接口,需要到时候进行屏蔽。
实现这个需求的方式有很多,我们可以从动态注册和卸载处理器的方式进行优雅实现:
修改版本注解,增加过期时间属性
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface APIVersion {
String version() default "V1"; //路径版本
String expire() default ""; //url失效时间,默认无时效时间
}
在controller方法上使用注解
在上一个案例的基础上,这里重写注册的方法,判断旧的接口过期了旧不再注册了
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping){
Class handlerType = method.getDeclaringClass();
//获取类上的注解
APIVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, APIVersion.class);
//获取方法上的注解
APIVersion methodAnnotation = AnnotationUtils.findAnnotation(method, APIVersion.class);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
if (typeAnnotation != null && !StringUtils.isEmpty(typeAnnotation.expire())
&& isExpire(typeAnnotation, format)){
return;
}else if (methodAnnotation != null && !StringUtils.isEmpty(methodAnnotation.expire())
&& isExpire(methodAnnotation, format)){
return;
}
super.registerMapping(mapping, handler, method);
}
private boolean isExpire(APIVersion typeAnnotation, SimpleDateFormat format) {
try {
Date parse = format.parse(typeAnnotation.expire());
if (parse.before(new Date())){ //已经过期,不注册,直接返回
return true;
}
} catch (ParseException e) {
throw new RuntimeException("过期时间格式设置有问题");
}
return false;
}
至此,我们就实现了超过接口过期时间后,只需要重启项目,项目下就不会注册旧的接口了,从而也请求不到了。这里有个不够好的点是,在过期时间后需要重启一次应用才能触发这个不注册。
需求4. 对接口实现版本管理
对同一个url的对应的三个接口通匹配不能的入参进行访问到不通的方法
定义注解,默认为空,作为不传参的默认处理方式
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Version {
String version() default "";
}
使用注解:不用注解的接口为基础版本
自定义匹配条件类:
public class CustoRequestCondition implements RequestCondition<CustoRequestCondition> {
private int version;
public CustoRequestCondition(int version) {
this.version = version;
}
//类上的条件和方法上的条件合并
@Override
public CustoRequestCondition combine(CustoRequestCondition other) {
if (other != null) {
return other; //返回方法级别的条件
}
return this;
}
//获取匹配后的筛选条件
@Override
public CustoRequestCondition getMatchingCondition(HttpServletRequest request) {
String version = request.getHeader("version");
if (version != null && version.matches("^\\d{1}\\.\\d{2}$")){
int requestVersion = Integer.parseInt(version.replace(".",""));
if (requestVersion >= this.version){
return this;
}
}
return null; //匹配不到返回空
}
//请求过了的时候,拿到多个处理器的匹配条件,会根据这个条件排序,排在第一的会被选中
//另外,排在第一位和第二位的compartTo方法不能返回0,否则会报错
@Override
public int compareTo(CustoRequestCondition other, HttpServletRequest request) {
if(other.version >= this.version){
return 1;
}else {
return -1 ;
}
}
}
注册让自定义逻辑生效:
//添加自定义的查询(类上的)匹配条件(请求过来是查询处理器使用)
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
Version annotation = AnnotationUtils.findAnnotation(handlerType, Version.class);
if (annotation == null || annotation.version().isEmpty()){
return super.getCustomTypeCondition(handlerType);
}else {
String version = annotation.version();
if(version.matches("^\\d{1}\\.\\d{2}$")){
int ver = Integer.parseInt(version.replace(".",""));
return new CustoRequestCondition(ver);
}
throw new RuntimeException("类上的版本号设置不符合规范:"+handlerType);
}
}
//添加自定义的查询(方法上的)匹配条件(请求过来是查询处理器使用)
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
Version annotation = AnnotationUtils.findAnnotation(method, Version.class);
if (annotation == null || annotation.version().isEmpty()){
return super.getCustomMethodCondition(method);
}else {
String version = annotation.version();
if(version.matches("^\\d{1}\\.\\d{2}$")){
int ver = Integer.parseInt(version.replace(".",""));
return new CustoRequestCondition(ver);
}
throw new RuntimeException("方法上的版本号设置不符合规范:"+method);
}
}
请求方式:通过请求头区分到调用的方法,没有携带请求头,则使用默认的方法
WebMvcConfigurer 扩展接口说明
@Component
public class MyWebMvcConfiguration implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch();
configurer.setUseRegisteredSuffixPatternMatch();
configurer.setUseTrailingSlashMatch();
//url路径解析(查找路径匹配的时候,解析url去匹配对应的映射器)
configurer.setUrlPathHelper();
//路径匹配器,拿到多个映射器后
configurer.setPathMatcher();
}
//配置视图解析器
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/WEB-INF/jsp/", ".jsp");
registry.enableContentNegotiation(new MappingJackson2JsonView()); //.jsp访问到的资源目录
}
//内容裁决器配置,处理器映射器和适配器公用组件,默认值是ContentNegotiationManager
//配置内容裁决的一些参数的
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
/* 是否通过请求Url的扩展名来决定media type */
configurer.favorPathExtension(true)
/* 不检查Accept请求头 */
.ignoreAcceptHeader(true)
.parameterName("mediaType")
/* 设置默认的media Type */
.defaultContentType(MediaType.TEXT_HTML)
/* 请求以.html结尾的会被当成MediaType.TEXT_HTML*/
.mediaType("html", MediaType.TEXT_HTML)
/* 请求以.json结尾的会被当成MediaType.APPLICATION_JSON*/
.mediaType("json", MediaType.APPLICATION_JSON);
}
//配置适配器(RequestMappingHandlerAdapter)中异步请求的相关处理组件
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
//关联 adapter.setTaskExecutor(); 设置异步处理自定义线程池
configurer.setTaskExecutor();
//关联 adapter.setAsyncRequestTimeout();设置异步处理超时时间
configurer.setDefaultTimeout();
//adapter.setCallableInterceptors(); 设置异步回调拦截器
configurer.registerCallableInterceptors();
//adapter.setDeferredResultInterceptors();,设置延迟处理结果拦截器
configurer.registerDeferredResultInterceptors();
}
//注册一个默认的Handler,处理静态资源文件,当到不到的文件的时候交个这个默认的处理
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
//添加格式化器
@Override
public void addFormatters(FormatterRegistry registry) {
}
//添加注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
}
//添加资源处理器
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resource/**").addResourceLocations("d://");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/cors/**")
.allowedHeaders("*")
.allowedMethods("POST","GET")
.allowedOrigins("*");
}
//实现一个请求到视图的映射,而无需书写controller
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//访问/login路径时直接返回index页面
registry.addViewController("/index").setViewName("index");
}
//添加参数解析器,排在内置的之后
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
//添加返回值解析器,排在内置的之后
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
}
//添加消息转换器(可能覆盖默认的,跟配置有关)
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
//增加消息转换器,放在默认的之后
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
//自定义异常解析器(覆盖默认)
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
//增加异常解析器,不覆盖默认的
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
//添加参数校验器,不建议重写
@Override
public Validator getValidator() {
return null;
}
@Override
public MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
扩展示例:
- 将参数自定义参数解析器放到系统内置之前生效
自定义参数解析器
public class ParamTestResover implements HandlerMethodArgumentResolver {
//被 MapMethodProcessor处理了,走不到自定义的
//解析map类型的入参,并且使用@ParamResoverTest注解标记
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean annotation = parameter.hasParameterAnnotation(ParamResoverTest.class);
boolean isMap = Map.class.isAssignableFrom(parameter.getParameterType());
return annotation && isMap ;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Iterator<String> parameterNames = webRequest.getParameterNames();
Map map = new HashMap();
while (parameterNames.hasNext()){
String next = parameterNames.next();
String arg = webRequest.getParameter(next);
map.put(next,arg);
}
return map;
}
}
注册并调整位置
public class RequestMappingHandlerAdapterSelf extends RequestMappingHandlerAdapter {
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
List<HandlerMethodArgumentResolver> resolverList = new ArrayList<HandlerMethodArgumentResolver>();
//将自定义的参数解析器添加到首位
resolverList.add(new ParamTestResover());
//复制内置的参数解析器
resolverList.addAll(super.getArgumentResolvers());
//覆盖原来内置的参数解析器
super.setArgumentResolvers(resolverList);
}
}
- url转换
public class CustomUrlPathHelper extends UrlPathHelper {
@Override
public String getLookupPathForRequest(HttpServletRequest request) {
String url = super.getLookupPathForRequest(request);
if (url.startsWith("/test")){
String substring = url.substring(5);
return substring;
}
return url;
}
}
@Component
public class CustomMvcConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUrlPathHelper(new CustomUrlPathHelper());
}
}