开源杂谈 : 理解开源框架的定制思路

时间:2022-10-09 17:58:57

一 . 前言

作为开源软件,是有可能无法满足业务场景的.

这一篇主要从思想的角度来看 ,如何对开源框架进行深度的改造. 主要以 SpringMVC DispatchServlet 的定制为案例.

二. 定制的目的和方向

定制源码时一定要考虑升级和适配等多个问题 ,定制不是 fork , 好的定制行为可以伴随框架升级.

定制的目的

定制并不是秀操作的行为 , 如果框架本身就提供了完善的解决方案 , 能基本满足要求 , 就不要去深度定制一些功能 , 过多的配置会破坏框架的稳定性

定制的方向

对开源框架的修改主要可以分为以下几类 :

  • 改源 : 对源头的参数进行修改
  • 重写 : 重写核心处理类或者方法
  • 插入 : 在链表中插入处理类
  • 切面 : 对前后部分进行包围处理

三. 定制的方式

DispatchServlet 是 Spring-Web 的核心入口 , 也是对 Java Servlet 的封装实现 , 定制化的思路主要包括以下几个步骤 :

  • S1 : 确定源码流程 , 读懂源码才能进行定制
  • S2 : 分析外部处理流程和可重写的方法
  • S3 : 对流程中可以插入的节点进行增强

DispatchServlet 核心处理流程

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 前置对象创建 , 此处省略
try {
// 节点一 : 定制请求体,修改参数类型
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 节点二 : 定制处理类
mappedHandler = getHandler(processedRequest);
//.....

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// ... 判断 Method 和逻辑处理
// 节点三 : 前置处理不要忘
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// 节点四 : 改写Handler处理类
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

applyDefaultViewName(processedRequest, mv);

// 节点五 : 后置处理也可处理
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception ex) {
dispatchException = ex;
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
//....... catch 处理
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
}
}
}

四. 细说不同节点的定制思路

4.1 节点一 : 请求参数的定制

进行这种定制的原因是由于很多开源框架中会通过​​链式处理的方式来选择具体的处理类​​ , 写个Demo演示一下 :

public class OAuthService interface AuthService {
public void excute(AuthDto authDto){
// .... 执行具体的操作
}

// 在这个环节中 , 会判断认证类型是否符合条件
public boolean filter(AuthDto authDto){
return authDto.getAuthType().equals("OAUTH");
}
}

// 而在外部会循环所有的认证类,判断是否符合认证的条件
public class OAuthManager {
for( AuthService authService : authServiceList ){
if(authService.filter(authDto)){
authService.excute(authDto);
}
}
}

以上就是一个典型的案例 , 如果想定制认证类型 , 判断类型后进入处理逻辑.

  • 通常这种场景下 , 不能修改实际处理类
  • 通过 filter 的方式对请求参数进行改写
  • 可以自行在外部进行复杂处理 , 最后组装成对于 Service 需要的模式

思路总结 : 请求参数的定制主要发生在主流程执行之前 ,通过拦截的方式去修改进入的参数 , 从而影响整个流程

4.2 节点二 : 定制处理类

上一种方式中 ,处理类是不可以定制的,而这一种处理类型则是通过定制Handler处理类来实现.

// S1 : 在 DispatchService 中获取具体的Handler处理类
mappedHandler = getHandler(processedRequest);

// S2 : 加载时会扫描指定接口下的类
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
// 可以发现 , 此处通过扫描指定接口类获取到处理类
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
}
else {
//.........
}
}

// 自己继承接口实现具体的Handler
@Service
public class DefaultHandlerMapping implements HandlerMapping {
@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
return null;
}
}

开源杂谈 : 理解开源框架的定制思路

可以看到 , 处理类就已经完成注入了. 这种方式最大的难点在于 Service 中有很多依赖需要自行完善 , 同时需要看源码.

  • S1 : 观察流程中是否存在 Collect 的处理类
  • S2 : 了解集合中的加载方式
  • 通过扫描指定接口实现类
  • 通过扫描标注注解的实现类
  • SpringFactories 实现加载
  • 通过 ConfigClass + SetBean 配置到管理类中
  • 本身由实现类自己完成注入
  • 写死的~~
  • S3 : 自行定制具体的处理类 ,注入相关依赖,处理逻辑

