一.业务背景
医院有很多的业务系统,像LIS、PACS、HIS、面向患者的小程序等等,这些系统都不是孤立存在的,系统间都有很多接口调用,数据交互。目前线上有一套集成引擎,不过这套集成引擎每次对接都要开人人员定制化开发,然后从中做转发,为了减少成本,对集成引擎进行了升级,使用配置化方式完成对接,这样就不需要开发人员接入,实施人员便可以完成这项工作,但是新集成引擎面临一个问题,就是如何替换线上已有的接口,同时还不需要调用方和被调用改造,实施人员又能通过配置实现替换,因此想到了使用Groovy动态加载代码实现创建接口。
二.实现思路
1.创建脚本
界面提供输入框,用于录入controller中的方法,该方法的url就是要替换的线上接口的url,参数可以是json对象或者json数组
2.加载脚本
录入脚本后,后台提供加载功能,将该脚本方法所表示的类动态创建并添加到Spring容器中
三.具体实现
1.引入Groovy
脚本加载使用Groovy实现
pom.xml引入Groovy
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>${groovy.version}</version>
</dependency>
2.动态创建对象
public void load(String javaText) {
Class<?> javaClazz = groovyLoader.parseClass(javaText);
//使用类名作为bean名称
String beanName = javaClazz.getSimpleName();
//创建bean信息.
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(javaClazz);
//动态注册bean.
defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getBeanDefinition());
// Object bean = defaultListableBeanFactory.getBean(beanName);
try {
/**
*启动注册Controller,使用反射调用原生内部方法,虽然spring提供了开放接口,但是接口参数比较复杂,直接反射省事
*/
Method method=RequestMappingHandlerMapping.class.getSuperclass().getSuperclass().getDeclaredMethod("detectHandlerMethods",Object.class);
//将private改为可使用
method.setAccessible(true);
method.invoke(requestMappingHandlerMapping,beanName);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
groovyLoader.clearCache();
}
说明:
1.需要将代码段以类的方式注册到Spring中,beanDefinition是Spring生命周期的一环,必须有
2.需要了解SpringMVC注册接口的过程,这里使用了反射的方法来注册,其实SpringMVC有提供api来注册接口,但是入参构造比较麻烦,果断放弃了,还是反射来的方便
3.文中只有方法有界面录入,这个方法所在的类在本业务中是后台自动生成的,这个类会在load方法入参时拼接进去
3.从Spring容器中移除接口
动态创建的接口难免有需要修改的时候,修改还是比较复杂的,需要将原来的类和方法全部卸载掉,再重新加载。不卸载不行吗?不卸载有可能造成再次加载报错。
当这个方法url和请求方式(post之类的)没变但是方法名变了这里就会报错,提示该接口已经mapped
所以为了安全,每次修改需要将原方法卸载,整个类从Spring容器中移除,再重新加载。
卸载代码:
public void unload(String javaText){
Class<?> javaClazz = groovyLoader.parseClass(javaText);
String beanName = javaClazz.getSimpleName();
if(defaultListableBeanFactory.containsBean(beanName)){
defaultListableBeanFactory.removeBeanDefinition(beanName);
defaultListableBeanFactory.destroySingleton(beanName);
}
/**
* 1.类上的注解获取url
*/
RequestMapping requestMapping = javaClazz.getAnnotation(RequestMapping.class);
String prefix = "";
if(requestMapping != null){
String[] value = requestMapping.value();
prefix = value[0];
}
List<RequestMappingInfo> requestMappingInfoList = new ArrayList<>();
//查询所有方法
Method[] declaredMethods = javaClazz.getDeclaredMethods();
/**
* 2.方法上的注解获取url
* 最终拼接到一快,防止出现//,最后统一替换一次
*/
for(Method method:declaredMethods){
PostMapping postMapping = method.getAnnotation(PostMapping.class);
if(postMapping != null){
String[] value = postMapping.value();
String url = ((prefix+value[0]).replaceAll("//","/"));
RequestMappingInfo requestMappingInfo = RequestMappingInfo.paths(url).methods(RequestMethod.POST).build();
requestMappingInfoList.add(requestMappingInfo);
continue;
}
GetMapping getMapping = method.getAnnotation(GetMapping.class);
if(getMapping != null){
String[] value = getMapping.value();
String url = ((prefix+value[0]).replaceAll("//","/"));
RequestMappingInfo requestMappingInfo = RequestMappingInfo.paths(url).methods(RequestMethod.GET).build();
requestMappingInfoList.add(requestMappingInfo);
continue;
}
PutMapping putMapping = method.getAnnotation(PutMapping.class);
if(putMapping != null){
String[] value = putMapping.value();
String url = ((prefix+value[0]).replaceAll("//","/"));
RequestMappingInfo requestMappingInfo = RequestMappingInfo.paths(url).methods(RequestMethod.PUT).build();
requestMappingInfoList.add(requestMappingInfo);
continue;
}
DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
if(deleteMapping != null){
String[] value = deleteMapping.value();
String url = ((prefix+value[0]).replaceAll("//","/"));
RequestMappingInfo requestMappingInfo = RequestMappingInfo.paths(url).methods(RequestMethod.DELETE).build();
requestMappingInfoList.add(requestMappingInfo);
}
}
for(RequestMappingInfo requestMappingInfo:requestMappingInfoList){
requestMappingHandlerMapping.unregisterMapping(requestMappingInfo);
}
groovyLoader.clearCache();
}
说明:
1.需要先移除Spring容器的beanDefinition和实例对象
2.构造RequestMappingInfo,一个RequestMappingInfo代表一个接口描述
3.使用SpringMVC提供的api来移除注册的接口(这个api入参比较好构造,就不反射了)