前面介绍了这么多种通知类型,具体该选哪一种呢?
我们可以通过一些案例加深下对通知类型的学习。
34-案例:利用AOP环绕通知计算业务层接口执行效率
需求分析
这个需求也比较简单,前面我们在介绍AOP的时候已经演示过:
- 需求:任意业务层接口执行均可显示其执行效率(执行时长)
这个案例的目的是查看每个业务层执行的时间,这样就可以监控出哪个业务比较耗时,将其查找出来方便优化
具体实现的思路:
- 开始执行方法之前记录一个时间
- 执行方法
- 执行完方法之后记录一个时间
- 用后一个时间减去前一个时间的差值,就是我们需要的结果
所以要在方法执行的前后添加业务,经过分析我们将采用环绕通知。
说明:原始方法如果只执行一次,时间太快,两个时间差可能为0,所以我们要执行万次来计算时间差。
环境准备
- 创建一个Maven项目
- pom.xml添加Spring依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
- 创建数据库与表
create database spring_db character set utf8;
use spring_db;
create table tbl_account(
id int primary key auto_increment,
name varchar(35),
money double
);
INSERT INTO tbl_account(`name`,money) VALUES
('Tom',2800),
('Jerry',3000),
('Jhon',3100);
- 添加Account、AccountDao、AccountService、AccountServiceImpl类
public class Account {
private Integer id;
private String name;
private Double money;
public Account() {
}
public Account(Integer id, String name, Double money) {
this.id = id;
this.name = name;
this.money = money;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public Double getMoney() {
return money;
}
public void setId(Integer id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setMoney(Double money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}
public interface AccountDao {
@Insert("insert into tbl_account(`name`,money) values(#{name},#{money}) ")
void save(Account account);
@Delete("delete from tbl_account where id=#{id}")
void delete(Integer id);
@Update("update tbl_account set `name`=#{name},money=#{money}")
void update(Account account);
@Select("select * from tbl_account")
List<Account> findAll();
@Select("select * from tbl_account where id=#{id}")
Account findById(Integer id);
}
public interface AccountService {
void save(Account account);
void update(Account account);
void delete(Integer id);
List<Account> findAll();
Account findById(Integer id);
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public void save(Account account) {
accountDao.save(account);
}
@Override
public void update(Account account) {
accountDao.update(account);
}
@Override
public void delete(Integer id) {
accountDao.delete(id);
}
@Override
public List<Account> findAll() {
return accountDao.findAll();
}
@Override
public Account findById(Integer id) {
return accountDao.findById(id);
}
}
- resources下提供一个jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=password
- 创建相关配置类
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
public class MyBatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setTypeAliasesPackage("com.yolo.pojo");
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage("com.yolo.dao");
return mapperScannerConfigurer;
}
}
- 第一个方法sqlSessionFactoryBean接受一个DataSource类型的参数,创建了一个SqlSessionFactoryBean对象。这个对象设置了类型别名包为 “com.yolo.pojo”,并设置了数据源dataSource。
这个方法的目的是创建并配置 MyBatis的SqlSessionFactoryBean,它是 MyBatis 和 Spring 集成的关键组件之一,用于创建SqlSession,从而执行数据库操作。- 第二个方法mapperScannerConfigurer创建了一个MapperScannerConfigurer对象,并设置了基础包为 “com.yolo.dao”。这个对象用于扫描指定包下的 MyBatis Mapper 接口,并将它们注册到 Spring 容器中,使得这些 Mapper 接口可以被自动注入到其他组件中,方便进行数据库操作。
@Configuration
@ComponentScan("com.yolo")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MyBatisConfig.class})
public class SpringConfig {
}
- 编写Spring整合Junit的测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
@Autowired
private AccountService accountService;
@Test
public void testFindById() {
Account byId = accountService.findById(2);
System.out.println(byId);
}
@Test
public void testFindAll(){
List<Account> accountList = accountService.findAll();
System.out.println(accountList);
}
}
运行测试类,结果如下
功能开发
- 步骤一:开启SpringAOP的注解功能
在Spring的主配置文件SpringConfig类中添加注解
@EnableAspectJAutoProxy
- 步骤二:创建AOP的通知类
- 该类要被Spring管理,需要添加
@Component
- 要标识该类是一个AOP的切面类,需要添加
@Aspect
- 配置切入点表达式,需要添加一个方法,并添加
@Pointcut
- 该类要被Spring管理,需要添加
@Component
@Aspect
public class ProjectAdvice {
@Pointcut("execution(* com.yolo.service.*Service(..))")
public void servicePt() {
}
public void runSpeed() {
}
}
- 步骤三:添加环绕通知
在runSpeed()方法上添加@Around
@Component
@Aspect
public class ProjectAdvice {
@Pointcut("execution(* com.yolo.service.*Service.*(..))")
public void servicePt() {
}
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint point) {
}
}
- 步骤四:完成核心业务,记录万次执行的时间
@Component
@Aspect
public class ProjectAdvice {
//匹配业务层的所有方法
@Pointcut("execution(* com.yolo.service.*Service.*(..))")
public void servicePt() {
}
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint point) throws Throwable {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
point.proceed();
}
long end = System.currentTimeMillis();
System.out.println("业务层接口万次执行时间:" + (end - start) + "ms");
}
}
-
步骤五:运行单元测试类
运行结果如下
-
步骤六: 程序优化
目前还存在一个问题,当我们一次执行多个方法时,控制台输出的都是业务层接口万次执行时间: XXXms
我们无法得知具体哪个方法的耗时,那么该如何优化呢?
ProceedingJoinPoint中有一个getSignature()
方法来获取签名,然后调用getDeclaringTypeName
可以获取类名,getName()
可以获取方法名
@Component
@Aspect
public class ProjectAdvice {
@Pointcut("execution(* com.yolo.service.*Service.*(..))")
public void servicePt() {
}
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint point) throws Throwable {
//Signature指签名信息,可理解为封装了这次执行过程
Signature signature = point.getSignature();
//获取类名
String className = String.valueOf(signature.getDeclaringType());
//获取方法名
String methodName = signature.getName();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
point.proceed();
}
long end = System.currentTimeMillis();
System.out.println("业务层接口万次执行时间:" + className + "." +methodName + "耗时" + (end - start) + "ms");
}
}
再次运行程序,结果如下