首頁 > 軟體

詳解Java中自定義註解的使用

2023-03-21 06:02:09

什麼是註解

在早期的工作的時候 ,自定義註解寫的比較多,可大多都只是因為 這樣看起來 不會存在一堆程式碼耦合在一起的情況,所以使用了自定義註解,這樣看起來清晰些,

Annontation是Java5開始引入的新特徵,中文名稱叫註解。

它提供了一種安全的類似註釋的機制,用來將任何的資訊或後設資料(metadata)與程式元素(類、方法、成員變數等)進行關聯。為程式的元素(類、方法、成員變數)加上更直觀、更明瞭的說明,這些說明資訊是與程式的業務邏輯無關,並且供指定的工具或框架使用。Annontation像一種修飾符一樣,應用於包、型別、構造方法、方法、成員變數、引數及本地變數的宣告語句中。

Java註解是附加在程式碼中的一些元資訊,用於一些工具在編譯、執行時進行解析和使用,起到說明、設定的功能。註解不會也不能影響程式碼的實際邏輯,僅僅起到輔助性的作用。

一般我們自定義一個註解的操作是這樣的:

public @interface MyAnnotation {
}

如果說我們需要給他加上引數,那麼大概是這樣的

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface MyAnnotation {
    public int age() default 18;
    String name() ;
    String [] books();
}

我們可以關注到上面有些我們不曾見過的註解,而這類註解,統稱為元註解 ,我們可以大概來看一下

@Document

是被用來指定自定義註解是否能隨著被定義的java檔案生成到JavaDoc檔案當中。

@Target

是專門用來限定某個自定義註解能夠被應用在哪些Java元素上面的,不定義說明可以放在任何元素上。

上面這個 Target這玩意有個列舉,可以清晰的看出來,他的 屬性

使用列舉類ElementType來定義

public enum ElementType {
    /** 類,介面(包括註解型別)或列舉的宣告 */
    TYPE,
    /** 屬性的宣告 */
    FIELD,
    /** 方法的宣告 */
    METHOD,
    /** 方法形式引數宣告 */
    PARAMETER,
    /** 構造方法的宣告 */
    CONSTRUCTOR,
    /** 區域性變數宣告 */
    LOCAL_VARIABLE,
    /** 註解型別宣告 */
    ANNOTATION_TYPE,
    /** 包的宣告 */
    PACKAGE
}

@Retention

即用來修飾自定義註解的生命週期。

使用了RetentionPolicy列舉型別定義了三個階段

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     * (註解將被編譯器丟棄)
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     * (註解將被編譯器記錄在class檔案中,但在執行時不會被虛擬機器器保留,這是一個預設的行為)
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     * (註解將被編譯器記錄在class檔案中,而且在執行時會被虛擬機器器保留,因此它們能通過反射被讀取到)
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

@Inherited

允許子類繼承父類別中的註解

註解的注意事項

1.存取修飾符必須為public,不寫預設為public;

2.該元素的型別只能是基本資料型別、String、Class、列舉型別、註解型別(體現了註解的巢狀效果)以及上述型別的一位陣列;

3.該元素的名稱一般定義為名詞,如果註解中只有一個元素,請把名字起為value(後面使用會帶來便利操作);

4.()不是定義方法引數的地方,也不能在括號中定義任何引數,僅僅只是一個特殊的語法;

5.default代表預設值,值必須和第2點定義的型別一致;

6.如果沒有預設值,代表後續使用註解時必須給該型別元素賦值。

註解的本質

所有的Java註解都基於Annotation介面。但是,手動定義一個繼承自Annotation介面的介面無效。要定義一個有效的Java註解,需要使用@interface關鍵字來宣告註解。Annotation介面本身只是一個普通的介面,並不定義任何註解型別。

public interface Annotation {  
    boolean equals(Object obj);
    /**
    * 獲取hashCode
    */
    int hashCode();
    
    String toString();
    /**
     *獲取註解型別 
     */
    Class<? extends Annotation> annotationType();
}

在Java中,所有的註解都是基於Annotation介面的,但是手動定義一個繼承自Annotation介面的介面並不會建立一個有效的註解。要定義有效的註解,需要使用特殊的關鍵字@interface來宣告註解型別。Annotation介面本身只是一個普通的介面,而不是一個定義註解的介面。因此,使用@interface宣告註解是定義Java註解的標準方法。

