JavaEE从入门到起飞 (三) ~AOP

 晚上好,愿这深深的夜色给你带来安宁,让温馨的夜晚抚平你一天的疲惫,美好的梦想在这个寂静的夜晚悄悄成长。

目录

文章目录

前言

了解面向切面编程(AOP)

什么是面向切面编程(AOP)?

举个例子理解AOP和代理思想

一、主要概念?

1.1 Aspect(切面)

1.2 切入点表达式

切入点表达式的标准格式:

 切入点表达式的通配符:

@Pointcut

1.3 通知类型 

二、日志案例

1.导入初始环境

1.1 引入AOP的依赖

1.2 初始化数据库和实体类

1.3 创建日志的持久层Mapper 

1.4 创建自定义注解

2.核心逻辑

2.1 对要增强的方法添加自定义注解

2.2 一些常用的获取请求信息的方法

2.3 编写切面类进行增强 


前言

了解面向切面编程(AOP)

在现代软件开发中,面向切面编程(Aspect-Oriented Programming,AOP)是一种重要的编程范式,它提供了一种有效的方式来解决横切关注点(cross-cutting concerns)的问题。本文将介绍AOP的概念、其主要组成部分以及如何在Java应用程序中应用AOP技术。

什么是面向切面编程(AOP)?

AOP是一种软件开发技术,它允许开发人员将横切关注点(如日志记录、事务管理、安全性、缓存等)从应用的核心逻辑中分离出来。这些横切关注点可能会散布在应用程序的多个模块中,传统的面向对象编程往往会导致这些关注点与核心业务逻辑混杂在一起,使得代码难以理解、维护困难。AOP的出现旨在通过模块化横切关注点的方式来提高代码的模块性、可维护性和可重用性。

举个例子理解AOP和代理思想

例如:理发店这个工作流程,这样来一个顾客,我们就需要这样的流程,十分繁琐,对于聪明的我们自然是不愿意这样干的

我们可以抽取一个公共的方法,来直接调用即可,但仍然还需要我们调用,不够懒。

因此我们可以引入切面的思想了,我们把匹配的流程,自动执行对应的操作。

主要原理:代码,通过代理对象伪装成真正的对象来为我们服务

剪发的时候代理对象,会给你找剪头发的托尼老师,如果你想染发,代理也可以给你找专门染发的托尼老师。这就是我们AOP的思想了。

将真实bean作为代理对象中的一个成员变量。


一、主要概念?

横向增强内容,是一种无侵入的对原始方法进行增强

三大条件需要声明:

  1. 要增强的方法(切入点表达式进行匹配)
  2. 增加方法
  3. 增强方法和切入点方法的执行顺序 (通知方式 四大通知类型和环绕通知)

在AOP中,有几个核心概念需要理解:

  • 连接点(Join Point): 连接点是程序执行过程中的一个特定点,例如方法调用或抛出异常。在AOP中,连接点是可以被增强(如添加日志、事务管理等)的程序执行点。

  • 切入点(Pointcut): 切入点定义了在程序中哪些连接点上应用通知(Advice)。它通过表达式或者模式匹配来描述要被增强的一组连接点。

  • 切面(Aspect): 切面是将通知(Advice)和切入点(Pointcut)结合起来的一个模块化单元。它描述了在何处以及何时应用通知来实现横切关注点的功能。

  • 通知(Advice): 通知是在切面的某个特定连接点上执行的动作。它定义了增强代码的类型和时机,如在方法调用之前、之后或者环绕方法执行。

  • 目标对象(Target Object): 目标对象是一个或多个切面所增强的原始对象。它包含业务逻辑,通常是不察觉被应用切面的存在。

  • 代理对象(Proxy Object): 代理对象是生成的对象,用作目标对象的替代品。它拦截对目标对象的方法调用,并允许AOP框架应用切面(如日志记录、安全性、事务管理等)。

1.1 Aspect(切面)

使用@Aspect和@Component的类

为什么切面还要声明成bean?

因为如果一个类没有被 Spring 管理,那么 Spring 将无法为它创建代理对象,也就无法实现 AOP 功能。

@Aspect的作用是什么?

