1、什么是 Spring 中的 AOP ?
第一代AOP框架是 AspectJ,那时候 AspectJ 采用的是静态字节码编译的方式,使用特殊的编译器,将实现写好的通知逻辑织入到目标类中,这样产生的 .class 字节码文件就是带有增强通知的代理类了。
第二代AOP框架是 AspectWerkz,它一开始设计就从动态AOP搞起,所以在 2005 年,AspectJ和AspectWerkz达成协议,将 AspectWerkz 的内容合并至 AspectJ 中。从那以后,AspectJ 成为了独有的同时支持静态 AOP 和动态 AOP 的强大的 AOP 框架。
之后 Spring 声明了对 AspectJ 的支持,可以使用 AspectJ 的方式定义切面类、声明通知方法等;最底层还是用 Spring 的逻辑来封装。
2、Spring 的 AOP 中包含哪些核心概念?它们分别都代表什么?
Target:目标对象,就是被代理的对象。
Proxy:代理对象,如 Proxy.newProxyInstance 返回的结果。
JoinPoint:连接点,可以理解为目标对象的所属类中,定义的所有方法。
Pointcut:切入点,可以理解为被增强的连接点。
切入点可以是 0 个或多个(甚至全部)连接点的组合。
切入点一定是连接点,连接点不一定是切入点。
Advice:增强的逻辑,就是对目标对象的增强代码。
Proxy代理对象 = Target目标对象+Advice通知。
Aspect:切面,切面可以理解为 Ponitcut切入点+Advice通知,等于JDK动态代理中的 InvocationHandler 的实现类和 Gclib 中的 MethodInterceptor 实现类,都可以看为是切面。
Weaving:织入,织入可以理解为:将 Advice 通知应用到 Target 目标对象,进而生成 Proxy 代理对象的过程;即 JDK 中 Proxy类 的 newInstance 方法和 Cglib 中 Enhancer 类的 create 方法。
Introduction:引介,引介是针对 Class 类,它可以在不修改原有类的代码的前提下,在运行期为原始类动态添加新的属性 / 方法
3、Spring 中的通知包含哪几种?分别的执行时机都是什么?
- Before 前置通知:目标对象的方法调用之前触发
- After 后置通知:目标对象的方法调用之后触发
- AfterReturning 返回通知:目标对象的方法调用完成,在返回结果值之后触发
- AfterThrowing 异常通知:目标对象的方法运行中抛出 / 触发异常后触发
- 注意一点,AfterReturning 与 AfterThrowing 两者是互斥的!如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。
- Around 环绕通知:编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后搞事,甚至不调用目标对象的方法。
4、AspectJ 与 AOP 联盟有什么联系和区别?它们与 SpringFramework 的关系是什么?
AOP联盟是对AOP思想制定规范的组织,它提出了以下五种通知类型:
- 前置通知
- 后置通知(返回通知)
- 异常通知
- 环绕通知
- 引介通知
AspectJ 和 AOP 联盟的区别:
- AspectJ 是一个AOP的实现框架,整合 AspectWerkz 后支持静态 AOP 和动态 AOP;而 AOP 联盟提出的是 AOP 的规范。
- AspectJ 提出的五种通知类型,但是和 AOP 联盟有一点区别;少了一个引介通知,多了一个后置通知。
SpringFramework 是基于 AOP 联盟制定的规范来实现 AOP,但同时它也支持使用 AspectJ 的方式来定义切面类和声明切面方法。
5、存在多个 Aspect 切面时,如何指定它们的执行顺序?
可以利用 Ordered 接口或者 @Order 注解声明切面的执行顺序(默认 Integer.MAX_VALUE )。
如果是同一个切面中有多个相同类型的通知,无法利用@Order注解来决定执行顺序,只能按照字母表的顺序来执行。
如果没有利用上述手段声明执行顺序,通过使用类名的 unicode 编码顺序,控制切面的执行顺序。
6、实战:利用动态代理实现事务控制
这里实现事务控制,最主要利用的是环绕通知,但是我们同时要注意一个点:因为事务的开启关闭、提交和回滚,都需要用到 Connection 这个类,所以对于增删改查操作我们也将直接用 Connection 的 PreparedStatement 来完成;而由于事务控制是利用动态代理来完成,那么增删改查的业务代码和增强逻辑是分开的,所以我们必须保证同一条线程获取到的 Connection 是同一个,否则无法完成事务控制。
那我们先直接上一个工具类,就是解决上面提到的「同一个线程里获取的Connection是同一个」场景:
/**
* jdbc工具类
* @author winfun
* @date 2021/8/3 8:58 上午
**/
@Component
public class JdbcUtils implements EnvironmentAware {
private static Environment ENV;
private static ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<>();
/**
* 获取当前线程持有的Connection对象
* @return Connection对象
*/
public static Connection getConnection(){
// 利用 ThreadLocal 保证同一个线程的 MySQL 链接是同一个
Connection connection = connectionThreadLocal.get();
if (null == connection){
try {
connection = DriverManager.getConnection(
ENV.getProperty("jdbc.url"),
ENV.getProperty("jdbc.username"),
ENV.getProperty("jdbc.password"));
connectionThreadLocal.set(connection);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return connection;
}
/**
* 移除当前线程Connection对象
*/
public static void removeConnection(){
connectionThreadLocal.remove();
}
/**
* Set the {@code Environment} that this component runs in.
*
* @param environment
*/
@Override
public void setEnvironment(Environment environment) {
ENV = environment;
}
}
我们可以看到,JdbcUtils 里面的数据库配置是从Envuronment 中读取的,所以会有对应的配置文件:
jdbc.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8
jdbc.username=root
jdbc.password=123456
接着上实体类、Dao层和Service层:
/**
* 实体类
* @author winfun
* @date 2021/8/3 8:58 上午
**/
@Data
@Accessors(chain = true)
public class User {
private Long id;
private String name;
private String gender;
private String userType;
}
/**
* dao层
* @author winfun
* @date 2021/8/3 8:58 上午
**/
@Repository
public class UserDao {
/**
* 添加用户
* @param user
* @return
*/
public int addUser(User user){
int result = 0;
try {
Connection connection = JdbcUtils.getConnection();
PreparedStatement preparedStatement = connection
.prepareStatement("insert into user(name,gender,user_type) values(?,?,?)");
preparedStatement.setString(1, user.getName());
preparedStatement.setString(2, user.getGender());
preparedStatement.setString(3, user.getUserType());
result = preparedStatement.executeUpdate();
preparedStatement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}finally {
}
return result;
}
/**
* 查询用户列表
* @return
*/
public List<User> listUser(){
List<User> userList = new LinkedList<>();
try {
Connection connection = JdbcUtils.getConnection();
PreparedStatement preparedStatement = connection
.prepareStatement("select * from user");
ResultSet rs = preparedStatement.executeQuery();
while(rs.next()){
long id = rs.getLong("id");
String name = rs.getString("name");
String gender = rs.getString("gender");
String userType = rs.getString("user_type");
User user = new User().setId(id).setName(name).setGender(gender).setUserType(userType);
userList.add(user);
}
preparedStatement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
return userList;
}
}
/**
* Service层
* @author winfun
* @date 2021/8/3 8:58 上午
**/
@Service
public class UserService {
@Autowired
private UserDao userDao;
/**
* 添加用户
* @param user
* @return
*/
@Transactional
public int addUser(User user){
int result = this.userDao.addUser(user);
// 模拟抛异常
result = 1/0;
return result;
}
/**
* 查询用户列表
* @return
*/
public List<User> listUser(){
return this.userDao.listUser();
}
}
接着我们先看看配置类,主要是扫描置顶路径的组件、导入配置文件和开启AOP功能:
/**
* 配置类
* @author winfun
* @date 2021/8/3 9:13 上午
**/
@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.github.howinfun.demo.aop.jdbc")
@PropertySource("aop_jdbc/jdbc.properties")
public class JdbcConfiguration {
}
接着是自定义注解和切面实现:
/**
* 自定义注解 -> 事务控制
* @author winfun
* @date 2021/8/3 8:04 上午
**/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Transactional {
}
/**
* 事务控制切面
* @author winfun
* @date 2021/8/3 9:18 上午
**/
@Aspect
@Component
public class JdbcTransactionalAspect {
@Pointcut("@annotation(com.github.howinfun.demo.aop.jdbc.Transactional)")
public void pointcut(){}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
// 获取当前线程的Connection对象
Connection connection = JdbcUtils.getConnection();
// 关闭自动提交
connection.setAutoCommit(false);
try {
Object retval = joinPoint.proceed();
// 方法执行成功,提交事务
connection.commit();
return retval;
} catch (Throwable e) {
// 方法出现异常,回滚事务
connection.rollback();
throw e;
} finally {
// 最后关闭连接,释放资源
JdbcUtils.removeConnection();
}
}
}
最后是测试应用:
/**
* 测试应用
* @author winfun
* 2021/8/3 9:18 上午
**/
public class Application {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(JdbcConfiguration.class);
UserService userService = applicationContext.getBean(UserService.class);
System.out.println("插入前查询:");
userService.listUser().forEach(System.out::println);
try {
userService.addUser(new User().setName("luff").setGender("m").setUserType("1"));
}catch (Exception e){
System.out.println("插入失败,异常信息:"+e.getMessage());
}
System.out.println("插入后查询:");
userService.listUser().forEach(System.out::println);
}
}
下面执行结果我们可以看到,因为我们在添加用户时,模拟了异常抛出,所以最后添加用户前后的用户列表数据是一致的。
执行结果:
/**
* 测试应用
* @author winfun
* 2021/8/3 9:18 上午
**/
public class Application {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(JdbcConfiguration.class);
UserService userService = applicationContext.getBean(UserService.class);
System.out.println("插入前查询:");
userService.listUser().forEach(System.out::println);
try {
userService.addUser(new User().setName("阿黄").setGender("m").setUserType("1"));
}catch (Exception e){
System.out.println("插入失败,异常信息:"+e.getMessage());
}
System.out.println("插入后查询:");
userService.listUser().forEach(System.out::println);
}
}