轻量ioc框架loveqq,支持接口上传jar格式的starter启动器并支持热加载其中的bean,组合注解工具类介绍

139 阅读3分钟

功能简介

热加载starter启动器

该功能依托于框架自定义的 JarIndexClassLoader,以及 loveqq 框架支持ioc容器的全量刷新。 通过api接口上传 jar 包后,全量刷新ioc容器,从而实现热加载启动器,动态扩展功能。

@This 注解支持自调用

注解了 @This 的 bean,即使在内部使用 this.xxx() 调用方法,也可以使代理生效,再也不用担心自调用导致的事务失效场景。

@Async.Await 注解自动将异步转同步调用

注解了 Async.Await 的方法,在其方法体调用链路中,所有的异步方法都将自动转为同步调用,不仅仅是 @Async 注解的方法,返回值类型为 Future 的同样支持。

组合注解工具类

该工具类基于 @AliasFor 注解实现了注解组合,但是相对spring的实现更为简单,仅仅50行代码就实现了组合注解注解, 解析结果就是直接的注解实例,和普通的注解实例没有区别,结果直观,易于使用。

代码示例

热加载starter启动器

先写一个web服务:

@Slf4j
@EnableWebMvc
@RestController
@BootApplication
@RequestMapping(expose = true)                  // 自动暴露 public 方法为 POST http 接口
public class Main {
    @Autowired
    private Extension extension;

    /**
     * 测试接口
     */
    @GetMapping
    public String sayHello() {
        return extension.getId();
    }

    /**
     * 加载插件
     *
     * @param jar jar 包 启动器
     * @return 上传后的 jar 包绝对路径,卸载启动器时需要提供该返回值
     */
    public String loadPlugin(MultipartFile jar) throws Exception {
        // 保存到本地
        String filePath = "D:\\temp\\jar\\" + UUID.randomUUID().toString().replace("-", "") + "\\" + jar.getOriginalFilename();
        File jarFile = new File(filePath);
        jar.transferTo(jarFile);

        // 添加到框架 ClassLoader
        JarIndexClassLoader classLoader = (JarIndexClassLoader) IOC.class.getClassLoader();
        classLoader.addJarIndex(Collections.singletonList(new JarFile(jarFile)));

        // 刷新上下文
        ContextRefresher.refresh(IOC.getApplicationContext());

        return jarFile.getAbsolutePath();
    }

    /**
     * 卸载启动器
     *
     * @param jarPath {@link #loadPlugin(MultipartFile)} 的返回值
     */
    public String unloadPlugin(String jarPath) throws Exception {
        // 构建 File 对象
        File jarFile = new File(jarPath);

        // 从框架 ClassLoader 移除
        JarIndexClassLoader classLoader = (JarIndexClassLoader) IOC.class.getClassLoader();
        classLoader.removeJarIndex(Collections.singletonList(new JarFile(jarFile)));

        // 刷新上下文
        ContextRefresher.refresh(IOC.getApplicationContext());

        return "ok";
    }

    public static void main(String[] args) throws Exception {
        K.run(Main.class, args);
    }

    /**
     * 默认实现
     */
    @Component
    @ConditionalOnMissingBean(Extension.class)
    public static class DefaultExtension implements Extension {

        @Override
        public String getId() {
            return "default";
        }

        @Override
        public boolean isCritical() {
            return false;
        }

        @Override
        public byte[] getValue() {
            return new byte[0];
        }

        @Override
        public void encode(OutputStream out) throws IOException {

        }
    }
}

然后,新建一个项目,添加如下类:

/**
 * 动态加载示例实现
 */
@Component
public class ExampleExtension implements Extension {

    @Override
    public String getId() {
        return "example";
    }

    @Override
    public boolean isCritical() {
        return false;
    }

    @Override
    public byte[] getValue() {
        return new byte[0];
    }

    @Override
    public void encode(OutputStream out) throws IOException {

    }
}

并在 k.factories 中添加:

com.kfyty.loveqq.framework.core.autoconfig.annotation.EnableAutoConfiguration=com.kfyty.graal.example.ExampleExtension

然后打成 jar 包,就是一个启动器了。

接着启动第一段代码的 main 方法后:

1、先访问:http://localhost:8080/sayHello,将返回 default

2、然后使用 postman 上传启动器 jar 包:http://127.0.0.1:8080/loadPlugin,此时将动态加载上传的启动器,并刷新 ioc 容器 3、然后再访问:http://localhost:8080/sayHello,将返回 example,原因是加载了新的启动器,条件注解生效,实现类变化了!

4、然后再访问:http://127.0.0.1:8080/unloadPlugin,将第二步的返回值作为入参传入,此时将卸载启动器,并刷新 ioc 容器 5、然后再访问:http://localhost:8080/sayHello,将返回 default,原因是卸载了之前加载的启动器,条件注解生效,实现类又变化了!

@Async.Await 注解自动将异步转同步调用

使用代码示例:

@Slf4j
@Async
@BootApplication
public class Main implements CommandLineRunner {
    @Autowired
    private Main main;

    @Autowired
    private AsyncService asyncService;

    @Override
    public void run(String... args) throws Exception {
//        main.test();
        main.testAwait();
    }

    /**
     * 结果输出 0,因为内部调用的是异步方法
     */
    public void test() {
        asyncService.asyncProcess();
        System.out.println(asyncService.getState());
    }

    /**
     * 结果输出 1,因为内部调用异步方法自动转为了同步
     */
    @Async.Await
    public void testAwait() {
        asyncService.asyncProcess();
        System.out.println(asyncService.getState());
    }

    public static void main(String[] args) throws Exception {
        K.run(Main.class, args);
    }

    @Async
    @Component
    public static class AsyncService {
        @Getter
        private int state = 0;

        @Async
        public void asyncProcess() {
            // 模拟业务耗时
            CommonUtil.sleep(500);

            // 修改状态
            this.state = 1;
        }
    }
}

示例中的 testAwait() 方法中,调用 asyncProcess() 时将自动异步转同步,为某些业务场景提供的简单易用的支持。

组合注解工具类

组合注解代码示例:

@Main.AAA("aaa")
public class Main {

    public static void main(String[] args) throws Exception {
        Annotation[] annotations = AnnotationUtil.findAnnotations(Main.class);
		BBB bbb = AnnotationUtil.findAnnotation(Main.class, BBB.class);
        System.out.println(CommonUtil.toString(annotations));
    }

    @BBB
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AAA {
        @AliasFor(value = "path", annotation = BBB.class)
        String value() default "";
    }

    @AAA
    @Retention(RetentionPolicy.RUNTIME)
    public @interface BBB {
        @AliasFor(value = "value", annotation = AAA.class)
        String path() default "";
    }
}

示例代码得到的 bbb 注解实例中,bbb.path() 属性直接就是 aaa,结果非常直观,没有那么多的繁杂的工厂类,合并类等。

有对 loveqq-framework 感兴趣的同学可以gitee/gitcode/github搜索尝鲜