Java APT 和Java SPI机制

前言

最近半年一直在做微服务开发,所使用的框架中用到了一些技术是之前未曾见过的,所以趁不忙之际赶紧学习一下,记录下来。我们框架中用到了java的apt和spi机制,所以在此记录一下两个概念的基本用法和使用场景。

Java APT

APT(Annotation Process Tool),注解处理工具。apt被设计为操作java源文件,也就是编译时对注解进行扫描并处理,按照一定的规则,生成相应的java文件。在java8之前,apt相关的源码(com.sun.mirror.apt.*)都在tools.jar里,如果要使用需要将tools.jar加到Libraries下。java8开始,相关代码移到了javax.annotation.processing和javax.lang.model下,相关的类也发生了变化,使用更加方便。下面说下apt在我们微服务框架中的使用场景。

@AutoService注解,可能做Android开发的都用过,这个注解是google的一个开源jar包中的注解(https://github.com/google/auto),用于生成java SPI描述文件,而我们框架手动实现了一个@AutoService自定义注解,实现大致过程如下:

1、先创建一个自定义注解,@Retention(RetentionPolicy.SOURCE)说明这个注解只在源码阶段生效,编译后就不再生效了。

import java.lang.annotation.*;

/**
 * An annotation for service providers as described in {@link java.util.ServiceLoader}. The {@link
 * AutoServiceProcessor} generates the configuration files which
 * allows service providers to be loaded with {@link java.util.ServiceLoader#load(Class)}.
 *
 * 

Service providers assert that they conform to the service provider specification. * Specifically, they must: * *

    *
  • be a non-inner, non-anonymous, concrete class *
  • have a publicly accessible no-arg constructor *
  • implement the interface type returned by {@code value()} *
* * @author google */ @Documented @Retention(RetentionPolicy.SOURCE) @Target(ElementType.TYPE) public @interface AutoService { /** * Returns the interfaces implemented by this service provider. * * @return interface array */ Class[] value(); }

2、编写注解处理器

APT提供了一个AbstractProcessor抽象类,我们新建一个类,继承这个抽象类,实现抽象类的一些方法,如下:


import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import java.util.Collections;
import java.util.Set;

/**
 * 注解处理器
 */
public class AutoServiceProcessor extends AbstractProcessor {

    /**
     * 支持的java版本,推荐直接返回SourceVersion.latestSupported(),代表最新java版本
     * 该方法可被{@code @SupportedSourceVersion}注解代替
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 扫描的自定义注解
     * 该方法可被{@code @SupportedAnnotationTypes}注解代替
     */
    @Override
    public Set getSupportedAnnotationTypes() {
        return Collections.singleton(AutoService.class.getName());
    }

    /**
     * 初始化方法
     * ProcessingEnvironment是一个包含很多功能的工具类,帮助我们生成想要的源文件
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    /**
     * 核心方法
     * 在这里编写要生成文件的代码
     */
    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        Filer filer = this.processingEnv.getFiler(); // 一种PrintWiter,可以用它来创建文件
        Elements elementUtils = this.processingEnv.getElementUtils(); // 处理Element的工具类,用于获取程序的元素,例如包、类、方法。
        Types typeUtils = this.processingEnv.getTypeUtils();  // 处理TypeMirror的工具类,用于取类信息
        Messager messager = this.processingEnv.getMessager(); // 错误处理工具 可以向用户报告消息,比如处理过程中发生了错误,错误再源代码中的位置

        //扫描整个工程   找出含有BindView注解的元素
        Set elements = roundEnv.getElementsAnnotatedWith(AutoService.class);
        //遍历元素
        for (Element element : elements) {
            // 该注解是加在类上的,所以可以强转成TypeElement
            TypeElement typeElement = (TypeElement) element;

            // 生成文件
            // 生成/META-INF/services/加注解类的全限定名 文件
        }
        return true;
    }
}

这里仅展示部分代码,通过以上注解处理器,在编译时就会扫描加了个@AutoService(Interface.class)注解的类,并在此类所在jar包的resources/META-INF/services下面生成以此类所继承的接口的全限定名命名的文件,内容是此类的全限定名。如下图:

Java APT 和Java SPI机制_第1张图片

而生成的这个文件,也是接下来要讲的java SPI机制中所要扫描的文件。

Java SPI

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件

使用场景:

概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略

