利用Android的编译时注解轻松实现一个线程切换的库

利用Android的编译时注解轻松实现一个线程切换的库

前言

相信大家经常都使用到注解,如果使用过AndroidAnnotations,Dagger2,EventBus,RxJava,BufferKnife等开源项目,对注解应该更为深刻,这些项目的原理基本都是基于编译时注解动态生成代码,效果等同于手写代码,因此相对反射来说性能基本无影响。

另外,已经实现了注解轻松实现线程切换开源项目,欢迎fork&star.

了解注解

注解的概念

注解(Annotation),也叫元数据(Metadata),是Java5的新特性,JDK5引入了Metadata很容易的就能够调用Annotations。注解与类、接口、枚举在同一个层次,并可以应用于包、类型、构造方法、方法、成员变量、参数、本地变量的声明中,用来对这些元素进行说明注释。

注解的语法与定义

  1. 以@interface关键字定义
  2. 注解可以包含成员,成员以无参数的方法的形式被声明,其方法名和返回值定义了该成员的名字和类型。
  3. 成员赋值是通过@Annotation(name=value)的形式。
  4. 注解需要标明注解的生命周期,注解的修饰目标等信息,这些信息是通过元注解实现。

以 java.lang.annotation 中定义的 Target 注解为例:

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

源码分析如下:
第一:元注解@Retention,成员value的值为RetentionPolicy.RUNTIME。
第二:元注解@Target,成员value是个数组,用{}形式赋值,值为ElementType.ANNOTATION_TYPE
第三:成员名称为value,类型为ElementType[]
另外,需要注意一下,如果成员名称是value,在赋值过程中可以简写。如果成员类型为数组,但是只赋值一个元素,则也可以简写。如上面的简写形式为:

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

注解的分类

1 基本内置注解,是指Java自带的几个Annotation,如@Override、Deprecated、@SuppressWarnings等;
2 元注解(meta-annotation),是指负责注解其他注解的注解,JDK 1.5及以后版本定义了4个标准的元注解类型,如下:

@Target
@Retention
@Documented
@Inherited

3 自定义注解,根据需要可以自定义注解,自定义注解需要用到上面的meta-annotation

元注解

  • Java定义了4个标准的元注解( java8之后新增了 @Repeatable@Native元注解):
  • @Documented:标记注解,注解表明这个注解应该被 javadoc工具记录. 默认情况下,javadoc是不包括注解的. 但如果声明注解时指定了 @Documented,则它会被 javadoc 之类的工具处理, 所以注解类型信息也会被包括在生成的文档中。
/**
 * Indicates that annotations with a type are to be documented by javadoc
 * and similar tools by default.  This type should be used to annotate the
 * declarations of types whose annotations affect the use of annotated
 * elements by their clients.  If a type declaration is annotated with
 * Documented, its annotations become part of the public API
 * of the annotated elements.
 *
 * @author  Joshua Bloch
 * @since 1.5
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

  • @Inherited:标记注解,允许子类继承父类的注解,此注解理解有难度,可以参考这里,总的来说就是子类在继承父类时如果父类上的注解有此@Inherited标记,那么子类就能把父类的这个注解继承下来,如果没有@Inherited标记,那么子类在继承父类之后并没有继承父类的注解(不知道有没有说明白了,不明白就还是点进链接去看下吧)。
/**
 * Indicates that an annotation type is automatically inherited.  If
 * an Inherited meta-annotation is present on an annotation type
 * declaration, and the user queries the annotation type on a class
 * declaration, and the class declaration has no annotation for this type,
 * then the class's superclass will automatically be queried for the
 * annotation type.  This process will be repeated until an annotation for this
 * type is found, or the top of the class hierarchy (Object)
 * is reached.  If no superclass has an annotation for this type, then
 * the query will indicate that the class in question has no such annotation.
 *
 * <p>Note that this meta-annotation type has no effect if the annotated
 * type is used to annotate anything other than a class.  Note also
 * that this meta-annotation only causes annotations to be inherited
 * from superclasses; annotations on implemented interfaces have no
 * effect.
 *
 * @author  Joshua Bloch
 * @since 1.5
 * @jls 9.6.3.3 @Inherited
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}  

  • @Retention:指Annotation被保留的时间长短,标明注解的生命周期
/**
 * Indicates how long annotations with the annotated type are to
 * be retained.  If no Retention annotation is present on
 * an annotation type declaration, the retention policy defaults to
 * {@code RetentionPolicy.CLASS}.
 *
 * <p>A Retention meta-annotation has effect only if the
 * meta-annotated type is used directly for annotation.  It has no
 * effect if the meta-annotated type is used as a member type in
 * another annotation type.
 *
 * @author  Joshua Bloch
 * @since 1.5
 * @jls 9.6.3.2 @Retention
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

注解需要标明注解的生命周期,这些信息是通过元注解 @Retention 实现,注解的值是 enum 类型的 RetentionPolicy,包括以下几种情况:

public enum RetentionPolicy {  
    /** 
     * 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃. 
     * 这意味着注解仅存在于编译器处理期间,编译器处理完之后,该注解就没用了,在class文件找不到了
     */  
    SOURCE,  
  
    /** 
     * 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期. 
     * 简单来说就是你在class文件中还能看到注解
     */  
    CLASS,  
  
    /** 
     * 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在, 
     * 保存到class对象中,可以通过反射来获取 
     */  
    RUNTIME  
}  

  • @Target:标明注解的修饰目标( java8为ElementType枚举增加了TYPE_PARAMETERTYPE_USE两个枚举值)
