likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 15

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜 文章合集 : 🎁 juejin.cn/post/694164… Github : 👉 github.com/black-ant CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

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

这一篇主要从思想的角度来看 ,如何对开源框架进行深度的改造. 主要以 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 动态生成类

总结

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

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

保命 : DispatchServlet 其实不好定制 ,太底层了,说的方法不见得能跑通 . 也只是看代码很模板化 , 和Spring家族的代码一套流程 , 所以拿出来举例 , 千万别较真呀!!!