Java的艺术(1)- 注解

1.前言

在学习任何一种java框架以前,咱们基本都要先了解这个框架的注解。例如:spring框架中的@Controller、@Bean、@Component、@EnableCaching等;mybatis框架中的@Select、@Delete、@ResultMap等;甚至于jdk自己也有@Override等注解。这些注解带给咱们一种感觉,注解自己表明了框架的一系列功能,咱们按照框架规定的方式使用注解,就能实现对应的功能。java注解想要表达的思想就是,约定大于配置。java

就像我如今打字用的键盘,键盘自己不能写字,但它提供了26个字母以及其余符号,我在键盘上按照约定敲击对应的键位,电脑会生成相应的指令作出相关的处理。一样,注解自己并不会实现功能,是java框架读取用户经过注解写入的值,经过反射机制实现一系列功能。spring

本文会简单介绍一下jdk自带的注解,以及如何自定义注解。接着会经过 “注解+反射”的例子,简单实现一个orm框架。sql

2.jdk注解

介绍jdk内置的几个经常使用注解:@Override,@Deprecated,@SuppressWarnings。数据库

@Override,很常见,代表这个方法是重写的父类的方法,当你把@Override放到一个方法上时,编译器会自动去父类中查找是否有相应的方法,若是没有,说明注解使用错误,或者重写的方法名、参数等写错了,那么编译器就会给出编译错误,让你去修改。api

@Deprecated,代表这个属性被弃用,能够修饰的范围很广,包括类、方法、字段、参数等等。当你使用它的时候,编译器就会给出提醒。不过,它是一种警告,而不是强制性的,在IDE中会给Deprecated元素加一条删除线以示警告,在 声明元素为@Deprecated 时,应该用Java 文档注释的方式同时说明替代方案,就像 Date 中的API文档那样,在调用@Deprecated 方法时,应该先考虑其 建议的替代方案。数组

@SuppressWarnings,代表这不是一个警告,那么编译器就不会把它当作警告给提示出来。参数,表示压制哪一种类型的警告,它也能够修饰大部分代码元素,在更大范围的修饰也会对内部元素起效,好比,在类上的注解会影响到方法,在方法上的注解会影响到代码行。对于 Date 方法的调用,能够这样压制警告安全

@SuppressWarnings({" deprecation", " unused"})
    public static void main(String[] args) {
        Date date = new Date(2017, 4, 12);
        int year = date.getYear();
    }

3.自定义注解

咱们先看看 几个注解的例子。mybatis

jdk中的@Override注解app

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

spring中的@FeignClient注解框架

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
    @AliasFor("name")
    String value() default "";

    /** @deprecated */
    @Deprecated
    String serviceId() default "";

    @AliasFor("value")
    String name() default "";

    String qualifier() default "";

    String url() default "";

    boolean decode404() default false;

    Class<?>[] configuration() default {};

    Class<?> fallback() default void.class;

    Class<?> fallbackFactory() default void.class;

    String path() default "";

    boolean primary() default true;
}

能够发现几个有意思的信息:

  1. 定义注解类的的关键字是 @interface,就是接口前面加了@
  2. 须要用注解来修饰注解(元注解)
  3. 注解类里面的属性是以一种“没有参数的方法”来表示,并且能够有默认值。

3.1.元注解

修饰注解的注解,叫作元注解。java里面有下面四种元注解:

(1)@Target:用来修饰注解的做用域。

  1. ElementType.TYPE:容许被修饰的注解做用在类、接口和枚举上
  2. ElementType.FIELD:容许做用在属性字段上
  3. ElementType.METHOD:容许做用在方法上
  4. ElementType.PARAMETER:容许做用在方法参数上
  5. ElementType.CONSTRUCTOR:容许做用在构造器上
  6. ElementType.LOCAL_VARIABLE:容许做用在本地局部变量上
  7. ElementType.ANNOTATION_TYPE:容许做用在注解上
  8. ElementType.PACKAGE:容许做用在包上

(2)@Retention:用于指明当前注解的生命周期。

  1. RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件
  2. RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件
  3. RetentionPolicy.RUNTIME:永久保存,能够反射获取

(3)@Documented:申明注解是否应当被包含在 JavaDoc 文档中。

(4)@Inherited:是否容许子类继承该注解。

3.2.属性

注解的属性也叫作成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。

成员变量的类型能够是java基本类型,加类、接口、枚举、注解,以及他们的数组。

注解中属性能够有默认值,默认值须要用 default 关键值指定。

还有一种约束,当注解类中只有一个成员变量时,约束成员变量名必须是value,应用这个注解时能够直接接属性值填写到括号内。例如:@Select("select from table") 和@Select(value="select from table")。

4.orm框架示例

不少全自动的orm框架,为了创建pojo类和数据库表之间的映射关系,都经过注解的方式,对pojo类注入表名,对pojo里面的属性注入表字段名。之前的hibernate就是经过这种方式,框架后台基于pojo对应的表和字段,动态生成jdbc所需的sql。mybatis不用,之前咱们介绍过,mybatis是基于sql的半自动的orm框架,并不须要pojo类的映射。

前面说过,注解自己并无功能,它是做为一种相似于键盘的约束,通常框架经过反射机制去赋予它相应的功能。那么这个示例就是经过 注解 + 反射,去模拟一个orm框架的功能。

