Day5 Spring AOP

Spring AOP

AOP 相关术语

Spring AOP是对方法的增强。

  • 连接点(JoinPoint)
    方法

  • 切入点(Pointcut)
    确定哪些方法需要增强

  • 通知(Advice)
    方法运行的哪一个阶段,开始时、结束时、返回时、异常时、开始+结束时?
    做了什么增强,添加日志、事务、负载均衡?

  • 切面(Aspect)
    在哪里做了哪些增强,即切面=切入点+通知

面向切面编程要做什么?
在哪些类的哪些方法,在方法运行到什么时候,做哪些增强;

在哪些类的哪些方法 = 切入点(Pointcut)
在方法运行到什么时候 + 做哪些增强 = 通知(Advice)

使用方法

基于接口

Spring AOP最早完全是几个接口来完成的。

advice

Spring AOP最早完全是几个接口来完成的。

  1. 通过实现AfterReturningAdvice接口,定义一个后置通知(被代理方法运行结束后,该通知被调用);
public class LogAdvice implements AfterReturningAdvice {

    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println(dateFormat.format(new Date()) + ": method " + method.getName() + " invoked.");
    }
}
  1. 配置xml文件
    重点是配置代理工厂:ProxyFactoryBean
    <!--被代理bean-->
    <bean id="userService" class="com.bailiban.day4.aop.dynamic_proxy.noproxy.UserServiceImpl" />

    <!--  代理通知bean  -->
    <bean id="logAdvice" class="com.bailiban.day4.aop.spring12.LogAdvice" />

    <!--  代理工厂bean  -->
    <bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--        被代理接口-->
        <property name="proxyInterfaces">
            <list>
                <value>com.bailiban.day4.aop.dynamic_proxy.noproxy.UserService</value>
            </list>
        </property>
<!--        被代理bean-->
        <property name="target" ref="userService"/>
<!--        通知-->
        <property name="interceptorNames">
            <list>
                <value>logAdvice</value>
            </list>
        </property>
    </bean>

从userServiceProxy的配置可以看出,其配置与手动编写基于JDK的动态代理很类似,都设计三个方面:

  • 被代理接口:UserService
  • 被代理类:UserServiceImpl
  • 增强方法:logAdvice

测试:

    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
                "com/bailiban/day4/aop/spring12/advice.xml");
        UserService userService = (UserService) context.getBean("userServiceProxy");
        userService.getUser();
        userService.createUser();
        userService.updateUser();
        userService.deleteUser();
    }

结果与手动编写动态代理代码一致:

getUser
2019-12-22 14:50:20: method getUser invoked.
createUser
2019-12-22 14:50:20: method createUser invoked.
updateUser
2019-12-22 14:50:20: method updateUser invoked.
deleteUser
2019-12-22 14:50:20: method deleteUser invoked.

基于接口还可以有其他更复杂的配置,来提供更丰富的AOP编程。不过现在很少使用基于接口来编写Spring AOP,在此略过。
其他用法可参考代码:https://github.com/Jun67/spring-demo/tree/master/src/main/java/com/bailiban/day4/aop/spring12

advisor

advice默认对指定代理对象的所有方法做增强,使用advisor可以进一步指定增强哪些方法。

  1. 新增一个advisor bean
<!--    在匹配(按方法名)的方法上通知-->
    <bean id="advisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<!--        1. 通知做哪些事情-->
        <property name="advice" ref="logAdvice" />
<!--        2. 指定哪些方法-->
        <property name="mappedNames" value="getUser" />
    </bean>
  1. 修改ProxyFactoryBean拦截器配置,添加advisor
<!--    动态代理-->
    <bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--        指定接口-->
        <property name="proxyInterfaces" value="com.bailiban.day5.aop.noproxy.UserService" />
<!--        指定代理对象-->
        <property name="target" ref="userService" />
<!--        指定增强的方法 -->
        <property name="interceptorNames">
            <list>
<!--                <value>beforeAdvice</value>-->
<!--                <value>logAdvice</value>-->
<!--                加入新增的advisor,用于控制指定方法做增强处理-->
                <value>advisor</value>
            </list>
        </property>
    </bean>
  1. 测试
    public static void main(String[] args) {

        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
                "com/bailiban/day5/aop/springaop/bean/advisor/advisor.xml");
        UserService userService = (UserService) context.getBean("userServiceProxy");
        userService.createUser();
        userService.deleteUser();
        userService.updateUser();
        userService.getUser();
    }
  1. 运行结果
创建了一个新用户。
删除一个用户。
更新一个用户。
查找一个用户。
2019-12-25 09:02:55: method getUser invoked.
autoProxyCreator

advice和advisor,在使用代理bean的时候,需要通过代理bean(userServiceProxy)来获取原bean名称(userService)。

