之前写过一篇文章聊聊如何实现热插拔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脚本实现,这个更简单点
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-filter-hot-loading