# 注解机制

在之后的开发中,注解是一定离不开的,所以前期的学习就要打好基础。注解的出现极大便利了开发过程,很多需要很麻烦的配置,代码之类的,都通过注解得以简化。其作用表现如下:

  • 生成文档,通过代码里标识的元数据生成 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 - 标识该注解生命周期,代码阶段 / 编译阶段 / 运行阶段,分别对应 SOURCECLASSRUNTIME

  • @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