聊聊如何实现动态加载spring拦截器

news2024/12/27 14:01:27

前言

之前写过一篇文章聊聊如何实现热插拔AOP,今天我们继续整一个类似的话题,聊聊如何实现spring拦截器的动态加载

实现核心思路

groovy热加载java + 事件监听变更拦截器

实现步骤

1、在项目的pom引入groovy GAV

 <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy</artifactId>
        </dependency>

2、编写groovy编译插件

public class GroovyCompiler implements DynamicCodeCompiler {

    private static final Logger LOG = LoggerFactory.getLogger(GroovyCompiler.class);

    /**
     * Compiles Groovy code and returns the Class of the compiles code.
     *
     */
    @Override
    public Class<?> compile(String sCode, String sName) {
        GroovyClassLoader loader = getGroovyClassLoader();
        LOG.info("Compiling filter: " + sName);
        Class<?> groovyClass = loader.parseClass(sCode, sName);
        return groovyClass;
    }

    /**
     * @return a new GroovyClassLoader
     */
    GroovyClassLoader getGroovyClassLoader() {
        return new GroovyClassLoader();
    }

    /**
     * Compiles groovy class from a file
     *
     */
    @Override
    public Class<?> compile(File file) throws IOException {
        GroovyClassLoader loader = getGroovyClassLoader();
        Class<?> groovyClass = loader.parseClass(file);
        return groovyClass;
    }
}

3、编写groovy加载java类

@Slf4j
public final class SpringGroovyLoader<T> implements GroovyLoader<T>, ApplicationContextAware {

    private final  ConcurrentMap<String, Long> groovyClassLastModified = new ConcurrentHashMap<>();

    private final DynamicCodeCompiler compiler;

    private final DefaultListableBeanFactory beanFactory;

    private ApplicationContext applicationContext;

    public SpringGroovyLoader(DynamicCodeCompiler compiler, DefaultListableBeanFactory beanFactory) {
        this.compiler = compiler;
        this.beanFactory = beanFactory;
    }

