天天看點

細說java系列之注解

寫在前面

Java從1.5版本之後開始支援注解,通過注解可以很友善地實作某些功能,使用得最普遍的就是Spring架構的注解,大大簡化了Bean的配置。

注解僅僅是一種Java提供的工具,并不是一種程式設計模式。

單純定義注解不能做任何事情,沒有任何意義。除了注解之外,還需要編寫注解處理器,通過注解處理器解析注解,完成特定功能。

注解作為Java的一個特性,它可以解決某些特定場景的問題,但并不是萬能的,更不能被當做“銀彈”。

在Java1.5+中自帶了一些注解,這些注解被稱為元注解。另外,還可以在應用程式中自定義注解。

自定義注解

關于自定義注解的文法及規則請自行Google,在這裡通過自定義注解記錄使用者在業務系統中的記錄檔。

/**
 * 記錄檔注解,對使用了該注解的Controller方法記錄日志.
 * @desc org.chench.test.web.annotation.OperationLog
 * @date 2017年11月29日
 */
@Documented
@Retention(RUNTIME)
@Target(METHOD)
public @interface OperationLog {
    enum OperationType {
        /**
         * 不是任何操作
         */
        NONE,
        /**
         * 登入系統
         */
        LOGIN,
        /**
         * 登出系統
         */
        LOGOUT
    }
    
    /**
     * 操作類型
     * @return
     */
    public OperationType type() default OperationType.NONE;
    
    /**
     * 操作别名
     * @return
     */
    public String alias() default "";
    
    /**
     * 日志内容
     * @return
     */
    public String content() default "";
}           

複制

編寫注解處理器應用實踐

在Serlvet中通過自定義注解記錄使用者記錄檔

1.首先,需要定義了一個基礎的Servlet,在其中實作對自定義注解的解析處理(即這個基礎Serlvet就是注解處理器)。

/**
 * Servlet基類,解析自定義注解。
 * @desc org.chench.test.web.servlet.BaseServlet
 * @date 2017年11月29日
 */
public abstract class BaseServlet extends HttpServlet {
    private static final long serialVersionUID = 2824722588609684126L;

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.service(req, resp);
        
        // 在基礎Servlet類中對自定義注解進行解析處理
        Method[] methods = getClass().getDeclaredMethods();
        for(Method method : methods) {
            if(method.isAnnotationPresent(OperationLog.class)) {
                String methodName = method.getName();
                OperationLog operationLog = method.getAnnotation(OperationLog.class);
                System.out.println("方法名稱: " + methodName + "\n" + 
                                   "操作類型: " + operationLog.type() + "\n" + 
                                   "操作别名:" + operationLog.alias() + "\n" + 
                                   "日志内容:" + operationLog.content());
            }
        }
    }
}           

複制

2.其次,業務Servlet都應該繼承自上述定義的基礎Servlet,并在業務方法中使用自定義注解。

public class AnnotationTestServlet extends BaseServlet {
    private static final long serialVersionUID = -854936757428055943L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

    // 在業務Servlet的方法中使用自定義注解,通過基礎Servlet解析注解記錄記錄檔
    @OperationLog(type=OperationLog.OperationType.LOGIN, alias="員工登入", content="員工登入")
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String userName = req.getParameter("username");
        String password = req.getParameter("password");
        
        System.out.println("使用者名: " + userName);
        System.out.println("密碼: " + password);
    }
}           

複制

在Spring架構中通過自定義注解記錄使用者記錄檔

由于Spring架構已經提供了HandlerInterceptor攔截器接口,是以對于業務方法進行攔截更加友善。

1.首先,在攔截器中解析自定義注解(即這個攔截器就是注解處理器)。

public class MyInterceptor implements HandlerInterceptor{
    // action執行之前執行
    public boolean preHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle");
        return true;
    }

    // 生成視圖之前執行
    public void postHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler,
            ModelAndView mv) throws Exception {
        System.out.println("postHandle");
    }

    // 執行資源釋放
    public void afterCompletion(HttpServletRequest request, 
            HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        System.out.println("afterCompletion");
        
        // 記錄記錄檔
        if(handler instanceof HandlerMethod) {
            Method method = ((HandlerMethod) handler).getMethod();
            if(method.isAnnotationPresent(OperationLog.class)) {
                String methodName = method.getName();
                OperationLog operationLog = method.getAnnotation(OperationLog.class);
                System.out.println("方法名稱: " + methodName + "\n" + 
                                   "操作類型: " + operationLog.type() + "\n" + 
                                   "操作别名:" + operationLog.alias() + "\n" + 
                                   "日志内容:" + operationLog.content());
            }
        }
        
        try {
            // 如果定義了全局異常處理器,那麼在這裡是無法擷取到異常資訊的: ex=null
            // 需要通過别的方式擷取處理異常
            if (ex == null) {
                ex = ThreadExceptionContainer.get();
                System.out.println("異常資訊:" + ex.getMessage());
            } 
        } finally {
            ThreadExceptionContainer.clear();
        }
    }
}           

