暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

什么是AOP?AOP记录操作日志功能如何实现?

有猿再见 2021-04-12
940

1.AOP的简单介绍

AOP:面向切面编程,相当于OOP面向对象编程;是一种编程思想。

AOP与IOC是Spring框架的两大核心,SpringAOP的存在目的是为了解藕。AOP可以让一组类共享相同行为。

在OOP中只能通过继承类和实现接口,来使代码的耦合度增强,且类继承只能为单继承,阻碍了更多行为添加到一组类上,AOP弥补了OOP的不足。

AOP基于代理思想,对原来目标对象,创建代理对象;在不修改原对象的情况下对原有方法进行增强。

2.AOP的使用

「Spring支持AspectJ的注解式切面编程」

1.在类上使用@Aspect
注解声明该类为一个切面类


2.在类中方法上使用@Before
@After
@Around
@AfterReturning
@AfterThrowing
注解定义建言,可直接将拦截规则(切点)作为参数。

3.@Before
@After
@Around
@AfterReturning
@AfterThrowing
参数的拦截规则为切点(PointCut),为了方便使切入点复用,可以使用@PointCut
注解定义一个拦截规则,然后在@Before
@After
@Around
@AfterReturning
@AfterThrowing
的参数中调用;其中符合条件的每一个被拦截处为连接点(JoinPoint)

3.切面的简单介绍

描述切面必须先了解以下几个概念:
1.目标类:需要被增强的类。
2.连接点:看你被增强的点;泛指目标类中的所有方法。
3.切入点:将会被增强的连接点,目标类中被增强的方法。
4.通知(增强):对切入点增强的内容;增强的内容通常以方法的形式体现,增强执行的位置不同,称呼不同。(前置通知、后置通知、环绕通知、抛出异常通知、最终通知)
5.切面:通知和切入点的组合;一个通知对应一个切入点形成一条线,多个通知对应多个切入点对应多条线,多条线形成一个面,这个面就是切面。

4.切面类中的注解介绍

@Aspect
:声明切面类
@Pointcut
:声明切入点
@Before
:前置通知,在方法执行前被调用
@After
:后置通知,在方法执行之后执行
@AfterReturning
:最终通知,在方法正常执行完返回后执行
@AfterThrowing
:异常通知,在方法发生异常会被调用
@Around
:环绕通知:在方法的执行前、后都会被调用

切面类中的@Before
@After
@Around
@AfterReturning
@AfterThrowing
这些都统称为通知,在切面类中通知的执行顺序为:
没有发生异常的情况:
环绕通知 >> 前置通知 >> 环绕通知 >> 后置通知 >> 最终通知
发生异常的情况:
环绕通知 >> 前置通知 >> 环绕通知 >> 后置通知 >> 异常通知

5.演示如何基于注解和基于方法规则拦截两种方式,输出记录操作的日志

「1.相关依赖」

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

「2.编写拦截规则注解」

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Action {
    //方法描述
    String description();
}

注意:
注解本身时没有功能的,注解是一种原数据,就是用来解释数据的数据,这就是所谓的配置。
注解的功能来自用这个注解的地方。

「3.编写使用注解的被拦截类」

@Service
public class TestAnnotationService {
    @Action(description = "注解式拦截一个add操作")
    public void add(){}
}

「4.编写使用方法规则被拦截类」

@Service
public class TestMethodService {
    public void add(){}
}

「5.编写切面」

@Slf4j
@Aspect
@Component
public class LogAspect {

    /**
     * 切入点
     */

    @Pointcut("@annotation(com.scholartang.anno.Action)")
    public void annotationPointCut() {

    }

    /**
     * 前置通知,用于测试方法式拦截规则
     *
     * @param joinPoint
     */

    @Before("execution(* com.scholartang.service.TestMethodService.*(..))")
    public void before(JoinPoint joinPoint) {
        //获取连接点的方法签名对象
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //通过签名对象获取类中的方法对象
        Method method = signature.getMethod();
        log.info("方法式拦截规则:" + method.getName());

    }

    /**
     * 后置通知,用于测试注解式拦截规则
     *
     * @param joinPoint
     */

    @After("annotationPointCut()")
    public void after(JoinPoint joinPoint) {
        //获取连接点的方法签名对象
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //通过签名对象获取类中的方法对象
        Method method = signature.getMethod();
        //通过方法对象获取方法上的注解对象
        Action action = method.getAnnotation(Action.class);
        log.info("注解式拦截:" + action.description());
    }
}


「6.测试」

在SpringBoot测试动类中注入两个Service层的对象,在测试方法中分别调用对应的方法进行测试

@SpringBootTest
class SpringAopApplicationTests {

    @Autowired
    TestAnnotationService annotationService;

    @Autowired
    TestMethodService methodService;

    @Test
    void aopTest() {
        annotationService.add();
        methodService.add();
    }
}

「7.运行效果」

6.在实际开发中通过AOP来输出控制层中的接口操作的日志案例

通过AOP对控制层中的方法进行增强,当方法被调用时,将一些必要信息通过日志的方式输出。请求URL
请求源IP
请求时间
请求方法
请求参数
请求处理耗时

