前言
最近半年一直在做微服务开发,所使用的框架中用到了一些技术是之前未曾见过的,所以趁不忙之际赶紧学习一下,记录下来。我们框架中用到了java的apt和spi机制,所以在此记录一下两个概念的基本用法和使用场景。
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 extends TypeElement> 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 extends Element> 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 SPI机制中所要扫描的文件。
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件
使用场景:
概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略
比较常见的例子:
使用介绍
要使用Java 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的实现类,代表了一个服务提供者,当然还可以在不同的服务中编写各自的实现类,这样在使用自定义启动类启动时,就会调用各自的实现类。
很多人都在使用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机制