    @Override
    public boolean putObject(File file) {
        try {
            removeCurBeanIfFileChange(file);
            return registerGroovyBean(file);
        } catch (Exception e) {
            log.error(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Error loading object! Continuing. file=" + file, e);
        }

        return false;
    }

    private void removeCurBeanIfFileChange(File file) {
        String sName = file.getAbsolutePath();
        if (groovyClassLastModified.get(sName) != null
                && (file.lastModified() != groovyClassLastModified.get(sName))) {
            log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>> Reloading object " + sName);
            if(beanFactory.containsBean(sName)){
                beanFactory.removeBeanDefinition(sName);
                beanFactory.destroySingleton(sName);
            }
        }
    }

    private boolean registerGroovyBean(File file) throws Exception {
        String sName = file.getAbsolutePath();
        boolean containsBean = beanFactory.containsBean(sName);
        if(!containsBean){
            Class<?> clazz = compiler.compile(file);
            if (!Modifier.isAbstract(clazz.getModifiers())) {
                return registerBean(sName,clazz, file.lastModified());
            }
        }
        return false;
    }

    private boolean registerBean(String beanName, Class beanClz,long lastModified) {
        try {
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
            AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
            beanDefinition.setBeanClass(beanClz);
            beanDefinition.setSource("groovyCompile");
            beanFactory.registerBeanDefinition(beanName,beanDefinition);
            BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
            String aliasBeanName = beanNameGenerator.generateBeanName(beanDefinition, beanFactory);
            beanFactory.registerAlias(beanName,aliasBeanName);
            groovyClassLastModified.put(beanName, lastModified);

            GroovyBeanRegisterEvent groovyBeanRegisterEvent = GroovyBeanRegisterEvent.builder()
                            .beanClz(beanClz).beanName(beanName).aliasBeanName(aliasBeanName).build();
            applicationContext.publishEvent(groovyBeanRegisterEvent);
            return true;
        } catch (BeanDefinitionStoreException e) {
           log.error(">>>>>>>>>>>>>>>>>>>>>>registerBean fail,cause:" + e.getMessage(),e);
        }
        return false;
    }



    @Override
    public List<T> putObjectsForClasses(String[] classNames) throws Exception {
        List<T> newObjects = new ArrayList<>();
        for (String className : classNames) {
            newObjects.add(putObjectForClassName(className));
        }
        return Collections.unmodifiableList(newObjects);
    }

    @Override
    public T putObjectForClassName(String className) throws Exception {
        Class<?> clazz = Class.forName(className);
        registerBean(className, clazz, System.currentTimeMillis());
        return (T) beanFactory.getBean(className);
    }

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

4、编写管理groovy文件变化的类

public class GroovyFileMonitorManager<T> {

    private static final Logger LOG = LoggerFactory.getLogger(GroovyFileMonitorManager.class);


    private final GroovyLoader<T> groovyLoader;
    private final GroovyProperties groovyProperties;

    public GroovyFileMonitorManager(GroovyProperties groovyProperties, GroovyLoader<T> groovyLoader) {
        this.groovyLoader = groovyLoader;
        this.groovyProperties = groovyProperties;
    }

    /**
     * Initialized the GroovyFileManager.
     *
     * @throws Exception
     */
    public void init() throws Exception {
        long startTime = System.currentTimeMillis();
        manageFiles();
        directoryChangeMonitor();
        LOG.info("Finished loading all classes. Duration = " + (System.currentTimeMillis() - startTime) + " ms.");
    }


    /**
     * Returns the directory File for a path. A Runtime Exception is thrown if the directory is in valid
     *
     * @param sPath
     * @return a File representing the directory path
     */
    public File getDirectory(String sPath) {
       return DirectoryUtil.getDirectory(sPath);
    }


    /**
     * Returns a List<File> of all Files from all polled directories
     *
     * @return
     */
    public List<File> getFiles() {
        List<File> list = new ArrayList<File>();
        if(groovyProperties.getDirectories() == null && groovyProperties.getDirectories().length == 0){
            return list;
        }
        for (String sDirectory : groovyProperties.getDirectories()) {
            if (sDirectory != null) {
                File directory = getDirectory(sDirectory);
                File[] aFiles = directory.listFiles(groovyProperties.getFilenameFilter());
                if (aFiles != null) {
                    list.addAll(Arrays.asList(aFiles));
                }
            }
        }
        return list;
    }

    @SneakyThrows
    void directoryChangeMonitor(){
          for (String sDirectory : groovyProperties.getDirectories()) {
            File directory = getDirectory(sDirectory);
            //创建文件观察器
            FileAlterationObserver observer = new FileAlterationObserver(
                    directory, FileFilterUtils.and(
                    FileFilterUtils.fileFileFilter(),
                    FileFilterUtils.suffixFileFilter(".groovy")));
            //轮询间隔时间
            long interval = TimeUnit.SECONDS.toSeconds(groovyProperties.getPollingIntervalSeconds());
            //创建文件观察器
            observer.addListener(new GroovyFileAlterationListener(this));
            //创建文件变化监听器
            FileAlterationMonitor monitor = new FileAlterationMonitor(interval, observer);
            //开始监听
            monitor.start();
        }
    }


    public void manageFiles() {
        List<File> aFiles = getFiles();
        for (File file : aFiles) {
            try {
                groovyLoader.putObject(file);
            }
            catch(Exception e) {
                LOG.error("Error init loading groovy files from disk by sync! file = " + file, e);
            }
        }

    }


    public GroovyLoader<T> getGroovyLoader() {
        return groovyLoader;
    }

    public GroovyProperties getGroovyProperties() {
        return groovyProperties;
    }
}

5、编写事件监听,变更处理拦截器

注: 核心点是利用MappedInterceptor bean能被AbstractHandlerMapping自动探测

@Component
public class InterceptorRegisterListener  {

    @Autowired
    private RequestMappingHandlerMapping requestMappingHandlerMapping;

    @Autowired
    private DefaultListableBeanFactory defaultListableBeanFactory;

    @EventListener
    public void listener(GroovyBeanRegisterEvent event){
   
        if(BaseMappedInterceptor.class.isAssignableFrom(event.getBeanClz())){
            BaseMappedInterceptor interceptor = (BaseMappedInterceptor) defaultListableBeanFactory.getBean(event.getBeanName());
            MappedInterceptor mappedInterceptor = build(interceptor);
            registerInterceptor(mappedInterceptor,event.getAliasBeanName() + "_mappedInterceptor");
        }


    }


    public MappedInterceptor build(BaseMappedInterceptor interceptor){
        return new MappedInterceptor(interceptor.getIncludePatterns(),interceptor.getExcludePatterns(),interceptor);
    }

    /**
     * @see org.springframework.web.servlet.handler.AbstractHandlerMapping#initApplicationContext()
     * @See org.springframework.web.servlet.handler.AbstractHandlerMapping#detectMappedInterceptors(java.util.List)
     * @param mappedInterceptor
     * @param beanName
     */
    @SneakyThrows
    public void registerInterceptor(MappedInterceptor mappedInterceptor, String beanName){
        if(defaultListableBeanFactory.containsBean(beanName)){
            unRegisterInterceptor(beanName);
            defaultListableBeanFactory.destroySingleton(beanName);
        }
        //将mappedInterceptor先注册成bean,利用AbstractHandlerMapping#detectMappedInterceptors从spring容器
        //自动检测Interceptor,并加入到当前的拦截器集合中
        defaultListableBeanFactory.registerSingleton(beanName,mappedInterceptor);
        Method method = AbstractHandlerMapping.class.getDeclaredMethod("initApplicationContext");
        method.setAccessible(true);
        method.invoke(requestMappingHandlerMapping);
    }

    @SneakyThrows
    public void unRegisterInterceptor(String beanName){
        MappedInterceptor mappedInterceptor = defaultListableBeanFactory.getBean(beanName,MappedInterceptor.class);
        Field field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
        field.setAccessible(true);
        List<HandlerInterceptor> handlerInterceptors = (List<HandlerInterceptor>) field.get(requestMappingHandlerMapping);
        handlerInterceptors.remove(mappedInterceptor);

    }



}

示例验证

1、编写测试服务类


public class HelloServiceImpl implements HelloService {
    @Override
    public String say(String username) {
        println ("hello:" + username)
        return "hello:" + username;
    }
}

2、编写测试控制器

@RestController
@RequestMapping("hello")
@RequiredArgsConstructor
public class HelloController {

    private final ApplicationContext applicationContext;

    @GetMapping("{username}")
    public String sayHello(@PathVariable("username")String username){
        HelloService helloService = applicationContext.getBean(HelloService.class);
        return helloService.say(username);
    }
}

浏览器访问http://localhost:8080/hello/lisi。观察控制台打印

3、在classpath目录下新增/META-INF/groovydir文件夹,并在底下放一个拦截器

@Component
public class HelloHandlerInterceptor extends BaseMappedInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("uri:" + request.getRequestURI());
       return true;

    }

