当前位置: 首页> 财经> 访谈 > Spring AOP

Spring AOP

时间:2025/7/10 7:43:58来源:https://blog.csdn.net/weixin_64308540/article/details/141001484 浏览次数:0次

在写这篇文章之前,小编开始背八股文了!!恰逢遇到了Spring AOP的相关内容,仔细回顾下来,发现,对应Spring AOP仅仅局限于: AOP(面向切面编程)就是将那些与业务无关,但是在项目中发挥着不可缺少的作用的代码封装起来,比如:事务,日志,权限等相关的代码,Spring AOP是基于动态代理的方式实现的,如果实现了某个接口,则会基于动态代理的方式实现,如果没有实现接口,则是基于CGLIB代理的方式来实现一个需要代理对象的子类作为代理对象!其实上面这些内容没什么错误!

这是八股文的正常范畴,但是,今日,小编不想再按照八股文来将一大批文字意思了!直接上代码,上案列,来带领大家走进Spring AOP(面向切面编程)!

场景:

假设,我们需要统计某项目中的某些方法的耗时较长的方法,此时就需要我们来统计每个业务方法的执行耗时!直接思路:在每个方法开始执行之前记录当前时间,然后在方法执行结束后在记录当前时间,然后在一相减就是最后的执行耗时!当然,这个想法是没有什么问题的,问题出现在,当这个项目中方法比较少时,所需要修改的代码比较少,但是,当这个项目中的方法比较多,成千上百的时候,所需要更改的代码就显得很繁琐了!这样就得不偿失了!

所以Spring AOP应运而生

Spring AOP就可以将公共的记录方法开始时间,结束时间的相关代码来进行抽取出来,封装成一个注解类,通过添加注解的方式来实现注入,那么,这样不就显得更加简单了吗?仅仅一个注解就解决了一大串代码的更改!!YYDS

那么,我们所谓的面向切面编程最简单的描述就是:面向一个方法或者多个方法进行编程!(不全面哈!)

实现:动态代理是面向切面编程最主流的实现,而Spring AOP是Spring框架的高级技术,旨在管理Bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程/

导入依赖:在pom.xml文件中导入AOP依赖:

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

编写AOP程序:针对特定方法根据业务需要进行编程!

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;@Slf4j
@Component //交给Spring容器进行管理,成为Bean
@Aspect  //有这个注解,说明当前类不是普通类,而是AOP类
public class TimeAspect {@Around("execution(* com.example.controller.*.*(..))")  //拦截所有controller包下的所有方法//当我们需要获取其他包/类下的方法时候,只需要在execution()中添加包/类名即可【支持|| 】public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {long start = System.currentTimeMillis(); //记录开始时间Object obj = joinPoint.proceed();//调用原始方法运行long end = System.currentTimeMillis(); //记录结束时间log.info(joinPoint.getSignature().getName() + "方法运行时间:" + (end - start) + "ms");return obj; //返回原始方法的返回值}}

重新启动程序,运行项目:然后在控制台中就会出现:【当然具体内容还得看您那边调用了哪个类/方法】

然后,大家就会发现,我们并没有修改任何有关项目中的代码,但是却实现了统计各个方法的运行时间!!YYDS啊!!这不就是我们想要的效果吗??

上面统计各个方法的运行时间,仅仅是Spring AOP的小试牛刀,其实Spring AOP不止这一个功能:Spring AOP也能进行记录操作日志,权限控制,事务管理………

Spring AOP核心概念:

  • 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
  • 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
  • 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
  • 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
  • 目标对象:Target,通知所应用的对象

Spring AOP的通知类型:

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning :返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing :异常后通知,此注解标注的通知方法发生异常后执行

注意事项:

  1. @Around环绕通知需要自己调用 Proceeding]oinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
  2. @Around环绕通知方法的返回值,必须指定为0bject,来接收原始方法的返回值。

参考写法1:

参考写法2:

根据参考写法1,我们可以看出,上述的切入点表达式重复了,当我们需要对切入点表达式进行改动,是否需要将上述的切入点表达式一个一个的来进行改动呢??这部就又变得繁琐了吗??那么,本着应简尽简的原则,那么,我们也需要将公共/重复的切入点表达式提取出来!

 值得注意的是:上面的切入点表达式是用private来修饰的,关于private的相关属性相比大家也都了解了!只能在当前类当中使用,那么,假如我想在其他类中引用当前切入点表达式就需要将其更改为public即可!

