溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java中自定義注解如何使用

發(fā)布時間:2023-03-20 13:47:58 來源:億速云 閱讀:127 作者:iii 欄目:開發(fā)技術(shù)

本文小編為大家詳細介紹“Java中自定義注解如何使用”,內(nèi)容詳細,步驟清晰,細節(jié)處理妥當,希望這篇“Java中自定義注解如何使用”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識吧。

什么是注解

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

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

它提供了一種安全的類似注釋的機制,用來將任何的信息或元數(shù)據(jù)(metadata)與程序元素(類、方法、成員變量等)進行關(guān)聯(lián)。為程序的元素(類、方法、成員變量)加上更直觀、更明了的說明,這些說明信息是與程序的業(yè)務(wù)邏輯無關(guān),并且供指定的工具或框架使用。Annontation像一種修飾符一樣,應(yīng)用于包、類型、構(gòu)造方法、方法、成員變量、參數(shù)及本地變量的聲明語句中。

Java注解是附加在代碼中的一些元信息,用于一些工具在編譯、運行時進行解析和使用,起到說明、配置的功能。注解不會也不能影響代碼的實際邏輯,僅僅起到輔助性的作用。

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

public @interface MyAnnotation {
}

如果說我們需要給他加上參數(shù),那么大概是這樣的

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

我們可以關(guān)注到上面有些我們不曾見過的注解,而這類注解,統(tǒng)稱為元注解 ,我們可以大概來看一下

@Document

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

@Target

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

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

使用枚舉類ElementType來定義

public enum ElementType {
    /** 類,接口(包括注解類型)或枚舉的聲明 */
    TYPE,
    /** 屬性的聲明 */
    FIELD,
    /** 方法的聲明 */
    METHOD,
    /** 方法形式參數(shù)聲明 */
    PARAMETER,
    /** 構(gòu)造方法的聲明 */
    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.該元素的類型只能是基本數(shù)據(jù)類型、String、Class、枚舉類型、注解類型(體現(xiàn)了注解的嵌套效果)以及上述類型的一位數(shù)組;

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

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

5.default代表默認值,值必須和第2點定義的類型一致;

6.如果沒有默認值,代表后續(xù)使用注解時必須給該類型元素賦值。

注解的本質(zhì)

所有的Java注解都基于Annotation接口。但是,手動定義一個繼承自Annotation接口的接口無效。要定義一個有效的Java注解,需要使用@interface關(guān)鍵字來聲明注解。Annotation接口本身只是一個普通的接口,并不定義任何注解類型。

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

在Java中,所有的注解都是基于Annotation接口的,但是手動定義一個繼承自Annotation接口的接口并不會創(chuàng)建一個有效的注解。要定義有效的注解,需要使用特殊的關(guān)鍵字@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接口,但即使接口本身支持多繼承,注解的定義仍無法使用繼承關(guān)鍵字來實現(xiàn)。定義注解的正確方式是使用特殊的關(guān)鍵字@interface聲明注解類型。

同時需要注意的是,通過@interface聲明的注解類型不支持繼承其他注解或接口。任何嘗試繼承注解類型的操作都會導(dǎo)致編譯錯誤。

public @interface MyAnnotation1 {
}
/** 錯誤的定義,注解不能繼承注解 */
@interface MyAnnotation2 extends MyAnnotation1 {
}
/** 錯誤的定義,注解不能繼承接口 */
@interface MyAnnotation3 extends Annotation {
}

自定義注解使用

使用方式 1

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

  • 聲明注解

  • 通過反射讀取

但是上面這種一般現(xiàn)在在開發(fā)中不怎么常用,最常用的就是,我們通過 切面去在注解的前后進行加載

創(chuàng)建注解

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

設(shè)置枚舉

public enum BusinessTypeEnum {
    /**
     * 其它
     */
    OTHER,
 
    /**
     * 新增
     */
    INSERT,
 
    /**
     * 修改
     */
    UPDATE,
 
    /**
     * 刪除
     */
    DELETE,
 
    /**
     * 授權(quán)
     */
    GRANT,
 
    /**
     * 導(dǎo)出
     */
    EXPORT,
 
    /**
     * 導(dǎo)入
     */
    IMPORT,
}

創(chuàng)建切面操作

@Slf4j
@Aspect
@Component
public class LogConfig {
 
    @Autowired
    private IUxmLogService uxmLogService;
 