4.1.注解

这里建立两个注解类,分别是 @KTable 和 @KColumn 。

KTable.java 做用在pojo类上,映射数据库的表,因此做用域是ElementType.TYPE。

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

KColumn.java 做用在pojo类上,映射数据库表里面的字段,因此做用域是ElementType.FIELD。

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

4.2.pojo

预先在数据库里面建立了一张表 fnd_user

| 字段名| 属性|
| --- | --- |
| id| varchar |
| name| varchar |
| age| int|
| role_code| varchar |

那么如今建立pojo类(FndUser.java),加上咱们以前定义好的注解。

首先是pojo加上表名@KTable的注解。pojo类里面的属性,咱们约束好,若是加了@KColumn的注解,则框架取注解里面的值做为表字段名,若是不加注解,则取pojo类的属性名做为表字段名。

@KTable("fnd_user")
public class FndUser {
    private String id;
    private String name;
    private int age;
    @KColumn("role_code")
    private String roleCode;
    
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getRoleCode() {
        return roleCode;
    }
    public void setRoleCode(String roleCode) {
        this.roleCode = roleCode;
    }
}

4.3.框架服务

框架核心的代码来了,咱们定义了一个方法,在传入pojo的对象后,会返回对应的查询sql。由于这部分代码的注释比较完整了,就不在多说了,主要经过反射,拿到注解的表名,列名,以及属性值,而后拼接sql。

KpaasQuery.java

@Component
public class KpaasQuery {

    public  String bindQuerySql(Object pojo) {
        StringBuffer stringBuffer = new StringBuffer();
        Class c = pojo.getClass();
        //拿到表名
        boolean isTableExist = c.isAnnotationPresent(KTable.class);
        if (!isTableExist) {
            return null;
        }
        KTable kTable = (KTable) c.getAnnotation(KTable.class);
        String tableName = kTable.value();
        //拼接表sql
        stringBuffer.append("select * from ").append(tableName).append(" where 1=1");
        //拿到列名
        // getDeclaredFields()拿到全部字段,getFields()拿到public字段
        Field[] fields = c.getDeclaredFields();
        for (Field field : fields) {
            //拿到字段名(fieldName)、列名(columnName)、字段值(fieldValue)
            String fieldName = field.getName();
            String columnName = fieldName;
            Object fieldValue = null;

            boolean isColumnExist = field.isAnnotationPresent(KColumn.class);
            if (isColumnExist) {
                KColumn kColumn = field.getAnnotation(KColumn.class);
                columnName = kColumn.value();
            }
            //拿到字段值
            String getMethodName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
            try {
                Method getMethod = c.getMethod(getMethodName);
                fieldValue = getMethod.invoke(pojo);
            } catch (Exception e) {
                e.printStackTrace();
            }
            //拼接列sql
            if (fieldValue == null || (fieldValue instanceof Integer && (Integer) fieldValue == 0)) {
                continue;
            }
            stringBuffer.append(" and " + columnName);
            if (fieldValue instanceof String) {
                stringBuffer.append(" = '").append(fieldValue).append("'");
            }else if(fieldValue instanceof Integer){
                stringBuffer.append(" = ").append(fieldValue);
            }
        }
        return stringBuffer.toString();
    }
}

4.4.功能测试

写一个controller,看能不能按照咱们的要求返回sql。

@RestController
public class UserController {
    @Autowired
    private KpaasQuery kpaasQuery;

    @RequestMapping(value = "/getSql", method = RequestMethod.GET)
    public String getSql() {
        FndUser fndUser = new FndUser();
        fndUser.setName("kerry");
        fndUser.setAge(24);
        fndUser.setRoleCode("admin");
        String sql = kpaasQuery.bindQuerySql(fndUser);
        return sql;
    } 
}

调用接口,返回的结果是:select * from fnd_user where 1=1 and name = 'kerry' and age = 24 and role_code = 'admin'

OK,符合咱们的预期,那么这个简单的orm功能就实现了。

5.备注

本公司的同事,看完这些,我猜测你的脑海中应该会浮现出一个框架--倚天。我也是最先在使用倚天的时候,对注解产生了莫大的兴趣。固然,倚天的orm部分代码实现高深的多,功能也丰富的多。

最先的时候我挺怀疑倚天框架的必要性,我想明明直接能够经过mybatis就能实现的功能,为何非要再封装成全自动的orm框架?后来我明白了,倚天给咱们带来最大的好处,不是实现orm的功能,而是框架的约定。而这些约定让咱们的代码更规范,开发人员只须要考虑业务逻辑和核心代码,不少涉及到代码质量、基础性能的问题,框架默默的就实现了。

拿本文的注解而言,由于倚天pojo类里面@RowID注解,咱们不用考虑根据主键删除的安全性,框架会转换成加密后的rowId。

包括@SystemColumn的五个基础字段的主键,咱们不用去写版本号的自增加,和建立人、建立时间、最后更新人、最后更新时间,这些重复而又枯燥无味的代码。

就像你写pojo类时,不想手动去敲那些get、set方法同样,你期待ide能自动生成。一个好的框架可以让你尽可能少的浪费时间,集中精力,更好的提升自身能力。

为倚天点赞!为剑峰点赞!

相关文章
相关标签/搜索