用自定义注解做点什么

作者: walidake | 来源:发表于2016-09-04 01:33 被阅读3507次

    请看前言


    你不一定听过注解,但你一定对@Override不陌生。

    当我们重写父类方法的时候我们就看到了@Override。我们知道它表示父类方法被子类重写了。

    现在告诉你,@Override就是一个注解。

    也许你会疑惑注解是什么?
    注解(annotation)是JDK5之后引进的新特性,是一种特殊的注释,之所以说它特殊是因为不同于普通注释(comment)能存在于源码,而且还能存在编译期跟运行期,会最终编译成一个.class文件,所以注解能有比普通注释更多的功能。
    简单来说,注解是一个比春药还猛的东西。

    接下来,先入个门,然后通过实战来证明注解有多“猛”。

    PS : 如果已经了解的小伙伴可自行跳到 自定义注解实战。

    自定义注解入门


    我们对于注解的认识大多数来源于标准注解(也称为内建注解)。

    标准注解 表示的意义
    @Override 用于标识该方法继承自超类
    当父类的方法被删除或修改了,编译器会提示错误信息
    @Deprecated 表示该类或者该方法已经不推荐使用
    如果用户还是要使用,会生成编译的警告
    @SuppressWarnings 用于忽略的编译器警告信息

    Java不仅仅提供我们原有的注解使用,它还允许我们自定义注解。比如你可以像这样:

    public @interface DoSomething {
        public String name() default "write";
    }
    

    这是最简单的注解声明。
    尽管看上去像是接口的写法,但完全不是一回事。这一点要注意。
    而使用注解也很简单,可以像这样:

    @DoSomething(name = "walidake")//可以显式传值进来,此时name=walidake
    public class UseAnnotation {
    
    }
    
    @DoSomething//如果不传值,则默认name=我们定义的默认值,即我们上面定义的"write"
    public class UseAnnotation {
    
    }
    

    需要注意的是当注解有value()方法时,不需要指明具体名称

    public @interface DoSomething {
        public String value();
        public String name() default "write";
    }
    
    @DoSomething("walidake")
    public class UseAnnotation {
    
    }
    

    然而“最简单的自定义注解”并没有特别的意义。所以,这时候我们需要引入一个元注解的概念。

    我们需要知道这些概念:
    “普通注解”只能用来注解“代码”,而“元注解”只能用来注解 “普通注解”
    自定义注解是“普通注解”

    JDK5时支持的元注解有@Documented @Retention @Target @Inherited,接下来分别介绍它们修饰注解的效果。

    @Documented
    @interface DocumentedAnnotation{
    
    }
    
    @interface UnDocumentedAnnotation{
    
    }
    
    @DocumentedAnnotation
    @UnDocumentedAnnotation
    public class UseDocumentedAnnotation{
    
    }
    

    打开小黑窗,运行javadoc UseDocumentedAnnotation.java

    运行结果:

    documented.png

    结论:可以看到,被@Documented修饰的注解会生成到javadoc中,如@DocumentedAnnotation。
    而不被@Documented修饰的注解(@UnDocumentedAnnotation)不会生成到javadoc中。

    注解的级别
    @Retention可以设置注解的级别,分为三种,都有其特定的功能。
    这个元注解是我们关注的重点,后面实战我们会用到。

    注解级别 存在范围 主要用途
    SOURCE 源码级别 注解只存在源码中 功能是与编译器交互,用于代码检测。
    如@Override,@SuppressWarings。
    额外效率损耗发生在编译时
    CLASS 字节码级别 注解存在源码与字节码文件中 主要用于编译时生成额外的文件,如XML,Java文件等,但运行时无法获得。
    这个级别需要添加JVM加载时候的代理(javaagent),使用代理来动态修改字节码文件
    RUNTIME 运行时级别 注解存在源码,字节码与Java虚拟机中 主要用于运行时反射获取相关信息

    限制注解使用的范围
    注解默认可以修饰各种元素,而使用@Target可以限制注解的使用范围。

    例如,可以限定注解只能修饰方法。

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    
    }
    

    上面的代码将注解的使用范围限制在了方法上,而不能用来修饰类。

    试着用@Override修饰类会得到“The annotation @Override is disallowed for this location”的错误。

    @Target支持的范围(参见ElementType):

    1) 类,接口,注解;
    2) 属性域;
    3) 方法;
    4) 参数;
    5) 构造函数;
    6) 局部变量;
    7) 注解类型;
    8) 包
    

    注解的继承
    @Inherited可以让注解类似被“继承”一样。
    通过使用@Inherited,可以让子类对象使用getAnnotations()获取父类@Inherited修饰的注解。

    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @interface Inheritable{
    
    }
    
    @interface UnInheritable{
    
    }
    
    public class UseInheritedAnnotation{
        @UnInheritable
        @Inheritable
        public static class Super{
        
        }
    
        public static class Sub extends  Super {
        
        }
    
        public static void main(String... args){
        
            Super instance=new Sub();
            //result : [@com.walidake.annotation.util.Inheritable()]
            System.out.println(Arrays.toString(instance.getClass().getAnnotations()));
        }
    }
    

    我们干脆用@Documented查看类结构。发现:

    inherit1.png inherit2.png

    这是不是恰恰证明了这种是伪继承的说法,而不是真正的继承。

    自定义注解实战##


    引言
    Java Web开发中,对框架的理解和掌握是必须的。而在使用大多数框架的过程中,一般有两种方式的配置,一种是基于xml的配置方式,一种是基于注解的方式。然而,越来越多的程序员(我)在开发过程中享受到注解带来的简便,并义无反顾地投身其中。

    ORM框架,像Hibernate,Mybatis就提供了基于注解的配置方式。我们接下来就使用自定义注解实现袖珍版的Mybatis,袖珍版的Hibernate。

    这很重要
    说明:实战的代码会被文章末尾附上。而实际上在之前做袖珍版框架的时候并没有想到会拿来做自定义注解的Demo。因此给出的代码涉及了其他的一些技术,例如数据库连接池,动态代理等等,比较杂。
    在这个篇幅我们只讨论关于自定义注解的问题,至于其他的技术后面会开多几篇博文阐述。(当然这么多前辈面前不敢造次,有个讨论学习的氛围是很好的~)

    那么在自定义注解框架前,我们需要花点时间浏览以下几个和Annotation相关的方法。

    方法名 用法
    Annotation getAnnotation(Class annotationType) 获取注解在其上的annotationType
    Annotation[] getAnnotations() 获取所有注解
    isAnnotationPresent(Class annotationType) 判断当前元素是否被annotationType注解
    Annotation[] getDeclareAnnotations() 与getAnnotations() 类似,但是不包括父类中被Inherited修饰的注解

    Mybatis 自定义注解

    本节目标:自定义注解实现Mybatis插入数据操作。
    本节要求:细心观察使用自定义注解的步骤。

    Step 1 :声明自定义注解。

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Insert {
        public String value();
    }
    

    Step 2 : 在规定的注解使用范围内使用我们的注解

    public interface UserMapper {
    
        @Insert("insert into user (name,password) values (?,?)")
        public void addUser(String name,String password);
    
    }
    

    Step 3 : 通过method.getAnnotation(Insert.class).value()使用反射解析自定义注解,得到其中的sql语句

    //检查是否被@Insert注解修饰
    if (method.isAnnotationPresent(Insert.class)) {
        //检查sql语句是否合法
        //method.getAnnotation(Insert.class).value()取得@Insert注解value中的Sql语句
        sql = checkSql(method.getAnnotation(Insert.class).value(),
            Insert.class.getSimpleName());
        //具体的插入数据库操作
        insert(sql, parameters);
    

    }

    Step 4 : 根据实际场景调用Step 3的方法

    UserMapper mapper = MethodProxyFactory.getBean(UserMapper.class);
    mapper.addUser("walidake","665908");
    

    运行结果:

    mybatis.png

    以上节选自annotation中Mybatis部分。具体CRUD操作请看源码。

    总结一下从上面学到的东西:
    1.声明自定义注解,并限制适用范围(因为默认是通用)
    2.规定范围内使用注解
    3.isAnnotationPresent(Insert.class)检查注解,getAnnotation(Insert.class).value()取得注解内容
    4.根据实际场景应用

    Hibernate 自定义注解

    本节目标:自定义注解使实体自动建表(即生成建表SQL语句)
    本节要求:动手操作,把未给全的代码补齐。
    本节规划:仿照Hibernate,我们大概会需要@Table,@Column,还有id,我们这里暂且声明为@PrimaryKey

    仿照自定义Mybatis注解的步骤:

    /**
     * 可根据需要自行定制功能
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface Table {
        String name() default "";
    
    }
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface Column {
        // 列名 默认为""
        String name() default "";
    
        // 长度 默认为255
        int length() default 255;
    
        // 是否为varchar 默认为true
        boolean varchar() default true;
    
        // 是否为空 默认可为空
        boolean isNull() default true;
    }
    
    /**
     * 有需要可以拆分成更小粒度
     * @author walidake
     *
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface PrimaryKey {
        String name() default "";
    }
    

    完成Step 1,接下来是Step 2。

    @Table
    public class Person {
        @PrimaryKey
        private int id;
    
        @Column(isNull = false, length = 20)
        private String username;
        ...
    }
    

    Step 3,新建一个叫做SqlUtil的类,使用Class(实体类).isAnnotationPresent(Table.class)取到@Table注解的内容。

    而我们如何取到@Column和@PrimaryKey的内容?
    使用反射,我们可以很容易做到。

    // 反射取得所有Field
    Field[] fields = clazz.getDeclaredFields();
    ...
    ...
    // 获取注解对象
    column = fields[i].getAnnotation(Column.class);
    // 设置访问私有变量
    fields[i].setAccessible(true);
    // 取得@Column的内容
    columnName = "".equals(column.name()) ? fields[i].getName(): column.name();
    

    反射的内容后面再写。(感觉每一篇都给自己挖了很多坑后面去填)

    Step 4套入使用场景

    String createSql = SqlUtil.createTable(clazz);
    ...
    connection.createStatement().execute(createSql);
    

    运行结果:

    hibernate.png

    运行结果正确!

    自此我们完成了实战模块的内容。当然关于Hibernate的CRUD也可以用同样的方法做到,更进一步还可以把二级缓存整合进来,实现自己的一个微型框架。尽管现有的框架已经很成熟了,但自己实现一遍还是能收获很多东西。

    可以看出来,注解简化了我们的配置。每次使用注解只需要@注解名就可以了,就跟吃春药一样“爽”。不过由于使用了反射,后劲太“猛”,jvm无法对代码优化,影响了性能。这一点最后也会提及。

    另外提一点,之前想格式化hibernate生成的SQL,做大量搜索后被告知“Hibernate 使用的是开源的语法解析工具 Antlr,需要进行 SQL 语法解析,将 SQL 语句整理成语法树”。也算一个坑吧~
    不过后来找到一个除了建表SQL以外的格式化工具类,觉得还不错就也分享了。可以在源码中找到。

    最后说点什么
    可以发现我们使用运行时注解来搭建我们的袖珍版ORM框架,因为运行时注解来搭建框架相对容易而且适用性也比较广,搭建的框架使用起来也比较简单。但在此基础上因为需要用到反射,其效率性能相对不高。因此,多数Web应用使用运行时注解,而像Android等对效率性能要求较高的平台一般使用源码级别注解来搭建。下一节我们讨论怎么玩一玩源码级注解。

    源代码地址: https://github.com/walidake/Annotation

    相关文章

      网友评论

      本文标题:用自定义注解做点什么

      本文链接:https://www.haomeiwen.com/subject/sjrlettx.html