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

前言

之前写过一篇文章聊聊如何实现热插拔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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

潍坊数字孪生元宇宙赋能智能制造,助力工业制造业数字化转型

潍坊工业元宇宙数字孪生赋能智能制造&#xff0c;助力工业制造业数字化转型。在当今数字化时代&#xff0c;工业智能制造已成为制造业发展的必然趋势。潍坊市作为山东省的重要工业基地&#xff0c;积极探索数字孪生技术在工业智能制造领域的应用&#xff0c;为制造业企业数字化…

算法竞赛备赛进阶之数位DP训练

数位DP的思想就是对每一位进行DP&#xff0c;计算时记忆化每一位可以有的状态&#xff0c;其作用是减少运算时间&#xff0c;避免重复计算。 数位DP是一种计数用的DP&#xff0c;一般就是要统计一个区间[A,B]内满足一些条件数的个数。 以1e9甚至1e18、1e100的问题为例&#x…

宏集案例 | 楼宇管理新智慧:Panorama SCADA楼宇管理系统应用实例

来源&#xff1a;宏集科技 工业物联网 宏集案例 | 楼宇管理新智慧&#xff1a;Panorama SCADA楼宇管理系统应用实例 原文链接&#xff1a;https://mp.weixin.qq.com/s/ikPOXHCCNJh5Zlgu7wKADw 欢迎关注虹科&#xff0c;为您提供最新资讯&#xff01; #数据采集与监控 #BMS #…

【正点原子】STM32电机应用控制学习笔记——8.FOC简介

FOC是适用于无刷电机的&#xff0c;而像有刷电机&#xff0c;舵机&#xff0c;步进电机是不适用FOC的。FOC是电机应用控制难度最大的部分了。 一.FOC简介&#xff08;了解&#xff09; 1.介绍 FOC&#xff08;Filed Oriented Control&#xff09;即磁场定向控制&#xff0c;…

亚马逊鲲鹏系统:全自动多账号下单,打造真实浏览轨迹

亚马逊鲲鹏系统是一款卓越的软件&#xff0c;其独特的功能让用户可以轻松设置多个账号同时进行自动下单&#xff0c;极大地提高了购物效率。操作流程简单明了&#xff0c;用户只需事先设置关键词及ASIN进行货比三家&#xff0c;为用户筛选最优的产品。随后&#xff0c;软件将模…

cesium设置近地天空盒 天空会倾斜

上篇文章讲解了如何设置近地天空盒&#xff0c;效果出来了还是发现天空是斜的 https://blog.csdn.net/m0_63701303/article/details/135618244 效果&#xff1a; 这里需要修改Cesium.skyBox的代码&#xff0c;代码如下直接全部复制组件内调用即可 skybox_nearground.js&…

福州真兰水表有限公司精益六西格玛项目总结发布会:推动质量改进的成功案例展示

2024年1月3日&#xff0c;福州真兰水表有限公司2023年度DMAIC项目总结发布会如期举行。ZENNER真兰集团中国区总裁杨燕明先生&#xff0c;张驰咨询公司精益六西格玛黑带大师朱成朝老师&#xff0c;福州真兰水表有限公司副总经理杨岚琴女士&#xff0c;以及生产经理&#xff08;倡…

一些平时很少用,但关键时刻很有用的华为手机功能

天灾&#xff0c;自古以来就是威不可知亦不可测的东西&#xff0c;但大自然中的很多意外&#xff0c;其实可以做到有迹可循。 地震预警功能 前段时间频繁地震&#xff0c;一个月内先是积石山&#xff0c;而后是日本能登。 这时候&#xff0c;手机上的地震预警功能就是能够帮…

复旦、交大、清华等公布2023届本科生毕业数据

近日多所高校发布《2022-2023学年本科教学质量报告》&#xff0c;包含各高校本科毕业生就业升学情况&#xff0c;一起来看看吧&#xff5e; 01 清华大学 清华大学推免率超60% 据报告显示&#xff0c;2023年清华大学本科毕业生总数 3609 人&#xff0c;授予学士学位3519 人。应…

什么样的耳机适合游泳?游泳耳机对人体有危害吗?

