在写这篇文章之前,小编开始背八股文了!!恰逢遇到了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 {
@Resource
private 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();
}
// 操作人IP
HttpServletRequest 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 {
@Resource
private LogService logService;
@PostMapping
public 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 {
@Resource
private 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等相关代码仅供参考!!不全!!