因为,Spring容器启动后,会优先读取声明到@Aspect类并且读取所有切面配置的切入点,然后在初始化bean判断bean中的方法是否匹配切入点,匹配失败就创建对象到IOC,匹配成功,就创建原始对象的代理对象,然后会将代理对象注入为该类型的Bean,然后当你注入该Bean的时候实际注入的是代理对象的Bean,执行的就是代理对象与连接点映射的方法。

代理对象内容:根据切面中的增强方法和原始对象生成 。

1.2 切入点表达式

主要思想:匹配所有的切点,然后根据条件筛选出连接点,然后进行增强。

切入点表达式的标准格式:

execution([访问修饰符] [返回值类型] [包名.类名.]方法名(参数列表) [throws 异常名])
  • 访问修饰符:可选项,例如 publicprotected 等,可以省略。
  • 返回值类型:方法的返回类型,例如 voidint 等。
  • 包名.类名:类的完整路径,可以省略。
  • 方法名:目标方法的名称。
  • 参数列表:方法的参数列表。
  • throws 异常名:方法声明的异常,可选,指定方法可能抛出的异常。

 切入点表达式的通配符:

  • *:匹配任意数量的字符(单个独立的任意符号,可以单独使用或者作为前缀或后缀)。
  • ..:匹配任意数量的字符序列(多个连续的任意符号,常用于简化包名和参数的书写)。
  • +:写在类或接口的后面,专用于匹配子类类型。

@Pointcut

是在 Spring AOP 中用来定义切入点的注解。

在 Spring AOP 中,切入点(Pointcut)是一组匹配连接点(Join Point)的规则。连接点是在应用执行过程中能够插入切面的点,例如方法执行时、方法调用时等。而切入点则是为了从连接点中筛选出我们真正关心的一部分。

execution:有一定规律的(可以按照规律模糊匹配)

  • execution 表达式:这种表达式按照一定的规则和格式来匹配方法的执行。它允许根据方法的访问修饰符、返回值类型、包名、类名、方法名、参数列表以及可能抛出的异常来进行精确或模糊的匹配。

    @Pointcut("execution(* com.example.service.*.*(..))")
    private void serviceMethods() {}
    
    @Before("serviceMethods()")
    public void beforeServiceMethods(JoinPoint joinPoint) {
        // 在 serviceMethods() 匹配的方法执行前执行此通知
    }

@annotion:没有规律,指定注解被匹配

  • @annotation 注解表达式:这种表达式是一种特殊的切入点表达式,用于匹配被特定注解标注的方法。它不需要关注方法的签名、包名等,而是专门指定某个或某些特定的注解。

    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    private void transactionalMethods() {}
    
    @Around("transactionalMethods()")
    public Object aroundTransactionalMethods(ProceedingJoinPoint joinPoint) throws Throwable {
        // 在使用 @Transactional 注解的方法周围执行此通知
        return joinPoint.proceed();
    }

1.3 通知类型 

我认为通知类型,就是决定了原始方法的执行位置

  1. @Before: 前置通知

  2. @After: 后置通知

  3. @Around: 环绕通知

    1. 需要有参数来确定原始方法所在的位置,然后调用proceed()方法来调用原始方法。
    2. 需要把原始方法返回值扔出去,就是proceed()的返回值。注意,proceed()方法如果要修改参数的内容可以传参。
  4. @AfterReturning: 返回后通知

    1. 在方法返回后执行,不抛出异常的情况下。
  5. @AfterThrowing: 抛出异常后通知

对于环绕方式(Around)通过调用原始方法的实际,可以完成上述四者所有的功能。

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
public class UserServiceAspect {

    @Before("execution(* com.example.service.UserService.*(..))")
    public void beforeAdvice() {
        System.out.println("前置通知: 在UserService方法执行前进行");
    }

    @After("execution(* com.example.service.UserService.*(..))")
    public void afterAdvice() {
        System.out.println("后置通知: 在UserService方法执行后进行(相当于finally块)");
    }

    @Around("execution(* com.example.service.UserService.*(..))")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知: UserService方法执行前");
        Object result = joinPoint.proceed(); // 执行方法
        System.out.println("环绕通知: UserService方法执行后");
        return result;
    }

    @AfterReturning(pointcut = "execution(* com.example.service.UserService.*(..))", returning = "result")
    public void afterReturningAdvice(Object result) {
        System.out.println("返回后通知: 在UserService方法返回后执行");
    }

    @AfterThrowing(pointcut = "execution(* com.example.service.UserService.*(..))", throwing = "ex")
    public void afterThrowingAdvice(Exception ex) {
        System.out.println("异常抛出通知: UserService方法中抛出的异常: " + ex.getMessage());
    }
}

二、日志案例

注意下述是使用SpringBoot的方式实现的Aop。

我们要实现的功能是,要记录controller中增 删 改方法的运行日志保存到日志表中

日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法作用、方法运行时参数、返回值、方法执行时长

1.导入初始环境

1.1 引入AOP的依赖

        <!--aop相关的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

1.2 初始化数据库和实体类

初始化数据库

-- 操作日志表
create table operate_log(
    id bigint unsigned primary key auto_increment comment 'ID',
    class_name varchar(100) comment '操作的类名',
    method_name varchar(100) comment '操作的方法名',
    method_desc varchar(100) comment '方法用途',
    method_params varchar(1000) comment '方法参数',
    return_value varchar(2000) comment '返回值',
    operate_user int unsigned comment '操作人ID',
    operate_time datetime comment '操作时间',
    cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';

 创建实体类

package com.itheima.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
    private Integer id; //ID
    private String className; //操作类名
    private String methodName; //操作方法名
    private String methodDesc; //方法用途
    private String methodParams; //操作方法参数
    private String returnValue; //操作方法返回值
    private Integer operateUser; //操作人ID
    private LocalDateTime operateTime; //操作时间
    private Long costTime; //操作耗时
}

1.3 创建日志的持久层Mapper 

package com.itheima.mapper;

import com.itheima.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OperateLogMapper {

    //插入日志数据
    @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name,method_desc, method_params, return_value, cost_time) " +
            "values (#{operateUser}, #{operateTime}, #{className}, #{methodName},#{methodDesc}, #{methodParams}, #{returnValue}, #{costTime});")
    public void insert(OperateLog log);
}

因为不是对Controller的所有方法进行增强,因此没有规律,所以需要注解的方法进行匹配切点。 

1.4 创建自定义注解

package com.csy.anno;

import java.lang.annotation.*;

/**
 * @author windStop
 * @version 1.0
 * @description 记录日志所有Controller的增删改的日志
 * @date 2024年08月08日19:42:23
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogMyAnno {
    String value() default "";
}

2.核心逻辑

2.1 对要增强的方法添加自定义注解

package com.csy.controller;

import com.csy.anno.LogMyAnno;
import com.csy.entity.Dept;
import com.csy.service.DeptService;
import com.csy.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

/**
 * @author windStop
 * @version 1.0
 * @description 关于部门管理的表现层
 * @date 2024年08月06日16:35:50
 */
@RestController
@RequestMapping("/depts")
public class DeptController {
    @Autowired
    private DeptService deptService;

    /**
     * 查询所有部门信息
     * 通过调用deptMapper的selectList方法,查询所有部门的信息
     * 该方法不需要任何查询条件,返回所有部门的列表
     *
     * @return 包含所有部门信息的List集合
     */
    @GetMapping
    public Result<List<Dept>> findAll() {
        return Result.success(deptService.findAll());
    }


    /**
     * 使用POST请求方式插入部门信息
     *
     * @param map 部门名称是json参数,string类型会把大括号和键值对一起转换,因此需要map
     * @return 包含操作结果的对象,成功时返回成功信息
     */
    @LogMyAnno("添加部门")
    @PostMapping
    public Result<Object> insertDept(@RequestBody Map<String,String> map) {
        deptService.insertDept(map.get("name"));
        return Result.success();
    }

    /**
     * 删除部门
     调用服务层的deleteDept方法来删除对应的部门
     * 不返回任何数据,使用Result对象表示操作结果
     * @param ids 部门的ID
     * @return Result<Object>对象,表示操作结果
     */
    @LogMyAnno("删除部门")
    @DeleteMapping("/{ids}")
    public Result<Object> deleteDept(@PathVariable List<Integer> ids) {
        deptService.deleteDept(ids);
        return Result.success();
    }

    /**
     * 更新部门信息
     * 通过PutMapping注解指定处理更新部门信息的HTTP Put请求
     * 该方法接收一个Dept对象,更新数据库中的部门信息,并返回操作结果
     *
     * @param dept 待更新的部门对象,包含部门的所有信息
     * @return Result<Object>类型的结果对象,包含操作是否成功的状态信息
     *         在本方法中,始终返回成功状态,不包含额外的数据
     * @see DeptService updateDept(Dept) 实际执行更新操作的服务方法
     */
    @LogMyAnno("更新员工")
    @PutMapping
    public Result<Object> updateDept(@RequestBody Dept dept) {
        deptService.updateDept(dept);
        return Result.success();
    }

    /**
     * 通过ID查找部门信息
     *
     * 此方法通过接收一个部门ID,调用部门服务(DeptService)的findById方法来查找特定的部门信息
     * 它使用了@GetMapping注解来处理HTTP GET请求,路径中的{id}是动态接收的参数,对应于数据库中的部门ID
     *
     * @param id 部门的唯一标识符,用于定位特定的部门
     * @return 返回一个Result对象,其中包含找到的部门信息如果找到,则Result的成功标志为true,数据为找到的部门;如果未找到,则成功标志为false,数据为空
     */
    @GetMapping("/{id}")
    public Result<Dept> findById(@PathVariable Integer id) {
        return Result.success(deptService.findById(id));
    }
}

2.2 一些常用的获取请求信息的方法

        // 获取Request对象
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) attributes;
        HttpServletRequest request = sra.getRequest();

        // 记录访问的URL
        String url = request.getRequestURI();
        logger.info("URL Accessed: {}", url);

        // 记录请求方式
        String method = request.getMethod();
        logger.info("Request Method: {}", method);

        // 获取客户端IP
        String clientIP = getIpAddr(request);
        logger.info("Client IP Address: {}", clientIP);

2.3 编写切面类进行增强 

package com.csy.log;

import com.csy.anno.LogMyAnno;
import com.csy.dao.OperateLogMapper;
import com.csy.entity.OperateLog;
import com.csy.utils.ThreadLocalUtil;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;

/**
 * @author windStop
 * @version 1.0
 * @description 记录增删改查的日志
 * @date 2024年08月08日19:49:54
 */
@Component
@Aspect //切面类,会先扫描该注解在创建bean,会将切入点匹配的方法所对应的类创建代理对象。
                    // 原始对象不加入ioc管理,该类的代理对象完全替代它
public class Logger {
    @Autowired
    private OperateLogMapper operateLogMapper;


    //1.定义切入点表达式
    @Pointcut("@annotation(com.csy.anno.LogMyAnno)")
    public void pt(){};

    //2.增强方法
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws RuntimeException {
        OperateLog log = new OperateLog();
        log.setOperateTime(LocalDateTime.now());//操作时间
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
//        log.setMethodParams(Arrays.toString(signature.getParameterNames()));操作变量名写错了
        log.setMethodParams(Arrays.toString(pjp.getArgs()));//操作方法参数
        log.setMethodName(method.getName());//操作方法名
        log.setMethodDesc(method.getAnnotation(LogMyAnno.class).value());//操作用途
        log.setClassName(pjp.getTarget().getClass().getName());//操作类名
        Map<String, Object> tokens = ThreadLocalUtil.get();
        log.setOperateUser((Integer) tokens.get("id"));//操作人ID
        long start = System.currentTimeMillis();
        Object proceed;
        //执行目标方法
        try {
            proceed = pjp.proceed();
            log.setReturnValue(proceed.toString());//操作返回值
            return proceed;
        }catch (Throwable t){
            throw new RuntimeException(t);
        }finally {
            long end = System.currentTimeMillis();
            log.setCostTime(end - start);//操作耗时(毫秒值)
            //保存日志
            operateLogMapper.insert(log);
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风止￴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值