public @interface MyAnnotation1 {
}
public interface MyAnnotation2 extends Annotation  {
}
// javap -c TestAnnotation1.class
Compiled from "MyAnnotation1.java"                                                                 
public interface com.spirimark.corejava.annotation.MyAnnotation1 extends java.lang.annotation.Annotation {}
​
// javap -c TestAnnotation2.class
Compiled from "MyAnnotation2.java"                                                                 
public interface com.spirimark.corejava.annotation.MyAnnotation2 extends java.lang.annotation.Annotation {}

雖然Java中的所有註解都是基於Annotation介面,但即使介面本身支援多繼承,註解的定義仍無法使用繼承關鍵字來實現。定義註解的正確方式是使用特殊的關鍵字@interface宣告註解型別。

同時需要注意的是,通過@interface宣告的註解型別不支援繼承其他註解或介面。任何嘗試繼承註解型別的操作都會導致編譯錯誤。

public @interface MyAnnotation1 {
}
/** 錯誤的定義,註解不能繼承註解 */
@interface MyAnnotation2 extends MyAnnotation1 {
}
/** 錯誤的定義,註解不能繼承介面 */
@interface MyAnnotation3 extends Annotation {
}

自定義註解使用

使用方式 1

自定義註解的玩法有很多,最常見的莫過於

  • 宣告註解
  • 通過反射讀取

但是上面這種一般現在在開發中不怎麼常用,最常用的就是,我們通過 切面去在註解的前後進行載入

建立註解

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BussinessLog {
 
    /**
     * 功能
     */
    BusinessTypeEnum value();
 
    /**
     * 是否儲存請求的引數
     */
    boolean isSaveRequestData() default true;
 
    /**
     * 是否儲存響應的引數
     */
    boolean isSaveResponseData() default true;
}

設定列舉

public enum BusinessTypeEnum {
    /**
     * 其它
     */
    OTHER,
 
    /**
     * 新增
     */
    INSERT,
 
    /**
     * 修改
     */
    UPDATE,
 
    /**
     * 刪除
     */
    DELETE,
 
    /**
     * 授權
     */
    GRANT,
 
    /**
     * 匯出
     */
    EXPORT,
 
    /**
     * 匯入
     */
    IMPORT,
}

建立切面操作

@Slf4j
@Aspect
@Component
public class LogConfig {
 
    @Autowired
    private IUxmLogService uxmLogService;
 
   /**
    * 後置通過,⽬標⽅法正常執⾏完畢時執⾏
    *
    */
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }
 
   /**
    * 異常通知,⽬標⽅法發⽣異常的時候執⾏
    *
    */
    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
        handleLog(joinPoint, controllerLog, e, null);
    }
 
    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
        try {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            String title = methodSignature.getMethod().getAnnotation(ApiOperation.class).value();
            // 獲取當前的使用者
            String userName = CurrentUser.getCurrentUserName();
 
            // *========資料庫紀錄檔=========*//
            UxmLog uxmLog = new UxmLog();
            uxmLog.setStatus(BaseConstant.YES);
            // 請求的地址
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            assert requestAttributes != null;
            HttpServletRequest request = requestAttributes.getRequest();
            String ip = getIpAddr(request);
            // 設定標題
            uxmLog.setTitle(title);
            uxmLog.setOperIp(ip);
            uxmLog.setOperUrl(request.getRequestURI());
            uxmLog.setOperName(userName);
 
            if (e != null) {
                uxmLog.setStatus(BaseConstant.NO);
                uxmLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            // 設定方法名稱
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            uxmLog.setMethod(className + "." + methodName + "()");
            // 設定請求方式
            uxmLog.setRequestMethod(request.getMethod());
            // 處理設定註解上的引數
            getControllerMethodDescription(joinPoint, controllerLog, uxmLog, jsonResult, request);
            // 儲存資料庫
            uxmLog.setOperTime(new Date());
            uxmLogService.save(uxmLog);
        } catch (Exception exp) {
            // 記錄本地異常紀錄檔
            log.error("==前置通知異常==");
            log.error("異常資訊:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }
 
    public static String getIpAddr(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        }
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
 
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
 
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, UxmLog uxmLog, Object jsonResult, HttpServletRequest request) throws Exception {
        // 設定action動作
        uxmLog.setBusinessType(log.value().ordinal());
        // 是否需要儲存request,引數和值
        if (log.isSaveRequestData()) {
            // 獲取引數的資訊,傳入到資料庫中。
            setRequestValue(joinPoint, uxmLog, request);
        }
        // 是否需要儲存response,引數和值
        if (log.isSaveResponseData()) {
            uxmLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
        }
    }
 
    private void setRequestValue(JoinPoint joinPoint, UxmLog uxmLog, HttpServletRequest request) throws Exception {
        String requestMethod = uxmLog.getRequestMethod();
        if (RequestMethod.PUT.name().equals(requestMethod) || RequestMethod.POST.name().equals(requestMethod)) {
            String params = argsArrayToString(joinPoint.getArgs());
            uxmLog.setOperParam(StringUtils.substring(params, 0, 2000));
        } else {
            Map<?, ?> paramsMap = (Map<?, ?>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            uxmLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
        }
    }
 
    private String argsArrayToString(Object[] paramsArray) {
        StringBuilder params = new StringBuilder();
        if (paramsArray != null && paramsArray.length > 0) {
            for (Object o : paramsArray) {
                if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
                    try {
                        Object jsonObj = JSON.toJSON(o);
                        params.append(jsonObj.toString()).append(" ");
                    } catch (Exception e) {
                        log.error(e.getMessage());
                    }
                }
            }
        }
        return params.toString().trim();
    }
 
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }
}

這樣的話,我們就可以 在 專案當中 去在標註註解的前後去進行輸出 紀錄檔

使用方式 2

我們可能還會在每次請求的時候去輸出紀錄檔,所以 我們也可以去定義一個 請求的 註解

@HttpLog 自動記錄Http紀錄檔

在很多時候我們要把一些介面的Http請求資訊記錄到紀錄檔裡面。通常原始的做法是利用紀錄檔框架如log4j,slf4j等,在方法裡面打紀錄檔log.info(“xxxx”)。但是這樣的工作無疑是單調而又重複的,我們可以採用自定義註解+切面的來簡化這一工作。通常的紀錄檔記錄都在Controller裡面進行的比較多,我們可以實現這樣的效果:
我們自定義@HttpLog註解,作用域在類上,凡是打上了這個註解的Controller類裡面的所有方法都會自動記錄Http紀錄檔。實現方式也很簡單,主要寫好切面表示式:

紀錄檔切面

下面程式碼的意思,就是當標註了註解,我們通過 @Pointcut 定義了切入點, 當標註了註解,我們會在標註註解的 前後進行輸出 ,當然也包含了 Spring 官方 自帶的註解 例如 RestController

// 切面表示式,描述所有所有需要記錄log的類,所有有@HttpLog 並且有 @Controller 或 @RestController 類都會被代理
    @Pointcut("@within(com.example.spiritmark.annotation.HttpLog) && (@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller))")
    public void httpLog() {
    }

    @Before("httpLog()")
    public void preHandler(JoinPoint joinPoint) {
        startTime.set(System.currentTimeMillis());
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
        log.info("Current Url: {}", httpServletRequest.getRequestURI());
        log.info("Current Http Method: {}", httpServletRequest.getMethod());
        log.info("Current IP: {}", httpServletRequest.getRemoteAddr());
        Enumeration<String> headerNames = httpServletRequest.getHeaderNames();
        log.info("=======http headers=======");
        while (headerNames.hasMoreElements()) {
            String nextName = headerNames.nextElement();
            log.info(nextName.toUpperCase() + ": {}", httpServletRequest.getHeader(nextName));
        }
        log.info("======= header end =======");
        log.info("Current Class Method: {}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        log.info("Parms: {}", null != httpServletRequest.getQueryString() ? JSON.toJSONString(httpServletRequest.getQueryString().split("&")) : "EMPTY");

    }

    @AfterReturning(returning = "response", pointcut = "httpLog()")
    public void afterReturn(Object response) {
        log.info("Response: {}", JSON.toJSONString(response));
        log.info("Spend Time: [ {}", System.currentTimeMillis() - startTime.get() + " ms ]");

    }

@TimeStamp 自動注入時間戳

如果我們想通過自定義註解,在我們每次儲存資料的時候,自動的幫我們將標註註解的方法內的時間戳欄位轉換成 正常日期,我們就需要

我們的很多資料需要記錄時間戳,最常見的就是記錄created_at和updated_at,通常我們可以通常實體類中的setCreatedAt()方法來寫入當前時間,然後通過ORM來插入到資料庫裡,但是這樣的方法比較重複枯燥,給每個需要加上時間戳的類都要寫入時間戳很麻煩而且不小心會漏掉。

另一個思路是在資料庫裡面設定預設值,插入的時候由資料庫自動生成當前時間戳,但是理想很豐滿,現實很骨感,在MySQL如果時間戳型別是datetime裡即使你設定了預設值為當前時間也不會在時間戳為空時插入資料時自動生成,而是會在已有時間戳記錄的情況下更新時間戳為當前時間,這並不是我們所需要的,比如我們不希望created_at每次更改記錄時都被重新整理,另外的方法是將時間戳型別改為timestamp,這樣第一個型別為timestamp的欄位會在值為空時自動生成,但是多個的話,後面的均不會自動生成。再有一種思路是,直接在sql裡面用now()函數生成,比如created_at = now()。

但是這樣必須要寫sql,如果使用的不是主打sql流的orm不會太方便,比如hibernate之類的,並且也會加大sql語句的複雜度,同時sql的可移植性也會降低,比如sqlServer中就不支援now()函數。為了簡化這個問題,我們可以自定義@TimeStamp註解,打上該註解的方法的入參裡面的所有物件或者指定物件裡面要是有setCreatedAt、setUpdatedAt這樣的方法,便會自動注入時間戳,而無需手動注入,同時還可以指定只注入created_at或updated_at。實現主要程式碼如下:

@Aspect
@Component
public class TimeStampAspect {

    @Pointcut("@annotation(com.example.spiritmark.annotation.TimeStamp)")
    public void timeStampPointcut() {}

    @Before("timeStampPointcut() && @annotation(timeStamp)")
    public void setTimestamp(JoinPoint joinPoint, TimeStamp timeStamp) {
        Long currentTime = System.currentTimeMillis();
        Class<?> type = timeStamp.type();
        Object[] args = joinPoint.getArgs();

        for (Object arg : args) {
            if (type.isInstance(arg)) {
                setTimestampForArg(arg, timeStamp);
            }
        }
    }

    private void setTimestampForArg(Object arg, TimeStamp timeStamp) {
        Date currentDate = new Date(System.currentTimeMillis());
        TimeStampRank rank = timeStamp.rank();
        Method[] methods = arg.getClass().getMethods();

        for (Method method : methods) {
            String methodName = method.getName();
            if (isSetter(methodName) && isRelevantSetter(methodName, rank)) {
                try {
                    method.invoke(arg, currentDate);
                } catch (IllegalAccessException | InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private boolean isSetter(String methodName) {
        return methodName.startsWith("set") && methodName.length() > 3;
    }

    private boolean isRelevantSetter(String methodName, TimeStampRank rank) {
        if (rank.equals(TimeStampRank.FULL)) {
            return methodName.endsWith("At");
        }
        if (rank.equals(TimeStampRank.UPDATE)) {
            return methodName.startsWith("setUpdated");
        }
        if (rank.equals(TimeStampRank.CREATE)) {
            return methodName.startsWith("setCreated");
        }
        return false;
    }
}

1.使用@Aspect和@Component註解分別標註切面和切面類,更符合AOP的實現方式。

2.將pointCut()和before()方法分別改名為timeStampPointcut()和setTimestamp(),更能表達它們的作用。

3.通過Class.isInstance(Object obj)方法,將原先的流操作改為了一個簡單的for迴圈,使程式碼更加簡潔。

4.將原先的setCurrentTime()方法改名為setTimestampForArg(),更能表達它的作用。

5.新增了兩個私有方法isSetter()和isRelevantSetter(),將原先在setTimestampForArg()中的邏輯分離出來,提高了程式碼的可讀性和可維護性

到此這篇關於詳解Java中自定義註解的使用的文章就介紹到這了,更多相關Java自定義註解內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com