可以是autoProxyCreator来自定生成代理类。在使用时,直接访问原bean名称(userService)即可获取代理对象。

  1. 创建了一个新的代理bean
    <!--    动态代理,自动生成代理对象-->
    <bean id="userServiceProxy" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<!--       按bean的名称来进行代理,以Service结尾的bean都会被代理-->
        <property name="beanNames" value="*Service" />
        <!--        指定增强的方法 -->
        <property name="interceptorNames">
            <list>
                <!--   advisor,用于控制指定方法做增强处理-->
                <value>advisor</value>
            </list>
        </property>
    </bean>
  1. 测试
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
                "com/bailiban/day4/aop/spring12/autoproxy/autoproxy.xml");
        UserService userService = (UserService) context.getBean("userService");
        userService.getUser();
        userService.createUser();
        userService.updateUser();
        userService.deleteUser();
    }

结果:

getUser
2019-12-25 17:55:43: method getUser invoked.
createUser
2019-12-25 17:55:43: method createUser invoked.
updateUser
deleteUser
DafaultAdvisor..

能不能不配置代理bean的属性?即不指定哪些方法做了什么增强?

将userServiceProxy替换为一个DefaultAdvisorAutoProxyCreator类型的bean即可

<!--    动态代理,自动生成代理对象-->
    <bean id="userServiceProxy" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
基于aop标签

Spring从2.0开始,提供了标签和@Aspectj注解两种AOP编程。

需要在pom.xml中添加如下依赖:

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
简单示例

使用示例:

    <!--被代理bean-->
    <bean id="userService" class="com.bailiban.day4.aop.dynamic_proxy.noproxy.UserServiceImpl" />

    <!--  代理通知bean  -->
    <bean id="logAdvice" class="com.bailiban.day4.aop.aopxml.LogAdvice" />
<!--    环绕切口-->
    <bean id="aroundAdvice" class="com.bailiban.day4.aop.aopxml.AroundAdvice" />

<!--    aop相关配置-->
    <aop:config>
<!--        定义切面,该切面的增强方法在logAdvice中定义的-->
        <aop:aspect ref="logAdvice">
<!--            指定在哪些类的哪些方法代理
        execution(* com.bailiban.day5.aop.noproxy.UserServiceImpl.*(..))
        execution:指定方法签名;
        *:表示任意字符,第一个* 表示方法的限定符(private,public),此处表示任意;
        com.bailiban.day5.aop.noproxy.UserServiceImpl:指定类名;
        第二个* :表示任意方法,即UserServiceImpl类下面的所有方法;
        (..):指定参数,此处使用..表示任意参数。注意".."也可以用来表示任意子包。
-->          
            <aop:pointcut id="log" expression="execution(* com..noproxy.UserServiceImpl.*(..))"/>
<!--            指定在代理方法运行结束之后,执行增强方法-->          
            <aop:after method="logger" pointcut-ref="log" />
        </aop:aspect>
      
<!--        定义切面,该切面的增强方法在aroundAdvice中定义的-->
        <aop:aspect ref="aroundAdvice">
            <aop:pointcut id="arround" expression="execution(* com..noproxy.UserServiceImpl.*(..))"/>
            <aop:around method="around" pointcut-ref="arround" />
        </aop:aspect>
    </aop:config>

关键点:

  1. <aop:config>:表明这是一个aop配置;
  2. <aop:aspect ref="logAdvice">
    配置切面,ref为该切面使用的通知(即增强代码)
  3. <aop:pointcut>:配置切入点
    重要:expression="execution(* com..noproxy.UserServiceImpl.*(..))"
    指定在哪里进行切入,即哪些方法会被增强。上面表达式表示UserServiceImpl类的所有方法都会被增强。
  4. <aop:around>:指定切入的时机,这里是环绕,即被代理的方法前后都会被增强。
    其他的切入点:
  • <aop:before>:方法运行之前;
  • <aop:after>:方法运行结束,但还没有返回值;
  • <aop:after-returning>:方法运行结束,且可以获得返回值;
  • <aop:after-throwing>:在异常中切入;

测试:

    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
                "com/bailiban/day4/aop/aopxml/aop.xml");
        UserService userService = (UserService) context.getBean("userService");
        userService.getUser();
        userService.createUser();
        userService.updateUser();
        userService.deleteUser();
    }

运行结果:

开始执行...
getUser
结束执行...
2019-12-23 21:58:14: method getUser invoked.
开始执行...
createUser
结束执行...
2019-12-23 21:58:14: method createUser invoked.
开始执行...
updateUser
结束执行...
2019-12-23 21:58:14: method updateUser invoked.
开始执行...
deleteUser
结束执行...
2019-12-23 21:58:14: method deleteUser invoked.
示例:事务处理

####### 手写事务

  1. 编写一个查询功能;
package com.bailiban.day5.aop.springaop.aop.transfer;

import java.sql.*;

public class JdbcUtil {