游泳是一项深受大家喜爱的运动&#xff0c;不仅可以锻炼身体&#xff0c;还能让我们享受到水中的乐趣。然而&#xff0c;对于喜欢在水中听音乐的人来说&#xff0c;选择一款适合游泳的耳机就显得尤为重要了。 游泳耳机是一种专为水上运动设计的防水耳机&#xff0c;可以在游泳、…

48-DOM

1.DOM基础 Document Object Module,文档对象模型,window对象,document文档,都可以获取和操作 1)文档节点 2)属性节点(标签内的属性href,src) 3)文本节点(标签内的文字) 4)注释节点 5)元素节点(标签) 2.获取元素节点 2.1通过标签名获取 <p>1</p><…

Apache StringUtils:Java字符串处理工具类

简介 在我们的代码中经常需要对字符串判空&#xff0c;截取字符串、转换大小写、分隔字符串、比较字符串、去掉多余空格、拼接字符串、使用正则表达式等等。如果只用 String 类提供的那些方法&#xff0c;我们需要手写大量的额外代码&#xff0c;不然容易出现各种异常。现在有…

chatgpt实用技巧之二反问式提示

大家好&#xff0c;今天跟大家讲实用gpt的小技巧二、反问式提示 有时候不知道怎么给 GPT 提示词&#xff0c;这时候&#xff0c;就可以反问 GPT 如何更好地给提示词。如图片所示 更详细内容可以看下这篇&#xff1a; 按照 GPT 给出的&#xff1a;故事设定角色故事发展主题结局…

git提交文本或者word到git教程,git创建仓库时候自带

简易的命令行入门教程: Git 全局设置: git config --global user.name “XX” git config --global user.email “XXXqq.com” 创建 git 仓库: mkdir test cd test git init touch README.md git add README.md git commit -m “first commit” git remote add origin https:…

Verilog语法——5.测试文件

参考资料 【明德扬_verilog零基础入门语法HDL仿真快速掌握-手把手教你写FPGA/ASIC代码设计流程中的应用】 5. 测试文件 5.1 认识测试文件&#xff08;testbench&#xff09; testbench是一种验证的手段。首先&#xff0c;任何设计都是会有输入输出的。 但是在软环境中没有激励…

50天精通Golang(第18天)

web开发介绍、iris框架安装、HTTP请求和返回、Iris路由处理 一 Web项目开发介绍及实战项目介绍 1.1 引言 本系列课程我们将学些Golang语言中的Web开发框架Iris的相关知识和用法。通过本系列视频课程&#xff0c;大家能够从零到一经历一个完整项目的开发&#xff0c;并在课程…

壹基金“安全家园”项目瑞金市城北社区志愿者救援队成立

项目介绍&#xff1a;阿里巴巴公益&壹基金安全家园项目 -社区志愿者救援队专项行动是一个以社区为本的防灾减灾公益项目。据介绍&#xff0c;该项目通过推动社区建立自己的志愿者救援队伍&#xff0c;为队伍“配物资”“办培训”&#xff0c;并协助他们动员居民参与“家庭—…

宝宝洗衣机买几公斤?婴儿专用洗衣机测评

由于幼龄时期的宝宝的皮肤比较娇嫩&#xff0c;很容易受到伤害。所以小宝宝的衣服一般都是棉质的&#xff0c;很柔软&#xff0c;很亲肤的&#xff0c;为的就是保护宝贝们娇嫩的肌肤。而宝宝们在日常中更换衣物会相对频繁&#xff0c;换的衣物也必须及时清洗晾晒&#xff0c;以…

1-数组-有效的数独

这是数组的第一题&#xff0c;从现在开始开启数组篇章。力扣链接。 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的…

锐意进取,蓬勃发展|爱基百客2023全景图

岁序更迭&#xff0c;2023年已悄然离去。对我们来说&#xff0c;这是充满挑战与机遇的一年。爱基百客作为一家专注于测序服务的公司&#xff0c;我们在这一年里经历了许多挑战&#xff0c;也取得了令人鼓舞的成绩。前面我们盘点了表观产品和单细胞产品&#xff0c;今天再邀您回…