4.3 节点三 : 前置后置处理器

通常很多开源框架会人为的留一下口子 ,用来处理一些额外的操作,不难发现很多处理类都实现了 preHandler 和 afterHandler 这些方法 , 很多时候这种方法中也能进行定制化的处理

以 SpringMVC 中的体系为例 , 节点三和节点五都是前/后置处理器的调用 , 其核心是调用 HandlerInterceptor

public interface HandlerInterceptor {
// 提供了 Default 实现 ,非必须实现
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 返回默认参数
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,@Nullable ModelAndView modelAndView) throws Exception {
}
}

// 到这里应该就很熟悉了 , 通过手段注册进去即可
public class WebCoustomerConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ConfigInterceptor());
}
}

总结:这种定制方式是最简单的 ,因为前后置处理器本身就是为了定制而准备的!!

当然有些场景下 , 开源方会将这种流程隐藏起来 , 如SpringMVC这个 , 实际上是在​​InterceptorRegistry​​中进行注册 , 在 ​​WebMvcConfigurationSupport​​ 中进行初始化.

如果没有文档 ,其实很难进行处理. 其实思路也很简单 :

  • 对应处理器的configuration加载类
  • 指定后缀如Registry,Manager的工具类
  • 通过Bean加载的类, 如果提供了 add 方法也可以快速注册

不过这种方式还是保持在 Handler 外网进行包装处理. 通过AOP也可以实现相关的功能

4.4 节点四 : 改造 Service 类

最极端的方法之一 , 这种方法限制比较强 , 如果是Bean注入的还比较简单.例如一些基于Spring的开源功能框架.

如果是比较底层的Spring家族 , 很多本身就没有使用Bean注入 ,太底层反而不好去改写.

这种改造方式主要有两种思路 :

  • 如果是Config注入的Bean , 进行覆盖
  • 如果不可以覆盖 ,​​通过继承后再重写方法进行逻辑上的覆写​​ ,通过优先级排序先执行自己的(重要)
@Configuration
public class DefaultRequestMappingConfig {

@Bean
@Primary
public RequestMappingHandlerMapping requestMappingHandlerMapping(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {

RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
//.......
}
}

常用的方式就是直接自定义 Config 进行自定义Bean的处理 , 运气好一把就可以成功.

​但是!!​​很多场景下可能一把过不了 , 内部配置类的优先级会大于我们自己的 :

  • 方案一 : spring.main.allow-bean-definition-overriding=true
  • 方案二 : exclude 排除内部配置类 , 执行自己的配置类
  • 方案三 : 控制配置类优先级,或者通过重写 spring.factories 修改排序
  • 方案四 : 对 SpringIOC 内的Bean器进行处理 , 把里面的类覆盖掉(没玩过,但是逻辑没毛病)
  • 方案五 : 百度如果覆盖配置类 (方案很多,有点不记得了...)

方法很多 ,一般前2个就能处理 , 而且如果不是Spring家族体系的代码 ,一般也没这个问题

节点N : 直接重写 DispatchServlet

大牛的玩法 ,不就是重新写个SpringMVC~~

其实简单点说 ,就是把ManagerService进行重写,也有搞头

其他方向 :

  • Java Assist 动态生成类

总结

对于开源框架的深度定制其实是无赖之举 , ​​定制的目的是为了不破坏原有的逻辑​​,其实前后置处理器链表中插入才是最合适的.不然别人出现个漏洞升级了 ,你代码就得完蛋

之前经历过一个产品就是对开源框架拉下源码进行深度定制.结果后期开源框架版本更新后 , 根本没办法跟上别人的节奏 ,对设计的思路破坏性改动 ,导致对后续拓展市场影响也很大.