细说java系列之注解

时间:2022-11-03 21:38:47

细说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)自定义注解入门