# 注解机制
在之后的开发中,注解是一定离不开的,所以前期的学习就要打好基础。注解的出现极大便利了开发过程,很多需要很麻烦的配置,代码之类的,都通过注解得以简化。其作用表现如下:
生成文档,通过代码里标识的元数据生成 javadoc 文档。
编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。
# 内置注解
JDK 内置了以下注解:
@Override
- 检查(仅仅是检查,不保留到运行时)该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。@Deprecated
- 标记过时方法。如果使用该方法,会报编译警告。@SuppressWarnings
- 指示编译器去忽略注解中声明的警告(仅仅编译器阶段,不保留到运行时)@FunctionalInterface
- Java 8 开始支持,标识一个匿名函数或函数式接口。@SafeVarargs
- Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
注意有些注解只是起检查作用,在编译时会被去除。 @FunctionalInterface
注解,举个例子, Comparator
接口就使用了:
@FunctionalInterface | |
public interface Comparator<T> { | |
// ... | |
} |
# 元注解
作用于注解上的注解,用于我们编写自定义的注解,这种注解是对内置注解,自定义注解的说明(配置),比如 @Override
:
@Target(ElementType.METHOD) | |
@Retention(RetentionPolicy.SOURCE) | |
public @interface Override { | |
} |
作为内置注解, @Override
源码上也有注解修饰,这就是元注解。
@Retention
- 标识该注解生命周期,代码阶段 / 编译阶段 / 运行阶段,分别对应SOURCE
,CLASS
,RUNTIME
。@Documented
- 标记这些注解是否包含在用户文档中,生成 API 文档时才需要用到该注解。@Target
- 标记这个注解的作用范围。@Inherited
- 被它修饰的 Annotation 将具有继承性。如果某个类使用了被 @Inherited 修饰的 Annotation,则其子类将自动具有该注解。@Repeatable
- Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
生命周期中, CLASS
阶段,尽管注解会被保存到 class 文件中,但是 JVM 加载该类文件时会被遗弃。重复注解可以看一下我另一篇博客:类型注解与重复注解。
# 自定义注解
在学习自定义注解前,先了解一下如何获取注解的内容,你首先要知道你想拿注解干什么,然后是否有可以通过注解来实现,才能去开发想要的自定义注解。
反射包下的 AnnotatedElement
接口提供了这些方法用于获取注解内容,前提是注解被定义为 RUNTIME
,该注解才能运行时可见。
AnnotatedElement
接口是所有程序元素(Class,Method,Constructor,Field)的父接口,这些程序元素实现了这些方法( getAnnotations()
, getAnnotationsByType
等方法),所以当我们获得一个程序元素时,就可以调用其 getAnnotations()
方法获取注解。
我们先自定义一个注解(不想麻烦的再去找哪个注解能活到运行时了):
@Target({ElementType.METHOD, ElementType.TYPE}) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface Cyan { | |
//default 设置默认值 | |
String[] value() default {"Java全栈知识体系", "宫水三叶の刷题日记", "青空の霞光"}; | |
} |
然后写个类,将注解放上去:
@Cyan(value = {"佐藤时雨", "才川晴香"}) | |
public class Main { | |
public static void main(String[] args) throws Exception { | |
Class<?> clazz = Main.class; | |
System.out.println(clazz.getAnnotation(Cyan.class)); | |
} | |
} | |
// 结果:@com.cyan.annotation.Cyan (value=[佐藤时雨,才川晴香]) |
一定要注解的生命周期是 RUNTIME
才行,不然你可以试试获取一下 @Override
注解,一定会失败。
如果数组里面只有一个内容,我们可以直接传入一个值,而不是创建一个数组:
@Cyan(value = "南慕青") | |
public class Main{ | |
} |
现在再补充一下自定义注解的一些知识(其实就是使用元注解和定义属性),如果注解只有一个属性,我们可以将其名字定义为 value
,这样在使用注解时,直接写值就可以了,不需要写 value = "xx"
。
一些点:
- 拿到
Class
对象之后,遍历对应程序元素(比如Method
类),可以通过isAnnotationPresent(Cyan.class)
来拿到所有被Cyan
注解修饰的方法,遍历嘛,反射做这些事的效率本来就和类的体量大小有关。 @getDeclaredAnnotation
会忽略继承过来的注解。
# 原理
Java 全栈知识体系推荐这两篇文章:
- https://blog.csdn.net/qq_20009015/article/details/106038023
- https://www.race604.com/annotation-processing/
# 例子:AOP 实现解耦
下面的内容,需要你知道什么是 AOP,即切面编程。不用会不会 Spring,实际上,很多人会用 Spring 也说不明白 AOP,如果你需要了解 AOP,可以参考我的另一篇博客:Spring--AOP。
最为常见的就是使用 Spring AOP 切面实现统一的操作日志管理,通过该例子了解自定义注解如何实现解耦的,本例子只展示了一些主要代码,类似里面自定义的工具类,业务实体类,Ajax 请求响应类都没有给出:
- 自定义
Log
注解:
// 作用于参数,方法,存活到运行时 | |
@Target({ ElementType.PARAMETER, ElementType.METHOD }) | |
@Retention(RetentionPolicy.RUNTIME) | |
@Documented | |
public @interface Log { | |
// 模块 | |
public String title() default ""; | |
// 功能 | |
public BusinessType businessType() default BusinessType.OTHER; | |
// 操作人类别 | |
public OperatorType operatorType() default OperatorType.MANAGE; | |
// 是否保存请求的参数 | |
public boolean isSaveRequestData() default true; | |
} |
- 使用
@Log
注解:以一个简单的 CRUD 操作为例,这里展示部分代码:每对 “部门” 进行操作就会产生一条操作日志存入数据库。我们使用@Log
注解,自然就是希望记录日志,对于那些被@Log
注解修饰过的方法,记录对应日志。
需要你有一些 Spring/SpringMVC 的知识,就是一些映射请求,看不懂也没关系,只需要知道这段代码有几个方法:保存,更新,删除等。这些方法被
@Log
修饰,每当调用这些方法,就会记录日志。
@Controller | |
@RequestMapping("/system/dept") | |
public class DeptController extends BaseController { | |
private String prefix = "system/dept"; | |
@Resource | |
private IDeptService deptService; | |
// 新增保存部门 | |
@Log(title = "部门管理", businessType = BusinessType.INSERT) | |
@RequiresPermissions("system:dept:add") | |
@PostMapping("/add") | |
@ResponseBody | |
public AjaxResult addSave(@Validated Dept dept) { | |
if (UserConstants.DEPT_NAME_NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) { | |
return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在"); | |
} | |
return toAjax(deptService.insertDept(dept)); | |
} | |
// 保存 | |
@Log(title = "部门管理", businessType = BusinessType.UPDATE) | |
@RequiresPermissions("system:dept:edit") | |
@PostMapping("/edit") | |
@ResponseBody | |
public AjaxResult editSave(@Validated Dept dept) { | |
if (UserConstants.DEPT_NAME_NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) { | |
return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在"); | |
} else if(dept.getParentId().equals(dept.getDeptId())) { | |
return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己"); | |
} | |
return toAjax(deptService.updateDept(dept)); | |
} | |
// 删除 | |
@Log(title = "部门管理", businessType = BusinessType.DELETE) | |
@RequiresPermissions("system:dept:remove") | |
@GetMapping("/remove/{deptId}") | |
@ResponseBody | |
public AjaxResult remove(@PathVariable("deptId") Long deptId) { | |
if (deptService.selectDeptCount(deptId) > 0) { | |
return AjaxResult.warn("存在下级部门,不允许删除"); | |
} | |
if (deptService.checkDeptExistUser(deptId)) { | |
return AjaxResult.warn("部门存在用户,不允许删除"); | |
} | |
return toAjax(deptService.deleteDeptById(deptId)); | |
} | |
} |
- 实现日志的切面,对自定义注解
Log
作切点进行拦截,即对注解了@Log
的方法进行了切点拦截。到这里才是使用 AOP 进行实现。
这里采用的是注解配置 AOP,如果不懂,可以看一下我的另一篇博客(很短,很快就看完了):Spring-- 注解开发。
我们再回顾一下,我们需要定义一个切面类,抽取了相同的切面代码,需要使用 @Aspect
注解。
@Aspect | |
@Component | |
public class LogAspect { | |
private static final Logger log = LoggerFactory.getLogger(LogAspect.class); | |
// 定义切点 - 自定义注解的包路径 | |
@Pointcut("@annotation(com.xxx.aspectj.lang.annotation.Log)") | |
public void logPointCut() { | |
// 什么都不用写,需要执行的逻辑可以放在通知里面 | |
} | |
// 处理完请求后执行 | |
@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult") | |
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) { | |
handleLog(joinPoint, null, jsonResult); | |
} | |
// 拦截异常操作 | |
@AfterThrowing(value = "logPointCut()", throwing = "e") | |
public void doAfterThrowing(JoinPoint joinPoint, Exception e) { | |
handleLog(joinPoint, e, null); | |
} | |
protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) { | |
try { | |
// 获得注解 | |
Log controllerLog = getAnnotationLog(joinPoint); | |
if (controllerLog == null) { | |
return; | |
} | |
// 下面就是根据 Log 的信息生成注解了 | |
// 获取当前的用户 | |
User currentUser = ShiroUtils.getSysUser(); | |
// *======== 数据库日志 =========*// | |
OperLog operLog = new OperLog(); | |
operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); | |
// 请求的地址 | |
String ip = ShiroUtils.getIp(); | |
operLog.setOperIp(ip); | |
// 返回参数 | |
operLog.setJsonResult(JSONObject.toJSONString(jsonResult)); | |
operLog.setOperUrl(ServletUtils.getRequest().getRequestURI()); | |
if (currentUser != null) { | |
operLog.setOperName(currentUser.getLoginName()); | |
if (StringUtils.isNotNull(currentUser.getDept()) | |
&& StringUtils.isNotEmpty(currentUser.getDept().getDeptName())) { | |
operLog.setDeptName(currentUser.getDept().getDeptName()); | |
} | |
} | |
if (e != null) { | |
operLog.setStatus(BusinessStatus.FAIL.ordinal()); | |
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); | |
} | |
// 设置方法名称 | |
String className = joinPoint.getTarget().getClass().getName(); | |
String methodName = joinPoint.getSignature().getName(); | |
operLog.setMethod(className + "." + methodName + "()"); | |
// 设置请求方式 | |
operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); | |
// 处理设置注解上的参数 | |
getControllerMethodDescription(controllerLog, operLog); | |
// 保存数据库 | |
AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); | |
} catch (Exception exp) { | |
// 记录本地异常日志 | |
log.error("==前置通知异常=="); | |
log.error("异常信息:{}", exp.getMessage()); | |
exp.printStackTrace(); | |
} | |
} | |
// 获取注解中对方法的描述信息 用于 Controller 层注解 | |
public void getControllerMethodDescription(Log log, OperLog operLog) throws Exception { | |
// 设置 action 动作 | |
operLog.setBusinessType(log.businessType().ordinal()); | |
// 设置标题 | |
operLog.setTitle(log.title()); | |
// 设置操作人类别 | |
operLog.setOperatorType(log.operatorType().ordinal()); | |
// 是否需要保存 request,参数和值 | |
if (log.isSaveRequestData()) { | |
// 获取参数的信息,传入到数据库中。 | |
setRequestValue(operLog); | |
} | |
} | |
// 获取请求的参数,放到 log 中 | |
private void setRequestValue(OperLog operLog) { | |
Map<String, String[]> map = ServletUtils.getRequest().getParameterMap(); | |
String params = JSONObject.toJSONString(map); | |
operLog.setOperParam(StringUtils.substring(params, 0, 2000)); | |
} | |
// 是否存在注解,如果存在就获取 | |
private Log getAnnotationLog(JoinPoint joinPoint) throws Exception { | |
Signature signature = joinPoint.getSignature(); | |
MethodSignature methodSignature = (MethodSignature) signature; | |
Method method = methodSignature.getMethod(); | |
if (method != null) | |
{ | |
return method.getAnnotation(Log.class); | |
} | |
return null; | |
} | |
} |
# 参考
https://blog.csdn.net/u013217730/article/details/103012817
https://pdai.tech/md/java/basic/java-basic-x-annotation.html
https://www.yuque.com/qingkongxiaguang/javase/muwq85#6e847e51