在写这篇文章之前,小编开始背八股文了!!恰逢遇到了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 :异常后通知,此注解标注的通知方法发生异常后执行
注意事项:
- @Around环绕通知需要自己调用 Proceeding]oinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
- @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等相关代码仅供参考!!不全!!