/**
 * Indicates the contexts in which an annotation type is applicable. The
 * declaration contexts and type contexts in which an annotation type may be
 * applicable are specified in JLS 9.6.4.1, and denoted in source code by enum
 * constants of {@link ElementType java.lang.annotation.ElementType}.
 *
 * <p>If an {@code @Target} meta-annotation is not present on an annotation type
 * {@code T} , then an annotation of type {@code T} may be written as a
 * modifier for any declaration except a type parameter declaration.
 *
 * <p>If an {@code @Target} meta-annotation is present, the compiler will enforce
 * the usage restrictions indicated by {@code ElementType}
 * enum constants, in line with JLS 9.7.4.
 *
 * <p>For example, this {@code @Target} meta-annotation indicates that the
 * declared type is itself a meta-annotation type.  It can only be used on
 * annotation type declarations:
 * <pre>
 *    &#064;Target(ElementType.ANNOTATION_TYPE)
 *    public &#064;interface MetaAnnotationType {
 *        ...
 *    }
 * </pre>
 *
 * <p>This {@code @Target} meta-annotation indicates that the declared type is
 * intended solely for use as a member type in complex annotation type
 * declarations.  It cannot be used to annotate anything directly:
 * <pre>
 *    &#064;Target({})
 *    public &#064;interface MemberType {
 *        ...
 *    }
 * </pre>
 *
 * <p>It is a compile-time error for a single {@code ElementType} constant to
 * appear more than once in an {@code @Target} annotation.  For example, the
 * following {@code @Target} meta-annotation is illegal:
 * <pre>
 *    &#064;Target({ElementType.FIELD, ElementType.METHOD, ElementType.FIELD})
 *    public &#064;interface Bogus {
 *        ...
 *    }
 * </pre>
 *
 * @since 1.5
 * @jls 9.6.4.1 @Target
 * @jls 9.7.4 Where Annotations May Appear
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}  

/**
 * The constants of this enumerated type provide a simple classification of the
 * syntactic locations where annotations may appear in a Java program. These
 * constants are used in {@link Target java.lang.annotation.Target}
 * meta-annotations to specify where it is legal to write annotations of a
 * given type.
 *
 * <p>The syntactic locations where annotations may appear are split into
 * <em>declaration contexts</em> , where annotations apply to declarations, and
 * <em>type contexts</em> , where annotations apply to types used in
 * declarations and expressions.
 *
 * <p>The constants {@link #ANNOTATION_TYPE} , {@link #CONSTRUCTOR} , {@link
 * #FIELD} , {@link #LOCAL_VARIABLE} , {@link #METHOD} , {@link #PACKAGE} ,
 * {@link #PARAMETER} , {@link #TYPE} , and {@link #TYPE_PARAMETER} correspond
 * to the declaration contexts in JLS 9.6.4.1.
 *
 * <p>For example, an annotation whose type is meta-annotated with
 * {@code @Target(ElementType.FIELD)} may only be written as a modifier for a
 * field declaration.
 *
 * <p>The constant {@link #TYPE_USE} corresponds to the 15 type contexts in JLS
 * 4.11, as well as to two declaration contexts: type declarations (including
 * annotation type declarations) and type parameter declarations.
 *
 * <p>For example, an annotation whose type is meta-annotated with
 * {@code @Target(ElementType.TYPE_USE)} may be written on the type of a field
 * (or within the type of the field, if it is a nested, parameterized, or array
 * type), and may also appear as a modifier for, say, a class declaration.
 *
 * <p>The {@code TYPE_USE} constant includes type declarations and type
 * parameter declarations as a convenience for designers of type checkers which
 * give semantics to annotation types. For example, if the annotation type
 * {@code NonNull} is meta-annotated with
 * {@code @Target(ElementType.TYPE_USE)}, then {@code @NonNull}
 * {@code class C {...}} could be treated by a type checker as indicating that
 * all variables of class {@code C} are non-null, while still allowing
 * variables of other classes to be non-null or not non-null based on whether
 * {@code @NonNull} appears at the variable's declaration.
 *
 * @author  Joshua Bloch
 * @since 1.5
 * @jls 9.6.4.1 @Target
 * @jls 4.1 The Kinds of Types and Values
 */