那么,当我们引入其他类的切入点表达式的时候,需要注意:该切入点表达式对应的包名+类名+方法名!!

@pointCut注解:

该注解的作用就是将公共的切点表达式抽取出来,需要用到时引用该切入点表达式即可!

通知顺序:

然而,当我们定义了多个切面类,而且每个切面类对应的目标方法都是同一个的时候,该如何处理呢??

显而易见的是:当有多个前面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行!

比如:

上述定义了三个切面类,而且每个切面类的切入点表达式通知方法都是一样的,只不过输出的日志标识不一样罢了!

最终程序的执行顺序为:

这样来看是:

  • 类名越靠前before越先执行,类名越靠后before越后执行!
  • 类名越靠前after越后执行,类名越靠后after越先执行!

前提:在没定义其他要求的情况下:跟类名有关!!

这种方式非常繁琐,而且不便管理!!

所以,Spring给我们提供了第二种方式,我们可以直接在切面类上添加@Order(数字)注解即可!!

用@Order(数字)注解加在切面类上来控制顺序:数字就是来控制执行顺序的

  • 目标方法前的通知方法:数字小的先执行!
  • 目标方法后的通知方法:数字小的后执行!

切入点表达式:

切入点表达式--execution(常见)

 

常见的切入点表达式的写法:

专门执行两个方法:可以使用“ || ”来连接(或者)

书写建议:

  • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update开头
  • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
  • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:忽名匹配尽量不使用…,使用*匹配单个包。
切入点表达式--@annotation

@annotation切入点表达式,用于匹配标识有特定注解的方法!

这就涉及到自定义注解的相关知识了!

这个注解起啥名,无所谓,自己知道就行!

自定义注解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)         // 注解作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时保留
public @interface MyLog {
}

该注解当中并没有指定属性,仅仅起到标识作用!!

然后在方法上加上刚刚自定义的注解即可!

注意!!

如果你还想在匹配一个方法呢??那么,此时只需要在想要匹配的方法上,加入该注解即可!!

一切改完后,重启程序!!即可!

连接点:

在Spring中用]oinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等,

  • 对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
  • 对于其他四种通知,获取连接点信息只能使用 JoinPoint,它是 ProceedingJoinPoint 的父类型

Spring AOP案列:记录操作日志:

后端相关代码:

引入依赖:

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

切面注解

import java.lang.annotation.*;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoLog {String value() default "";
}