「1.切面类code」

@Slf4j
@Aspect
@Component
public class ApiLogAspect {

    /**
     * 切入点
     * public:被public修饰的方法
     * *:任意返回值
     * com.scholartang.controller..*:目标类中的任意方法
     * .*(..):任意参数
     */

    @Pointcut("execution(public * com.scholartang.controller..*.*(..))")
    public void apiLog() {
    }

    /**
     * 环绕通知:在方法的执行前、后都会被调用
     *
     * @param point
     * @return
     */

    @Around(value = "apiLog()")
    private Object doAfter(ProceedingJoinPoint point) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = null;
        try {
            result = point.proceed();
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            log.info("--- <><><><><><><><><><><><><><> ---");
            log.info("请求源IP:" + getIpAddr(request));
            log.info("请求URL:" + request.getRequestURL());
            log.info("请求时间:" + new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(new Date()));
            log.info("请求方法:" + point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName());
            log.info("请求参数:{}", point.getArgs());
            log.info("请求处理耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");
            log.info("--- <><><><><><><><><><><><><><> ---");
        } catch (Throwable e) {
            log.info("{} Use time : {} ms with exception : ", point, (System.currentTimeMillis() - startTime), e.getMessage());
            throw e;
        }
        return result;
    }


    /**
     * 获取当前请求网络ip
     *
     * @param request
     * @return
     */

    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = request.getHeader("x-forwarded-for");
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
                InetAddress inet = null;
                try {
                    inet = InetAddress.getLocalHost();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
                ipAddress = inet.getHostAddress();
            }
        }
        if (ipAddress != null && ipAddress.length() > 15) {
            if (ipAddress.indexOf(",") > 0) {
                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
            }
        }
        return ipAddress;
    }
}


「2.当控制层的接口被请求时输出的日志效果图」

7.知识点补充

「1.自定义注解类中的注解说明」

@Target
:专门用来限定某个自定义注解能够被应用在哪些Java元素上面的,它使用一个枚举类型定义如下:

public enum ElementType {
   /** 类,接口(包括注解类型)或枚举的声明 */
   TYPE,

   /** 属性的声明 */
   FIELD,

   /** 方法的声明 */
   METHOD,

   /** 方法形式参数声明 */
   PARAMETER,

   /** 构造方法的声明 */
   CONSTRUCTOR,

   /** 局部变量声明 */
   LOCAL_VARIABLE,

   /** 注解类型声明 */
   ANNOTATION_TYPE,

   /** 包的声明 */
   PACKAGE
}



@Retention
:翻译为持久力、保持力。即用来修饰自定义注解的生命力。 
注解的生命周期有三个阶段:
1、Java源文件阶段;
2、编译到class文件阶段;
3、运行期阶段。
同样使用了RetentionPolicy枚举类型定义了三个阶段:

public enum RetentionPolicy {
   /**
    * Annotations are to be discarded by the compiler.
    * 注释将被编译器丢弃。
    */

   SOURCE,

   /**
    * Annotations are to be recorded in the class file by the compiler
    * but need not be retained by the VM at run time.  This is the default
    * behavior.
    * 注释将由编译器记录在类文件中,但是不需要在运行时被VM保留。这是默认的的行为。
    */

   CLASS,

   /**
    * Annotations are to be recorded in the class file by the compiler and
    * retained by the VM at run time, so they may be read reflectively.
    *
    * 注释将由编译器和在类文件中记录虚拟机在运行时保留它们,因此可以反射地读取它们。
    * @see java.lang.reflect.AnnotatedElement
    */

   RUNTIME
}


@Documented
:被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。
@Inherited
:指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解。
注意:@Inherited
注解只对那些@Target被定义为ElementType.TYPE的自定义注解起作用。

「2.连接点JoinPoint对象的介绍:org.aspectj.lang.JoinPoint-中文简要API」

AspectJ使用org.aspectj.lang.JoinPoint接口表示目标类连接点对象,如果是环绕增强时,使用org.aspectj.lang.ProceedingJoinPoint表示连接点对象,该类是JoinPoint的子接口。任何一个增强方法都可以通过将第一个入参声明为JoinPoint访问到连接点上下文的信息。我们先来了解一下这两个接口的主要方法:
1.JoinPoint

java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;
Signature getSignature() :获取连接点的方法签名对象;
java.lang.Object getTarget() :获取连接点所在的目标对象; 
java.lang.Object getThis() :获取代理对象本身; 

2.ProceedingJoinPoint

ProceedingJoinPoint继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法: 

java.lang.Object proceed() throws java.lang.Throwable:通过反射执行目标对象的连接点处的方法; 
ava.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable:通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。

End

无论艳阳高照还是雨雪纷飞,从旭日东升到华灯初上,从我们用坚实的脚步丈量每一个真实的日子。过去的岁月只剩下模糊的背影,那些美好或辛酸的记忆,只不过是某种形式的纪念。未来究竟会怎样,我们不得而知,但与今天绝对息息相关。找到迷失的自我,活出真我的风采。把握今天,活在当下!

更多技术分享,可以关注我微信公众号🙂。


文章转载自有猿再见,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论