Annotation 使用案例

Java 的 annotation 自 JDK1.5就拥有了,主要作用就是给代码打标注,这个系列文章将从头到尾进行一个梳理,当然不仅仅是 Annotation,也包括他的解析;一些比较常见的做法,百度谷歌一搜一大把的就再赘述。

还是直奔主题,我们以一个目标进行,比如我们需要做个一个文档生成器,使用Annotation来对 API 进行标注, 然后结合 Maven 生成文档;或者在编译的时候生成文档,先结合 Maven 来使用。

构建一些 Annotation

首先,我们需要准备一些 Annotation,这个 Annotation 将会给我们的其他项目使用,建议单独是一个项目,按照思路,某些类比如 UserController需要标注, 以确定这个类是我们需要扫描的,然后这个类的若干方法也需要标注,方法里面包括参数,目前先整这基础的3个
,先看类标注

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Target(ElementType.TYPE)//对类接口进行注解
@Retention(RetentionPolicy.RUNTIME)//在运行时也保留该注解
@Documented//可以被 javadoc 文档化
public @interface DocAction {
    String value() default"";//功能描述
    String[] author() default "";//作者,可能有多个
    String name() default "";//名称
    String date() default "";//开始日期
    String update() default "";//结束日期
}

Target

Target标识注解到什么地方,这里的ElementType是一个枚举类,源码如下

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    //类、接口(包括注解类型),或者枚举
    TYPE,

    /** Field declaration (includes enum constants) */
    //字段,包括枚举
    FIELD,

    /** Method declaration */
    //方法
    METHOD,

    /** Parameter declaration */
    //参数
    PARAMETER,

    /** Constructor declaration */
    //构造方法
    CONSTRUCTOR,

    /** Local variable declaration */
    //本地变量
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    //注解
    ANNOTATION_TYPE,

    /** Package declaration */
    //包
    PACKAGE
}

上面就是所有的 target 注解地方,也顺便看下@Target的源码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

他的 target 就是ANNOTATION_TYPE,也就是只能在 Annotation 上使用,而里面 value() 方法返回值是一个数组,所以,target 里面可以是多个地方,比如@Target({ElementType.FIELD, ElementType.METHOD});

Retention

Retention是保存期,也就是该 Annotation 是存活时间,他有3个值,如下源码解释

public enum RetentionPolicy {
    /**
     * 编译的时候就没了,但是在预编译的时候还是存在的
     */
    SOURCE,

    /**
     * 注解被保存在 class 文件中,但是在 VM 运行时候并不能获得
     */
    CLASS,

    /**
     * 和上个 CLASS 的区别是在运行时候也能获得
     */
    RUNTIME
}

接下来继续把 DocMethodDocParam 的注解也写出来

DocMethod

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DocMethod {
    String value() default "";//方法描述
    String[] author() default "";//作者
    String[] version() default "1.0.0";//版本,可以存在多个版本
    String url() default "";//接口
    Method method() default Method.GET;//请求方式
    String date() default "";//日期
    String update() default "";//更新日期
    Type returnType();//返回类型
    
    DocParam[] params();
    
    public enum Method{
        GET,POST,CALL
    }
    
}

DocParam

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value={ElementType.METHOD,ElementType.PARAMETER})//方法和参数中都可以使用
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DocParam {

    String value();//参数描述
    Type type() default Type.String;//参数类型
    
    public enum Type{
        String,Long,Integer,Date,FLOAT,DOUBLE,OBJECT
    }
}

到这里基本的注解都已经完成,实际情况根据自己项目的需要自行定制。

Maven 插件

如何写一个插件,搜索一下都有,不再赘述,按照流程做个

1、插件POM

按照普通 maven 项目骨架构建一个 maven 项目,项目的名称位doc-maven-plugin,按照 XXX-maven-plugin的格式来,这样在运行 maven 时候比较方便,修改 package 形式为maven-plugin,具体如下

  <groupId>cn.ts</groupId>
    <artifactId>doc-maven-plugin</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>maven-plugin</packaging>

然后需要依赖如下的包

    <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>3.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.plugin-tools</groupId>
            <artifactId>maven-plugin-annotations</artifactId>
            <version>3.4</version>
            <scope>provided</scope>
        </dependency>

为了插件编写的方便,下面连个依赖也一并加入

    <dependency>
            <groupId>org.codehaus.plexus</groupId>
            <artifactId>plexus-utils</artifactId>
            <version>3.0.8</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-project</artifactId>
            <version>2.2.1</version>
        </dependency>

如果上面的 annotation 是单独一个项目,也需要引入,否则解析 Annotation 找不到

2、写个 Mojo

Mojo 必须继承AbstractMojo,具体情况如下代码详述

@Mojo(name = "gen",
    requiresProject=true,
    requiresDependencyResolution=ResolutionScope.COMPILE_PLUS_RUNTIME,
    requiresDependencyCollection=ResolutionScope.COMPILE_PLUS_RUNTIME)
public class DocMojo extends AbstractMojo {
  @Override
    public void execute() throws MojoExecutionException, MojoFailureException {}
}

说明下@Mojo:name表示 MVN doc:XXX这个 XXX,这里就是mvn doc:gen,requiresProject需要项目支持,我们的注解是注解到项目上的,需要扫描项目代码;requiresDependencyResolution表示需要项目在编译运行时的 CLASSPATH,方便我们获取项目的 CLASS,从而获得 CLASS 上面的注解。

项目参数和配置参数
  @Parameter(defaultValue = "${project}",required = true,readonly=true)
    private MavenProject project;
    @Parameter(property="port",defaultValue="3306")
    private String port;

第一个获取项目对应 POM 构造,第二个是自己定义的一个参数,比如我们需要获取数据库连接参数等,注解的含义一目了然。

核心解析方法

execute是整个 Mojo 的核心方法,我们是业务实现也在这里,这里把伪代码写出


//根据配置参数,构造数据库连接
Connection c=getConnByConfig(url,driver,user,pswd)

//从配置参数中获得项目中的 CLASS 路径
String targetPath=project.getBuild().getOutputDirectory();
//获取路径下所有类文件
List<File>classFile=scanClassFile(targetPath);
//根据类文件获得类
List<Class<?>> classes=classForClassFile(classFile);
//解析类

//分析类的 Annotation
if(c==null)continue;
DocAction docAction = c.getAnnotation(DocAction.class);
if(docAction==null)continue;//没有标注文档输出的过滤掉

Method[] methods = c.getDeclaredMethods();
for(Method method:methods){
    DocMethod docMethod = method.getAnnotation(DocMethod.class);
    if(docMethod==null)continue;
    DocParam[] params = docMethod.params();
    for(DocParam param:params){
        String name = param.value();
        Type type=param.type();
      //TODO 收集需要的Annotation
      //信息进行存储到数据库,根据类名+方法名称作为主键,不存在插入,存在就更新
    }
}

使用

插件写好了,我们需要在项目中使用,建立一个maven项目,依赖annotation,插件依赖上面的插件,同时插件里面依赖当前项目。如下所示

<plugin>
    <groupId>cn.ts</groupId>
    <artifactId>doc-maven-plugin</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <configuration>
        <port>3309</port><!-- 自定义参数  -->
    </configuration>
    <dependencies><!-- 依赖本身项目,为了获取 classpath  -->
        <dependency>
            <groupId>cn.ts</groupId>
            <artifactId>myrpc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</plugin>

对项目使用注解

import cn.ts.annotation.DocAction;
import cn.ts.annotation.DocMethod;
import cn.ts.annotation.DocMethod.Method;
import cn.ts.annotation.DocParam;
import cn.ts.annotation.DocParam.Type;
@DocAction(value="Test",author={"老唐"},date="2017-01-10",name="用户信息")
public class UserAnnotation {

    @DocMethod(value="测试用户信息",
            author={"Tangshun"},
            method=Method.GET,
            url="user",
            version="1.0.0",
            params={
            @DocParam(value="用户姓名",type=Type.String),
            @DocParam(value="用户年龄",type=Type.Integer)
            }, returnType = Type.NONE)
    public void test( String name,Integer age){}
}

执行命令

mvn doc:gen,在 eclipse 环境配置如下


不出意外,你能获得需要的东西



上面是基于 Maven 插件的形式,我们需要把 Annotation 存活时间设置为 RunTime,如果设置为 Source 或者 CLASS 又怎样解析?

Annotation CLASS解析

首先把注解的存活时间设置为 CLASS。这里我构建三个 maven 项目来进行


先看下annotation 部分


重点是 annotation-process,先看他的 pom 文件,把依赖加入

<dependency>
    <groupId>cn.ts</groupId>
    <artifactId>x-annotation</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

<!-- 可选插件 不再自己增加 META-INF/service/插件处理 -->
<dependency>
    <groupId>com.google.auto.service</groupId>
    <artifactId>auto-service</artifactId>
    <version>1.0-rc2</version>
</dependency>

接下来,我们需要编写自己的 Annotation 处理器,该处理器需要实现AbstractProcessor,并且复写核心方法process

public class ClassAnnotationProcess extends AbstractProcessor{
  @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {}
}

参数说明:
输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素,比如roundEnv.getElementsAnnotatedWith(FirstAnotation.class),获得注解了FirstAnotation的元素,元素有
PACKAGE, ENUM, CLASS, ANNOTATION_TYPE, INTERFACE, ENUM_CONSTANT, FIELD, PARAMETER, LOCAL_VARIABLE, EXCEPTION_PARAMETER, METHOD, CONSTRUCTOR, STATIC_INIT, INSTANCE_INIT, TYPE_PARAMETER, OTHER, RESOURCE_VARIABLE;
根据元素类型枚举值转换对应的元素类型,进一步根据typeElement.getAnnotation(FirstAnotation.class)获得注解的详细情况,做进一步操作。

还有一些方法也需要override,如下

/** 
 * init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。 
 * ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer 
 * @param processingEnv 提供给 processor 用来访问工具框架的环境 
 */  
@Override  
public synchronized void init(ProcessingEnvironment processingEnv) {  
    super.init(processingEnv);  
 // Filer是个接口,支持通过注解处理器创建新文件  
    filer = processingEnv.getFiler();  
}  
/** 
 * 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称 
 * @return  注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合 
 */  
@Override  
public Set<String> getSupportedAnnotationTypes() {  
    Set<String> annotataions = new LinkedHashSet<String>();  
    annotataions.add(FirstAnotation.class.getCanonicalName());  
    return annotataions;  
}

/** 
 * 指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6 
 * @return  使用的Java版本 
 */  
@Override  
public SourceVersion getSupportedSourceVersion() {  
   return SourceVersion.latestSupported();  
} 
   

写完 annotation 处理器,打包,在demo项目依赖中引用

<dependencies>
        <dependency>
            <groupId>cn.ts</groupId>
            <artifactId>x-annotation</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        
        <dependency>
            <groupId>cn.ts</groupId>
            <artifactId>x-annotation-process</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        
    </dependencies>

可能的问题:
在处理类上注解@AutoService(Processor.class) ,可能没有作用,需要在 src/main/resources/目录下构建 META_INF/service 子目录,并且建立文件javax.annotation.processing.Processor,并且在里面输入插件的具体名称,如cn.ts.x_annotation_process.ClassAnnotationProcess

版权印为您的作品印上版权32805660

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容