Spring源码解读(第七弹)-自定义HandlerMapping,写属于你的另类Controller


如果学技术不是为了装逼,那就毫无意义。

Spring源码解读(第七弹)-自定义HandlerMapping,写属于你的另类Controller

liqing.png

只有自定义的才是自己的,才能装得高大尚!想想你的项目中,你高逼格的痕迹无处不在,偶尔传来一句”卧槽,还能这样,牛逼啊”。程序员的高光时刻,胜过吃两斤蜜蜂屎。

今天的目的就是看懂了,但是还要学会写,学会自定义!让你的高光时刻来得更猛烈一些吧!

上一篇《Spring源码解读(第六弹)-梦开始的地方,webmvc相关组件的初始化-initStrategies》的最后,有一个类叫做HandlerMethod,如果忘记了的先去回顾一下哦!接下来就是咱的重点了,为了自定义,咱得先理解一下RequestHandlerMapping,这个就是和昨天的RequestMappingHandlerAdapter配对使用的东西哦。

RequestHandlerMapping

这货负责处理我们以@Controller定义的这一类Controller接口,对没错,是这一类。

负责存储我们Contoller的接口路径,以及接口信息,方法,handler对应的bean等信息,并建立请求路径到方法的一个映射,当请求到达Servlet之后,最终会从mapping中找到对应的handler,也就是我们的controller,然后通过我们上一篇的HandlerAdapter适配器来进行反射调用。

依然是先来看一下它的继承结构。

Spring源码解读(第七弹)-自定义HandlerMapping,写属于你的另类Controller

request_mapping_handler_mapping.png

这里,我们依然和解读HandlerAdapter一样,关注两个核心接口,亲生父类HandlerMapping和百搭父类InitializingBean

HandlerMapping

我们先看一下亲生父类。

public interface HandlerMapping {
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

这个父类就一个getHandler(),这个方法返回一个为当前请求封装好的处理链HandlerExecutionChain,里面包含本次需要调用的目标Controller以及接口方法,还有我们写过的请求拦截器HandlerInterceptor等信息。这坨的逻辑在后续的请求处理流程会详细介绍,这里因为提到了,就简单先介绍一下,以便有个印象。

InitializingBean

哈哈哈,这个百搭类已经很熟悉了,直接看目标方法实现吧。

public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper());
this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
this.config.setContentNegotiationManager(getContentNegotiationManager());

super.afterPropertiesSet();
}

可以看到,这个类初始化完毕之后,在当前的方法中,会执行如上一系列初始化流程。

1. 初始化配置信息类BuilderConfiguration
2. 设置UrlPathHelper,直接new出来的
3. 设PatchMather, 用来匹配mapping中的路径,直接new出来的
4. useSuffixPatternMatch表示是否启用后缀模式匹配,默认启用.
5. useTrailingSlashMatch表示是否启用末尾斜线匹配,默认启用.
6. useRegisteredSuffixPatternMatch表示是否启用注册后缀模式匹配,默认禁用,若启用那么后缀模式匹配只针对显式注册到内容协商管理器的路径扩展名。启用useRegisteredSuffixPatternMatch会启用useSuffixPatternMatch
7. 设置ContentNegotiationManager,也是直接new一个.它提供解析requst可接受的MediaType以及根据MediaType解析对应文件扩展名.

上面提到的三个属性可以继承WebMvcConfigurerAdapter类并重写configurePathMatch方法进行显式配置,当然,你如果直接自定义HandlerMapping进行覆盖,那也是相当骚的,但就是稍(xue)微有点装过头了。

@Configuration
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {

@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(true);
configurer.setUseRegisteredSuffixPatternMatch(true);
configurer.setUseTrailingSlashMatch(true);
}
}

好了,自己的部分初始化完了,但是还是没有见到Controller的方式及映射从哪里进去的。我们继续跟进super#afterProperties()方法。

父类afterProperties

父类是AbstractHandlerMethodMapping

public void afterPropertiesSet() {
initHandlerMethods();
}

/**
* Scan beans in the ApplicationContext, detect and register handler methods.
* @see #getCandidateBeanNames()
* @see #processCandidateBean
* @see #handlerMethodsInitialized
*/

protected void initHandlerMethods() {
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}

直接看注释,舒服吧,今天我来当一下翻译官。

原句: Scan beans in the ApplicationContext, detect and register handler methods
翻译: 扫描当前容器内的bean, 检测并注册handler method(接口方法)。

逻辑很简单,通过遍历beanName,然后在processCandidateBean()方法中处理。

我们继续跟进这个方法。

processCandidateBean