   /**
    * 后置通過,?標?法正常執(zhí)?完畢時執(zhí)?
    *
    */
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }
 
   /**
    * 異常通知,?標?法發(fā)?異常的時候執(zhí)?
    *
    */
    @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();
 
            // *========數(shù)據(jù)庫日志=========*//
            UxmLog uxmLog = new UxmLog();
            uxmLog.setStatus(BaseConstant.YES);
            // 請求的地址
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            assert requestAttributes != null;
            HttpServletRequest request = requestAttributes.getRequest();
            String ip = getIpAddr(request);
            // 設(shè)置標題
            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));
            }
            // 設(shè)置方法名稱
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            uxmLog.setMethod(className + "." + methodName + "()");
            // 設(shè)置請求方式
            uxmLog.setRequestMethod(request.getMethod());
            // 處理設(shè)置注解上的參數(shù)
            getControllerMethodDescription(joinPoint, controllerLog, uxmLog, jsonResult, request);
            // 保存數(shù)據(jù)庫
            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 {
        // 設(shè)置action動作
        uxmLog.setBusinessType(log.value().ordinal());
        // 是否需要保存request,參數(shù)和值
        if (log.isSaveRequestData()) {
            // 獲取參數(shù)的信息,傳入到數(shù)據(jù)庫中。
            setRequestValue(joinPoint, uxmLog, request);
        }
        // 是否需要保存response,參數(shù)和值
        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;
    }
}

這樣的話,我們就可以 在 項目當中 去在標注注解的前后去進行輸出 日志

Java中自定義注解如何使用

使用方式 2

我們可能還會在每次請求的時候去輸出日志,所以 我們也可以去定義一個 請求的 注解

@HttpLog 自動記錄Http日志

在很多時候我們要把一些接口的Http請求信息記錄到日志里面。通常原始的做法是利用日志框架如log4j,slf4j等,在方法里面打日志log.info(“xxxx”)。但是這樣的工作無疑是單調(diào)而又重復(fù)的,我們可以采用自定義注解+切面的來簡化這一工作。通常的日志記錄都在Controller里面進行的比較多,我們可以實現(xiàn)這樣的效果:
我們自定義@HttpLog注解,作用域在類上,凡是打上了這個注解的Controller類里面的所有方法都會自動記錄Http日志。實現(xiàn)方式也很簡單,主要寫好切面表達式:

日志切面

下面代碼的意思,就是當標注了注解,我們通過 @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 自動注入時間戳

如果我們想通過自定義注解,在我們每次保存數(shù)據(jù)的時候,自動的幫我們將標注注解的方法內(nèi)的時間戳字段轉(zhuǎn)換成 正常日期,我們就需要

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

另一個思路是在數(shù)據(jù)庫里面設(shè)置默認值,插入的時候由數(shù)據(jù)庫自動生成當前時間戳,但是理想很豐滿,現(xiàn)實很骨感,在MySQL如果時間戳類型是datetime里即使你設(shè)置了默認值為當前時間也不會在時間戳為空時插入數(shù)據(jù)時自動生成,而是會在已有時間戳記錄的情況下更新時間戳為當前時間,這并不是我們所需要的,比如我們不希望created_at每次更改記錄時都被刷新,另外的方法是將時間戳類型改為timestamp,這樣第一個類型為timestamp的字段會在值為空時自動生成,但是多個的話,后面的均不會自動生成。再有一種思路是,直接在sql里面用now()函數(shù)生成,比如created_at = now()。

但是這樣必須要寫sql,如果使用的不是主打sql流的orm不會太方便,比如hibernate之類的,并且也會加大sql語句的復(fù)雜度,同時sql的可移植性也會降低,比如sqlServer中就不支持now()函數(shù)。為了簡化這個問題,我們可以自定義@TimeStamp注解,打上該注解的方法的入?yún)⒗锩娴乃袑ο蠡蛘咧付▽ο罄锩嬉怯衧etCreatedAt、setUpdatedAt這樣的方法,便會自動注入時間戳,而無需手動注入,同時還可以指定只注入created_at或updated_at。實現(xiàn)主要代碼如下:

@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的實現(xiàn)方式。

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

3.通過Class.isInstance(Object obj)方法,將原先的流操作改為了一個簡單的for循環(huán),使代碼更加簡潔。

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

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

讀到這里,這篇“Java中自定義注解如何使用”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領(lǐng)會,如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI