利用远程加载class/jar实现业务逻辑分离
0x0 背景
近期项目中遇到两个问题:
一、现场升级问题
现场定制一些新的功能,我们开发完成后需要给项目升级;这个功能我们只修改了一个jar包,仅仅需要在现场替换掉该jar包即可实现功能,但是升级组件的时候,往往需要对整个组件包进行升级!
二、分布式环境下各个节点升级
如果某个组件是多节点部署,一旦修改或升级就需要对每个节点都进行升级,在节点较少时自然没有大的问题,当节点较多且分布在全国各地时,这种方式就会比较麻烦
0x1 解决方案
为了解决以上的问题,近期看了一些关于关于容器的概念,忽然想到我们的业务逻辑本质上就是一个个运行在spring容器中的bean,为什么不能把我们的业务逻辑脱离出去!为什么不能把这些bean的class放在项目外部!例如放在中心,然后各个节点启动时从中心通过网络协议加载class生成bean!
这样做就可以解决以上的两个项目中遇到的问题,而且配合springcloud中心配置,可以实现中心化管理集群!每个组件或每个微服务,本质上成为一个没有任何业务逻辑的spring容器。
0x2 Demo
要实现以上功能,需要改造我们的springboot项目,让项目在启动的时候加载业务逻辑的jar包即可,以下是我写的一个demo:
2.1 容器服务
代码很简单,只有一个springboot启动类,如下:
@SpringBootApplication
@ComponentScan(basePackages = {"com.fly"})
@EnableSwagger2
public class ServiceContainerApplication {
public static void main(String[] args) throws Exception {
//项目启动时,添加path,可以通过启动参数注入
loadRemoteJars("http://127.0.0.1:9999/remote.jar");
SpringApplication.run(ServiceContainerApplication.class, args);
}
/**
* main方法中执行,加载远程jar包到classpath
* 这样spring的包扫描可以扫描到我们的目标jar包
*
* @param paths 远程jar路径,url格式
*/
private static void loadRemoteJars(String... paths) throws Exception {
//1.获取系统class loader
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
//1.使用spring工具类获取class loader
//URLClassLoader classLoader = (URLClassLoader) ClassUtils.getDefaultClassLoader();
//2.设置添加url方法为public
Method add = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
boolean accessible = add.isAccessible();
add.setAccessible(true);
//3.添加我们的目标url
for (String path : paths) {
URI uri = new URI(path);
add.invoke(classLoader, uri.toURL());
}
//4.还原现场
add.setAccessible(accessible);
}
}
这里的核心在于项目启动的时候对class loader进行了改造,将我们的远程url加入到了他的classpath url列表中
2.2 业务逻辑jar
业务逻辑没有什么注意的地方,以下仅仅是个普通的controller和service
@RestController
@RequestMapping("/remote")
@Slf4j
public class RemoteController {
@Autowired
private RemoteService remoteService;
@GetMapping
public String sayHello(@RequestParam String name) {
log.info("name = {}", name);
return remoteService.sayHello(name);
}
}
@Service
public class RemoteService {
@PostConstruct
public void init() {
System.out.println("post construct: test remote service init...");
}
@EventListener(classes = ApplicationReadyEvent.class)
public void onApplicationReady() {
System.out.println("application ready event received by remote service...");
}
public String sayHello(String name) {
return "hello " + name;
}
}
2.3 jar包管理服务
将2.2中的业务逻辑代码打成jar包,放到一个文件服务器上即可,本次使用nginx服务作为文件服务,将jar包放到nginx下的html下(默认的静态文件路径),设置端口9999,启动nginx;
2.4 启动容器查看效果:
项目启动日志:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.6.RELEASE)
2020-04-07 14:55:36.241 INFO 8544 --- [ restartedMain] c.f.c.ServiceContainerApplication : Starting ServiceContainerApplication on pc-HZ20094274 with PID 8544 (E:\Workspace\service-container\target\classes started by guoxiang6 in E:\Workspace\service-container)
2020-04-07 14:55:36.242 INFO 8544 --- [ restartedMain] c.f.c.ServiceContainerApplication : No active profile set, falling back to default profiles: default
2020-04-07 14:55:36.612 INFO 8544 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2020-04-07 14:55:36.613 INFO 8544 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-04-07 14:55:36.613 INFO 8544 --- [ restartedMain] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.33]
2020-04-07 14:55:36.625 INFO 8544 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-04-07 14:55:36.626 INFO 8544 --- [ restartedMain] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 381 ms
post construct: test remote service init...
2020-04-07 14:55:36.705 INFO 8544 --- [ restartedMain] pertySourcedRequestMappingHandlerMapping : Mapped URL path [/v2/api-docs] onto method [springfox.documentation.swagger2.web.Swagger2Controller#getDocumentation(String, HttpServletRequest)]
2020-04-07 14:55:36.732 INFO 8544 --- [ restartedMain] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-04-07 14:55:36.737 WARN 8544 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : Unable to start LiveReload server
2020-04-07 14:55:36.771 INFO 8544 --- [ restartedMain] d.s.w.p.DocumentationPluginsBootstrapper : Context refreshed
2020-04-07 14:55:36.772 INFO 8544 --- [ restartedMain] d.s.w.p.DocumentationPluginsBootstrapper : Found 1 custom documentation plugin(s)
2020-04-07 14:55:36.776 INFO 8544 --- [ restartedMain] s.d.s.w.s.ApiListingReferenceScanner : Scanning for api listing references
2020-04-07 14:55:36.805 INFO 8544 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2020-04-07 14:55:36.805 INFO 8544 --- [ restartedMain] c.f.c.ServiceContainerApplication : Started ServiceContainerApplication in 0.59 seconds (JVM running for 23.688)
application ready event received by remote service...
2020-04-07 14:55:36.807 INFO 8544 --- [ restartedMain] .ConditionEvaluationDeltaLoggingListener : Condition evaluation unchanged
看到以下两行日志
post construct: test remote service init...
application ready event received by remote service...
可以发现我们的bean已经被加载到了容器中!
上swagger查看下我们的controller:
image.png
0xFF 总结
核心思想是:在项目启动的时候从网络上加载class,从而使得我们的业务逻辑和基础设施进行分离,便于项目升级和集群管理。
只是一时的想法,没有经过项目实践,大家斟酌使用。
2020-07-10 更新
考虑到jar包路径可能是从配置文件读取的,我们可以使用SpringApplicationRunListener来操作:
@Slf4j
public class RemoteJarSpringApplicationRunListener implements SpringApplicationRunListener {
public RemoteJarSpringApplicationRunListener(SpringApplication application, String[] args){
}
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
log.info("environment准备就绪,加载远程jar包");
String paths = environment.getProperty("remote.jar.path", "");
log.info("jar路径 = {}", paths);
Set<String> jarSet = Stream.of(paths.split(";"))
.filter(StringUtils::hasText)
.collect(Collectors.toSet());
if (jarSet.isEmpty()) {
return;
}
log.info("开始加载jar包...");
//项目启动时,添加path,可以通过启动参数注入
try {
loadRemoteJars(jarSet);
} catch (Exception e) {
log.error("加载jar包失败:", e);
}
log.info("加载jar包完成...");
}
/**
* main方法中执行,加载远程jar包到classpath
* 这样spring的包扫描可以扫描到我们的目标jar包
*
* @param paths 远程jar路径,url格式
*/
private void loadRemoteJars(Collection<String> paths) throws Exception {
//1.获取系统class loader
URLClassLoader classLoader = (URLClassLoader) ClassUtils.getDefaultClassLoader();
//2.设置添加url方法为public
Method add = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
boolean accessible = add.isAccessible();
add.setAccessible(true);
//3.添加我们的目标url
for (String path : paths) {
URI uri = new URI(path);
add.invoke(classLoader, uri.toURL());
}
//4.还原现场
add.setAccessible(accessible);
}
}
然后在resource下新建一个META-INF文件夹,配置一个文件:spring.factories,内容如下:
org.springframework.boot.SpringApplicationRunListener=\
com.fly.data.RemoteJarSpringApplicationRunListener