【JVM知识】插入式注解处理器实现java编程规范检测
- 一、前言
- 二、Java 规范提案
- 三、注解分类
- 四、java编译器
- 五、插入式注解处理器
- 六、代码规范检测实现(代码示例)
- 七、项目版本统一控制实现(代码示例)
一、前言
最近在看**《深入理解Java虚拟机》**时,看到了插入式注解处理器,在了解之后发现这个功能在我们平时工作中用到的还是比较广的,虽然我们工作中很少涉及这一块的编码,但是用到的很多组件都是通过这个功能实现的,例如,checkStyle
、FindBug
、lombok
、mapstruct
等插件,尤其是lombok插件应该是程序开发必备插件,之前一直没有仔细了解它的实现原理。
二、Java 规范提案
JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)
提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
jsr | 描述 | 相关实现 |
---|---|---|
jsr107 | 缓存规范 | spring基于此实现的缓存体系 |
jsr250 | java平台Common Annontations 如@PostConstruct | spring |
jsr269 | Pluggable Annotation Processing API | lombok,mapstruct |
jsr303,jsr349,jsr380 | bean validation | hibernate-validitor |
三、注解分类
注解
分为运行时注解和编译时期注解
运行时注解
:java运行时,JVM中反射技术获取注解信息,执行特定操作;
例如:spring IOC aop mybatis
编译时期注解
:java编译器运行时,此时根本没有字节文件,因为编译没有完成;我们按照规范编写一个插件,植入java编译器中;我们修改java源文件对应的抽象语法树,实现特定操作。
例如:lombok mapstruct
区别:
- 保留阶段不同。运行时注解保留到运行时,可在运行时访问。而编译时注解保留到编译时,运行时无法访问。
- 原理不同。运行时注解是Java反射机制,Retrofit运行时注解,需要用的时候才用到,而编译时注解通过APT、AbstractProcessor。
- 性能不同。运行时注解由于使用Java反射,因此对性能上有影响。编译时注解对性能没影响。这也是为什么ButterKnife从运行时切换到了编译时的原因。
- 产物不同。运行时注解只需自定义注解处理器即可,不会产生其他文件。而编译时注解通常会产生新的Java源文件。
四、java编译器
概念:javac 是java语言编程编译器。全称java compiler。javac工具读由java语言编写的类和接口的定义,并将它们编译成字节代码的class文件。
javac编译器的工作流程:
从javac代码的总体结构来看,编译过程大致可以分为1个准备过程和3个处理过程,它们分别为:
1)准备过程:初始化插入式注解处理器;
2)解析与填充符号表过程,包括:
词法、语法分析
。将将源代码的字符流转变为标记集合,构造出抽象语法树;填充符号表
。产生符号地址和符号信息;
3)插入式注解处理器的注解处理过程:插入式注解处理器的执行阶段;
4)分析与字节码生成过程,包括:
标注检查
。对语法的静态信息进行检查;数据流及控制流分析
。对程序动态运行过程进行检查;解语法糖
,将简化代码编写的语法糖还原为原有的形式;字节码生成
。将前面各个步骤所生成的信息转化成字节码。
注
:执行插入式注解时又可能会产生新的符号,如果有新的符号产生,就必须转回到之前的解析、填充符号表的过程中重新处理这些新符号;
五、插入式注解处理器
1、概念:注解处理器(Annotation Processor)
是javac内置的一个用于编译时扫描和处理注解(Annotation)的工具。简单的说,在源代码编译阶段,通过注解处理器,我们可以获取源文件内注解(Annotation)相关内容javax.annotation.processing.Processor
用于替换JDK6之前的APT(Annotatino Processing Tool)。
2、使用场景:
由于注解处理器可以在程序编译阶段工作,所以我们可以在编译期间通过注解处理器进行我们需要的操作。比较常用的用法就是在编译期间获取相关注解数据,然后动态生成.java源文件(让机器帮我们写代码),通常是自动产生一些有规律性的重复代码,解决了手工编写重复代码的问题,大大提升编码效率
3、javax.annotation.processing.Processor
public interface Processor {
//返回此 processor 识别的选项。处理工具的实现必须提供一种方式来传递特定于 processor 的选项,这些选项不同于传递给工具自身的选项,
Set<String> getSupportedOptions();
//返回此 processor 支持的注释类型的名称。结果元素可能是某一受支持注释类型的规范(完全限定)名称。它也可能是 "name.*" 形式的名称,表示所有以 "name." 开头的规范名称的注释类型集合。最后,"*" 自身表示所有注释类型的集合,包括空集。注意,processor 不应声明 "*",除非它实际处理了所有文件;声明不必要的注释可能导致在某些环境中的性能下降。
Set<String> getSupportedAnnotationTypes();
//用于指定你的java版本,一般返回:SourceVersion.latestSupported()
SourceVersion getSupportedSourceVersion();
//用处理环境初始化处理器。
void init(ProcessingEnvironment processingEnv);
//处理先前 round 产生的类型元素上的注释类型集,并返回这些注释是否由此 processor 声明。如果返回 true,则这些注释已声明并且不要求后续 processor 处理它们;如果返回 false,则这些注释未声明并且可能要求后续 processor 处理它们。processor 可能总是返回相同的 boolean 值,或者可能基于所选择的标准而返回不同的结果。
如果 processor 支持 "*" 并且根元素没有注释,则输入集合将为空。processor 必须妥善处理空注释集。
boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv);
quot;),
Iterable<? extends Completion> getCompletions(Element element,
AnnotationMirror annotation,
ExecutableElement member,
String userText);
}
类 AbstractProcessor API说明
六、代码规范检测实现(代码示例)
1、NameChecker类
package com.sk.service.check;
import javax.annotation.processing.Messager;
import javax.lang.model.element.*;
import javax.lang.model.util.ElementScanner6;
import javax.tools.Diagnostic;
import java.util.EnumSet;
public class NameChecker {
private final Messager messager;
NameCheckScanner nameCheckScanner = new NameCheckScanner();
public NameChecker(Messager messager) {
this.messager = messager;
}
public void checkNames(Element element){
nameCheckScanner.scan(element);
}
public class NameCheckScanner extends ElementScanner6<Void,Void> {
@Override
public Void visitType(TypeElement e, Void p){
scan(e.getTypeParameters(),p);
checkCamelCase(e,true);
super.visitType(e,p);
return null;
}
public Void visitExecutable(ExecutableElement e, Void p){
if(e.getKind() == ElementKind.METHOD){
Name name = e.getSimpleName();
if(name.contentEquals(e.getEnclosingElement().getSimpleName())){
messager.printMessage(Diagnostic.Kind.WARNING,"一个普通方法 "+name+" 不应当与类名重复,避免与构造函数产生混淆",e);
}
checkCamelCase(e,false);
}
super.visitExecutable(e,p);
return null;
}
public Void visitVariable(VariableElement e,Void p){
//如果这个Variable是枚举或常量,则按大写命名检查,否则按照驼式命名规则检查
if(e.getKind() == ElementKind.ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e)){
checkAllCaps(e);
}else{
checkCamelCase(e,false);
}
return null;
}
private void checkAllCaps(VariableElement e) {
String name = e.getSimpleName().toString();
boolean conventional = true;
int firstCodePoint = name.codePointAt(0);
if(!Character.isUpperCase(firstCodePoint)){
conventional = false;
}else{
boolean previousUnderscore = false;
int cp = firstCodePoint;
for(int i= Character.charCount(cp);i<name.length();i+=Character.charCount(cp)){
cp = name.codePointAt(i);
if(cp == (int)'_'){
if(previousUnderscore){
conventional = false;
break;
}
previousUnderscore = true;
}else{
previousUnderscore = false;
if(!Character.isUpperCase(cp) && !Character.isDigit(cp)){
conventional = false;
break;
}
}
}
}
if(!conventional){
messager.printMessage(Diagnostic.Kind.WARNING,"常量 "+name+" 应当全部以大写字母或者下划线命令,并且以字母开头",e);
}
}
private boolean heuristicallyConstant(VariableElement e) {
if(e.getEnclosingElement().getKind() == ElementKind.INTERFACE){
return true;
}else if(e.getKind() == ElementKind.FIELD && e.getModifiers().containsAll(EnumSet.of(PUBLIC,STATIC,FINAL))){
return true;
}else{
return false;
}
}
private void checkCamelCase(Element e, boolean b) {
String name = e.getSimpleName().toString();
boolean previousUpper = false;
boolean conventional = true;
int firstCodePoint = name.codePointAt(0);
if(Character.isUpperCase(firstCodePoint)){
previousUpper = true;
if(!b){
messager.printMessage(Diagnostic.Kind.WARNING,"名称 "+name+" 应当以小写字母开头",e);
}
}else if(Character.isLowerCase(firstCodePoint)){
if(b){
messager.printMessage(Diagnostic.Kind.WARNING,"名称 "+name+" 应当以大写字母开头",e);
}
}else{
conventional = false;
}
if(b){
int cp = firstCodePoint;
for(int i=Character.charCount(cp);i<name.length();i+=Character.charCount(cp)) {
cp = name.codePointAt(cp);
if (Character.isUpperCase(cp)) {
if (previousUpper) {
conventional = false;
break;
}
previousUpper = true;
} else {
previousUpper = false;
}
}
}
if(!conventional){
messager.printMessage(Diagnostic.Kind.WARNING,"名称 "+name+" 应当符合驼式命名法",e);
}
}
}
}
2、NameCheckProcessor 类
package com.sk.service.check;
import com.sun.xml.internal.xsom.impl.Ref;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.Set;
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class NameCheckProcessor extends AbstractProcessor {
private NameChecker nameChecker;
public void init(ProcessingEnvironment processingEnv){
super.init(processingEnv);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if(!roundEnv.processingOver()){
for(Element element:roundEnv.getRootElements()){
nameChecker.checkNames(element);
}
}
return false;
}
}
七、项目版本统一控制实现(代码示例)
package com.sk.service;
import com.google.auto.service.AutoService;
import com.sk.annotation.BussVersion;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.HashSet;
import java.util.Set;
@AutoService(Processor.class)
public class BussVersionProcessor extends AbstractProcessor {
private JavacTrees javacTrees;
private TreeMaker treeMaker;
private ProcessingEnvironment processingEnv;
/**
* 初始化处理器
*
* @param processingEnv 提供了一系列的实用工具
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.processingEnv = processingEnv;
this.javacTrees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> set = new HashSet<>();
set.add(BussVersion.class.getName()); // 支持解析的注解
return set;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement t : annotations) {
for (Element e : roundEnv.getElementsAnnotatedWith(t)) { // 获取到给定注解的element(element可以是一个类、方法、包等)
// JCVariableDecl为字段/变量定义语法树节点
JCTree.JCVariableDecl jcv = (JCTree.JCVariableDecl) javacTrees.getTree(e);
String varType = jcv.vartype.type.toString();
if (!"java.lang.String".equals(varType)) { // 限定变量类型必须是String类型,否则抛异常
printErrorMessage(e, "Type '" + varType + "'" + " is not support.");
}
jcv.init = treeMaker.Literal(getVersion()); // 给这个字段赋值,也就是getVersion的返回值
}
}
return true;
}
/**
* 利用processingEnv内的Messager对象输出一些日志
*
* @param e element
* @param m error message
*/
private void printErrorMessage(Element e, String m) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, m, e);
}
private String getVersion() {
/**
* 获取version,这里省略掉复杂的代码,直接返回固定值
*/
return "v1.0.1";
}
}