// AbstractHandlerMethodMapping.java
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
// 从容器中拿到bean的类型
beanType = obtainApplicationContext().getType(beanName);
// isHandler这个方法在子类
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
// RequestMappingHandlerMapping.java
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class))
;
}

processCandidateBean()方法中拿出了bean的类型,然后通过isHandler()方法中判断是否是我们的Controller接口类,在进行检测detectHandlerMethods().方法名其实也很达意对吧。

detectHandlerMethods

// AbstractHandlerMethodMapping.java
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
return getMappingForMethod(method, userType);
});
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}

protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
}
}
return info;
}

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
// 注册handler, 这个方法的细节就留个道友了
this.mappingRegistry.register(mapping, handler, method);
}

这个地方先通过ClassUtils#getUserClass()获取到用户自己的类,因为类有可能被代理之后名字就会变成包含$$的一串,有兴趣的朋友可以去看一下这个方法的实现。

然后找出所有的方法进行遍历,通过getMappingForMethod()方法过滤出符合要求的方法,比如携带注解@RequestMapping的方法。并返回一个RequestMappingInfo类型的对象。这个对象就是方法上标记的路径到方法的一个映射。想更深入的道友就自己去看这getMappingForMethod()方法了。

紧接着,对遍历出来的复合要求的methods(看上一行,这是一个map),通过registerHandlerMethod()方法,注册成对应的handler。

人工分割线

好了,是时候回到最初的地方了,你是不是已经忘了我们是从AbstractHandlerMethodMapping#initHandlerMethods()这个方法进来的了,忘记了的话请翻到前面父类afterProperties的地方稍微回顾一下。

我们还有最后一行方法没有说。这个方法就不贴代码了,没有必要了,它的逻辑就是获取之前注册的mapping,然后打了个日志,说实话,我人有点麻~

好了,HandlerMapping的介绍已经差不多了。我们来开始我们的自定义吧。

自定义

开始自定义之前,还是先提及一下这坨的核心逻辑,加深一下各位道友的印象。

请求到达之后,会根据当前请求的路路径,在所有的RequestMapping对象中找到一个匹配当前路劲的,我们以RequestMappingHandler为例,然后从这货注册的mapping中找到我们的目标handler。因为不同种类的Controller由不同种类的适配器去处理。因此就会找到我们的RequestHandlerAdapter去执行最终handler的 调用

所以,自定义流程就变得很简单了,首先我们要实现一个只有我们自己能识别一个HandlerMapping,用来存储我们另类的Controller的映射,然后用你自己的适配器HandlerAdapter去处理我们另类的Controller的调用

清单

开干!先看一下所有的代码清单。

Spring源码解读(第七弹)-自定义HandlerMapping,写属于你的另类Controller

code_file_list.png
MyEndpoint: 自定义注解
MyEndpointConfiguration: 配置类,用来配置我们自定义的HandlerMapping和HandlerAdapter的bean,以及调用HandlerMapping初始化方法的bean
MyEndpointHandlerMapping: 自定义的HandlerMapping
MyEndpointHandlerAdapter: 自定义的HandlerAdapter
MyEndpointHandlerMethod: 用来存放目标接口与目标bean的信息
TestMyEndpoint: 类似我们的Controller

这里实现一个比较简单的自定义,通过扫描自定义的注解,来将我们自定义注解的另类Controller注册到HandlerMapping上,然后通过HandlerAdapter去执行调用。

代码明细

MyEndpoint

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyEndPoint {

String path() default "";

}

注解就比较简单了,就指定一个path就ok。

MyEndpointHandlerMapping

public class MyEndpointHandlerMapping extends AbstractHandlerMapping {

/**
* 请求路径到方法的映射
*/

private Map<String, MyEndpointHandlerMethod> mappings = new HashMap<>();

@Override
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
// 根据请求的路劲,返回对应的处理方法
return mappings.get(request.getServletPath());
}

public void initMappings() {
Map<String, Object> beansWithAnnotation = obtainApplicationContext().getBeansWithAnnotation(MyEndPoint.class);
// 遍历所有的bean,注册对应的mapping
beansWithAnnotation.forEach((beanName, bean) -> {
MyEndPoint clzAnno = bean.getClass().getAnnotation(MyEndPoint.class);
// 只处理public方法的
Arrays.stream(bean.getClass().getDeclaredMethods()).forEach(method -> {
MyEndPoint anno;
if ((anno = method.getAnnotation(MyEndPoint.class)) != null) {
// 创建路径到方法的映射
mappings.put(clzAnno.path() + anno.path(), new MyEndpointHandlerMethod(method, bean));
}
});

});
}

@Override
public int getOrder() {
// 设置最高优先级
return Ordered.HIGHEST_PRECEDENCE;
}
}

