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能自動生成。一個好的框架可以讓你儘可能少的浪費時間,集中精力,更好的提升自身能力。

爲倚天點贊!爲劍峯點贊!

相關文章
相關標籤/搜索