利用远程加载class/jar实现业务逻辑分离

2020-04-07  本文已影响0人  TinyThing

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
上一篇下一篇

猜你喜欢

热点阅读