SpringBoot自定义注解+AOP实现日志记录

2,944 阅读3分钟

一、关于注解

Java中定义了4个元注解,分别是: @Target,@Retention,@Documented,@Inherited

1. @Target注解

说明了注解所修饰的对象范围:注解可被用于 包、类、接口、枚举、Annotation类型、方法、构造方法、成员变量、枚举值、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。

**作用:**用于描述注解的使用范围(即:被描述的注解可以用在什么地方)

取值(ElementType)有:

1.CONSTRUCTOR:用于描述构造器 2.FIELD:用于描述域 3.LOCAL_VARIABLE:用于描述局部变量 4.METHOD:用于描述方法 5.PACKAGE:用于描述包 6.PARAMETER:用于描述参数 7.TYPE:用于描述类、接口(包括注解类型) 或enum声明

2. @Retention注解

定义了该注解被保留的时间长短:某些注解仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的注解可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为注解与class在使用上是被分离的)。使用这个元注解可以对 Annotation的“生命周期”限制。

**作用:**表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)

取值(RetentionPoicy)有:

1.SOURCE:在源文件中有效(即源文件保留) 2.CLASS:在class文件中有效(即class保留) 3.RUNTIME:在运行时有效(即运行时保留)

3. @Documented注解

用于描述其它类型的注解应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。------------摘抄

4. @Inherited注解

是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

二、实现自定义注解

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogRecord {
    /**
     * 被标注的方法的功能
     * @return
     */
    String effect() default "";
}

三、整合AOP功能

1. maven添加AOP功能相关依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2. 编写AOP代码

import com.almond.mpdemo.common.annotation.LogRecord;
import com.almond.mpdemo.common.utils.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * @Classname LogAspect
 * @Description 扫描自定义注解 实现日志记录增强
 * @Date 2020/9/4 9:27
 * @Created by hongxing
 */
@Slf4j
@Component
@Aspect
public class LogAspect {

    @Resource
    private TLogMapper tLogMapper;

    // 设置切点 是注解的路径
    @Pointcut("@annotation(com.almond.mpdemo.common.annotation.LogRecord)")
    public void annotationAspect() {
    }

    /**
     * 环绕通知 获取方法相关信息
     * 线程id
     * 请求ip
     */

    @Around(value = "annotationAspect()")
    @Transactional(rollbackFor = {Exception.class})
    public Object doAround(ProceedingJoinPoint joinPoint) {
        log.info("代理执行, 代理方法{}", ((MethodSignature) joinPoint.getSignature())
                .getMethod().getName());
        LogRecord logRecord = ((MethodSignature) joinPoint.getSignature())
                .getMethod()
                .getAnnotation(LogRecord.class);
        String name = "";
        if (logRecord != null) {
            name = ((MethodSignature) joinPoint.getSignature())
                    .getMethod().toGenericString();
        }

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getRequest();
        String host = HttpUtil.getIpAddress(request);
        Long id = Thread.currentThread().getId();


        // 日志落库
        // 在此处持久化日志信息
        try {
            // 放行,执行原方法
            joinPoint.proceed();
        } catch (Throwable throwable) {
            log.error("代理发生异常, 异常信息{}", throwable.getMessage());
        }
        return null;
    }

}

3. 相关工具类

package com.almond.mpdemo.common.utils;

import javax.servlet.http.HttpServletRequest;

/**
 * @Classname HttpUtil
 * @Description
 * @Date 2020/9/4 10:49
 * @Created by hongxing
 */
public class HttpUtil {
    /**
     * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址,
     * 参考文章: http://developer.51cto.com/art/201111/305181.htm
     *
     * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
     * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。
     *
     * 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130,
     * 192.168.1.100
     *
     * 用户真实IP为: 192.168.1.110
     *
     * @param request
     * @return
     */
    public static String getIpAddress(HttpServletRequest request) {
        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("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

4. 测试demo

将注解标注在方法上,调用方法,观察控制日志,如果出现代理执行, 代理方法XXX则表示功能实现.