    @Override
    public String[] getIncludePatterns() {
        return ["/**"];
    }

    @Override
    public String[] getExcludePatterns() {
        return new String[0];
    }
}

注: 原来的spring拦截器是没getIncludePatterns()和getExcludePatterns() ,这边是对原有拦截器稍微做了一下扩展

添加后,观察控制台

此时再次访问http://localhost:8080/hello/lisi,并观察控制台

会发现拦截器生效。接着我们将拦截器的拦截路径由/**调整成如下

Component
public class HelloHandlerInterceptor extends BaseMappedInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("uri:" + request.getRequestURI());
       return true;

    }

    @Override
    public String[] getIncludePatterns() {
        return ["/test"];
    }

    @Override
    public String[] getExcludePatterns() {
        return new String[0];
    }
}

观察控制台,会发现有如下内容输出


此时再访问http://localhost:8080/hello/lisi,观察控制台


此时说明拦截器已经发生变更

总结

动态变更java的方式有很多种,比如利用ASM、ByteBuddy等操作java字节码来实现java变更,而本文则是采用groovy脚本来变更,主要是因为groovy的学习门槛很低,只要会java基本上等于会groovy。对groovy感兴趣的同学可以通过如下链接进行学习
https://www.w3cschool.cn/groovy/

不过在使用groovy时,要特别注意因为groovy每次都是新创建class,如果没注意很容易出现OOM,其次因为groovy比较易用,很容易被拿来做成攻击的脚本,因而容易造成安全隐患。因此在扩展性和性能以及安全性之间要做个取舍

另外本文的实现其实是借鉴了zuul动态更新filter的源码,感兴趣的朋友,可以通过下载zuul源码进行学习。不过也可以看xxl-job的groovy脚本实现,这个更简单点

demo链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-filter-hot-loading

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1388910.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ 设计模式之桥接模式

【声明】本题目来源于卡码网&#xff08;题目页面 (kamacoder.com)&#xff09; 【提示&#xff1a;如果不想看文字介绍&#xff0c;可以直接跳转到C编码部分】 【简介】什么是桥接模式 桥接模式&#xff08;Bridge Pattern&#xff09;是⼀种结构型设计模式&#xff0c;它的U…

【技术分享】远程透传网关-单网口快速实现三菱 Q03UDV PLC程序远程上下载

准备工作 一台可联网操作的电脑一台单网口的远程透传网关及博达远程透传配置工具网线一条&#xff0c;用于实现网络连接和连接PLC一台三菱 FX5U PLC及其编程软件一张4G卡或WIFI天线实现通讯(使用4G联网则插入4G SIM卡&#xff0c;WIFI联网则将WIFI天线插入USB口&#xff09; …

在校大学生可以考哪些 ?(38个考证时间表)

这是整理的在校大学生可以考的&#xff0c;有的对报名条件没有要求&#xff0c;有的是高中以上学历&#xff0c;还有一些应届生可以报考的。可以在支付宝搜索【亿鸣证件照】或者微信搜索【随时照】制作这些考证要求的证件照哦 1、教师资格证 2、英语四六级 3、计算机二级 4、普…

adb wifi 远程调试 安卓手机 命令

使用adb wifi 模式调试需要满足以下前提条件&#xff1a; 手机 和 PC 需要在同一局域网下。手机需要开启开发者模式&#xff0c;然后打开 USB 调试模式。 具体操作步骤如下&#xff1a; 将安卓手机通过 USB 线连接到 PC。&#xff08;连接的时候&#xff0c;会弹出请求&#x…

2. goLand安装及外配置参数通用用法

目录 概述测试代码解决外配置参数结束 概述 选择版本安装 go 安装的版本 1.go安装及相关配置 goLand 对于 习惯 idea 系列使用的人&#xff0c;还是很友好的。 测试代码 package mainimport ("flag""fmt""os" )func main() {name : flag.St…

layabox_2d游戏A*寻路实践

使用工具 Red Blob Games 效果 项目地址 LayaAStar2D: Laya2.0引擎2D游戏使用AStar实践。

加速 Android Studio 依赖项下载

在某些网络环境中&#xff0c;访问互联网可能受到限制&#xff0c;在Android Studio中&#xff0c;项目构建时可能需要下载依赖项&#xff0c;如果网络受到限制&#xff0c;就无法下载或下载速度非常慢只有十几 kb/s &#xff0c;设置可以帮助解决下载问题。 进入设置页面找到…

【杂七杂八】GyP-DataLoader

前言&#xff1a; pytorch :2.0.0cuda 11.8Jupyter Notebook:7.0.6所以 PyG不需要安装依赖包&#xff0c;直接cuda/pip install ; Jupyter Notebook 有扩展管理器&#xff0c;不需要也装不了 很多教程里的扩展管理插件 &#x1f601; 1 基本数据结构 torch_geometric.data.Data…

Spring AOP 源码分析

【阅读前提】&#xff1a; 需了解AOP注解开发流程&#xff1a;链接 一、注解 EnableAspectJAutoProxy 在配置类中添加注解EnableAspectJAutoProxy&#xff0c;便开启了AOP&#xff08;面向切面编程&#xff09; 功能。此注解也是了解AOP源码的入口。 EnableAspectJAutoProxy…

蓝桥杯备赛 | 洛谷做题打卡day2

​ 蓝桥杯备赛 | 洛谷做题打卡day2 嵌套循环yyds&#xff01;&#xff01; 题目来源&#xff1a;洛谷P2670 [NOIP2015 普及组] 扫雷游戏 题目背景 NOIP2015 普及组 T2 题目描述 扫雷游戏是一款十分经典的单机小游戏。在 n n n 行 m m m 列的雷区中有一些格子含有地雷&am…

❤ Uniapp使用二 ( 日常使用篇)

❤ Uniapp使用二 ( 日常使用篇) 一、表单 1、基础表单验证 form <form submit"formSubmit" reset"formReset"> <view class"uni-form-item uni-column"><view class"title">请选择类型{{selectvalue}}</view&…

海思SD3403,SS928/926,hi3519dv500,hi3516dv500移植yolov7,yolov8(9)

上一节yolov8的训练已经完成了,现在要开始做模型的转换了,这里和yolov7方式相似,但是有一些差异,尤其是yolov7的不带NMS部分的输出顺序和yolov8的输出顺序与格式是有差异的。 首先还是要自己手动加入rpn_op,这里包含了filter,sort,nms部分。 我们一个一个看,首先filter.py…

深度学习基础知识整理

自动编码器 Auto-encoders是一种人工神经网络&#xff0c;用于学习未标记数据的有效编码。它由两个部分组成&#xff1a;编码器和解码器。编码器将输入数据转换为一种更紧凑的表示形式&#xff0c;而解码器则将该表示形式转换回原始数据。这种方法可以用于降维&#xff0c;去噪…

高级分布式系统-第15讲 分布式机器学习--分布式机器学习算法

高级分布式系统汇总&#xff1a;高级分布式系统目录汇总-CSDN博客 分布式机器学习算法 按照通信步调&#xff0c;大致可以分为同步算法和异步算法两大类。 同步算法下&#xff0c;通信过程中有一个显式的全局同步状态&#xff0c;称之为同步屏障。当工作节点运行到同步屏障 …

MySQL索引创建原则和失效问题

一.索引创建原则 数据量较大,且查询比较频繁的表常作为查询条件、分组、排序的字段字段内容区分度高内容较长,使用前缀索引尽量创建联合索引要控制索引的数量如果索引列不能存储NULL值,请在创建表示使用NOT NULL约束它 二.索引失效 违反最左前缀法则范围查询右边的列,不能使用…

可持续技术:2024 年技术趋势的绿色创新

随着我们步入2024年&#xff0c;对可持续技术解决方案的关注从未如此强烈。从可再生能源到环保小工具&#xff0c;科技行业正朝着更环保、更可持续的未来大步迈进。 在快速发展的技术领域&#xff0c;创新是推动我们走向可持续未来的动力。随着我们步入2024年&#xff0c;对可持…

基于Docker Compose单机实现多级缓存架构2024

文章目录 一、环境参考二、专栏简介三、扩展 一、环境参考 NameVersionDocker Desktop for Windows4.23.0Openjdk8MySQL8.2.0Redis7.2Canal1.1.7OpenResty1.21.4.3-3-jammy-amd64Lua-Caffeine- 二、专栏简介 多级缓存实现过程比较长&#xff0c;将拆分为多个文章分步讲述。如…

[我的rust付费栏目]rust跟我学(一)已上线

大家好&#xff0c;我是开源库get_local_info的作者带剑书生&#xff0c;get_local_info诞生半个月&#xff0c;现在已经获得500的下载量&#xff0c;并获社区日更前五名&#xff0c;后被西安城市开发者社区收录&#xff08;【我的Rust库】get_local_info 0.1.5发布_rust_科比布…

CMake 实战构建TcpServer项目 静态库/动态库

爱编程的大丙CMake&#xff1a; 20. 举例 - 下_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV14s4y1g7Zj?p20&spm_id_frompageDriver&vd_sourcea934d7fc6f47698a29dac90a922ba5a3 hehedalinux:~/Linux/LinuxServerCpp$ ls Buffer.cpp Log.h Buffe…

墙地砖外形检测的技术方案-图像分割

基础原理 由于对碗口进行缺口检测&#xff0c;因此只需要碗口的边界信息。得到陶瓷碗区域填充后的图像&#xff0c;对图像进行边缘检测。这是属于图像分割中的内容&#xff0c;在图像的边缘中&#xff0c;可以利用导数算子对数字图像求差分&#xff0c;将边缘提取出来。 案例…