2021-08-04:Spring AOP 之 AOP 介绍与实战

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);
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值