複制

如上代碼所示,由于隻是需要記錄記錄檔,是以對于自定義注解的解析放在攔截器的afterCompletion()方法中,這樣做是為了不影響正常的請求響應。

很顯然,afterCompletion()方法的參數清單中存在一個Exception對象,理論上我們可以在這裡擷取到業務方法抛出的異常資訊。

但是,如果已經在SpringMVC中定義了全局異常處理器,那麼在這裡是無法擷取到異常資訊的,如下為配置的預設全局異常處理器。

<!-- 使用Spring自帶的全局異常處理器 -->
<bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="defaultErrorView">
        <value>error/error</value>
    </property>
    <property name="defaultStatusCode">
        <value>500</value>
    </property>
    <property name="warnLogCategory">
        <value>org.springframework.web.servlet.handler.SimpleMappingExceptionResolver</value>
    </property>
</bean>           

複制

這是因為Spring的實作就存在這個限制,參考:https://jira.spring.io/browse/SPR-8467,官方的解釋是:

We are calling afterCompletion without an exception in case of what we consider a successful outcome, which includes an 
exception having been handled (and turned into an error view) by a HandlerExceptionResolver. I guess the latter scenario 
is what you're getting confused by? In other words, we effectively only pass in an actual exception there when no 
HandlerExceptionResolver was available to handle it, that is, when it eventually gets propagated to the servlet container...
I see that this is debatable. At the very least, we should be more explicit in the javadoc there.           

複制

那麼,如果我們确實需要在afterCompletion()中擷取到業務方法抛出的異常資訊,應該怎麼做呢?

在這裡,采用了通過ThreadLocal儲存異常資料的方式實作。為此,我們需要擴充一下Spring自帶的異常處理器類。

/**
 * 自定義全局異常解析類
 * @desc org.chench.test.springmvc.handler.MyExceptionResolver
 * @date 2017年11月29日
 */
public class MyExceptionResolver extends SimpleMappingExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
            Exception ex) {
        ModelAndView mv = super.resolveException(request, response, handler, ex);
        if(ex != null) {
            // 當異常資訊不為空時,儲存到ThreadLocal中
            ThreadExceptionContainer.set(ex);
        }
        return mv;
    }
}

/**
 * 儲存線程上下文異常資訊
 * @desc org.chench.test.springmvc.util.ThreadExceptionContainer
 * @date 2017年11月29日
 */
public class ThreadExceptionContainer {
    private static final ThreadLocal<Exception> exceptionCache = new ThreadLocal<Exception>();
    
    public static void set(Exception exception) {
        exceptionCache.set(exception);
    }
    
    public static Exception get() {
        return exceptionCache.get();
    }
    
    public static void clear() {
        exceptionCache.remove();
    }
    
    private ThreadExceptionContainer() {
    }
}           

複制

然後在Spring中使用擴充的全局異常處理器類:

<!-- 使用自定義擴充的全局異常處理器 -->
<bean id="exceptionResolver" class="org.chench.test.springmvc.handler.MyExceptionResolver">
    <property name="defaultErrorView">
        <value>error/error</value>
    </property>
    <property name="defaultStatusCode">
        <value>500</value>
    </property>
    <property name="warnLogCategory">
        <value>org.springframework.web.servlet.handler.SimpleMappingExceptionResolver</value>
    </property>
</bean>           

複制

2.在Controller方法中使用自定義注解

// 在業務方法中使用自定義注解
@OperationLog(type=OperationType.LOGIN, alias="使用者登入", content="使用者登入")
@RequestMapping("/login")
public String login(HttpServletRequest req,
        @RequestParam("username") String username,
        @RequestParam("password") String password) throws NotFoundException, CredentialException {
    Account account = accountService.getAccountByUsername(username);
    // 使用者不存在
    if(account == null) {
        logger.error("account not found! username: {}, password: {}", new Object[] {username, password});
        throw new NotFoundException(String.format("account not found for username: %s", username));
    }
    
    // 密碼不正确
    if(!account.getPassword().equals(password)) {
        logger.error("credentials error for account: " + username);
        throw new CredentialException("credentials error for account: " + username);
    }
    
    req.getSession().setAttribute("account", account);
    return "redirect:home";
}           

複制

自定義注解總結

實際上,在編寫注解處理器時使用的是Java的另一個功能:反射機制。

本質上來講,所謂的注解解析器就是利用反射機制擷取在類,成員變量或者方法上的注解資訊。

Java反射機制可以讓我們在運作期獲得任何一個類的位元組碼,包括接口、變量、方法等資訊。還可以讓我們在運作期執行個體化對象,通過調用get/set方法擷取變量的值等。

也就是說,如果Java僅僅支援了注解,卻未提供反射機制,實際上是不能做任何事情的,反射機制是我們能夠在Java中使用注解的基礎。

【參考】

http://www.cnblogs.com/peida/archive/2013/04/24/3036689.html 注解(Annotation)自定義注解入門