起因
在SpringCloud项目中,每一个SpringBoot工程都要在bootstrap里面配置nacos的注册地址和命名空间等等,多个服务每个服务里面都配置同样的内容,于是就想这些通用的配置能不能集中起来进行配置,于是借助bladex框架中的处理方式,进行了一定研究
System.getProperties()
在bladex中,自定义了一个springboot启动器类的方式,在run方法执行之前进行了根据项目运行环境的动态配置,在最后通过ServiceLoader加载LauncherService接口的实现类,这个接口的实现类中可以启动参数进行配置
public static SpringApplicationBuilder createSpringApplicationBuilder(String appName, Class source, String... args) {
...
// 加载自定义组件
List<LauncherService> 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, isLocalDev()));
return builder;
}
进入到实现类中,发现这里面是通过java原生的的,System.setProperties()方法,设置系统参数,由此可以发现,Springboot的环境变量会收集System.GetProperties里面的参数
/**
* 启动参数拓展服务
*/
@AutoService(LauncherService.class)
public class LauncherPropsServiceImpl implements LauncherService {
@Override
public void launcher(SpringApplicationBuilder builder, String appName, String profile, boolean isLocalDev) {
Properties props = System.getProperties();
// nacos 通用配置
PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.server-addr", LauncherConstant.nacosAddr(profile));
PropsUtil.setProperty(props, "spring.cloud.nacos.config.server-addr", LauncherConstant.nacosAddr(profile));
PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.namespace", LauncherConstant.nacosNameSpace(profile));
PropsUtil.setProperty(props, "spring.cloud.nacos.config.namespace", LauncherConstant.nacosNameSpace(profile));
PropsUtil.setProperty(props, "spring.cloud.nacos.discovery.service", appName);
}
}
ServiceLoad
上面在启动类里面看到了,通过ServiceLoader加载项目中自定义的参数拓展接口的实现类并运行,这是一种符合开闭原则的设计方式,新增或者修改启动参数,新建一个实现类就可以了 ServiceLoad的原理目前还没有细看,只知道他的作用就是: 能够根据接口加载到散布在系统各个包中的实现类 ,这些实现类必须在resource/META-INF/services包下面声明,文件的名字是接口的全限定名,文件里面配置实现类的全限定名,换行作为分隔
AutoService注解
我们已经知道了,ServiceLoad的注册需要手动创建注册文件,然而在bladex中发现他是通过在实现类上标注@AutoService注解 ,value属性中填入接口的全限定名这种方式,自动创建的注册文件这里的实现方式,就是通过自定义注解,搭配注解处理器,这个注解在google-auto包中有现成的实现,但是bladex中似乎是自己仿写了一下,估计是为了学习一下注解处理器的运用吧,那么我也就了解一下这个东西
注解处理器
注解处理器的作用就是在代码编译的阶段,对指定注解标注了的类,方法,属性等进行处理,一般用于在编译阶段生成一些代码,著名的lombok插件就是利用这个原理,在编译阶段根据注解生成相应的代码,下面首先了解一下注解处理器的使用:
- 注解处理器类需要集成 AbstractProcessor 类 主要关注里面的四个方法
- init: 初始化方法,这个方法通常用于实例化一些工具类用于下面处理使用
- getSupportedSourceVersion: 重写此方法可以指定支持的java版本
/**
* 指定支持安卓版本
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported()
- getSupportedAnnotationTypes : 指定支持的注解类型
/**
* 指定支持的注解类型
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return ImmutableSet.of(AutoService.class.getName());
}
- process : 这个方法就是真正对关注的注解进行处理的方法
- 自定义的注解处理器如何生效
- 在META-INF/service文件夹下面,创建javax.annotation.processing.Processor文件,里面指定该接口的实现类,由此可发现,java在加载这些处理器的方式也使用了ServiceLoader
- 注意的是:处理器和javax.annotation.processing.Processor文件一定要分开在两个包中,因为在同一个包中会导致处理也还没有编译完成而报错
- 注意使用注解处理器不能使用spingboot自带的maven打包工具,使用下面的插件
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
使用注解处理器创建ServiceLoader注册文件
- 大体的思路就是,自定义AutoService注解,将注解标记在ServiceLoader服务接口的实现类上面,在注解的value属性中指定该实现类的接口类型,然后通过注解处理器处理该注解,在编译阶段,对AutoService的注解进行信息收集,然后根据收集完的数据生成META-INF/services下面的文件
- 代码:
package com.liuhq.auto.processor;
import com.google.common.collect.ImmutableSet;
import com.liuhq.auto.annotation.AutoService;
import com.liuhq.auto.common.MultiSetMap;
import com.liuhq.auto.common.TypeHelper;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.SimpleAnnotationValueVisitor8;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.util.*;
import java.util.stream.Collectors;
/**
* 自动生成spi接口实现类映射文件注解处理器
*/
@SupportedOptions({ "debug", "verify" })
public class AutoServiceProcessor extends AbstractProcessor {
private TypeHelper typeHelper;
/**
* spi 服务集合,key 接口 -> value 实现列表
*/
private final MultiSetMap<String, String> providers = new MultiSetMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.typeHelper = new TypeHelper(processingEnv);
}
/**
* 指定支持安卓版本
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 指定支持的注解类型
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return ImmutableSet.of(AutoService.class.getName());
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return processImpl(annotations,roundEnv);
}
/**
* 处理注解
* @param annotations
* @param roundEnv
* @return
*/
private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 注解处理操作是否结束
if (roundEnv.processingOver()) {
//2.处理过程结束后生成文件。
generateConfigFiles();
} else {
// 1. 先解析处理注解 收集信息
processAnnotations(annotations, roundEnv);
}
return true;
}
private void generateConfigFiles() {
Filer filer = processingEnv.getFiler();
for (String providerInterface : providers.keySet()) {
String resourceFile = "META-INF/services/" + providerInterface;
log("Working on resource file: " + resourceFile);
try {
SortedSet<String> allServices = new TreeSet<>();
try {
FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "", resourceFile);
log("Looking for existing resource file at " + existingFile.toUri());
Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
log("Existing service entries: " + oldServices);
allServices.addAll(oldServices);
} catch (IOException e) {
log("Resource file did not already exist.");
}
Set<String> newServices = new HashSet<>(providers.get(providerInterface));
if (allServices.containsAll(newServices)) {
log("No new service entries being added.");
return;
}
allServices.addAll(newServices);
log("New service file contents: " + allServices);
FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", resourceFile);
OutputStream out = fileObject.openOutputStream();
ServicesFiles.writeServiceFile(allServices, out);
out.close();
log("Wrote to: " + fileObject.toUri());
} catch (IOException e) {
fatalError("Unable to create " + resourceFile + ", " + e);
return;
}
}
}
private void processAnnotations(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 1.获取被注解 AutoService 修饰的Element集合
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);
// 2. 遍历被注解 AutoService修饰的elements
for (Element element : elements) {
// 3. 因为我们知道AutoService只能修饰在类上 所以直接强转为TypeElement
TypeElement providerImplementer = (TypeElement) element;
// 4. 因为在第一次扫描注解的时候 被修饰的类还没有编译成class对象 所以不能通过反射来获取到上面的注解 这时候通过AnnotationMirror来表示一个注解
AnnotationMirror annotationMirror = getAnnotationMirror(element, AutoService.class);
if (annotationMirror == null) {
continue;
}
// 5. 获取注解中所有的字段集合
Set<TypeMirror> typeMirrors = getValueFieldOfClasses(annotationMirror);
if (typeMirrors .isEmpty()) {
error("No service interfaces provided for element!", element, annotationMirror);
continue;
}
// 6. 遍历字段集合
for (TypeMirror typeMirror : typeMirrors ) {
String providerInterfaceName = typeHelper.getType(typeMirror);
Name providerImplementerName = providerImplementer.getQualifiedName();
log("provider interface: " + providerInterfaceName);
log("provider implementer: " + providerImplementerName);
if (checkImplementer(providerImplementer, typeMirror)) {
providers.put(providerInterfaceName, typeHelper.getType(providerImplementer));
} else {
String message = "ServiceProviders must implement their service provider interface. "
+ providerImplementerName + " does not implement " + providerInterfaceName;
error(message, element, annotationMirror);
}
}
}
log("注解处理结果集" + providers.toString());
}
/**
* 读取 AutoService 上的 value 值
*
* @param annotationMirror AnnotationMirror
* @return value 集合
*/
private Set<TypeMirror> getValueFieldOfClasses(AnnotationMirror annotationMirror) {
return getAnnotationValue(annotationMirror, "value")
.accept(new SimpleAnnotationValueVisitor8<Set<TypeMirror>, Void>() {
@Override
public Set<TypeMirror> visitType(TypeMirror typeMirror, Void v) {
Set<TypeMirror> declaredTypeSet = new HashSet<>(1);
declaredTypeSet.add(typeMirror);
return Collections.unmodifiableSet(declaredTypeSet);
}
@Override
public Set<TypeMirror> visitArray(
List<? extends AnnotationValue> values, Void v) {
return values
.stream()
.flatMap(value -> value.accept(this, null).stream())
.collect(Collectors.toSet());
}
}, null);
}
public AnnotationValue getAnnotationValue(AnnotationMirror annotationMirror, String elementName) {
Objects.requireNonNull(annotationMirror);
Objects.requireNonNull(elementName);
for (Map.Entry<ExecutableElement, AnnotationValue> entry : getAnnotationValuesWithDefaults(annotationMirror).entrySet()) {
if (entry.getKey().getSimpleName().contentEquals(elementName)) {
return entry.getValue();
}
}
String name = typeHelper.getType(annotationMirror);
throw new IllegalArgumentException(String.format("@%s does not define an element %s()", name, elementName));
}
public Map<ExecutableElement, AnnotationValue> getAnnotationValuesWithDefaults(AnnotationMirror annotation) {
Map<ExecutableElement, AnnotationValue> values = new HashMap<>(32);
Map<? extends ExecutableElement, ? extends AnnotationValue> declaredValues = annotation.getElementValues();
for (ExecutableElement method : ElementFilter.methodsIn(annotation.getAnnotationType().asElement().getEnclosedElements())) {
// Must iterate and put in this order, to ensure consistency in generated code.
if (declaredValues.containsKey(method)) {
values.put(method, declaredValues.get(method));
} else if (method.getDefaultValue() != null) {
values.put(method, method.getDefaultValue());
} else {
String name = typeHelper.getType(method);
throw new IllegalStateException(
"Unset annotation value without default should never happen: " + name + '.' + method.getSimpleName() + "()");
}
}
return Collections.unmodifiableMap(values);
}
/**
* 判断是否为接口的实现类
* @param providerImplementer 实现类
* @param providerType 接口
* @return
*/
private boolean checkImplementer(TypeElement providerImplementer, TypeMirror providerType) {
// TODO: We're currently only enforcing the subtype relationship
// constraint. It would be nice to enforce them all.
Types types = processingEnv.getTypeUtils();
return types.isSubtype(providerImplementer.asType(), providerType);
}
/**
* 获取AnnotationMirror
* @param element 被注解修饰的类的element对象
* @param annotationClass 直接的class对象
* @return
*/
public AnnotationMirror getAnnotationMirror(Element element, Class<? extends Annotation> annotationClass) {
// 获取注解的全类名
String annotationClassName = annotationClass.getCanonicalName();
// 遍历被修饰类的AnnotationMirrors
for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
String name = typeHelper.getType(annotationMirror);
// 根据名称找到AnnotationMirror返回
if (name.contentEquals(annotationClassName)) {
return annotationMirror;
}
}
return null;
}
/**
* 输出错误信息
* @param msg
* @param element
* @param annotation
*/
protected void error(String msg, Element element, AnnotationMirror annotation) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, element, annotation);
}
/**
* 输出日志
* @param msg
*/
protected void log(String msg) {
if (processingEnv.getOptions().containsKey("debug")) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);
}
}
protected void fatalError(String msg) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR: " + msg);
}
}
总结:
这个小功能里面主要涉及到了ServiceLoader, 注解处理器这两个知识点,之前都很少接触这次有了一个小的应用,ServiceLoader是java对SPI 思想的实现,和api的不同是,api调用者定义接口,框架的拓展者可以提供自己的实现,然后通过接口可以加载所有的实现,api的调用者则无法选择多个实现, 里面的原理涉及到了java类加载机制和一些设计模式以后值得好好研究,可以加强基础,注解处理器在编译阶段可以生成代码在框架的自动化装配上面应该有很大的作用,或者是开发类似于Lombok那种基于注解的敏捷开发组件 以后要跟深入的使用和研究