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最开头,使用统一从处理方式

  1. 定义注解如下:默认是V1版本
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface APIVersion {
    String version() default "V1";
}
  1. 在类中使用注解
@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);
    }
}
  1. 重写处理器映射器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应该保持不变,修改路径版本即可,使得新旧接口可用。

  1. 升级之前的版本注解,使得可以使用在方法上
  2. 在方法使用注解如下:
    在这里插入图片描述
  3. 修改映射器实现
@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;
    }
}

扩展示例:

  1. 将参数自定义参数解析器放到系统内置之前生效

自定义参数解析器

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);
    }
}
  1. 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());
    }
}

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值