美文网首页
聊聊如何实现动态加载spring拦截器

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

作者: linyb极客之路 | 来源:发表于2024-01-15 10:52 被阅读0次

    前言

    之前写过一篇文章聊聊如何实现热插拔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。观察控制台打印

    25a40c068d071f00208cc6c157ff2d5d_f48bff1ff53b924663c25a4944cfdb16.png

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

    6647b7726b64512f61081871f1ea2caf_02ddb086c1b2e4f89715e8f76743ad2b.png
    @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() ,这边是对原有拦截器稍微做了一下扩展

    添加后,观察控制台


    eb9b89fbc8e7462665a685fd7593a3fd_22d68c4e11655bb6be31c49b33b05d50.png

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

    bbcc32b95ebb10dbcc3e97ee60b1387b_9a23cc6a96ed189dc6d51b4329f48d6b.png

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

    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];
        }
    }
    
    

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

    8890e8e2319645fa7b83ced21890b1c7_652147dab4de5eba1691bdbb1a07cec8.png

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

    810fbd9761b50302ea5888cd683d5a8e_fa503db4da3c1fe5df4a4ab58eafe579.png

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

    总结

    动态变更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

    相关文章

      网友评论

          本文标题:聊聊如何实现动态加载spring拦截器

          本文链接:https://www.haomeiwen.com/subject/saqhodtx.html