public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

  • @Repeatable : 可对同一个注解多次使用
/**
 * The annotation type {@code java.lang.annotation.Repeatable} is
 * used to indicate that the annotation type whose declaration it
 * (meta-)annotates is <em>repeatable</em>. The value of
 * {@code @Repeatable} indicates the <em>containing annotation
 * type</em> for the repeatable annotation type.
 *
 * @since 1.8
 * @jls 9.6 Annotation Types
 * @jls 9.7 Annotations
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
    /**
     * Indicates the <em>containing annotation type</em> for the
     * repeatable annotation type.
     * @return the containing annotation type
     */
    Class<? extends Annotation> value();
}
  • 例如,一个人有多重身份
@Target(ElementType.TYPE)  
@Retention(RetentionPolicy.RUNTIME)
public   @interface Persons {
	Person[] value();
}
@Repeatable(Persons.class)
public  @interface Person{
	String role() default "";
}
@Person(role="CEO")
@Person(role="husband")
@Person(role="father")
@Person(role="son")
public   class Man {
}


  • @Native
/**
 * Indicates that a field defining a constant value may be referenced
 * from native code.
 *
 * The annotation may be used as a hint by tools that generate native
 * header files to determine whether a header file is required, and
 * if so, what declarations it should contain.
 *
 * @since 1.8
 */
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Native {
}

注解处理器(Annotation Processor)

概述

注解处理器是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这些生成的Java代码是在生成的.java文件中,所以你不能修改已经存在的Java类,例如向已有的类中添加方法。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。

简单来说,在源代码编译阶段,通过注解处理器,将标记了注解的类、方法等作为输入内容,经过注解处理器进行处理,产生需要的java代码。

Android Gradle插件2.2版本发布后,Android 官方提供了annotationProcessor来代替android-apt,annotationProcessor支持 javac 和 jack 编译方式,而android-apt只支持 javac 编译方式。

使用

  • 直接在Module中使用,比之前Android-apt使用方式更加简单。
dependencies {
	        compile 'com.github.huweijian5:AwesomeTool:latest_version'
		annotationProcessor 'com.github.huweijian5:AwesomeTool-compiler:latest_version'
}

实例说明

接下来以本人写的一个注解实现线程切换的项目为例,讲解下编译时注解的编码过程。

项目结构

本项目主要分为三个Module,分别为lib_api,lib_annotation,lib_compiler。
其中lib_api主要存放供外界使用的接口,是对外开放的
lib_annotation里指定了自定义注解的定义
lib_compiler里实现注解处理器,是本项目的核心

  • 项目目录结构如下图:
    这里写图片描述

  • 依赖关系图如下:

app lib_api lib_annotation lib_compiler lib_annnotaion dependence dependence dependence app lib_api lib_annotation lib_compiler lib_annnotaion

值得注意的是,lib_annotation和lib_compiler都是java工程(apply plugin: ‘java’),而lib_api是android工程(apply plugin: ‘com.android.library’)

lib_annotation

此Module主要实现自定义的注解定义