逻辑很简单,注释都有了,就不解释了。init方法的调用地方等哈在配置那坨会讲。

MyEndpointHandlerAdapter

public class MyEndpointHandlerAdapter implements HandlerAdapter, Ordered {

@Override
public boolean supports(Object handler) {
// 只处理handler是MyEndpointHandlerMethod的
return handler instanceof MyEndpointHandlerMethod;
}

@Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
MyEndpointHandlerMethod handlerMethod = (MyEndpointHandlerMethod) handler;
// 调用对应的方法获结果
Object ret = handlerMethod.getMethod().invoke(handlerMethod.getBean());
// 结果写回response的输出流
response.getOutputStream().write(ret.toString().getBytes(StandardCharsets.UTF_8));
return null;
}

@Override
public long getLastModified(HttpServletRequest httpServletRequest, Object o) {
return -1L;
}

@Override
public int getOrder() {
// 设置最高优先级
return Ordered.HIGHEST_PRECEDENCE;
}
}

代码注释也很详细,流程比较简单,直接略过了。

MyEndpointHandlerMethod

public class MyEndpointHandlerMethod {
/**
* 目标方法,相当于我们写的Controller中的接口方法
*/

private Method method;
/**
* Endpoint的bean,相当于我们写的Controller类
*/

private Object bean;
}

TestMyEndpoint

类似我们的controller,这里加上@Component的目的是让其加入Spring容器,我们后面才能从容器中找到它,并把目标方法做映射。

@Component
@MyEndPoint(path = "/myEndpoint")
public class TestMyEndpoint {

@MyEndPoint(path = "/test")
public String test() {
return "hello My Endpoint!";
}

}

MyEndpointConfiguration

@Configuration(proxyBeanMethods = false)
public class MyEndpointConfiguration {

@Bean
public HandlerAdapter myEndpointHandlerAdapter() {
return new MyEndpointHandlerAdapter();
}

@Bean
public HandlerMapping myEndpointHandlerMapping() {
return new MyEndpointHandlerMapping();
}

@Bean
public SmartInitializingSingleton myEndpointSmartInitializingSingleton(@Autowired MyEndpointHandlerMapping handlerMapping) {
return handlerMapping::initMappings;
}

}

在这个配置类中,将我们自定义的MyEndpointHandlerAdapterMyEndpointHandlerMapping注册成立Spring的bean。

SmartInitializingSingleton这个bean大家可能很陌生,这个是Spring的一个扩展点,它是在Spring容器中所有非懒加载的bean都初始化完毕之后被调用。也就是在这个时候,去调用我们MyEndpointHandlerMapping的初始化方法,建立对应的mappings.

当然不使用这个扩展点也是可以的,比如你也可以使用BeanPostProcessor或者和默认的一样采用InitializingBean来实现,只不过我觉得SmartInitializingSingleton这个更简单些罢了。

见证奇迹的时刻

我们来发起一次请求看看。

$ curlhttp://localhost:8080/myEndpoint/test

Spring源码解读(第七弹)-自定义HandlerMapping,写属于你的另类Controller

custom_handler_mapping.png

可以看到,在我们HandlerMapping的断点处,可以看到我们的mappings中已经有了我们想要的映射数据。这里如果我们的请求路径没有问题,就会返回对应的MyEndpointHandlerMethod了,我们接着往下调。

我们拿到了handler,就需要找到对应的适配器去调用了,这个时候就来到了我么你自定义的HandlerAdapter中了。

Spring源码解读(第七弹)-自定义HandlerMapping,写属于你的另类Controller

custom_handler_adapter

这里的这个handler就是上一步从mapping中拿到的handler,我们这个adapter刚好可以处理这个handler,所以这里返回会返回true,然后我们在Adapter的handle方法中打个断点,等待程序进入。

Spring源码解读(第七弹)-自定义HandlerMapping,写属于你的另类Controller

custom_handler_adapter_handle.png

如你所愿,程序正常,通过反射调用我们的目标方法后,返回了目标字符串Hellow My Enndpoint!.

Spring源码解读(第七弹)-自定义HandlerMapping,写属于你的另类Controller

result.png

恭喜你!自定义就算完成了,全程没有使用任何@Controller,@RequestMapping等注解。至此,你也有属于自己的另类Controller了~

自定义虽然是完成了,但是对于整个请求的流程,代码是什么时候执行到这里的,从哪里执行过来的,等等这些问题都还没有答案。不过没有关系,先会用,先尝到甜头,我们才有继续的动力!下一次,我们再去了解这些为什么!

Good Luck~


原文始发于微信公众号(心猿易码):Spring源码解读(第七弹)-自定义HandlerMapping,写属于你的另类Controller

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/133306.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之家——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!