为了账号安全,请及时绑定邮箱和手机立即绑定

Spring MVC 中如何实现自定义路由?

标签:
Java Spring 源码

所谓的路由,其实就是将不同的请求分发给不同的子控制器。大家平常都是通过 @Controller 或者 @RestController加上@RequestMapping 或者 @GetMapping(@PostMapping/@DeleteMapping)等注解来实现的路由,当然这离不开 Spring MVC 的支持。
其实在 Spring MVC 中,除了可以通过上面这些接口来定义子控制之外,还可以通过其它两种方式来定义子控制器。这两种方式分别是实现 Controller 接口和实现 HttpRequestHandler 接口。那么 Spring MVC 是如何兼容这 3 中路由的方式呢?这时候就不得不提到 HandlerMapping 和 HandlerAdapter 这两个接口了。
从 HandlerAdapter 的命名上不难看出,其是一个适配接口,用到了适配器模式。那么其适配的到底是什么呢?其实其适配的就是不同的定义子控制的方式所带来的处理方法的不同。HandlerMapping 对应的就是不同的子控制器的定义方式,为了将最终子控制的执行逻辑进行统一,所以有了 HandlerAdapter。
到这里,大家应该就能意识到,如果我们想要在 Spring MVC 中定义自己的路由方式,将请求交给我们自定义的子控制来处理的话,那么直接实现 HandlerMapping 和 HandlerAdapter 就可以了。
那么如果不适用这两个接口,可以实现自定义路由吗?当然也是可以,但是会显得有点奇怪。
具体的方式如下:

  • 定义一个接口,用于接收所有的请求
  • 在接口的参数中增加子控制的标识字段
  • 根据请求中的子控制的标识字段,找到自定义的子控制器

通过这样的方式也能实现需求,不过会存在几个问题。
第一个问题就是从接口的定义上无法辨别出当前的业务是什么?一般我们从接口的定义上就能知道这个接口是干什么的,但是通过这样的方式定义的接口是无法做大这一点的。
第二个问题是无法在接口维度进行业限流降级。因为所有的业务请求都是这个接口,所以从接口的维度上是无法进行针对性的限流降级的。

而这些问题通过 HandlerMapping 和 HandlerAdapter 都可以解决。
首先我们先引入下依赖

<dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-web</artifactId>
     <version>5.3.23</version>
 </dependency>

 <dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-webmvc</artifactId>
     <version>5.2.9.RELEASE</version>
 </dependency>

 <dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-databind</artifactId>
     <version>2.11.2</version>
 </dependency>

一般情况下,这3个包,项目中都有,如果没有的话,可以引入下。

接下来我们创建一个 HandlerMethod,其是用来存储子控制器的相关信息的。

public class THandlerMethod {

    private final Object bean;

    private final Method method;

    private final Object[] args;

    public THandlerMethod(Object bean, Method method, Object[] args) {
        this.bean = bean;
        this.method = method;
        this.args = args;
    }

    public Object getBean() {
        return bean;
    }

    public Method getMethod() {
        return method;
    }

    public Object[] getArgs() {
        return args;
    }
}

接下来我们来实现 HandlerMapping 接口

@Component
@Order(1)
public class THandlerMapping implements HandlerMapping {

    private final UrlPathHelper urlPathHelper = new UrlPathHelper();

    @Override
    public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        String path = urlPathHelper.resolveAndCacheLookupPath(request);
        if (!StringUtils.hasLength(path)) {
            return null;
        }

        if (!path.startsWith("/custom/api")) {
            return null;
        }

        // 根据 path 获取子控制器
        Object handlerBean = getHandlerBean(path);
        Method method = getHandlerMethod(path);
        Object[] args = resolveArgumentValues(method, request);

        THandlerMethod handlerMethod = new THandlerMethod(handlerBean, method, args);
        return new HandlerExecutionChain(handlerMethod);
    }

    private Object[] resolveArgumentValues(Method method, HttpServletRequest request) {
        return new Object[0];
    }

    private Method getHandlerMethod(String path) {
        return null;
    }

    private Object getHandlerBean(String path) {
        return null;
    }
}

这里大家需要注意几点:

  • @Order 这个注解一定要加。Spring MVC 中有很多的 HandlerMapping,而 MVC 在进行路由匹配时的逻辑是,只要匹配到就返回。如果我们自定义的 HandlerMapping 的执行顺序比较靠后的话,那么可能还没有等待其执行,MVC 就已经找到了合适的 HandlerMapping,尽管这个 HandlerMapping 不是我们想要的。
  • UrlPathHelper 是 spring web 中提供的一个用于解析请求地址的工具类,大家平常工作中有类似的述求,可以用起来,不用自己去写。
  • 对于子控制的获取逻辑,这里我并没有去实现。大家可以根据自己的具体诉求去实现。这里可以给大家提供一个思路。你可以使用自定义注解或者接口的方式去定义你的子控制器,然后在程序启动的时候,去将所有被自定义注解或者接口所标记的类全部获取到,然后将子控制器和子控制器的标识(可以是 path 路径)用 map 的方式存储起来,后续可以通过 path 或者请求参数中的某个值获取到这个子控制器。

最后我们来实现下 HandlerAdapter 这个接口。

@Component
public class THandlerAdapter implements HandlerAdapter, ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public boolean supports(Object handler) {
        return handler instanceof THandlerMethod;
    }

    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        THandlerMethod handlerMethod = (THandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        Object[] args = handlerMethod.getArgs();
        Object bean = handlerMethod.getBean();
        Object result = method.invoke(bean, args);
        if (result == null) {
            return null;
        }

        Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
        if (applicationContext != null) {
            builder.applicationContext(this.applicationContext);
        }

        ObjectMapper objectMapper = builder.build();
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper);
        MediaType mediaType = MediaType.APPLICATION_JSON;
        ServletServerHttpResponse outputResponse = new ServletServerHttpResponse(response);
        Type targetType = GenericTypeResolver.resolveType(method.getGenericReturnType(), bean.getClass());
        converter.write(result, targetType, mediaType, outputResponse);
        return null;
    }

    @Override
    public long getLastModified(HttpServletRequest request, Object handler) {
        return 0;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

这里大家需要注意的就是对于返回值的处理。由于我们的返回值都是 json 格式,所以这里使用了 MappingJackson2HttpMessageConverter 来进行返回结果的处理。

这样就完成了自定义路由了。

希望以上内容对你有所帮助,谢谢大家。
如果大家希望了解更多 spring 源码和实战相关的内容的话,可以关注我的 spring 源码实战课。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

0 评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
7
粉丝
0
获赞与收藏
0

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消