/**
 * 注入对象实例
 * Created by HWJ on 2017/3/12.
 */
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface InjectObject {

    /**
     * 线程优先级-20~19,-20代表优先级最高,详见android.os.Process,默认为THREAD_PRIORITY_DEFAULT(0)
     * @return
     */
    int priority() default 0;
}
/**
 * 后台线程注解
 * Created by HWJ on 2017/3/12.
 */
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface WorkInBackground {

}
/**
 1. UI线程注解
 2. Created by HWJ on 2017/3/12.
 */
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface WorkInMainThread {
}

lib_api

  • 定义接口
 public interface IObjectInjector<T> {
    void inject(T t);
}
  • 利用反射进行注入实例
public static void inject(Object target) {

        //获取生成类全称
        Class<?> clazz = target.getClass();
        String proxyClassFullName = clazz.getName() + ConstantValue.SUFFIX;
        Class<?> proxyClazz = null;
        try {
            //反射生成类实例对象并进行注入
            proxyClazz = Class.forName(proxyClassFullName);
            IObjectInjector objectInjector = (IObjectInjector) proxyClazz.newInstance();
            objectInjector.inject(target);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("注入失败:"+e.getMessage());
        }
    }

lib_compiler

lib_compiler中实现注解处理器,是项目的核心,本项目是使用Handler和HandlerThread实现线程切换,对于HandlerThread线程切换的使用网络文章已经有很多,本文不再赘述。

1 添加以下依赖:

dependencies {

...

//auto-service库可以帮我们去生成META-INF等信息
compile 'com.google.auto.service:auto-service:1.0-rc4'
//用于生成源代码
compile 'com.squareup:javapoet:1.9.0'

}

//只有android N支持java8,如果你写1.8之后,强制要你使用buildToolsVersion为24.0.0
sourceCompatibility = "1.7"
targetCompatibility = "1.7"

...


注意此Module为java工程,而不是android工程,如果弄错了就会报找不到类AbstractProcessor的错误


2 继承AbstractProcessor,并实现方法init,getSupportedAnnotationTypes,getSupportedSourceVersion,process四个方法即可,参考代码如下:

@AutoService(Processor.class)
public class AwesomeToolProcessor extends AbstractProcessor {
    private static final String TAG = "AwesomeToolProcessor";
    private Filer mFileUtils;//跟文件相关的辅助类,生成JavaSourceCode
    private Elements elementUtils;//跟元素相关的辅助类,帮助我们去获取一些元素相关的信息
    private Messager messager;//跟日志相关的辅助类
    private Map<String, AwesomeToolProxyInfo> proxyInfoMap = new HashMap<String, AwesomeToolProxyInfo>();//key为注解所在类的全名

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
        mFileUtils=processingEnv.getFiler();
    }

    /**
     * 支持的注解类型
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(InjectObject.class.getCanonicalName());
        supportTypes.add(WorkInBackground.class.getCanonicalName());
        supportTypes.add(WorkInMainThread.class.getCanonicalName());
        return supportTypes;
    }

    /**
     * 注解处理器支持到的JAVA版本
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        printMessage("SupportedSourceVersion=%s",SourceVersion.latestSupported().name());
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        printMessage("process:annotations size=%d", annotations.size());
        proxyInfoMap.clear();
        handleInjectObjectAnnotation(roundEnv);
        handleWorkInBackgroundAnnotation(roundEnv);
        handleWorkInMainThreadAnnotation(roundEnv);

        printMessage("AwesomeToolProxyInfo Map size=%d", proxyInfoMap.size());

        generateSourceFiles();
        return false;//如果返回true,当有两个注解作用在同一方法上,那么第一个处理完了之后就不会再处理第二个
    }

注解@AutoService(Processor.class)可以自动帮我们处理一些工作,简化代码

init中可以获得Messager用来打印信息,打印的信息会显示在AndroidStudio的Gradle Console窗口
同时也可以获得Elements,用来获取元素的相关信息,还有Filer,可以用来生成代码

getSupportedAnnotationTypes里需要返回支持的注解类型,就是lib_annotation中定义的注解

getSupportedSourceVersion为注解处理器支持到的java版本

process里处理注解元素作用的类方法等,根据自己的业务逻辑处理并生成相应代码


3 处理注解

通过RoundEnvironment.getElementsAnnotatedWith()可以获得注解所在的方法类等,如下

 Set<? extends Element> elesWithBind = roundEnv.getElementsAnnotatedWith(WorkInMainThread.class);
  • 其中Element的类型及说明如下:
类型说明
ExecutableElementRepresents a method, constructor, or initializer (static or instance) of a class or interface, including annotation type elements.
VariableElementRepresents a field, {@code enum} constant, method or constructor parameter, local variable, resource variable, or exception parameter.
PackageElementRepresents a package program element. Provides access to information about the package and its members.
  • 获取方法参数,参考代码如下:
 for (VariableElement variableElement : executableElement.getParameters()) {
                System.out.println("参数类型及名称:" + variableElement.asType() + "," + variableElement.getSimpleName());
            }

4 生成代码

生成代码的方式可以通过手动拼接字符串,也可以通过开源库javapoet实现。

            try {
                JavaFileObject jfo = processingEnv.getFiler().createSourceFile(
                        proxyInfo.getProxyClassFullName(),//类名全称
                        proxyInfo.getTypeElement());//类元素
                Writer writer = jfo.openWriter();
                writer.write(proxyInfo.generateJavaCode());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                error(proxyInfo.getTypeElement(),
                        "Unable to write injector for type %s: %s",
                        proxyInfo.getTypeElement(), e.getMessage());
            }


至此已经走完了编译时注解的整个流程,最后贴下生成的代码:

//Generated code. Do not modify!
//自动生成代码,请勿修改!
package com.junmeng.aad;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;

import com.junmeng.api.inter.IObjectInjector;

import java.lang.ref.WeakReference;

import java.util.ArrayList;
import java.util.List;

public class BlankFragmentHelper implements IObjectInjector<BlankFragment> {
    public static final int MESSAGE_NEEDWORKINTHREAD = 1;
    public static final int MESSAGE_NEEDWORKINMAINTHREAD = 2;
    private Handler mainHandler;
    private Handler workHandler;
    private HandlerThread handlerThread;
    private WeakReference<BlankFragment> target;

    @Override
    public void inject(final BlankFragment target) {
        if (target.blankFragmentHelper != null) {
            target.blankFragmentHelper.quit();
        }
        target.blankFragmentHelper = new BlankFragmentHelper();
        target.blankFragmentHelper.init(target);
    }

    public void init(final BlankFragment target) {
        this.target = new WeakReference<BlankFragment>(target);
        handlerThread = new HandlerThread("thread_BlankFragmentHelper", -16);
        handlerThread.start();
        mainHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                List<Object> params;
                switch (msg.what) {
                    case MESSAGE_NEEDWORKINMAINTHREAD:
                        params = (List<Object>) msg.obj;
                        target.needWorkInMainThread();
                        break;
                }
            }
        };
        workHandler = new Handler(handlerThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                List<Object> params;
                switch (msg.what) {
                    case MESSAGE_NEEDWORKINTHREAD:
                        params = (List<Object>) msg.obj;
                        target.needWorkInThread((java.lang.String) params.get(0), (int) params.get(1), (double) params.get(2), (com.junmeng.aad.Test) params.get(3));
                        break;
                }
            }
        };
    }

    public void needWorkInThread(java.lang.String str, int i, double d, com.junmeng.aad.Test test) {
        List<Object> params = new ArrayList<>();
        params.add(str);
        params.add(i);
        params.add(d);
        params.add(test);
        workHandler.sendMessage(workHandler.obtainMessage(MESSAGE_NEEDWORKINTHREAD, params));
    }

    public void needWorkInMainThread() {
        List<Object> params = new ArrayList<>();
        mainHandler.sendMessage(mainHandler.obtainMessage(MESSAGE_NEEDWORKINMAINTHREAD, params));
    }

    /**
     * 在不用时务必调用此方法,防止内存泄漏
     */
    public void quit() {
        if (handlerThread != null && handlerThread.isAlive()) {
            handlerThread.quitSafely();
        }
    }
}

另外,说明下google的auto-service实际上会帮助我们生成jar包并添加META-INF信息,如下图
这里写图片描述

如有错误之处请指正,谢谢。

待解决的关键问题

  • 按照上面的实现只能在build的时候才能生成代码,有没有即时生成的技术呢?还没找到答案。

参考:

  • http://blog.csdn.net/wzgiceman/article/details/54580745
  • http://blog.csdn.net/lmj623565791/article/details/43452969
  • http://blog.csdn.net/github_35180164/article/details/52107204
  • https://www.jianshu.com/p/28edf5352b63
  • Java注解——Repeatable - ljcgit的博客 - CSDN博客
    https://blog.csdn.net/ljcgit/article/details/81708679
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值