文章目录
- 1. 什么是AOP
- 2. AOP的核心概念
- 3. AOP的入门案例
- 原始代码
- 思路分析
- 第一步:导入坐标
- 第二步:制作连接点(原始操作,Dao接口与实现类)
- 第三步:制作共性功能(通知类与通知)
- 第四步:定义切入点
- 第五步:绑定切入点与通知关系(切面)
- 第六步:让Spring“看到”这个切面
- 第七步:在Spring配置类中加上注解
- 运行主方法
- 4. AOP工作流程
- 动态代理
- 为什么需要代理?代理长什么样?Java通过什么来保证代理的样子?
- 动态代理3问(根据以上内容给出答案)
- 1. Java提供了什么API帮我们创建代理?
- 2. newProxyInstance方法在创建时,需要接几个参数?每个参数的含义是什么?
- 3. 通过invokehandler的invoke方法指定代理干的事时,这个invoke会被谁调用?需要接哪几个参数?
- 动态代理实例
- AOP工作流程
- 5. AOP切入点表达式
- 语法格式
- 通配符
- 书写技巧
- 6. AOP通知类型
- 前置通知和后置通知
- 环绕通知
- 返回后通知
- 抛出异常后通知
- 7. AOP案例:测量业务接口执行效率
- 案例代码
- 整体架构
- pom.xml
- config下的包:SpringConfig、JdbcConfig、MyBatisConfig
- 数据源
- 实体类
- 数据层
- 业务层
- 测试类
- 进行切面编程
- 8. AOP通知获取数据
- 获取参数
- 获取返回值
- 获取异常
- 9. AOP总结
1. 什么是AOP
Spring的两大理念:IOC(Inversion of Control
)和AOP
AOP是Aspect Oriented Programming
,面向切面编程,是一种编程范式,指导开发者如何组织程序结构
(PS:原本的OOP,Object Oriented Programming
,面向对象编程,指导我们如何根据对象属性来进行类的开发)
作用: 在不惊动原始设计的基础上为其进行功能增强
举个例子:
在一个类中有四个方法:
通过测试类分别调用这个四个方法,save
会打印出10000次book dao save
,若update
和delete
在代码里没有显示定义这些内容的情况下也分别打印了10000次book dao update
和book dao delete
,则说明在不惊动原始设计的基础上为其进行功能增强(不对原始方法做修改的情况下加上了打印10000次的功能)
这也符合Spring的理念:无入侵式/无侵入式编程
2. AOP的核心概念
首先将我们想要实现的共通的功能从代码中抽取出来,写成一个通知类
在通知类
中定义一个方法,称为通知
,就是把共同的功能抽出来写在其中
不是所有的方法都要执行这个通知,所以要把执行对应通知的方法找出来,定义成切入点
(匹配某些方法)
有了切入点
和通知
,我们将他们绑定起来形成切面
,切面
就是在哪些切入点
上执行哪些通知
3. AOP的入门案例
案例目标:在接口执行前输出当前系统的时间
原始代码
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>project3</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>project3</name>
<description>project3</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.25</version>
</dependency>
</dependencies>
</project>
代码结构:
SpringConfig.java
package com.example.project3.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("com.example.project3")
public class SpringConfig {
}
BookDao.java
package com.example.project3.dao;
import org.springframework.stereotype.Repository;
public interface BookDao {
void save();
void update();
}
BookDaoImpl.java
package com.example.project3.dao.impl;
import com.example.project3.dao.BookDao;
import org.springframework.stereotype.Repository;
@Repository
public class BookDaoImpl implements BookDao {
@Override
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save...");
}
@Override
public void update() {
System.out.println("book dao update...");
}
}
Project3Application.java(主方法)
package com.example.project3;
import com.example.project3.config.SpringConfig;
import com.example.project3.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Project3Application {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
此时执行主方法:
若将bookDao.update()
换成bookDao.save()
:
思路分析
- 导入坐标
- 制作
连接点
(原始操作,Dao接口与实现类) - 制作共性功能(
通知类
与通知
) - 定义
切入点
- 绑定
切入点
与通知
关系(切面
)
第一步:导入坐标
实际上,在导入spring-context
的时候,相关的aop包已经导入进来了:
但还需要在pom.xml中进一步导入:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
第二步:制作连接点(原始操作,Dao接口与实现类)
这个其实已经做好了,就是我们的Dao接口及实现类
第三步:制作共性功能(通知类与通知)
在项目下新建一个aop包,在下面新建一个MyAdvice.java,这个MyAdvice
就是通知类
,里面写抽取出来的共性方法method
就是通知
package com.example.project3.aop;
import org.aspectj.lang.annotation.Pointcut;
public class MyAdvice {
public void method(){
System.out.println(System.currentTimeMillis());
}
}
第四步:定义切入点
通过@Pointcut
定义切入点,里边的内容意思为:执行()内的方法时为切入点
切入点依托一个不具有实际意义的方法进行,即无参数、无返回值,方法体无实际逻辑
package com.example.project3.aop;
import org.aspectj.lang.annotation.Pointcut;
public class MyAdvice {
@Pointcut("execution(void com.example.project3.dao.BookDao.update())")
private void pt(){}
public void method(){
System.out.println(System.currentTimeMillis());
}
}
第五步:绑定切入点与通知关系(切面)
通过@Before
绑定关系,意为在pt()
开始之前执行该通知(方法)
package com.example.project3.aop;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
public class MyAdvice {
@Pointcut("execution(void com.example.project3.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
第六步:让Spring“看到”这个切面
在该类上加上两个注解,一个是@Component
,将其作为一个bean,并告诉Spring,将其作为切面处理,所以加上@Aspect
package com.example.project3.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.example.project3.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
第七步:在Spring配置类中加上注解
在配置类中加上注解@EnableAspectJAutoProxy
,此注解主要用来导入 Spring 切面功能类
package com.example.project3.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.example.project3")
@EnableAspectJAutoProxy
public class SpringConfig {
}
运行主方法
package com.example.project3;
import com.example.project3.config.SpringConfig;
import com.example.project3.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Project3Application {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
结果如下:
4. AOP工作流程
动态代理
视频:【黑马磊哥】Java动态代理深入剖析,真正搞懂Java核心设计模式:代理设计模式
动态代理(Spring、MyBatis、SpringMVC、Spring Boot)几乎是所有框架的核心原理之一
为什么需要代理?代理长什么样?Java通过什么来保证代理的样子?
举一个最普通的例子, 有一位明星,她能够唱歌和跳舞(具备sing()
和dance()
方法):
class 明星{
void 唱歌(){
准备话筒、收钱
开始唱歌
}
void 跳舞(){
准备场地、收钱
开始跳舞
}
}
但是作为一个大明星,她不会自己去做准备话筒、收钱, 准备场地、收钱
这件事,所以请了一个中介公司来进行代理(对象如果嫌身上干的事太多的话,可以通过代理来转移职责)
中介公司会派一位代理人,来管理准备场地、话筒, 准备场地、收钱
这些事。作为一个代理,它也同样拥有sing()
和dance()
方法,对象有什么方法想被代理,代理就一定要有对应的方法:
class 代理{
void 唱歌(){
准备话筒、收钱
找明星唱歌
}
void 跳舞(){
准备场地、收钱
找明星跳舞
}
}
同时这个 “中介” 是通过 “接口” 了解具体需要代理的方法的
明星:
package com.example.demo.agent;
public class star implements Istar{
String starName;
public star(String starName) {
this.starName = starName;
}
@Override
public String sing(String name) {
System.out.println(this.starName + "正在唱" + name);
return "谢谢!谢谢!谢谢!";
}
@Override
public void dance() {
System.out.println(this.starName + "正在跳舞");
}
}
接口:
package com.example.demo.agent;
public interface Istar {
String sing(String name);
void dance();
}
代理:
首先代理类返回的就是接口的一个对象,其中参数为Star类(代理为明星做代理,需要传入明星类,以获知明星能够做的事情)
通过Proxy.newProxyInstance()
获得一个新的代理实例,返回的是一个(Object)类型的对象,将其强转成IStar类型,在该方法中需要传入3个参数:
- 类加载器,这个一般默认使用当前代理类的类加载器:
ProxyUtil.class.getClassLoader()
- 了解明星能够实现哪一些方法,也就是要知道代理类长什么样子,这里是给了一个数组接口:
new Class[]{IStar.class}
InvocationHandler
类,定义这个匿名类的方法类写清楚代理对象需要做什么事。其中proxy
代表的是代理本身,把代理本身作为一个对象传给invoke方法,method
表示当前star调用了哪个方法,args
表示调用方法时传入的参数。
package com.example.demo.agent;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyUtil {
public static IStar createProxy(Star star){
/*
* newProxyInstance(ClassLoader loader,
* Class<?>[] interfaces,
* InvocationHandler h)
* 参数1:用于指定一个类加载器
* 参数2:指定生成的代理长什么样子,也就是有哪些方法
* 参数3:用来指定生成的代理对象要干什么事
* */
// 代理干什么事,由invoke决定
// Star star = new Star("xxx")
// IStar starProxy = ProxyUtil.createProxy(s)
// starProxy.sing("xxx"), starProxy.dance()
IStar iStar = (IStar) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(), new Class[]{IStar.class},
new InvocationHandler() {
@Override // 回调方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 代理对象要做的事,在这里写代码
if (method.getName().equals("sing")){
System.out.println("准备话筒,收钱");
} else if (method.getName().equals("dance")) {
System.out.println("准备场地,收钱");
}
return method.invoke(star, args);
}
});
return iStar;
}
}
测试:
package com.example.demo.agent;
public class Test {
public static void main(String[] args) {
Star star = new Star("大明星");
IStar starProxy = ProxyUtil.createProxy(star);
String rs = starProxy.sing("一首歌");
System.out.println(rs);
starProxy.dance();
}
}
结果:
方法调用顺序:
动态代理3问(根据以上内容给出答案)
1. Java提供了什么API帮我们创建代理?
创建代理的API:
Proxy.newProxyInstance(xxxxx)
使用该API并使用强转实现类型转换
2. newProxyInstance方法在创建时,需要接几个参数?每个参数的含义是什么?
3个参数
第一个参数:类加载器,默认使用当前代理类的类加载器:NowClass.class.geetClassLoader()
第二个参数:给出代理类的“模样”,即代理类应该参照的接口,这是一个数组,可以有多个模样:new Class[]{xxx.class}
第三个参数:通过InvocationHandler
类写清楚代理对象需要做的事
3. 通过invokehandler的invoke方法指定代理干的事时,这个invoke会被谁调用?需要接哪几个参数?
执行方法之前会被调用,有参数proxy
表示代理本身,method
表示即将调用的方法,args
表示调用方法时传入的参数
动态代理实例
代码如下:
结构目录如下:
其中,UserService.java
package com.example.project3.aoppractice;
// 用户业务接口
public interface UserService {
// 登录功能
void login(String loginName, String password) throws Exception;
// 删除用户
void deleteUsers() throws Exception;
// 查询用户,返回数组形式
String[] selectUsers() throws Exception;
}
UserServiceImpl.java
package com.example.project3.aoppractice;
public class UserServiceImpl implements UserService{
@Override
public void login(String loginName, String password) throws Exception {
long starTime = System.currentTimeMillis();
if("admin".equals(loginName) && "123456".equals(password)){
System.out.println("登陆成功,欢迎光临");
} else {
System.out.println("登录失败,用户名/密码错误");
}
Thread.sleep(1000);
long endTime = System.currentTimeMillis();
System.out.println("login方法执行耗时:" + (endTime - starTime)/ 1000.0 + "s");
}
@Override
public void deleteUsers() throws Exception {
long startTime = System.currentTimeMillis();
System.out.println("删除了1万个用户");
Thread.sleep(1000);
long endTime = System.currentTimeMillis();
System.out.println("deleteUsers方法耗时:" + (endTime - startTime)/1000.0 + "s");
}
@Override
public String[] selectUsers() throws Exception {
long startTime = System.currentTimeMillis();
System.out.println("查询了3个用户");
String[] names = {"用户1", "用户2", "用户3"};
Thread.sleep(1000);
long endTime = System.currentTimeMillis();
System.out.println("deleteUsers方法耗时:" + (endTime - startTime)/1000.0 + "s");
return names;
}
}
这样的程序中存在的问题是:在方法中存在了大量重复的求程序开始时间和程序结束时间的操作,为此,我们可以通过动态代理来简化这些方法
- 定义动态代理类
package com.example.project3.aoppractice;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyUtil{
public static UserService createProxy(UserServiceImpl impl){
UserService userService = (UserService) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(), new Class[]{UserService.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("login") || method.getName().equals("deleteUsers") || method.getName().equals("selectUsers")){
long startTime = System.currentTimeMillis();
Object rs = method.invoke(impl, args);
long endTime = System.currentTimeMillis();
System.out.println(method.getName() + "方法执行耗时:" + (endTime - startTime) / 1000.0 + "s");
return rs;
} else {
Object rs = method.invoke(impl, args);
return rs;
}
}
});
return userService;
}
}
这里就是将公共的获取时间的方法从中抽出来,然后再invoke中执行该方法,并返回方法的执行结果
- 测试
package com.example.project3.aoppractice;
public class Test {
public static void main(String[] args) throws Exception {
UserService userServiceProxy = ProxyUtil.createProxy(new UserServiceImpl());
userServiceProxy.login("admin", "12345");
System.out.println("--------------------------------");
userServiceProxy.deleteUsers();
System.out.println("--------------------------------");
String[] names = userServiceProxy.selectUsers();
System.out.println("查询到的用户是:" + Arrays.toString(names));
System.out.println("--------------------------------");
}
}
- 运行结果
AOP工作流程
- Spring容器启动
- 读取所有切面配置中的切入点,如下图,因为只绑定了通知和切入点
pt()
,所以这里只读取切入点pt()
而不读取ptx()
- 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
匹配失败,创建对象
匹配成功,创建原始对象(目标对象)的代理对象 - 获取bean执行方法
获取bean,调用方法并执行,完成操作
获取的是bean的代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
这里第3、第4点可能讲的有点抽象。我理解这几句话的意思如下:
“判定bean对应的类中的方法是否匹配到任意切入点”,这句话的意思判断我们测试时候new的对象:BookDao bookDao = ctx.getBean(BookDao.class);
中的方法:
是否与我们定义的切入点(@Pointcut
中指定的具体的方法)匹配上了。
在这里update()
方法的实现如下:
现在我们BookDao定义了方法update()
、实现类中实现了方法update()
。
然后在@Pointcut
中又指定了:execution(void com.example.project3.dao.BookDao.update())
,也就是指定到了BookDao中的update()
方法
这时候说明已经形成了切面
,即通知和切入点已经绑定上了,这时候初始化的对象就是代理对象
而非原始对象
本身,则通过对象名.方法
调用就会走代理对象中的内容,先打印当前的时间,再执行对象方法中的内容
1702475078485
book dao update...
如果我们将@Pointcut
中的内容改成execution(void com.example.project3.dao.BookDao.update1())
,则此时没有办法匹配上,因为在BookDao中没有方法update1()
,那么这时候就会初始化原始对象,通过对象名.方法
调用不会走代理对象中的内容,只会执行对象本身的内容:
book dao update...
在初始化一个对象时候,我们可以通过对象名.getClass()
方法来判断具体产生的是哪个对象,代理对象打印的结果如下:
5. AOP切入点表达式
切入点:要进行增强的方法
切入点表达式:要进行增强的方法的描述方式
描述方式一:execution(void com.itheima.dao.BookDao.update())
描述方式二:execution(void com.itheima.dao.impl.BookDaoImpl.update())
描述接口类及其实现皆可
语法格式
通配符
可以使用通配符描述切入点,快速描述
注意:假如我们要求参数为(*)
,则方法中必须有1个或多个参数,若方法没有参数则无法匹配到
书写技巧
6. AOP通知类型
代码如下,针对五种类型集中/分别进行说明:
BookDao.java
package com.example.project3.dao;
import org.springframework.stereotype.Repository;
public interface BookDao {
void update();
int select();
}
BookDaoImpl.java
package com.example.project3.dao.impl;
import com.example.project3.dao.BookDao;
import org.springframework.stereotype.Repository;
@Repository
public class BookDaoImpl implements BookDao {
@Override
public void update() {
System.out.println("book dao update is running ...");
}
@Override
public int select() {
System.out.println("book dao select is running ...");
return 100;
}
}
MyAdvice.java
package com.example.project3.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.example.project3.dao.BookDao.update())")
private void pt(){}
public void before(){
System.out.println("Before advice");
}
public void after(){
System.out.println("After advice");
}
public void around(){
System.out.println("around before advice ... ");
System.out.println("around after advice ... ");
}
public void afterReturning(){
System.out.println("afterReturning advice ... ");
}
public void afterThrowing(){
System.out.println("afterThrowing advice ... ");
}
}
测试方法:
package com.example.project3;
import com.example.project3.config.SpringConfig;
import com.example.project3.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Project3Application {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
前置通知和后置通知
两种最简单的通知方式:@Before
和@After
:
@Before("pt()")
public void before(){
System.out.println("Before advice");
}
@After("pt()")
public void after(){
System.out.println("After advice");
}
结果为:
环绕通知
环绕通知注解:
@Around("pt()")
public void around(){
System.out.println("around before advice ... ");
System.out.println("around after advice ... ");
}
结果为:
这样会发现对象的方法没有被调用,问题在于当我们使用环绕通知的时候,要在某一个地方进行方法的调用,格式如下:
@Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("around before advice ... ");
// 表示对原始操作的调用
pjp.proceed();
System.out.println("around after advice ... ");
}
在这里强制地抛出了一个异常,为什么要抛出异常呢?因为它不知道pjp调用的过程中是否有异常抛出,所以它这里就提前抛出,如果有异常要抛出不由此处处理而由pjp本身去处理
运行结果如下:
如果我们加上一个切入点:
@Pointcut("execution(int com.example.project3.dao.BookDao.select())")
private void pt2(){}
写一个新方法并加上环绕通知:
@Around("pt2()")
public void aroundSelect(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("around before advice ... ");
// 表示对原始操作的调用
pjp.proceed();
System.out.println("around after advice ... ");
}
在测试方法中调用bookDao.select()
,此时运行结果为:
这是因为select()
方法本身是有返回值的,而我们没有把这个返回值给丢出去。
正确的写法如下:
@Around("pt2()")
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("around before advice ... ");
// 表示对原始操作的调用
Object result = pjp.proceed();
System.out.println("around after advice ... ");
return result;
}
在这其中将类的返回值改成了Object
,这才是标准的写法。如果pjp.proceed()
方法本身没有返回值,则返回结果为null
。
运行结果为:
通过这个@Around
可以对原始方法做隔离,比如只有经理/会员才能执行该方法,就可以通过@Around
在这其中进行处理
返回后通知
这是在方法正常执行完毕后返回的通知
假如目前在这两个方法上加了注解:
@After("pt2()")
public void after(){
System.out.println("After advice");
}
@AfterReturning("pt2()")
public void afterReturning(){
System.out.println("afterReturning advice ... ");
}
测试类改为:
package com.example.project3;
import com.example.project3.config.SpringConfig;
import com.example.project3.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Project3Application {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
int result = bookDao.select();
System.out.println(result);
}
}
运行测试类结果为:
如果在select()
方法中加上:
则此时运行会在int a = 1/0
处报错,此时返回结果为:
可以看出来只执行了After advice
,而没有执行afterReturning advice
这说明 afterReturning advice
是在返回值返回之后才执行的,可以将其视为“最后的通知”
抛出异常后通知
这是在方法抛出异常后返回的通知
保持上面的select()
方法不变,测试类不变,只保持以下方法的注解:
@AfterThrowing("pt2()")
public void afterThrowing(){
System.out.println("afterThrowing advice ... ");
}
此时运行结果为:
可以发现有异常抛出时候回执行异常抛出的通知类里的代码
若我们将异常注释掉,再次运行结果为:
此时不会执行AfterThrowing
内的通知
7. AOP案例:测量业务接口执行效率
需求:任意业务层接口执行均可显示其执行效率(执行时长)
分析:
- 业务功能:业务层执行接口执行前后分别记录时间,求差值得到执行效率
- 通知类型:根据业务功能选择环绕通知
PS: 在完成这部分内容的时候,我是重新建立了项目并写了代码,遇到了之前可能没有注意的一些坑,会使用红字进行标注。
案例代码
整体架构
pom.xml
pom.xml,其中导入的包包含spring-context,以及一些mybatis,JUnit,和AOP相关的aspectj包。
注意,这里Spring版本和MyBatis版本的匹配尤为重要!如果出现版本没办法匹配上,在下面写代码时候会报一些莫名其妙的错误,比如我就是报了:Invalid value type for attribute ‘factoryBeanObjectType’: java.lang.String
,一开始觉得莫名其妙,后来发现我是<parent>
处的spring版本不对,修改以后就可以运行了
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>project4</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>project4</name>
<description>project4</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.3</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.13</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.25</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
</dependencies>
</project>
config下的包:SpringConfig、JdbcConfig、MyBatisConfig
SpringConfig.java,这其中包含的一些配置之前都有介绍过,@Configuration
指明这是一个配置类,@ComponentScan
指示扫描的范围,@EnableAspectJAutoProxy
允许切面编程!我后面就是由于少了这个注解,导致AOP失效,@PropertySource
引入外部数据源,@Import
表示导入的其他配置类
package com.example.project4.config;
import org.springframework.context.annotation.*;
@Configuration
@ComponentScan("com.example.project4")
@EnableAspectJAutoProxy
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MyBatisConfig.class})
public class SpringConfig {
}
JdbcConfig.java,配置数据源
package com.example.project4.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
public class JdbcConfig {
@Value("${jdbc.driver}")
String driverClassName;
@Value("${jdbc.url}")
String url;
@Value("${jdbc.password}")
String password;
@Value("${jdbc.username}")
String username;
@Bean
public DataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setPassword(password);
dataSource.setUsername(username);
return dataSource;
}
}
MyBatis.java,配置工厂方法,及配置MyBatis的扫描范围:
package com.example.project4.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
public class MyBatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("com.example.project4.domain");
ssfb.setDataSource(dataSource);
return ssfb;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.example.project4.dao");
return msc;
}
}
数据源
jdbc.properties指定数据库的配置
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=123456
实体类
Account.java,指定实体的属性
package com.example.project4.domain;
import org.springframework.context.annotation.Bean;
public class Account {
int id;
String username;
double money;
@Override
public String toString() {
return "Account{" +
"id=" + id +
", username='" + username + '\'' +
", money=" + money +
'}';
}
public Account() {
}
public Account(int id, String username, double money) {
this.id = id;
this.username = username;
this.money = money;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
数据层
AccountDao.java,使用MyBatis注解进行开发
package com.example.project4.dao;
import com.example.project4.domain.Account;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface AccountDao {
@Insert("INSERT INTO account(username, money) VALUES (#{username}, #{money})")
void save(Account account);
@Delete("DELETE FROM account WHERE id=#{id}")
void delete(Integer id);
@Update("UPDATE account SET username=#{username}, money=#{money} where id=#{id}")
void update(Account account);
@Select("SELECT * FROM account")
List<Account> findAll();
@Select("SELECT * FROM account where id=#{id}")
Account findById(Integer id);
}
业务层
AccountService.java,业务层接口
package com.example.project4.service;
import com.example.project4.domain.Account;
import java.util.List;
public interface AccountService {
void save(Account account);
void delete(Integer id);
void update(Account account);
List<Account> findAll();
Account findById(Integer id);
}
AccountServiceImpl.java,业务层实现类,在里面使用自动注解将AccountDao注入进来,并调用具体的方法。
package com.example.project4.service.impl;
import com.example.project4.dao.AccountDao;
import com.example.project4.service.AccountService;
import com.example.project4.domain.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public void save(Account account) {
accountDao.save(account);
}
@Override
public void delete(Integer id) {
accountDao.delete(id);
}
@Override
public void update(Account account) {
accountDao.update(account);
}
@Override
public List<Account> findAll() {
return accountDao.findAll();
}
@Override
public Account findById(Integer id) {
return accountDao.findById(id);
}
}
测试类
Project4ApplicationTest.java
package com.example.project4;
import com.example.project4.config.SpringConfig;
import com.example.project4.domain.Account;
import com.example.project4.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class Project4ApplicationTests {
@Autowired
private AccountService accountService;
@Test
public void testFindById(){
Account ac = accountService.findById(2);
System.out.println(ac);
}
@Test
public void testFindAll(){
System.out.println(accountService.getClass());
List<Account> accountList = accountService.findAll();
System.out.println(accountList);
}
}
进行切面编程
结合4和6进行开发即可。由于这里我们根据需求使用环绕通知,所以注解会使用@Around
在项目下建立一个包aop,在下边建立一个ProjectAdvice.java
第一步:使用@Pointcut
定义切入点,运用了通配符进行描述,这里指的是任意返回类型 com.example.project4.service下所有以Service结尾的类下的任意方法
@Pointcut("execution(* com.example.project4.service.*Service.*(..))")
private void servicePt(){}
第二步:定义具体的“通知”,并在通知上加上注解,说明在哪些切入点执行该方法中的内容。方法中的参数ProceedingJointPoint
用来进行方法的调用:pjp.proceed()
,同时通过pjp.getSignature()
可以获得signature
参数,并获得执行方法所在的类名getDeclaringTypeName
和执行方法的方法名getName
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable{
Signature signature = pjp.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++){
pjp.proceed();
}
long endTime = System.currentTimeMillis();
System.out.println("万次执行:" + className + "." + methodName + "--->" + (endTime-startTime) + "ms");
}
使用JUnit得到结果为(由于这里没有返回参数,所以返回值为null):
8. AOP通知获取数据
此处案例使用的是AOP工作流程中的案例,只有以下代码稍有差别:
BookDao.java
package com.example.project4.dao;
public interface BookDao {
public String findName(int id);
}
BookDaoImpl.java
package com.example.project4.dao.impl;
import com.example.project4.dao.BookDao;
import org.springframework.stereotype.Repository;
@Repository
public class BookDaoImpl implements BookDao {
@Override
public String findName(int id) {
System.out.println("id:" + id);
return "itcast";
}
}
MyAdvice.java(这个是切面编程的类)
package com.example.project4.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.example.project4.dao.BookDao.findName(..))")
private void pt(){}
@Before("pt()")
public void before(){
System.out.println("before advice ...");
}
@After("pt()")
public void after(){
System.out.println("after advice ...");
}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object ret = pjp.proceed();
return ret;
}
@AfterReturning("pt()")
public void afterReturning(){
System.out.println("afterReturning advice ...");
}
@AfterThrowing("pt()")
public void afterThrowing(){
System.out.println("afterThrowing advice ...");
}
}
测试类
package com.example.project4;
import com.example.project4.config.SpringConfig;
import com.example.project4.dao.BookDao;
import com.example.project4.dao.impl.BookDaoImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Project4Application {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = (BookDao) ctx.getBean(BookDao.class);
String result = bookDao.findName(100);
System.out.println(result);
}
}
AOP相关的类:
package com.example.project4.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.example.project4.dao.BookDao.findName(..))")
private void pt(){}
@Before("pt()")
public void before(){
System.out.println("before advice ...");
}
// @After("pt()")
public void after(){
System.out.println("after advice ...");
}
// @Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object ret = pjp.proceed();
return ret;
}
// @AfterReturning("pt()")
public void afterReturning(){
System.out.println("afterReturning advice ...");
}
// @AfterThrowing("pt()")
public void afterThrowing(){
System.out.println("afterThrowing advice ...");
}
}
获取参数
在@Before,@After,@AfterReturning,@AfterThrowing
中都可以加入这个参数JoinPoint jp
:
@Before("pt()")
public void before(JoinPoint jp){
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before advice ...");
}
通过这个jp可以获取参数,返回值为Object类型的数组。执行一下测试方法:
对于参数ProceedingJoinPoint pjp
,ProceedingJoinPoint
作为JoinPoint
的子类,同样使用pjp.getArgs()
可以获得具体的参数。
有意思的是,我们可以修改参数,并在调用方法时传入我们修改好的参数(注意,这里方法不传入参数也是可以执行的)
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666;
Object ret = pjp.proceed(args);
return ret;
}
结果会变成:
获取返回值
想要获取方法的返回值,方法如下:
@AfterReturning(value = "pt()", returning = "ret")
public void afterReturning(Object ret){
System.out.println("afterReturning advice ..." + ret);
}
也就是在注解@AfterReturning
中,除了定义value
以外,还定义returning
,里面的参数写的事括号里的参数,也就是告诉这个注解,将返回值放到参数ret
中
执行测试方法,结果:
注意:如果JointPoint和Object同时作为参数时,必须将JointPoint写在第一个!
获取异常
和获取返回值类似,获取异常的方法:
@AfterThrowing(value = "pt()", throwing = "t")
public void afterThrowing(Throwable t){
System.out.println("afterThrowing advice ..." + t);
}
修改一下方法并测试:
9. AOP总结
概念: AOP(Aspect Oriented Programming)面向切面编程,是一种编程范式
作用:在不惊动原始设计的基础上为方法功能进行增强
核心概念:
切入点表达式:
通知类型: