美文网首页
利用远程加载class/jar实现业务逻辑分离

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

作者: TinyThing | 来源:发表于2020-04-07 15:20 被阅读0次

    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
    

    相关文章

      网友评论

          本文标题:利用远程加载class/jar实现业务逻辑分离

          本文链接:https://www.haomeiwen.com/subject/onhnphtx.html