之前写过一篇文章聊聊如何实现热插拔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脚本实现,这个更简单点