Spring注解驱动开发之AOP
1 设计模式--代理模式
1.1 概述
- 代理模式:给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下,客户不想或者不能够直接引用一个对象,代理对象可以在客户和目标对象中间起到中介的作用。客户端分辨不出代理主题对象和真实主题对象的区别。代理模式可以并不知道被代理对象,而仅仅持有一个被代理对象的接口,这时代理对象不能够创建被代理对象,被代理对象必须有系统的其他角色代为创建并传入。
- 为什么需要代理模式?
- 1️⃣它有间接的特点,可以起到中介隔离作用。就好比在租房的时候,房东可能不在本地,而短期内又不能赶回来,此时中介的出场,就作为房东的代理实现和我们签订承租合同。而我们和房东之间就没有耦合了。
- 2️⃣它有增强的功能。还以租房为例,我们首先考虑的是找一个靠谱的中介,由中介给我们提供房源信息,并且告诉我们房屋的细节、合同的细节等等。当然我们也可以自己去找一些个人出租的房屋,但是在这之中,我们要了解租赁合同,房屋验收、租金监管等情况,这无疑对我们是繁重的工作。而有了中介作为我们的代理中间人,他把了解到的信息告诉我们,我们一样可以租到房子,而不必做哪些繁重的工作。
1.2 应用示例
1.2.1 准备工作
- sql脚本:
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`money` double NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `account` VALUES (1, '张三', 1000);
INSERT INTO `account` VALUES (2, '李四', 1000);
- 导入相关jar包的Maven坐标:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<!-- 导入YAML解析工厂坐标 -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.26</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
- 日志文件log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--Configuration后面的status用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,可以看到log4j2内部各种详细输出-->
<configuration status="INFO">
<!--先定义所有的appender-->
<appenders>
<!--输出日志信息到控制台-->
<console name="Console" target="SYSTEM_OUT">
<!--控制日志输出的格式-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</console>
</appenders>
<!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
<!--root:用于指定项目的根日志,如果没有单独指定Logger,则会使用root作为默认的日志输出-->
<loggers>
<root level="info">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>
- jdbc.yml
# Yet Another Markup Language 另一种标记语言
jdbc:
url: jdbc:mysql://192.168.2.112:3306/spring5?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
driver: com.mysql.cj.jdbc.Driver
user: root
password: 123456
- 自定义YAML的PropertySourceFactory
package com.sunxiaping.spring5.factory;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
import java.io.IOException;
import java.util.Properties;
/**
* 自定义YAML的PropertySourceFactory
*/
public class YAMLPropertySourceFactory implements PropertySourceFactory {
/**
* 返回PropertySource对象
*
* @param name
* @param resource
* @return
* @throws IOException
*/
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
//创建YAML文件解析工厂
YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean();
//设置要解析的资源内容
factoryBean.setResources(resource.getResource());
//把资源解析为properties文件
Properties properties = factoryBean.getObject();
return (name != null ? new PropertiesPropertySource(name, properties) : new PropertiesPropertySource(resource.getResource().getFilename(), properties));
}
}
- Account.java
package com.sunxiaping.spring5.domain;
import java.io.Serializable;
public class Account implements Serializable {
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 void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}
- AccountDao.java
package com.sunxiaping.spring5.dao;
import com.sunxiaping.spring5.domain.Account;
public interface AccountDao {
/**
* 保存账户信息
*
* @param account
*/
void save(Account account);
/**
* 根据id查询账户信息
*
* @param id
* @return
*/
Account findById(Integer id);
/**
* 更新账户信息
*
* @param account
*/
void update(Account account);
/**
* 删除账户信息
*
* @param id
*/
void delete(Integer id);
/**
* 根据名称查询用户信息
*
* @param name
* @return
*/
Account findByName(String name);
}
- AccountDaoImpl.java
package com.sunxiaping.spring5.dao.impl;
import com.sunxiaping.spring5.dao.AccountDao;
import com.sunxiaping.spring5.domain.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.util.CollectionUtils;
import java.util.List;
@Repository
public class AccountDaoImpl implements AccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void save(Account account) {
jdbcTemplate.update(" INSERT INTO `account` (`name`,`money`) VALUES (?,?) ", account.getName(), account.getMoney());
}
@Override
public Account findById(Integer id) {
List<Account> accountList = jdbcTemplate.query(" SELECT * FROM `account` WHERE id = ? ", new BeanPropertyRowMapper<>(Account.class), id);
if (CollectionUtils.isEmpty(accountList)) {
return null;
}
return accountList.get(0);
}
@Override
public void update(Account account) {
jdbcTemplate.update(" UPDATE `account` set name =?,money =? WHERE id = ? ", account.getName(), account.getMoney(), account.getId());
}
@Override
public void delete(Integer id) {
jdbcTemplate.update(" DELETE FROM account WHERE id = ? ", id);
}
@Override
public Account findByName(String name) {
List<Account> accountList = jdbcTemplate.query(" SELECT * FROM `account` WHERE name = ? ", new BeanPropertyRowMapper<>(Account.class), name);
if (CollectionUtils.isEmpty(accountList)) {
return null;
}
if (accountList.size() > 1) {
throw new RuntimeException("名称name不唯一");
}
return accountList.get(0);
}
}
- 事务管理器TransactionManager
package com.sunxiaping.spring5.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.sql.SQLException;
@Component
public class TransactionManager {
@Autowired
private Connection connection;
/**
* 开始事务
*/
public void begin() {
try {
connection.setAutoCommit(false);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
/**
* 提交事务
*/
public void commit() {
try {
connection.commit();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
/**
* 回滚事务
*/
public void rollback() {
try {
connection.rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
/**
* 释放资源
*/
public void close() {
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
- JdbcConfig.java
package com.sunxiaping.spring5.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.sunxiaping.spring5.factory.YAMLPropertySourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.sql.DataSource;
import java.sql.Connection;
@Configuration
@PropertySource(value = "classpath:jdbc.yml",factory = YAMLPropertySourceFactory.class)
public class JdbcConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.user}")
private String user;
@Value("${jdbc.password}")
private String password;
/**配置数据源
* @return
*/
@Bean
public DataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(user);
dataSource.setPassword(password);
return dataSource;
}
@Bean
public Connection getConnection(DataSource dataSource){
//初始化事务同步管理器
TransactionSynchronizationManager.initSynchronization();
//使用Spring的数据源工具类获取当前线程的连接
Connection connection = DataSourceUtils.getConnection(dataSource);
//返回连接
return connection;
}
}
- SpringConfig.java
package com.sunxiaping.spring5.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
/**
* Spring的配置类
*/
@Configuration
@Import({JdbcConfig.class})
@ComponentScan(value = "com.sunxiaping")
public class SpringConfig {
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
return new JdbcTemplate(dataSource);
}
}
1.2.2 没有动态代理的情况下实现事务管理
- AccountService.java
package com.sunxiaping.spring5.service;
public interface AccountService {
/**
* 转账
*
* @param sourceName 转出账户名称
* @param targetName 转入账户名称
* @param money 金额
*/
void transfer(String sourceName, String targetName, Double money);
}
- AccountServiceImpl.java
package com.sunxiaping.spring5.service.impl;
import com.sunxiaping.spring5.dao.AccountDao;
import com.sunxiaping.spring5.domain.Account;
import com.sunxiaping.spring5.service.AccountService;
import com.sunxiaping.spring5.utils.TransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private TransactionManager transactionManager;
@Override
public void transfer(String sourceName, String targetName, Double money) {
try {
//开启事务
transactionManager.begin();
//查询转出账户
Account source = accountDao.findByName(sourceName);
//查询转入账户
Account target = accountDao.findByName(targetName);
//转出账户减钱
source.setMoney(source.getMoney() - money);
//转入账户加钱
target.setMoney(target.getMoney() + money);
//更新账户
accountDao.update(source);
//模拟异常
int num = 10 / 0;
accountDao.update(target);
//提交事务
transactionManager.commit();
} catch (Exception e) {
//回滚事务
transactionManager.rollback();
e.printStackTrace();
} finally {
//释放连接
transactionManager.close();
}
}
}
- 测试:
package com.sunxiaping.spring5;
import com.sunxiaping.spring5.config.SpringConfig;
import com.sunxiaping.spring5.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;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class Spring5Test {
@Autowired
private AccountService accountService;
@Test
public void test() {
accountService.transfer("张三","李四",100d);
}
}
在没有动态代理实现事务管理的情况下,几乎每个方法都需要写事务开启、事务提交、事务回滚和释放资源的代码,太繁琐了。
1.2.3 动态代理实现事务管理
- AccountService.java
package com.sunxiaping.spring5.service;
public interface AccountService {
/**
* 转账
*
* @param sourceName 转出账户名称
* @param targetName 转入账户名称
* @param money 金额
*/
void transfer(String sourceName, String targetName, Double money);
}
- AccountServiceImpl.java
package com.sunxiaping.spring5.service.impl;
import com.sunxiaping.spring5.dao.AccountDao;
import com.sunxiaping.spring5.domain.Account;
import com.sunxiaping.spring5.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public void transfer(String sourceName, String targetName, Double money) {
//查询转出账户
Account source = accountDao.findByName(sourceName);
//查询转入账户
Account target = accountDao.findByName(targetName);
//转出账户减钱
source.setMoney(source.getMoney() - money);
//转入账户加钱
target.setMoney(target.getMoney() + money);
//更新账户
accountDao.update(source);
//模拟异常
int num = 10 / 0;
accountDao.update(target);
}
}
- ProxyServiceFactory.java
package com.sunxiaping.spring5.factory;
import com.sunxiaping.spring5.service.AccountService;
import com.sunxiaping.spring5.utils.TransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.lang.reflect.Proxy;
/**
* 实现对AccountService的代理创建,同时加入事务
*/
@Component
public class ProxyServiceFactory {
@Autowired
private AccountService accountService;
@Autowired
private TransactionManager transactionManager;
@Bean("proxyAccountService")
public AccountService proxyAccountService() {
//创建代理对象
AccountService proxy = (AccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), (proxy1, method, args) -> {
Object result = null;
try {
//开启事务
transactionManager.begin();
result = method.invoke(accountService, args);
//提交事务
transactionManager.commit();
} catch (Exception e) {
//回滚事务
transactionManager.rollback();
e.printStackTrace();
} finally {
//释放资源
transactionManager.close();
}
return result;
});
return proxy;
}
}
- 测试:
package com.sunxiaping.spring5;
import com.sunxiaping.spring5.config.SpringConfig;
import com.sunxiaping.spring5.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class Spring5Test {
@Autowired
@Qualifier(value = "proxyAccountService")
private AccountService accountService;
@Test
public void test() {
accountService.transfer("张三","李四",100d);
}
}
2 AOP思想及实现原理
2.1 AOP思想
- 在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
2.2 实现原理
- 在上面的概念中描述出的AOP的实现原理是基于动态代理技术实现的。
- 动态代理的特点:字节码随用随创建,随用随加载。
- 动态代理的分类:基于接口的动态代理,基于子类的动态代理。
- 动态代理的作用:不修改源码的基础上对方法进行增强。
- 基于接口的动态代理:JDK官方。
- 基于子类的动态代理:第三方CGLIB包。
2.3 Spring中的AOP术语
- Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在Spring中,指的是方法,因为Spring只支持方法类型的连接点。
- Pointcut(切入点):所谓切入点是指我们要对那些Pointcut进行拦截的定义。
- Advice(通知/增强):
- 所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。
- 通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
- Introduction(引介):引介是一种特殊的通知,在不修改类代码的前提下,可以在运行期为类动态的添加一个方法或属性。
- Target(目标对象):代理的目标对象。
- Weaving(织入):
- 是指把增强应用到目标对象来创建新的代理对象的过程。
- Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
- Proxy(代理):一个类被AOP织入增强后,就产生了一个结果代理类。
- Aspect(切面):是切入点和通知(引介)的结合。
3 Spring注解驱动AOP开发入门
3.1 注解驱动AOP入门案例介绍
- 实现在执行Service方法时输出日志。
3.2 案例实现
- 导入相关jar包的Maven坐标:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<!-- 导入YAML解析工厂坐标 -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.26</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
- 实体类User.java
package com.sunxiaping.spring5.domain;
import java.io.Serializable;
import java.util.Date;
public class User implements Serializable {
private String id;
private String username;
private String password;
private Date birthday;
private String gender;
private String mobile;
private String nickname;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
", birthday=" + birthday +
", gender='" + gender + '\'' +
", mobile='" + mobile + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
- 业务层接口UserService.java
package com.sunxiaping.spring5.service;
import com.sunxiaping.spring5.domain.User;
public interface UserService {
/**
* 模拟保存用户信息
*
* @param user
*/
void saveUser(User user);
}
- 业务层实现类UserServiceImpl.java
package com.sunxiaping.spring5.service.impl;
import com.sunxiaping.spring5.domain.User;
import com.sunxiaping.spring5.service.UserService;
import org.springframework.stereotype.Service;
@Service("userService")
public class UserServiceImpl implements UserService {
@Override
public void saveUser(User user) {
System.out.println("保存用户信息" + user);
}
}
- 切面类LogUtil.java
package com.sunxiaping.spring5.utils;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect //表明当前类是一个切面类
public class LogUtil {
@Before(value = "execution(* com.sunxiaping.spring5.service.impl.*.*(..))")
public void printLog() {
System.out.println("输出日志");
}
}
- Spring的配置类SpringConfig.java
package com.sunxiaping.spring5.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* Spring的配置类
*/
@Configuration
@ComponentScan(value = "com.sunxiaping.spring5")
@EnableAspectJAutoProxy //开启Spring的AOP注解的支持
public class SpringConfig {
}
- 测试:
package com.sunxiaping.spring5;
import com.sunxiaping.spring5.config.SpringConfig;
import com.sunxiaping.spring5.domain.User;
import com.sunxiaping.spring5.service.UserService;
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.Date;
import java.util.UUID;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class Spring5Test {
@Autowired
private UserService userService;
@Test
public void test() {
User user = new User();
user.setId(UUID.randomUUID().toString());
user.setUsername("张三");
user.setPassword("123456");
user.setBirthday(new Date());
user.setGender("男");
user.setMobile("110");
user.setNickname("唐三葬");
userService.saveUser(user);
}
}
4 AOP常用注解分析
4.1 用于开启注解AOP注解的注解
4.1.1 @EnableAspectJAutoProxy注解
源码
- @EnableAspectJAutoProxy注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
/**
* Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
* to standard Java interface-based proxies. The default is {@code false}.
*/
boolean proxyTargetClass() default false;
/**
* Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal}
* for retrieval via the {@link org.springframework.aop.framework.AopContext} class.
* Off by default, i.e. no guarantees that {@code AopContext} access will work.
* @since 4.3.1
*/
boolean exposeProxy() default false;
}
说明
- 作用:
- 表明开启Spring对注解AOP的支持。它有两个属性,分别是指定采用的代理方式和是否暴露代理对象,通过AopContext可以进行访问。从定义中可以看出,它引入AspectJAutoProxyRegistrar的字节码对象,该对象是基于注解@EnableAspectJAutoProxy注册了一个AnnotationAwareAspectJAutoProxyCreator,该对象通过调用AopConfigUtils的registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry):注册一个AOP代理对象生成器。
- 属性:
- proxyTargetClass:指定是否采用CGLIB进行代理。默认值是false,表示使用JDK的代理。
- exposeProxy:指定是否暴露代理对象,通过AopContext可以进行访问。
- 使用场景:
- 当我们注解驱动开发的时候,在需要使用AOP实现某些功能的情况下,都需要用到此注解。
应用示例
- 示例:
package com.sunxiaping.spring5.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* Spring的配置类
*/
@Configuration
@ComponentScan(value = "com.sunxiaping.spring5")
@EnableAspectJAutoProxy //开启Spring的AOP注解的支持
public class SpringConfig {
}
4.2 用于配置切面的注解
4.2.1 @Aspect注解
源码
- @Aspect注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Aspect {
/**
* @return the per clause expression, defaults to singleton aspect.
* Valid values are "" (singleton), "perthis(...)", etc
*/
public String value() default "";
}
说明
- 作用:声明当前类是一个切面类。
- 属性:
- value:支持指定切入点表达式,或者用@Pointcut修改的方法名称(要求全限定方法名)。
- 使用场景:
- 此注解也是一个注解驱动开发AOP的必备注解。
应用示例
- 示例:
package com.sunxiaping.spring5.utils;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect //表明当前类是一个切面类
public class LogUtil {
@Before(value = "execution(* com.sunxiaping.spring5.service.impl.*.*(..))")
public void printLog() {
System.out.println("输出日志");
}
}
4.2.2 @Order注解
源码
- @Order注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
/**
* The order value.
* <p>Default is {@link Ordered#LOWEST_PRECEDENCE}.
* @see Ordered#getOrder()
*/
int value() default Ordered.LOWEST_PRECEDENCE;
}
说明
-
作用:@Order注解是定义Spring的IOC容器中的Bean的执行顺序的优先级。
-
属性:
- value:最低优先级,值越小,优先级越高。
-
使用场景:
- 可以改变默认的Spring加载多个切面的执行顺序(默认情况下是按照切面类名的自然顺序排序)。
应用示例
- 示例:
- UserService.java
package com.sunxiaping.spring5.service;
import com.sunxiaping.spring5.domain.User;
public interface UserService {
/**
* 模拟保存用户信息
*
* @param user
*/
void saveUser(User user);
}
- UserServiceImpl.java
package com.sunxiaping.spring5.service.impl;
import com.sunxiaping.spring5.domain.User;
import com.sunxiaping.spring5.service.UserService;
import org.springframework.stereotype.Service;
@Service("userService")
public class UserServiceImpl implements UserService {
@Override
public void saveUser(User user) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("保存用户信息" + user);
}
}
- LogUtil.java
package com.sunxiaping.spring5.utils;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 日志工具类
*/
@Component
@Aspect
@Order(1)
public class LogUtil {
@Before(value = "execution(* com.sunxiaping.spring5.service.impl.*.*(..))")
public void printLog() {
System.out.println("输出日志");
}
}
- EfficiencyUtil.java
package com.sunxiaping.spring5.utils;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 方法效率的工具类
*/
@Component
@Aspect
@Order(2)
public class EfficiencyUtil {
private LocalDateTime startTime;
/**
* 记录方法执行之前的时间
*/
@Before(value = "execution(* com.sunxiaping.spring5.service.impl.*.*(..))")
public void printBeforeExecutionTime() {
startTime = LocalDateTime.now();
System.out.println("方法开始执行时间 = " + startTime);
}
/**
* 记录方法执行之后的时间
*/
@After(value = "execution(* com.sunxiaping.spring5.service.impl.*.*(..))")
public void printAfterExecutionTime() {
LocalDateTime endTime = LocalDateTime.now();
System.out.println("方法结束执行时间 = " + endTime);
Duration duration = Duration.between(startTime, endTime);
System.out.println("方法执行的时间 = " + duration.getSeconds() );
}
}
- 测试:
package com.sunxiaping.spring5;
import com.sunxiaping.spring5.config.SpringConfig;
import com.sunxiaping.spring5.domain.User;
import com.sunxiaping.spring5.service.UserService;
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.Date;
import java.util.UUID;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class Spring5Test {
@Autowired
private UserService userService;
@Test
public void test() {
User user = new User();
user.setId(UUID.randomUUID().toString());
user.setUsername("张三");
user.setPassword("123456");
user.setBirthday(new Date());
user.setGender("男");
user.setMobile("110");
user.setNickname("唐三葬");
userService.saveUser(user);
}
}
4.3 用于配置切入点表达式的注解
4.3.1 @Pointcut注解
源码
- @Pointcut注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Pointcut {
/**
* @return the pointcut expression
* We allow "" as default for abstract pointcut
*/
String value() default "";
/**
* When compiling without debug info, or when interpreting pointcuts at runtime,
* the names of any arguments used in the pointcut are not available.
* Under these circumstances only, it is necessary to provide the arg names in
* the annotation - these MUST duplicate the names used in the annotated method.
* Format is a simple comma-separated list.
*
* @return argNames the argument names (should match those in the annotated method)
*/
String argNames() default "";
}
说明
- 作用:此注解是用于指定切入点表达式的。
- 属性:
- value:用于指定切入点表达式。
- argNames:用于指定切入点表达式中的参数。参数可以是execution中的,也可以是args中的。通常情况下不使用此属性也可以获得切入点的方法参数。
- 使用场景:
- 在实际开发中,当我们的多个通知需要执行,同时
应用示例
- 示例:
- LogUtil.java
package com.sunxiaping.spring5.utils;
import com.sunxiaping.spring5.domain.User;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 日志工具类
*/
@Component
@Aspect
@Order(1)
public class LogUtil {
/**
* 用于定义通用的切入点表达式
* 在value属性中使用了&&符号,表示并且的关系。
* &&符号后面的args和execution一样,都是切入点表达式支持的关键字,表示匹配参数。指定的内容可以是全限定类名或者名称,当指定参数名称时,要求和方法中形参名称相同。
* argNames属性,是定义参数的名称,该名称必须和args关键字中的名称一致。
*/
@Pointcut(value = "execution(* com.sunxiaping.spring5.service.impl.*.*(..)) && args(user))", argNames = "user")
public void pointcut(User user) {
}
@Before(value = "pointcut(user)", argNames = "user")
public void printLog(User user) {
System.out.println("输出日志" + user);
}
}
4.4 用于配置通知的注解
4.4.1 @Before注解
源码
- @Before注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Before {
/**
* @return the pointcut expression where to bind the advice
*/
String value();
/**
* When compiling without debug info, or when interpreting pointcuts at runtime,
* the names of any arguments used in the advice declaration are not available.
* Under these circumstances only, it is necessary to provide the arg names in
* the annotation - these MUST duplicate the names used in the annotated method.
* Format is a simple comma-separated list.
*
* @return the argument names (should match the annotated method parameter names)
*/
String argNames() default "";
}
说明
-
作用:被此注解修改的方法为前置通知。前置通知的执行时间点实在切入点方法执行之前。
-
属性:
- value:用于指定切入点表达式。可以是表达式,也可以是表达式的引用。
- argNames:用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称一致。通常不指定也可以获取切入点的参数内容。
-
使用场景:
- 在实际开发中,我们需要对切入点方法执行之前进行增强,此时就需要用到前置通知。在通知(增强的方法)中获取切入点方法中的参数进行处理时,就需要配合切入点表达式参数来使用。
应用示例
- 略。
4.4.2 @AfterReturning注解
源码
- @AfterReturning注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterReturning {
/**
* @return the pointcut expression where to bind the advice
*/
String value() default "";
/**
* @return the pointcut expression where to bind the advice, overrides "value" when specified
*/
String pointcut() default "";
/**
* @return the name of the argument in the advice signature to bind the returned value to
*/
String returning() default "";
/**
* When compiling without debug info, or when interpreting pointcuts at runtime,
* the names of any arguments used in the advice declaration are not available.
* Under these circumstances only, it is necessary to provide the arg names in
* the annotation - these MUST duplicate the names used in the annotated method.
* Format is a simple comma-separated list.
* @return the argument names (that should match names used in the annotated method)
*/
String argNames() default "";
}
说明
-
作用:
- 用于配置后置通知。
- 后置通知的执行时在切入点正常执行之后执行。
-
属性:
- value:用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
- pointcut:它的作用和value是一样的。
- returning:指定切入点方法返回值的变量名称。它必须和切入点方法返回值名称一致。
- argNames:用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称一致。通常不指定也可以获取切入点的参数内容。
-
使用场景:
- 此注解是用于配置后置增强切入点方法的。被此注解修饰方法会在切入点方法正常执行之后执行。
- 在我们实际中,像提交事务,记录访问日志,统计方法执行效率等等都可以利用后置通知实现。
应用示例
- 略。
4.4.3 @AfterThrowing注解
源码
- @AfterThrowing注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterThrowing {
/**
* @return the pointcut expression where to bind the advice
*/
String value() default "";
/**
* @return the pointcut expression where to bind the advice, overrides "value" when specified
*/
String pointcut() default "";
/**
* @return the name of the argument in the advice signature to bind the thrown exception to
*/
String throwing() default "";
/**
* When compiling without debug info, or when interpreting pointcuts at runtime,
* the names of any arguments used in the advice declaration are not available.
* Under these circumstances only, it is necessary to provide the arg names in
* the annotation - these MUST duplicate the names used in the annotated method.
* Format is a simple comma-separated list.
* @return the argument names (that should match names used in the annotated method)
*/
String argNames() default "";
}
说明
- 作用:用于配置异常通知。
- 属性:
- value:用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
- pointcut:它的作用和value是一样的。
- throwing:指定切入点方法执行产生异常时的异常对象变量名称。它必须和异常变量名称一致。
- argNames:用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称一致。通常不指定也可以获取切入点的参数内容。
应用示例
- 略。
4.4.4 @After注解
源码
- @After注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface After {
/**
* @return the pointcut expression where to bind the advice
*/
String value();
/**
* When compiling without debug info, or when interpreting pointcuts at runtime,
* the names of any arguments used in the advice declaration are not available.
* Under these circumstances only, it is necessary to provide the arg names in
* the annotation - these MUST duplicate the names used in the annotated method.
* Format is a simple comma-separated list.
*
* @return the argument names (should duplicate the names used for the annotated method parameters)
*/
String argNames() default "";
}
说明
-
作用:用于指定最终通知。
-
属性:
- value:用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
- argNames:用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称一致。通常不指定也可以获取切入点的参数内容。
-
使用场景:
- 最终通知的执行时机,是在切入点方法执行完成之后执行,无论切入点方法执行是否产生异常最终通知都会执行。所以被此直接修饰的方法,通常都是做一些清理工作。
应用示例
- 示例:
- UserService.java
package com.sunxiaping.spring5.service;
import com.sunxiaping.spring5.domain.User;
public interface UserService {
/**
* 模拟保存用户信息
*
* @param user
*/
void saveUser(User user);
}
- UserServiceImpl.java
package com.sunxiaping.spring5.service.impl;
import com.sunxiaping.spring5.domain.User;
import com.sunxiaping.spring5.service.UserService;
import org.springframework.stereotype.Service;
@Service("userService")
public class UserServiceImpl implements UserService {
@Override
public void saveUser(User user) {
// int i = 10 / 0;
System.out.println("保存用户信息" + user);
}
}
- LogUtil.java
package com.sunxiaping.spring5.utils;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 日志工具类
*/
@Component
@Aspect
public class LogUtil {
@Before(value = "execution(* com.sunxiaping.spring5.service.impl.*.*(..))")
public void beforePrintLog() {
System.out.println("前置通知:输出日志");
}
@After(value = "execution(* com.sunxiaping.spring5.service.impl.*.*(..))")
public void afterPrintLog() {
System.out.println("最终通知:输出日志");
}
@AfterReturning(value = "execution(* com.sunxiaping.spring5.service.impl.*.*(..))", returning = "obj")
public void afterReturningPrintLog(Object obj) {
System.out.println("后置通知:输出日志" + obj);
}
@AfterThrowing(value = "execution(* com.sunxiaping.spring5.service.impl.*.*(..))", throwing = "ex")
public void afterThrowingPrintLog(Throwable ex) {
System.out.println("异常通知:输出日志" + ex);
}
}
- SpringConfig.java
package com.sunxiaping.spring5.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* Spring的配置类
*/
@Configuration
@ComponentScan(value = "com.sunxiaping.spring5")
@EnableAspectJAutoProxy //开启Spring的AOP注解的支持
public class SpringConfig {
}
- 测试:
package com.sunxiaping.spring5;
import com.sunxiaping.spring5.config.SpringConfig;
import com.sunxiaping.spring5.domain.User;
import com.sunxiaping.spring5.service.UserService;
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.Date;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class Spring5Test {
@Autowired
private UserService userService;
@Test
public void test() {
User user = new User();
user.setId("1");
user.setUsername("张三");
user.setPassword("123456");
user.setBirthday(new Date());
user.setGender("男");
user.setMobile("110");
user.setNickname("唐三葬");
userService.saveUser(user);
}
}
4.4.5 @Around注解
源码
- @Around注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Around {
/**
* @return the pointcut expression where to bind the advice
*/
String value();
/**
* When compiling without debug info, or when interpreting pointcuts at runtime,
* the names of any arguments used in the advice declaration are not available.
* Under these circumstances only, it is necessary to provide the arg names in
* the annotation - these MUST duplicate the names used in the annotated method.
* Format is a simple comma-separated list.
* @return the argument names (should match the names on the annotated method)
*/
String argNames() default "";
}
说明
-
作用:用于指定环绕通知。
-
属性:
- value:用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
- argNames:用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称一致。通常不指定也可以获取切入点的参数内容。
-
使用场景:
- 环绕通知有别于前面介绍的四种通知类型。它不是指定增强方法执行时机的,而是Spring为我们提供的一种可以通过编码的方式手动增强方法何时执行的机制。
应用示例
- 示例:
package com.sunxiaping.spring5.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class LogUtil {
@Around(value = "execution(* com.sunxiaping.spring5.service.impl.*.*(..))")
public Object printLog(ProceedingJoinPoint joinPoint) {
Object proceed = null;
try {
System.out.println("前置通知");
proceed = joinPoint.proceed();
System.out.println("后置通知");
} catch (Throwable throwable) {
System.out.println("异常通知");
throwable.printStackTrace();
}finally {
System.out.println("最终通知");
}
return proceed;
}
}
4.5 用于扩展目标类的注解
4.5.1 @DeclareParents注解
源码
- @DeclareParents注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DeclareParents {
/**
* @return the target types expression
*/
String value();
/**
* Optional class defining default implementation
* of interface members (equivalent to defining
* a set of interface member ITDs for the
* public methods of the interface).
*
* @return define the default implementation of interface members (should always be specified)
*/
Class defaultImpl() default DeclareParents.class;
// note - a default of "null" is not allowed,
// hence the strange default given above.
}
说明
- 作用:用于给被增强的类提供新的方法。(实现新的接口)
- 属性:
- value:用于指定目标类型额表达式。当在全限定类名后面加上“+”时,表示当前类及其子类。
- defaultImpl:指定提高方法或者字段的默认实现类。
- 使用场景:
- 当我们已经完成了一个项目的某个阶段开发,此时需要对已完成的某个类加入一些新的方法,我们首先想到的是写一个接口,然后让这些需要方法的类实现此接口,但是如果目标类非常复杂,牵一发而动全身,改动的话可能非常麻烦。此时就可以使用此注解,然后建一个代理类,同时代理该类和目标类。
应用示例
- 示例:
- UserService.java
package com.sunxiaping.spring5.service;
import com.sunxiaping.spring5.domain.User;
public interface UserService {
/**
* 模拟保存用户信息
*
* @param user
*/
void saveUser(User user);
}
- UserServiceImpl.java
package com.sunxiaping.spring5.service.impl;
import com.sunxiaping.spring5.domain.User;
import com.sunxiaping.spring5.service.UserService;
import org.springframework.stereotype.Service;
@Service("userService")
public class UserServiceImpl implements UserService {
@Override
public void saveUser(User user) {
System.out.println("保存用户信息" + user);
}
}
- ValidateExtensionService.java
package com.sunxiaping.spring5.extension;
import com.sunxiaping.spring5.domain.User;
/**
* 用于扩展用户Service验证的接口
*/
public interface ValidateExtensionService {
/**
* 检查用户信息
*
* @param user
* @return
*/
boolean checkUser(User user);
}
- ValidateExtensionServiceImpl.java
package com.sunxiaping.spring5.extension.impl;
import com.sunxiaping.spring5.domain.User;
import com.sunxiaping.spring5.extension.ValidateExtensionService;
import org.springframework.util.StringUtils;
public class ValidateExtensionServiceImpl implements ValidateExtensionService {
@Override
public boolean checkUser(User user) {
//判断用户昵称中是否包含"孙子"
return StringUtils.isEmpty(user.getNickname()) || !user.getNickname().contains("孙子");
}
}
-
测试方式一:在调用的时候将UserService看成是ValidateExtensionService
- LogUtil.java
package com.sunxiaping.spring5.utils; import com.sunxiaping.spring5.extension.ValidateExtensionService; import com.sunxiaping.spring5.extension.impl.ValidateExtensionServiceImpl; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.DeclareParents; import org.springframework.stereotype.Component; @Component @Aspect public class LogUtil { /** * 让目标类具备当前声明接口中的方法,用的也是动态代理 */ @DeclareParents(value = "com.sunxiaping.spring5.service.UserService+", defaultImpl = ValidateExtensionServiceImpl.class) private ValidateExtensionService validateExtensionService; }
- 测试:
package com.sunxiaping.spring5; import com.sunxiaping.spring5.config.SpringConfig; import com.sunxiaping.spring5.domain.User; import com.sunxiaping.spring5.extension.ValidateExtensionService; import com.sunxiaping.spring5.service.UserService; 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.Date; import java.util.UUID; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class SpringTest { @Autowired private UserService userService; @Test public void test() { User user = new User(); user.setId(UUID.randomUUID().toString()); user.setUsername("张三"); user.setPassword("123456"); user.setBirthday(new Date()); user.setGender("男"); user.setMobile("110"); user.setNickname("孙子"); //验证用户昵称是否合法 // 把userService看成是ValidateExtensionService ValidateExtensionService validateExtensionService = (ValidateExtensionService) userService; if(validateExtensionService.checkUser(user)){ userService.saveUser(user); } } }
-
测试方式二:使用前置通知,使用this关键字,引入新目标类对象,调用方法的时候触发
- LogUtil.java
package com.sunxiaping.spring5.utils; import com.sunxiaping.spring5.domain.User; import com.sunxiaping.spring5.extension.ValidateExtensionService; import com.sunxiaping.spring5.extension.impl.ValidateExtensionServiceImpl; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.DeclareParents; import org.springframework.stereotype.Component; @Component @Aspect public class LogUtil { /** * 让目标类具备当前声明接口中的方法,用的也是动态代理 */ @DeclareParents(value = "com.sunxiaping.spring5.service.UserService+", defaultImpl = ValidateExtensionServiceImpl.class) private ValidateExtensionService validateExtensionService; @Before(value = "execution(* com.sunxiaping.spring5.service.UserService.*(..)) && this(validateExtensionService) && args(user)", argNames = "user,validateExtensionService") public void beforePrintLog(User user, ValidateExtensionService validateExtensionService) { if (!validateExtensionService.checkUser(user)) { throw new RuntimeException("用户昵称非法"); } System.out.println("打印日志"); } }
- 测试:
package com.sunxiaping.spring5; import com.sunxiaping.spring5.config.SpringConfig; import com.sunxiaping.spring5.domain.User; import com.sunxiaping.spring5.service.UserService; 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.Date; import java.util.UUID; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class SpringTest { @Autowired private UserService userService; @Test public void test() { User user = new User(); user.setId(UUID.randomUUID().toString()); user.setUsername("张三"); user.setPassword("123456"); user.setBirthday(new Date()); user.setGender("男"); user.setMobile("110"); user.setNickname("孙子"); userService.saveUser(user); } }
4.5.2 @EnableLoadTimeWeaving注解
源码
- @EnableLoadTimeWeaving注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(LoadTimeWeavingConfiguration.class)
public @interface EnableLoadTimeWeaving {
/**
* Whether AspectJ weaving should be enabled.
*/
AspectJWeaving aspectjWeaving() default AspectJWeaving.AUTODETECT;
/**
* AspectJ weaving enablement options.
*/
enum AspectJWeaving {
/**
* Switches on Spring-based AspectJ load-time weaving.
*/
ENABLED,
/**
* Switches off Spring-based AspectJ load-time weaving (even if a
* "META-INF/aop.xml" resource is present on the classpath).
*/
DISABLED,
/**
* Switches on AspectJ load-time weaving if a "META-INF/aop.xml" resource
* is present in the classpath. If there is no such resource, then AspectJ
* load-time weaving will be switched off.
*/
AUTODETECT;
}
}
说明
- 作用:用于切换不同场景实现增强。
- 属性:
- aspectjWeaving:是否开启LTW的支持。
- ENABLED:开启。
- DISABLED:不开启。
- AUTODETECT:如果类路径下能读取到META-INF/aop.xml文件,则开启LTW,否则关闭。
- aspectjWeaving:是否开启LTW的支持。
- 使用场景:
- 在Java语言中,从织入切面的方式来看,存在是三种织入方式:编译期织入、类加载期织入和运行期织入。编译期织入是指在Java编译期,采用特殊的编译器,将切面织入到Java类中。而类加载期织入则指通过特殊的类加载器,在类字节码加载到JVM时,织入切面;运行期织入则是采用CGLIB或JDK动态代理进行切面的织入。
- AspectJ提供了两种切面织入方式,第一种通过特殊编译器,在编译期,将AspectJ语言编写的切面类织入到Java类中,可以通过一个Ant或Maven任务来完成这个操作;第二种方式是类加载期织入,也简称为LTW(Load Time Weaving)。
应用示例
- 示例:
- 导入相关jar包的Maven坐标:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-instrument</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
- 在META-INF下新建aop.xml
<?xml version="1.0"?>
<!--
AspectJ load-time weaving config file to install common Spring aspects.
-->
<aspectj>
<!--
<weaver options="-showWeaveInfo"/>
-->
<aspects>
<!-- 切面类 -->
<aspect name="com.sunxiaping.spring5.utils.ProfileAspect"/>
</aspects>
<weaver>
<!-- 指定需要进行织入操作的目标类范围 -->
<include within="com.sunxiaping.spring5.service..*"/>
</weaver>
</aspectj>
- 切面类ProfileAspect.java
package com.sunxiaping.spring5.utils;
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.util.StopWatch;
@Component
@Aspect
public class ProfileAspect {
/**
* 统计执行效率
* 希望是开发环境和测试环境有,而生产环境没有
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("execution(* com.sunxiaping.spring5.service.impl.*.*(..))")
public Object profile(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//创建秒表对象
StopWatch sw = new StopWatch(this.getClass().getSimpleName());
try {
//记录执行
sw.start(proceedingJoinPoint.getSignature().getName());
//调用切入点方法并返回
Object proceed = proceedingJoinPoint.proceed();
return proceed;
} finally {
//停止计时
sw.stop();
//输出
System.out.println(sw.prettyPrint());
}
}
}
- Spring的配置类SpringConfig.java
package com.sunxiaping.spring5.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableLoadTimeWeaving;
/**
* Spring的配置类
*/
@Configuration
@ComponentScan(basePackages = "com.sunxiaping.spring5")
//@EnableAspectJAutoProxy // 开启运行期的增强
@EnableLoadTimeWeaving //开启类加载时期的增强
public class SpringConfig {
}
- 测试:
package com.sunxiaping.spring5;
import com.sunxiaping.spring5.config.SpringConfig;
import com.sunxiaping.spring5.domain.User;
import com.sunxiaping.spring5.service.UserService;
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.Date;
import java.util.UUID;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class SpringTest {
@Autowired
private UserService userService;
@Test
public void test() {
User user = new User();
user.setId(UUID.randomUUID().toString());
user.setUsername("张三");
user.setPassword("123456");
user.setBirthday(new Date());
user.setGender("男");
user.setMobile("110");
user.setNickname("孙子");
userService.saveUser(user);
}
}
- 测试配置:
5 AOP注解执行过程及核心对象的分析
5.1 执行过程分析
5.1.1 加载@EnableAspectJAutoProxy注解
5.1.2 解析切入点表达式
- 如果使用了@Pointcut注解,Spring在解析切入点的时候,会将其封装为Pointcut接口的实现类。
- Pointcut接口的源码:
public interface Pointcut {
/**
* @return the declared name of the pointcut.
*/
String getName();
/**
* @return the modifiers associated with the pointcut declaration.
* Use java.lang.reflect.Modifier to interpret the return value
*/
int getModifiers();
/**
* @return the pointcut parameter types.
*/
AjType<?>[] getParameterTypes();
/**
* @return the pointcut parameter names. Returns an array of empty strings
* of length getParameterTypes().length if parameter names are not
* available at runtime.
*/
String[] getParameterNames();
/**
* @return the type that declared this pointcut
*/
AjType getDeclaringType();
/**
* @return the pointcut expression associated with this pointcut.
*/
PointcutExpression getPointcutExpression();
}
- PointcutImpl的源码:
public class PointcutImpl implements Pointcut {
private final String name;
private final PointcutExpression pc;
private final Method baseMethod;
private final AjType declaringType;
private String[] parameterNames = new String[0];
protected PointcutImpl(String name, String pc, Method method, AjType declaringType, String pNames) {
this.name = name;
this.pc = new PointcutExpressionImpl(pc);
this.baseMethod = method;
this.declaringType = declaringType;
this.parameterNames = splitOnComma(pNames);
}
/* (non-Javadoc)
* @see org.aspectj.lang.reflect.Pointcut#getPointcutExpression()
*/
public PointcutExpression getPointcutExpression() {
return pc;
}
public String getName() {
return name;
}
public int getModifiers() {
return baseMethod.getModifiers();
}
public AjType<?>[] getParameterTypes() {
Class<?>[] baseParamTypes = baseMethod.getParameterTypes();
AjType<?>[] ajParamTypes = new AjType<?>[baseParamTypes.length];
for (int i = 0; i < ajParamTypes.length; i++) {
ajParamTypes[i] = AjTypeSystem.getAjType(baseParamTypes[i]);
}
return ajParamTypes;
}
public AjType getDeclaringType() {
return declaringType;
}
public String[] getParameterNames() {
return parameterNames;
}
private String[] splitOnComma(String s) {
StringTokenizer strTok = new StringTokenizer(s,",");
String[] ret = new String[strTok.countTokens()];
for (int i = 0; i < ret.length; i++) {
ret[i] = strTok.nextToken().trim();
}
return ret;
}
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(getName());
sb.append("(");
AjType<?>[] ptypes = getParameterTypes();
for (int i = 0; i < ptypes.length; i++) {
sb.append(ptypes[i].getName());
if (this.parameterNames != null && this.parameterNames[i] != null) {
sb.append(" ");
sb.append(this.parameterNames[i]);
}
if (i+1 < ptypes.length) sb.append(",");
}
sb.append(") : ");
sb.append(getPointcutExpression().asString());
return sb.toString();
}
}
- 如果没有使用@Pointcut注解,Spring在解析切入点表达式的时候,会将其封装为KindedPointcut。
- KindedPointcut的源码:
public class KindedPointcut extends Pointcut {
Shadow.Kind kind;
private SignaturePattern signature;
private int matchKinds;
private ShadowMunger munger = null; // only set after concretization
public KindedPointcut(Shadow.Kind kind, SignaturePattern signature) {
this.kind = kind;
this.signature = signature;
this.pointcutKind = KINDED;
this.matchKinds = kind.bit;
}
public KindedPointcut(Shadow.Kind kind, SignaturePattern signature, ShadowMunger munger) {
this(kind, signature);
this.munger = munger;
}
public SignaturePattern getSignature() {
return signature;
}
//其他略
}
5.2 AnnotationAwareAspectJAutoProxyCreator对象的分析
5.2.1 AnnotationAwareAspectJAutoProxyCreator的类图
5.2.2 加载过程
5.2.2.1 执行父类AbstractAutoProxyCreator的postProcessBeforeInstantiation()方法
- AbstractAutoProxyCreator的部分源码:
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
//首先根据Bean的class和name构建出key
Object cacheKey = getCacheKey(bean.getClass(), beanName);
//判断是否需要创建指定的Bean
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
5.2.2.2 判断是否需要增强或者已经被增强过了
- AbstractAutoProxyCreator的部分源码:
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
//如果已经处理过了
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
//如果不需要进行AOP增强
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
//判断这个Bean是不是基础设施类,或者配置了跳过自动大代理
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
5.2.2.3 判断是否为基础类(通知类)
- AbstractAutoProxyCreator的部分源码:
//判断是否为基础设施类,基础设施类不需要代理
protected boolean isInfrastructureClass(Class<?> beanClass) {
boolean retVal = Advice.class.isAssignableFrom(beanClass) ||
Pointcut.class.isAssignableFrom(beanClass) ||
Advisor.class.isAssignableFrom(beanClass) ||
AopInfrastructureBean.class.isAssignableFrom(beanClass);
if (retVal && logger.isTraceEnabled()) {
logger.trace("Did not attempt to auto-proxy infrastructure class [" + beanClass.getName() + "]");
}
return retVal;
}
5.2.2.4 获取增强的代码
- AbstractAutoProxyCreator的部分源码:
@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}
5.2.2.5 根据增强创建代理对象
- AbstractAutoProxyCreator的部分源码:
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {
if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
return proxyFactory.getProxy(getProxyClassLoader());
}