    private static String jdbcUrl = "jdbc:mysql://192.168.1.132:3306/spring_demo?serverTimezone=Asia/Shanghai";
    private static String username = "root";
    private static String password = "root";
    private static String driver = "com.mysql.jdbc.Driver";

    public static void main(String[] args) {

        // 1. Class.forName,加载用于mysql的驱动程序
        try {
            Class.forName(driver);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        // Java7 支持try-with-resource语法,只要在try语句里面创建的资源,会自动关闭。
        try (
           // 2. 建立数据库连接
           Connection conn = DriverManager.getConnection(jdbcUrl, username, password);
           // 3. 创建statement
           Statement statement = conn.createStatement();
           // 4. 查询语句,并返回结果
           ResultSet rs = statement.executeQuery("select id, name, money, role from account");
        ) {
            // 5. 遍历查询结果
            while (rs.next()) {
                System.out.println(rs.getInt(1) + "\t" +
                        rs.getString(2) + "\t" +
                        rs.getDouble(3) + "\t" +
                        rs.getString(4));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  1. 编写非事务的转账功能
    public void transfer(int fromId, int toId, double money) throws SQLException {
        Account fromAccount = accountDao.findById(fromId);
        Account toAccount = accountDao.findById(toId);
        fromAccount.setMoney(fromAccount.getMoney() - money);
        accountDao.update(fromAccount);
//        此处可以手动抛出一个异常,此时钱被转走,但接收者并不会收到。
//        if (true)
//            throw new RuntimeException();
        toAccount.setMoney(toAccount.getMoney() + money);
        accountDao.update(toAccount);
    }

当没有事务管理时,程序功能可能出现异常。例如,上述最后一句accountDao.update(toAccount);执行失败(即接收者没有收到钱),而此时accountDao.update(fromAccount);以及执行成功了。就会出现钱减少,但没到账的问题。

  1. 编写支持事务的转账代码
    public void transfer2(int fromId, int toId, double money) {
        Account fromAccount = accountDao.findById(fromId);
        Account toAccount = accountDao.findById(toId);

        try {
            System.out.println("begin");
            JdbcUtil.connection.setAutoCommit(false);
            fromAccount.setMoney(fromAccount.getMoney() - money);
            accountDao.update(fromAccount);
//            if (true)
//                throw new RuntimeException();
            toAccount.setMoney(toAccount.getMoney() + money);
            accountDao.update(toAccount);
            System.out.println("commit");
            JdbcUtil.connection.commit();
        } catch (Exception e) {
            e.printStackTrace();
            try {
                System.out.println("rollback");
                JdbcUtil.connection.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }
    }

关键点:

  • 开启事务
    JdbcUtil.conn.setAutoCommit(false);
  • 提交事务
    JdbcUtil.conn.commit();
  • 事务失败时,回滚事务
    JdbcUtil.conn.rollback();

JdbcUtil 定义见:https://github.com/Jun67/spring-demo/blob/master/src/main/java/com/bailiban/day4/aop/aopxml/transaction/JdbcUtil.java

####### 使用AOP写事务

  1. 添加四个拦截方法
  • 开始事务
    // 开始事务,用于 before advice
    public void transaction_begin() throws SQLException {
        System.out.println("begin");
        connection.setAutoCommit(false);
    }

  • 提交事务
    // 提交事务,用于 after-returning advice
    public void transaction_commit() throws SQLException {
        System.out.println("commit");
        connection.commit();
    }
  • 回滚事务
    // 回滚事务,用于 after-throwing advice
    public void transaction_rollback() throws SQLException {
        System.out.println("rollback");
        connection.rollback();
    }
  • 回收资源
    // 回收资源,此处并没有关闭连接,对应于 after advice
    public void transaction_release() {
        System.out.println("release");
    }
  1. 将拦截器配置到transfer方法上
<!--    事务处理相关方法-->
    <bean id="jdbcUtil" class="com.bailiban.day4.aop.aopxml.transaction.JdbcUtil" />

<!--    事务AOP配置-->
    <aop:config>
        <aop:aspect ref="jdbcUtil">
<!--            拦截transfer方法-->
            <aop:pointcut id="pc1" expression="execution(* com..transaction.*.transfer(..))"/>
<!--            transfer运行前,开启事务-->
            <aop:before method="transaction_begin" pointcut-ref="pc1" />
<!--            transfer运行结束后,提交事务-->
            <aop:after-returning method="transaction_commit" pointcut-ref="pc1" />
<!--            异常回滚-->
            <aop:after-throwing method="transaction_rollback" pointcut-ref="pc1" />
<!--            即使出现异常,after方法也会执行,模拟连接资源回收-->
            <aop:after method="transaction_release" pointcut-ref="pc1" />
        </aop:aspect>
    </aop:config>
posted @ 2019-12-25 18:53  cheng_18  阅读(291)  评论(0编辑  收藏  举报