比较常见的例子:

  • 数据库驱动加载接口实现类的加载
    JDBC加载不同类型数据库的驱动
  • 日志门面接口实现类加载
    SLF4J加载不同提供商的日志实现类
  • Spring
    Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
  • Dubbo
    Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

使用介绍

要使用Java SPI,需要遵循如下约定:

  • 1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
  • 2、接口实现类所在的jar包放在主程序的classpath中;
  • 3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
  • 4、SPI的实现类必须携带一个不带参数的构造方法;

比如我们微服务框架中定义了一个接口LauncherService.java,如下:


import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.core.Ordered;

/**
 * launcher 扩展 用于一些组件发现
 *
 */
public interface LauncherService extends Ordered, Comparable {

	/**
	 * 启动时 处理 SpringApplicationBuilder
	 *
	 * @param builder    SpringApplicationBuilder
	 * @param appName    SpringApplicationAppName
	 * @param profile    SpringApplicationProfile
	 * @param isLocalDev SpringApplicationIsLocalDev
	 */
	void launcher(SpringApplicationBuilder builder, String appName, String profile, boolean isLocalDev);

	/**
	 * 获取排列顺序
	 *
	 * @return order
	 */
	@Override
	default int getOrder() {
		return 0;
	}

	/**
	 * 对比排序
	 *
	 * @param o LauncherService
	 * @return compare
	 */
	@Override
	default int compareTo(LauncherService o) {
		return Integer.compare(this.getOrder(), o.getOrder());
	}

}

接口是提供了一种能力,该接口提供了一个方法launcher(),需要其他服务实现该接口方法,实现该接口的服务称为提供者,实现类如下:


import org.springblade.core.auto.service.AutoService;
import org.springblade.core.launch.service.LauncherService;
import org.springframework.boot.builder.SpringApplicationBuilder;

/**
 * 日志启动器
 *
 * @author L.cm
 */
@AutoService(LauncherService.class)
public class LogLauncherServiceImpl implements LauncherService {

	@Override
	public void launcher(SpringApplicationBuilder builder, String appName, String profile, boolean isLocalDev) {
		System.setProperty("logging.config", String.format("classpath:log/log4j2_%s.xml", profile));
		if (!isLocalDev) {
			System.setOut(LogPrintStream.out());
			System.setErr(LogPrintStream.err());
		}
	}

}

其中@AutoService注解上面介绍过,它的作用是在编译时会在resources/META-INF/services/下生成一个以“接口全限定名”为命名的文件,内容为实现类的全限定名。launcher()方法的作用时在springboot启动时,加载额外的配置到系统变量里

如何调用:

在我们框架中是在自定义的启动类里,通过SPI提供的ServiceLoader.load()方法来调用。具体看代码:

// 加载自定义组件
List launcherList = new ArrayList<>();
ServiceLoader.load(LauncherService.class).forEach(launcherList::add);

launcherList.stream().sorted(Comparator.comparing(LauncherService::getOrder)).collect(Collectors.toList())
			.forEach(launcherService -> launcherService.launcher(builder, appName, profile, true));

ServiceLoader.load(LauncherService.class)此方法会扫描所有classpath中的jar包里的META-INF/service/接口全限定名 文件,ServiceLoader本身继承了迭代器接口,扫描出来后是个集合,可以通过遍历集合中每一个实现类,执行实现类的launcher方法,称为服务调用。

我们当前只写了一种LauncherService的实现类,代表了一个服务提供者,当然还可以在不同的服务中编写各自的实现类,这样在使用自定义启动类启动时,就会调用各自的实现类。

Spring的SPI

很多人都在使用Springboot,很喜欢springboot的自动配置,如jdbc的自动配置、redis、kafka等,只需要将ip端口和一些参数配置在application.yaml里,就可以启动并使用了,非常方便。但其中能实现自动配置的原理,跟spring的SPI机制拖不了干系。

与jdk中的SPI一样,spring自己也实现了这一机制,只不过不同的是扫描的文件名变成了META-INF/spring.factories,扫描所使用的类变成了SpringFactoriesLoader.java。通过翻看springboot的jar包源码不难发现,在一些以xxx-spring-boot-starter命名的jar包中的META-INF/下面一般都有个spring.factories文件,spring在启动时就会扫描这些文件,并执行文件内容中列出的配置类,从而实现自动配置。

spirngboot 有个spring-boot-autoconfigure.jar包,这个包里面就是jdbc、redis、kafka等等这些的自动配置类。


参考文档:
高级开发必须理解的Java中SPI机制

你可能感兴趣的:(java)