AOP实现操作日志的记录:
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.example.entity.Admin;
import com.example.entity.Log;
import com.example.service.LogService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;/*** 处理切面的“监控”*/
@Component
@Aspect
public class LogAspect {@Resourceprivate LogService logService;@Around("@annotation(autoLog)")public Object doAround(ProceedingJoinPoint joinPoint, AutoLog autoLog) throws Throwable {// 操作内容,我们在注解里已经定义了value(),然后再需要切入的接口上面去写上对应的操作内容即可String name = autoLog.value();// 操作时间(当前时间)String time = DateUtil.now();// 操作人String username = "";Admin user = JwtTokenUtils.getCurrentUser();if (ObjectUtil.isNotNull(user)) {username = user.getName();}// 操作人IPHttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String ip = request.getRemoteAddr();// 执行具体的接口Result result = (Result) joinPoint.proceed();Object data = result.getData();if (data instanceof Admin) {Admin admin = (Admin) data;username = admin.getName();}// 再去往日志表里写一条日志记录Log log = new Log(null, name, time, username, ip);logService.add(log);// 你可以走了,去返回前台报到吧~return result;}
}

关于操作日志整个相关代码:仅供参考:

数据库表:
CREATE TABLE `log` (`id` int(10) NOT NULL AUTO_INCREMENT COMMENT '主键ID',`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作内容',`time` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作时间',`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作人',`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作人IP',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
Entity
import javax.persistence.*;@Table(name = "log")
public class Log {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Integer id;@Column(name = "name")private String name;@Column(name = "time")private String time;@Column(name = "username")private String username;@Column(name = "ip")private String ip;
}
Controller
import com.example.common.Result;
import com.example.entity.Log;
import com.example.entity.Params;
import com.example.service.LogService;
import com.github.pagehelper.PageInfo;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;@CrossOrigin
@RestController
@RequestMapping("/log")
public class LogController {@Resourceprivate LogService logService;@PostMappingpublic Result save(@RequestBody Log log) {logService.add(log);return Result.success();}@GetMapping("/search")public Result findBySearch(Params params) {PageInfo<Log> info = logService.findBySearch(params);return Result.success(info);}@DeleteMapping("/{id}")public Result delete(@PathVariable Integer id) {logService.delete(id);return Result.success();}}
Service
import com.example.dao.LogDao;
import com.example.entity.Log;
import com.example.entity.Params;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.List;@Service
public class LogService {@Resourceprivate LogDao logDao;public void add(Log type) {logDao.insertSelective(type);}public PageInfo<Log> findBySearch(Params params) {// 开启分页查询PageHelper.startPage(params.getPageNum(), params.getPageSize());// 接下来的查询会自动按照当前开启的分页设置来查询List<Log> list = logDao.findBySearch(params);return PageInfo.of(list);}public void delete(Integer id) {logDao.deleteByPrimaryKey(id);}}
Dao和Mapper
import com.example.entity.Log;
import com.example.entity.Params;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;import java.util.List;@Repository
public interface LogDao extends Mapper<Log> {List<Log> findBySearch(@Param("params") Params params);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.dao.LogDao"><select id="findBySearch" resultType="com.example.entity.Log">select * from log<where><if test="params != null and params.name != null and params.name != ''">and name like concat('%', #{ params.name }, '%')</if><if test="params != null and params.username != null and params.username != ''">and username like concat('%', #{ params.username }, '%')</if></where></select></mapper>
LogView.vue
<template><div><div style="margin-bottom: 15px"><el-input v-model="params.name" style="width: 200px" placeholder="请输入操作内容"></el-input><el-input v-model="params.username" style="width: 200px; margin-left: 5px" placeholder="请输入操作人"></el-input><el-button type="warning" style="margin-left: 10px" @click="findBySearch()">查询</el-button><el-button type="warning" style="margin-left: 10px" @click="reset()">清空</el-button></div><div><el-table :data="tableData" style="width: 100%"><el-table-column prop="name" label="操作内容"></el-table-column><el-table-column prop="time" label="操作时间"></el-table-column><el-table-column prop="username" label="操作人"></el-table-column><el-table-column prop="ip" label="ip"></el-table-column><el-table-column label="操作"><template slot-scope="scope"><el-popconfirm title="确定删除吗?" @confirm="del(scope.row.id)"><el-button slot="reference" type="danger" style="margin-left: 5px">删除</el-button></el-popconfirm></template></el-table-column></el-table></div><div style="margin-top: 10px"><el-pagination@size-change="handleSizeChange"@current-change="handleCurrentChange":current-page="params.pageNum":page-sizes="[5, 10, 15, 20]":page-size="params.pageSize"layout="total, sizes, prev, pager, next":total="total"></el-pagination></div></div>
</template><script>
import request from "@/utils/request";export default {data() {return {params: {name: '',username: '',pageNum: 1,pageSize: 5},tableData: [],total: 0,dialogFormVisible: false,form: {}}},// 页面加载的时候,做一些事情,在created里面created() {this.findBySearch();},// 定义一些页面上控件出发的事件调用的方法methods: {findBySearch() {request.get("/log/search", {params: this.params}).then(res => {if (res.code === '0') {this.tableData = res.data.list;this.total = res.data.total;} else {this.$message({message: res.msg,type: 'success'});}})},reset() {this.params = {pageNum: 1,pageSize: 5,name: '',username: '',}this.findBySearch();},handleSizeChange(pageSize) {this.params.pageSize = pageSize;this.findBySearch();},handleCurrentChange(pageNum) {this.params.pageNum = pageNum;this.findBySearch();},submit() {request.post("/log", this.form).then(res => {if (res.code === '0') {this.$message({message: '操作成功',type: 'success'});this.dialogFormVisible = false;this.findBySearch();} else {this.$message({message: res.msg,type: 'error'});}})},del(id) {request.delete("/log/" + id).then(res => {if (res.code === '0') {this.$message({message: '删除成功',type: 'success'});this.findBySearch();} else {this.$message({message: res.msg,type: 'success'});}})}}
}
</script>

警告!!!

上述记录操作日志的Controller,Service,Dao,Mapper,Vue等相关代码仅供参考!!不全!!

关键字:Spring AOP

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: