SSM框架学习----Spring(3)

1. AOP的相关概念

1.1. 什么是AOP

AOP:面向切面编程,通过预编译的方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务层逻辑的各个部分进行隔离,从而使得业务层逻辑各部分之间的耦合度降低,提高程序的可重用性和开发效率。

1.2. 作用以及优势

作用:

在程序运行期间,不修改源码对已有方法的增强

优势:

  • 减少重复代码
    +== 提高开发效率==
  • 维护方便

1.3. AOP的本质

使用动态代理技术

案例:完成账户的转账操作

业务层中增加转账的方法:

    public void transfer(String sourceName, String targetName, Double money) {

        System.out.println("begin transfer...");
        Account account1 = accountDao.findByName(sourceName);
        Account account2 = accountDao.findByName(targetName);
        account1.setRevenue(account1.getRevenue() - money);
        account2.setRevenue(account2.getRevenue() + money);
//        int i=1/0;
        accountDao.updateAccount(account1);
        accountDao.updateAccount(account2);
        }

可以看到,如果将int i=1/0;语句加入到更新账户之前,那么会发生异常,也就是转账的人已经把钱减少了,但是另一个人却没有收到,这是不允许的。
请添加图片描述

原因在于:上面4个AccountService是同一个对象,但是获取了4个不同的Connection连接,而每个Connection有不同的事务,当存在异常使得某个事务不能提交时会发生问题:
已经提交的事务正常执行,没有提交事务的回滚操作,可能会导致账户已经转账了,但是另一个账户并没有收到钱

解决思路:保证下面的Connection是同一个,使用ThreadLocal对象将Connection和当前线程绑定,使得一个线程中
只能有一个控制事务的对象

1.3.1. 定义ConnectionUtils类

用于从数据源中获取一个连接,并且实现和线程的绑定

public class ConnectionUtils {

    private ThreadLocal<Connection> tl = new ThreadLocal<>();
    private DataSource dataSource;
    // 通过set方法,让Spring注入数据
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 获取当前线程上的连接
     * 1. 从ThreadLocal中获取连接
     * 2. 判断当前线程上是否有连接
     * 3. 从数据源中获取一个链接,存入到ThreadLocal
     * 4. 返回当前线程上的连接
     * @return
     */
    public Connection getConnection(){
        Connection connection = tl.get();
        if(connection == null){
            try {
                connection = dataSource.getConnection();
                tl.set(connection);
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        return connection;
    }

    /**
     * 由于当前线程已经绑定了一个connection连接,所以归还连接并不能释放掉当前线程中的连接
     * 因此需要将线程和该连接进行解绑
     */
    public void removeConnection(){
        tl.remove();
    }
}

1.3.2. 定义事务管理类TransactionManager

用于实现开启事务,提交事务,回滚事务和释放连接的4个操作

public class TransactionManager {

    private ConnectionUtils connectionUtils;
    // 让Spring通过set注入方法获取连接
    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    public void open(){
        try {
            // 设置自动提交为false
            connectionUtils.getConnection().setAutoCommit(false);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void commit(){
        try {
            connectionUtils.getConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void rollback(){
        try {
            connectionUtils.getConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    // 将连接释放,归还到连接池中; 并且解除绑定
    public void close(){
        try {
            connectionUtils.getConnection().close();
            connectionUtils.removeConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

1.3.3. 业务层接口service

/**
 * 账户的业务层接口
 */
public interface AccountService {

    /**
     * 查询所有账户信息
     * @return
     */
    List<Account> findAll();

    /**
     * 根据id查询账户信息
     */
    Account findAccountById(Integer id);

    /**
     * 增加账户信息
     */
    void saveAccount(Account account);

    /**
     * 更新账户信息
     */
    void updateAccount(Account account);

    /**
     * 根据id删除账户信息
     */
    void deleteAccount(Integer id);
    
    /**
     * 转账
     * @param sourceName 转入账户name
     * @param targetName 转出账户name
     * @param money 转账金额
     */
    void transfer(String sourceName, String targetName, Double money);

}

1.3.4. 业务层实现类serviceImpl

事务控制都应该在业务层中实现,由于需要在每个线程中保证只有一个Connection,因此之前的业务层方法都需要修改成:开启事务,执行操作,提交事务,回滚事务,返回结果以及释放连接。

public class AccountServiceImpl implements AccountService {
    // 业务层调用持久层的对象
    private AccountDao accountDao;
    // 由于使用xml进行配置来注入数据,需添加set方法
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
    private TransactionManager tsManager;
    // 让spring通过set方法来注入事务管理器
    public void setTsManager(TransactionManager tsManager) {
        this.tsManager = tsManager;
    }

    /**
     * 之前的操作步骤都需要改成:
     * 开启事务,执行操作,提交事务,回滚事务,返回结果以及释放连接
     * @return
     */
    public List<Account> findAll() {
        List<Account> res = null;
        try{
            tsManager.open();
            res = accountDao.findAll();
            tsManager.commit();
        }catch(Exception e){
            tsManager.rollback();
        }finally{
            tsManager.close();
        }
        return res;
    }

    public Account findAccountById(Integer id) {
        Account res = null;
        try{
            tsManager.open();
            res = accountDao.findAccountById(id);
            tsManager.commit();
        }catch(Exception e){
            tsManager.rollback();
        }finally{
            tsManager.close();
        }
        return res;
    }

    public void saveAccount(Account account) {
        try{
            tsManager.open();
            accountDao.saveAccount(account);
            tsManager.commit();
        }catch (Exception e){
            tsManager.rollback();
        }finally {
            tsManager.close();
        }
    }

    public void updateAccount(Account account) {
        try{
            tsManager.open();
            accountDao.updateAccount(account);
            tsManager.commit();
        }catch (Exception e){
            tsManager.rollback();
        }finally {
            tsManager.close();
        }
    }

    public void deleteAccount(Integer id) {
        try{
            tsManager.open();
            accountDao.deleteAccount(id);
            tsManager.commit();
        }catch (Exception e){
            tsManager.rollback();
        }finally {
            tsManager.close();
        }
    }

    /**
     * 根据SourceName查询转出账户,根据targetName查询转入账户
     * 转出账户金额减少,转入账户金额增加,更新转出账户和转入账户
     * @param sourceName 转入账户name
     * @param targetName 转出账户name
     * @param money 转账金额
     */
    public void transfer(String sourceName, String targetName, Double money) {
 		try{
                tsManager.open();
                Account account1 = accountDao.findByName(sourceName);
                Account account2 = accountDao.findByName(targetName);
                account1.setRevenue(account1.getRevenue() - money);
                account2.setRevenue(account2.getRevenue() + money);
                // int i=1/0;
                accountDao.updateAccount(account1);
                accountDao.updateAccount(account2);
                tsManager.commit();
            }catch(Exception e){
                tsManager.rollback();
                e.printStackTrace();
            }finally {
                tsManager.close();
            }
    }
}

1.3.5. 持久层的dao接口

/**
 * 账户的持久层接口
 */
public interface AccountDao {

    /**
     * 查询所有账户信息
     * @return
     */
    List<Account> findAll();

    /**
     * 根据id查询账户信息
     */
    Account findAccountById(Integer id);

    /**
     * 增加账户信息
     */
    void saveAccount(Account account);

    /**
     * 更新账户信息
     */
    void updateAccount(Account account);

    /**
     * 根据id删除账户信息
     */
    void deleteAccount(Integer id);

    /**
     * 根据名称查询账户,如果没有结果则返回null,如果结果集超一个则抛出异常
     * @param name
     * @return
     */
    Account findByName(String name);

}

1.3.6. 持久层实现类daoImpl

需要保证每个线程中只有一个Connection连接,因此在执行数据库操作时,需要在runner对象的query和update方法中的第一个参数中增加获取连接,而这个连接是由ConnectionUtils类的getConnection方法所提供的

public class AccountDaoImpl implements AccountDao {
    // 使用DBUtils工具类来帮助Dao层的开发,简化操作,不需要再使用JDBC那一套冗余的工作:
    // 注册驱动,获取连接,获取代理对象,代理对象执行sql,释放资源
    private QueryRunner runner;
    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }
    // 让Spring注入数据,在每个操作的sql语句之前加入一个获取连接,保证同一个事务中只有一个connection连接
    private ConnectionUtils connectionUtils;
    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    public List<Account> findAll() {
        try {
            return runner.query(connectionUtils.getConnection(),
                    "select * from account", new BeanListHandler<Account>(Account.class));
        } catch (SQLException e) {
          throw new RuntimeException();
        }
    }
    
    public Account findAccountById(Integer id) {
        try {
            return runner.query(connectionUtils.getConnection(),
                    "select * from account where id=?", new BeanHandler<Account>(Account.class), id);
        } catch (SQLException e) {
           throw new RuntimeException();
        }
    }

    public void saveAccount(Account account) {
        try {
            runner.update(connectionUtils.getConnection(),
                    "insert  into account(name, revenue) values(?,?)",
                    account.getName(), account.getRevenue());
        } catch (SQLException e) {
            throw new RuntimeException();
        }
    }

    public void updateAccount(Account account) {
        try {
            runner.update(connectionUtils.getConnection(),
                    "update account set name=?, revenue=? where id=?",
                    account.getName(), account.getRevenue(), account.getId());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void deleteAccount(Integer id) {
        try {
            runner.update(connectionUtils.getConnection(),
                    "delete from account where id=?", id);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    // 根据姓名查找账户信息
    public Account findByName(String name) {
        try {
            List<Account> res = runner.query(connectionUtils.getConnection(),
                    "select * from account where name=?",
                    new BeanListHandler<Account>(Account.class),
                    name);
            if(res == null || res.size() == 0){
                return null;
            } else if(res.size() > 1){
                throw new RuntimeException("结果集长度不为1");
            }else{
                return res.get(0);
            }
        } catch (SQLException e) {
          throw new RuntimeException();
        }
    }
}

1.3.7. BeanFactory工厂类

用于创建service的代理对象,被代理对象是AccountService,增加set方法让Spring帮助注入数据;定义一个获取代理对象的方法getAccountService:使用Proxy类的newProxyInstance方法来获取代理对象。

1.3.7.1. 基于接口的动态代理

字节码只有使用时才创建和加载,涉及的类为Proxy(JDK提供)

作用:不修改源码的基础上,对原方法进行增强

创建对象:使用Proxy类中的newProxyInstance方法

要求:被代理类 最少实现一个接口,如果没有则不能使用

newProxyInstance中的参数:

  • ClassLoader loader :类加载器,用于加载代理对象的字节码,和被代理对象使用相同的类加载器(代理谁就写谁的类加载器) xxx.getClass().getClassLoader()

  • Class[] interfaces :字节码数组,用于让代理对象和被代理对象有相同方法,实现同一个接口,必然有相同的方法 xxx.getClass().getInterfaces()

  • InvocationHandler h: 用于提供增强的方法即如何代理, 一般是一个该接口的实现类(可使用匿名内部类),此接口的实现类是谁使用谁写; 其中的invoke方法:执行被代理对象的任何接口都会经过该方法,有拦截的功能。

    • 其中的参数:proxy ---- 代理对象的引用
    • method ---- 当前执行的方法
    • objects ----当前执行方法所需要的参数
    • invoke方法返回值:和被代理对象(工厂)有相同的返回值

    注意:外层的invoke是InvocationHandler接口中的invoke的重写,而增强代码中的invoke是使用反射机制进行方法(根据传递进去的业务层方法返回不同的值)的调用,第一个参数为被代理对象也就是AccountService,第二个参数是业务层方法的参数列表(有可能存在有可能为空),这两个invoke的含义是不一样的。

public class BeanFactory {
    private AccountService accountService; // 定义被代理对象
    private TransactionManager tsManager;

    public final void setAccountService(AccountService accountService) {
        this.accountService = accountService;
    }
    // 让spring通过set方法来注入事务管理器
    public void setTsManager(TransactionManager tsManager) {
        this.tsManager = tsManager;
    }

    /**
     * 获取Service的代理对象
     * @return
     */
    public AccountService getAccountService(){
        // 定义service的动态代理对象
        AccountService proxyService = (AccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] objects) throws Throwable {
                        Object val = null;
                        try {
                            tsManager.open();
                            // 调用方法(第一个参数为被代理对象,第二个参数为参数列表)
                            val = method.invoke(accountService, objects);
                            tsManager.commit();
                        } catch (Exception e) {
                            tsManager.rollback();
                            e.printStackTrace();
                        } finally {
                            tsManager.close();
                        }
                        return val;
                    }
                });
        return proxyService;
    }
}
1.3.7.2. 基于子类的动态代理

要求有第三方jar包的支持(cglib)

涉及的类:Enhancer,提供者:cglib库

如何创建对象:使用Enhancer类中的create方法

创建代理对象的要求:被代理的类不能是最终类(需要其子类)
create方法的参数:

  • class:字节码,用于指定被代理对象的字节码,而不再是类加载器。由于只是对某一个类的对象进行代理,所以这里不再需要获取其字节码数组。

  • Callback: 用于提供增强的代码,一般使用该接口的子接口实现类(MethodInterceptor)

    执行被代理对象的任何方法都会经过该方法,具体参数有:

    • proxy

    • method

    • args

      以上三个与基于接口的代理中的invoke方法的参数是一致的

    • methodProxy:当前执行方法的代理对象

为了演示,定义一个Producer类,其中包括两个方法saleProduct和afterService

public class Producer {

    public void saleProduct(Double money){
        System.out.println("sale product, receive: " + money);
    }

    public void afterService(Double money){
        System.out.println("offer after-service, receive: " + money);
    }
}

定义一个client类用于使用cglib来获取Producer的代理对象,在不修改Producer源码的基础上并且对saleProduct方法进行增强

public class client {

    final static Producer producer = new Producer();

    public static void main(String[] args) {
        Producer producerCglib = (Producer) Enhancer.create(producer.getClass(),
                new MethodInterceptor() {
                public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                Double money = (Double) args[0];
                Object val = null;
                if ("saleProduct".equals(method.getName())) {
                  val = method.invoke(producer, money * 0.8);
                }
                return val;
            }
        });
        producerCglib.saleProduct(10000d);
    }

同样,在子接口实现类的内部方法intercept中进行增强代码的编写,使用invoke反射机制去调用被代理对象的方法,并且对其增强:输入10000,返回输入值 * 0.8的结果,打印输出:
请添加图片描述

1.3.8. 配置XML

最重要的一步就是配置XML:

  • 配置ConnectionUtils对象:可以看到在ConnectionUtils类中需要注入DataSource,通过set方法来注入
  • 配置TransactionManager对象,使用set方法注入ConnectionUtils数据
  • 配置dao对象,通过set方法注入QueryRunner和ConnectionUtils
  • 配置AccountService对象,通过set方法注入dao和TransactionManager
  • 配置BeanFactory对象,通过set方法注入accountService和TransactionManager
  • 配置动态代理的service对象,使用工厂模式方法来创建,Factory-bean指向上一层配置的Beanfactory对象,factory-method指向BeanFactory类中的getAccountService方法,获取一个Service的代理对象
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置Service对象-->
    <bean id="accountService" class="com.huzhen.service.impl.AccountServiceImpl">
        <!--在Service中注入dao对象-->
        <property name="accountDao" ref="accountDao"></property>
        <!--在service对象中注入事务管理器数据-->
        <property name="tsManager"  ref="transManager"></property>
    </bean>
    <!--配置dao对象-->
    <bean id="accountDao" class="com.huzhen.dao.impl.AccountDaoImpl">
        <!--在Dao中注入queryRunner对象-->
        <property name="runner" ref="runner"></property>
        <!--dao接口的实现类中需要注入ConnectionUtils数据-->
        <property name="connectionUtils" ref="connectionUtils"></property>
     </bean>

    <!--配置queryRunner对象 为了防止多个dao使用runner对象发生线程安全问题,因此配置成多例对象-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <!--注入数据源 使用构造函数注入-->
        <constructor-arg name="ds" ref="dataSource"></constructor-arg>
    </bean>

    <!--配置数据源 使用c3p0来连接数据库 使用set方法注入数据-->
    <bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql:///springIoC?serverTimezone=UTC&amp;useSSL=false"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
      </bean>

    <!--配置ConnectionUtils对象 ConnectionUtils类中需要注入DataSource-->
    <bean id="connectionUtils" class="com.huzhen.utils.ConnectionUtils">
        <!--注入数据源 通过set方法注入-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    
    <!--配置transactionManager事务管理器对象 需要传递ConnectionUtils数据-->
    <bean id="transManager" class="com.huzhen.utils.TransactionManager">
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置beanFactory对象-->
    <bean id="beanFactory" class="com.huzhen.factory.BeanFactory">
        <!--beanFactory类中需要注入Service对象和事务管理对象-->
        <property name="accountService" ref="accountService"></property>
        <property name="tsManager" ref="transManager"></property>
    </bean>
    <!--配置动态代理的service对象 使用工厂方法创建对象(通过类中的方法来返回对象)-->
    <bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
</beans>

1.3.8. JUnit测试

需要注意的是:此时不能直接使用==@Autowired注解让Spring来注入数据,因为在IoC容器中存在两个service对象,所以需要使用@Qualifier==注解来指定使用哪一个

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class ProxyTest {
    // 再使用Autowired注解不能注入数据,因为配置文件中存在两个同类型的accountService对象,需要指定使用哪一个
    @Autowired
    @Qualifier("proxyAccountService")
    private AccountService accountService;

    @Test
    public void testFindAll(){
        List<Account> res = accountService.findAll();
        for(Account account : res){
            System.out.println(account);
        }
    }

    @Test
    public void testTransfer(){
        accountService.transfer("大司马", "五五开", 2000d);
    }
}

此时,如果在业务层的transfer方法中出现异常,不再会出现一个账户减少金额而另一个账户并没有收到:
请添加图片描述
可以看到,由于发生by zero异常,数据库中的id为1和2的revenue并没有改变,已经正常的回滚事务:
请添加图片描述
删除int i=1/0;语句,可以正常执行成功,打印输出结果:
请添加图片描述

存在的问题:之前在serviceImpl类中每个方法的操作步骤都需要:开启事务,执行操作,提交事务,(返回结果)回滚事务和释放资源一旦事务管理器中的方法改变了或者增加新的方法,那么业务层中的每一个都需要进行修改,这是非常不合理的。因此此需要保证方法的独立,消除方法之间的依赖性也就是使用上面提及到的动态代理。

由于使用代理对象来创建service对象,实现事务控制和业务层的方法进行分离,业务层不再需要事务管理,交给代理对象来处理即可,也就是BeanFactory类中的getAccountService方法所执行的。所以在serviceImpl实现类中调用业务层的方法,不再需要繁琐的去实现开启事务,事务提交…而是仅一个方法的执行和返回即可。因此在业务层实现类中的TransactionManager对象可以进行删除,在xml文件中也不再需要进行配置Service对象中的TransactionManager,因为获取的不再是service对象,而是其代理对象proxyAccountService。

2. Spring中的AOP

2.1. 相关术语

  • Joinpoint(连接点)

所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。如业务层中的所有方法findAll,findById等。

  • Pointcut(切入点)

所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。指的是那些被增强的方法,所有的切入点都是连接点,并不是所有的连接点都是切入点。

  • Advice(增强/通知)

所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。通知的类型:

前置通知,后置通知,异常通知,最终通知,环绕通知。try中切入点方法执行之前的advice即为前置通知,方法执行之后的即为后置通知,在catch中执行的为异常通知,finally中执行的是最终通知;整个的invoke方法在执行即为环绕通知,在环绕通知中有明确的切入点方法调用。具体如下图:
请添加图片描述

  • Introduction(引介)

引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field。

  • Target(目标对象)

代理的目标对象也就是被代理对象(accountService)

  • Weaving(织入)

是指把增强应用到目标对象来创建新的代理对象的过程。spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。上例子中invoke方法中引入事务控制即为织入

  • Proxy(代理对象)

一个类被 AOP 织入增强后,就产生一个结果代理类(proxyService)

  • Aspect(切面)

切入点和通知的结合,如果通过配置的方法来实现,开启事务需要在被代理对象被执行时创建,提交事务在之后执行,而finally不需要被代理对象中的方法被执行,也可以执行。哪些方法需要配置和配置的时机也就是切面。

2.2. 学习AOP中需要明确的问题

  • 开发阶段(我们做的)

编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。把公用代码抽取出来,制作成通知。(开发阶段最后再做):AOP 编程人员来做。在配置文件中,声明切入点与通知间的关系,即切面:AOP 编程人员来做。

  • 运行阶段(Spring框架完成的)

Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

2.3. 关于代理的选择

Spring会根据目标类是否实现了接口来决定采用哪种动态代理的方式(类实现了某一个接口,则采用动态代理)

2.4. 基于XML的AOP配置

需求:对于业务层中的方法增加一个公共方法printLog,并且在切入点方法执行之前被执行,不使用动态代理来实现。

2.4.1 service接口

public interface AccountService {

    void saveAccount();

    void updateAccount(Integer id);

    int deleteAccount();

}

2.4.2. service接口实现类

仅仅是模拟业务层的方法,并不需要真正的去改变表中的记录,因此不需要增加持久层接口和对应的实现类

public class AccountServiceImpl implements AccountService {

    public void saveAccount() {
        System.out.println("execute save...");
    }

    public void updateAccount(Integer id) {
        System.out.println("execute update" + id);
    }

    public int deleteAccount() {
        System.out.println("execute delete...");
        return 1;
    }
}

2.4.3. utils包下的Logger类

/**
 * 用于记录日志的工具类,提供公共的代码(操作)
 */
public class Logger {

    /**
     * 用于打印日志,计划让其在切入点方法执行(切入点方法就是业务层中的方法)
     */
    public void printLog(){
        System.out.println("Logger类中的方法开始记录日志...");
    }

}

2.4.4. 测试类

public class aopTest {

    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        AccountService accountService = ac.getBean("accountService", AccountService.class);
        // 由于切入点表达式中只配置了saveAccount方法与advice结合,因此只能完成saveAccount方法能够执行前置通知
        accountService.saveAccount();
        accountService.updateAccount(20201001);
        accountService.deleteAccount();
    }
}

2.4.5 XML文件的配置

由于需要使用到AOP配置,需要加入AOP的约束,可以在Spring Framework Documentation -> core中搜索xmlns:aop
请添加图片描述

将该约束复制到配置文件中:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置Spring的IoC 将service对象进行配置-->
    <bean id="accountService" class="com.hz.service.impl.AccountServiceImpl"></bean>
    
    <!--配置logger对象-->
    <bean id="logger" class="com.hz.utils.Logger"></bean>
    
    <!--配置AOP-->
    <aop:config>
        <!--配置切面Aspect-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置advice类型 建立通知方法(前置advice)和切入点方法的关联-->
            <aop:before method="printLog"
                        pointcut="execution(* com.hz.service.impl.*.*(..))" ></aop:before>
        </aop:aspect>
    </aop:config>
</beans>

关于service对象和logger对象的配置在Spring的IoC配置中已经叙述过,主要解析AOP的配置是如何进行的:

  • 通知bean交给spring来管理

  • 使用aop:config标签表明开始AOP的配置

  • 在aop:config标签中使用aop:aspect标签表明开始配置切面,其属性有:

    • id: 给切面提供一个唯一的标识

    • ref: 指定通知类bean的id

    在aop:aspect标签内部中使用对应标签来配置通知的类型

    需求:让printLog方法在切入点方法之前执行也就是前置通知name使用aop:before标签配置前置通知,其属性有:

    • method属性:用于指定Logger类中的哪个方法作为前置通知
    • pointcut:用于指定切入点表达式,也就是针对业务层中的哪些方法进行增强

    格式:execution(访问修饰符 返回值 全限定类名.类名.方法名(参数列表)),对于saveAccount方法与advice的配置,如下:

    pointcut="execution(public void com.hz.service.impl.AccountServiceImpl.saveAccount())

    此时,只能保证在运行saveAccount方法时,才能正确执行前置通知,如下:
    请添加图片描述

对于其他两个业务层的方法并没有执行前置通知,因此可以使用通配符表达式对业务层的所有连接点进行配置:

execution = (* *..*.*(..))

execution(访问修饰符 返回值 全限定类名.类名.方法名(参数列表))是如何转换得到通配符表达式的呢?

  • 访问修饰符可以省略返回值 全限定类名.类名.方法名(参数列表)
  • 返回值可以使用通配符表示任意返回值* 全限定类名.类名.方法名(参数列表)
  • ==包名可以使用通配符表示任意包,但是有几级包需要写几个 *.==上面的例子中对于业务层实现类有4级包,因此改造成* *.*.*.*.类名.方法名(参数列表)或者可以使用…表示当前包及其子包,即:* *..类名.方法名(参数列表)
  • 类名和方法名都可以使用*实现通配* *..*.*(参数列表)
  • 参数列表:

对于基本类型可以使用 int,double等;对于引用类型使用包名.类名:java.lang.String。可以使用通配符…表示任意类型,有参数或者无参数均可:* *..*.*(..);也可以使用*表示有参数的任意类型的方法:* *..*.*(*)。但是在实际开发中的通常写法:切到业务层实现类下的所有方法 最终得到:

* com.hz.service.impl.*.*(..) 在测试类中打印结果为:

请添加图片描述

2.4.5. 四种常用的通知类型

在logger类中增加三种方法用于表示后置,异常和最终通知

    /** 后置advice
     * 用于打印日志,计划让其在切入点方法执行之后执行(切入点方法就是业务层中的方法)
     */
    public void afterPrintLog(){
        System.out.println("后置advice! Logger类中的afterPrintLog方法开始记录日志...");
    }

    /** 异常advice
     */
    public void exceptionPrintLog(){
        System.out.println("异常advice! Logger类中的exceptionPrintLog方法开始记录日志...");
    }

    /** 最终advice
     */
    public void finallyPrintLog(){
        System.out.println("最终advice! Logger类中的finallyPrintLog方法开始记录日志...");
    }

在xml中进行配置:

    <!--配置AOP-->
    <aop:config>
        <!--配置切面Aspect-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置advice类型(前置通知) 建立通知方法和切入点方法的关联-->
            <aop:before method="printLog"
                        pointcut="execution(* com.hz.service.impl.*.*(..))" ></aop:before>
            <!--后置通知 在切入点方法执行之后执行 如果切入点方法发生异常则不会执行-->
            <aop:after-returning method="afterPrintLog"
                                 pointcut="execution(* com.hz.service.impl.*.*(..))"></aop:after-returning>
            <!--异常通知 在切入点方法发生异常时执行 使用point-ref指定某一个共有的切入点表达式-->
            <aop:after-throwing method="exceptionPrintLog"
                              pointcut="execution(* com.hz.service.impl.*.*(..))"></aop:after-throwing>
            <!--最终通知 无论切入点方法是否发生异常都执行-->
            <aop:after method="finallyPrintLog"
                       pointcut="execution(* com.hz.service.impl.*.*(..))"></aop:after></aop:after>
        </aop:aspect>
    </aop:config>

仅使用savaAccount方法来测试,输出为:

请添加图片描述

当在saveAccount增强方法之前加入int i=1/0;再进行输出:
请添加图片描述

可以发现已经没有后置通知了。因此,得出结论:

后置通知和异常通知有且只能执行一个,当切入点方法正常执行则会执行后置通知,当切入点方法发生异常则执行异常通知。正如使用动态代理时中的try{...}catch(...){...}finally{...},事务提交和事务回滚只能执行一个,不可能又提交又回滚。

2.4.6. 通用化切入点表达式

<aop:aspect></aop:aspect>标签内部可以使用标签aop:pointcut来配置切入点表达式,属性:

  • id:用于指定该表达式的唯一表示
  • expression:用于指定该切入点表达式内容
    <!--配置AOP-->
    <aop:config>
        <!--配置切面Aspect-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置advice类型(前置通知) 建立通知方法和切入点方法的关联-->
            <aop:before method="printLog"
                        pointcut="execution(* com.hz.service.impl.*.*(..))" ></aop:before>
            <!--后置通知 在切入点方法执行之后执行 如果切入点方法发生异常则不会执行-->
            <aop:after-returning method="afterPrintLog"
                                  pointcut-ref="pt1"></aop:after-returning>
            <!--异常通知 在切入点方法发生异常时执行 使用point-ref指定某一个共有的切入点表达式-->
            <aop:after-throwing method="exceptionPrintLog"
                             pointcut-ref="pt1"</aop:after-throwing>
            <!--最终通知 无论切入点方法是否发生异常都执行-->
            <aop:after method="finallyPrintLog"
                      pointcut-ref="pt1"></aop:after></aop:after>
        <!--配置切入点表达式 id用于指定该表达式的唯一标识 expression用于指定切入点表达式的内容-->
        <aop:pointcut id="pt1" expression="execution(* com.hz.service.impl.*.*(..))"></aop:pointcut>
        </aop:aspect>
    </aop:config>


那么在通知标签的内部使用pointcut-ref属性来指定该切入点表达式(指向该id),但是aop:pointcut标签写在aop:aspect标签内部只能给当前切面使用,不是该切面则不能使用;因此可以将aop:pointcut标签写在aop:aspect标签外部,此时该切入点表达式可以给所有切面使用。需要注意的是:

由于约束上的顺序要求,aop:pointcut标签需要配置在aop:aspect标签之前才能正常使用,否则无法使用。

 <!--配置AOP-->
    <aop:config>
        <!--配置切入点表达式 id用于指定该表达式的唯一标识 expression用于指定切入点表达式的内容-->
        <aop:pointcut id="pt1" expression="execution(* com.hz.service.impl.*.*(..))"></aop:pointcut>

        <!--配置切面Aspect-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置advice类型(前置通知) 建立通知方法和切入点方法的关联-->
            <aop:before method="printLog"
                        pointcut="execution(* com.hz.service.impl.*.*(..))" ></aop:before>
            <!--后置通知 在切入点方法执行之后执行 如果切入点方法发生异常则不会执行-->
            <aop:after-returning method="afterPrintLog"
                                 pointcut-ref="pt1"></aop:after-returning>
            <!--异常通知 在切入点方法发生异常时执行 使用point-ref指定某一个共有的切入点表达式-->
            <aop:after-throwing method="exceptionPrintLog"
                                pointcut-ref="pt1"></aop:after-throwing>
            <!--最终通知 无论切入点方法是否发生异常都执行-->
            <aop:after method="finallyPrintLog"
                       pointcut-ref="pt1"></aop:after>
        </aop:aspect>
    </aop:config>

2.4.7. 环绕通知

在logger类中加入环绕通知方法:

public void aroundPrintLog(){
     System.out.println("环绕advice!!!");
}

在xml中进行配置:

    <!--配置AOP-->
    <aop:config>
        <!--配置切入点表达式 id用于指定该表达式的唯一标识 expression用于指定切入点表达式的内容-->
        <aop:pointcut id="pt1" expression="execution(* com.hz.service.impl.*.*(..))"></aop:pointcut>
        <!--配置切面Aspect-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置advice类型(前置通知) 建立通知方法和切入点方法的关联-->
            <aop:before method="printLog"
                        pointcut="execution(* com.hz.service.impl.*.*(..))" ></aop:before>
            <!--后置通知 在切入点方法执行之后执行 如果切入点方法发生异常则不会执行-->
            <aop:after-returning method="afterPrintLog"
                                 pointcut-ref="pt1"></aop:after-returning>
            <!--异常通知 在切入点方法发生异常时执行 使用point-ref指定某一个共有的切入点表达式-->
            <aop:after-throwing method="exceptionPrintLog"
                                pointcut-ref="pt1"></aop:after-throwing>
            <!--最终通知 无论切入点方法是否发生异常都执行-->
            <aop:after method="finallyPrintLog"
                       pointcut-ref="pt1"></aop:after>
            <!--配置环绕通知-->
            <aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>

输出结果:
请添加图片描述

惊奇的发现,业务层中的切入点方法没有执行,而是执行了环绕通知。通过对比动态代理中的环绕通知的代码,发现动态代理中的环绕通知有明确的切入点方法调用,而在xml配置中并没有

解决方法:

Spring框架提供了一个接口ProceedingJoinPoint,该接口的proceed方法相当于明确调用了切入点方法。该接口可作为环绕通知的方法参数,在程序执行时,Spring框架会提供该接口的实现类供我们使用

环绕通知:4种基本通知类型使用配置的方式指定增强代码何时执行,而现在是使用代码的方式来控制增强方法何时执行,如果通知在proceed方法之前执行的即为前置通知,在proceed方法之后执行的即为后置通知,在catch中执行的即为异常通知,在finally中执行的即为最终通知。
spring中的around advice:spring框架提供的一种可以在代码中手动控制增强方法何时执行的方式

更改aroundLogger增强方法:

 public Object aroundPrintLog(ProceedingJoinPoint pjp){
        Object val = null;
        try {
            Object[] args = pjp.getArgs(); // 得到方法执行所需要的参数
            System.out.println("logger类中的aroundPrintLog方法执行... 前置");
            val = pjp.proceed(args);       // 明确调用业务层方法(切入点方法)
            System.out.println("logger类中的aroundPrintLog方法执行... 后置");
        } catch (Throwable throwable) {
            System.out.println("logger类中的aroundPrintLog方法执行... 异常");
            throwable.printStackTrace();
        }finally {
            System.out.println("logger类中的aroundPrintLog方法执行... 最终");
        }
        return val;
    }

正常打印结果:
请添加图片描述

2.5. 基于注解的AOP配置

对比xml配置中的方式:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置Spring的IoC 将service对象进行配置-->
    <bean id="accountService" class="com.hz.service.impl.AccountServiceImpl"></bean>
    
    <!--配置logger对象-->
    <bean id="logger" class="com.hz.utils.Logger"></bean>
    
    <!--配置AOP-->
    <aop:config>
        <!--配置切面Aspect-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置advice类型 建立通知方法(前置advice)和切入点方法的关联-->
            <aop:before method="printLog"
                        pointcut="execution(* com.hz.service.impl.*.*(..))" ></aop:before>
        </aop:aspect>
    </aop:config>
</beans>

2.5.1. 配置service对象

首先需要在xml文件中加入context约束,并且配置注解所在的包,告诉Spring创建IoC容器时需要扫描的包

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--配置spring创建容器时需要扫描的包-->
    <context:component-scan base-package="com.hz"></context:component-scan>

在serviceImpl类上加上@Service注解并且指明id

Service("accountService")
public class AccountServiceImpl implements AccountService {

    public void saveAccount() {
        System.out.println("execute save...");
    }

    public void updateAccount(Integer id) {
        System.out.println("execute update" + id);
    }

    public int deleteAccount() {
        System.out.println("execute delete...");
        return 1;
    }
}

2.5.2. 配置logger对象以及通知的配置

在xml中告诉Spring开启AOP的支持

    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

在logger类中进行切面和通知的配置,使用@Component注解配置logger对象并且指明id,由于logger类不是三层的实现类,因此不使用@Control,@Service和@Repository注解,当然想使用也是没有问题的。

@Component("Logger")
@Configuration
@ComponentScan(basePackages = "com.hz")
@EnableAspectJAutoProxy // 告诉spring开始启用aop注解进行配置
@Aspect                 // 表示当前类是一个切面类
public class Logger {

    @Pointcut("execution(* com.hz.service.impl.*.*(..))")
    private void pointCut1(){
    }
    /** 前置advice
     * 用于打印日志,计划让其在切入点方法执行之前执行(切入点方法就是业务层中的方法)
     */
    @Before("pointCut1()") // 传入切入点表达式需要带上括号
    public void printLog(){
        System.out.println("前置advice! Logger类中的printLog方法开始记录日志...");
    }

    /** 后置advice
     * 用于打印日志,计划让其在切入点方法执行之后执行(切入点方法就是业务层中的方法)
     */
    @AfterReturning("pointCut1()")
    public void afterPrintLog(){
        System.out.println("后置advice! Logger类中的afterPrintLog方法开始记录日志...");
    }

    /** 异常advice
     */
    @AfterThrowing("pointCut1()")
    public void exceptionPrintLog(){
        System.out.println("异常advice! Logger类中的exceptionPrintLog方法开始记录日志...");
    }

    /** 最终advice
     */
    @After("pointCut1()")
    public void finallyPrintLog(){
        System.out.println("最终advice! Logger类中的finallyPrintLog方法开始记录日志...");
    }
	/** 环绕通知
	*/
    @Around("pointCut1()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp){
        Object val = null;
        try {
            Object[] args = pjp.getArgs(); // 得到方法执行所需要的参数
            System.out.println("logger类中的aroundPrintLog方法执行... 前置");
            val = pjp.proceed(args);       // 明确调用业务层方法(切入点方法)
            System.out.println("logger类中的aroundPrintLog方法执行... 后置");
        } catch (Throwable throwable) {
            System.out.println("logger类中的aroundPrintLog方法执行... 异常");
            throwable.printStackTrace();
        }finally {
            System.out.println("logger类中的aroundPrintLog方法执行... 最终");
        }
        return val;
    }
}
  • @Aspect注解:表示该类是一个切面
  • @Before注解:前置通知
  • @AfterReturning:后置通知
  • @AfterThrowing:异常通知
  • @After:最终通知
  • Around:环绕通知

此时xml文件中还有两行语句,如果想使用纯注解的方式,在logger类中加上:

@Configuration @ComponentScan(basePackages = "com.hz") @EnableAspectJAutoProxy

前两个注解告诉Spring这是一个配置类和创建IoC容器需要扫描的包,@EnableAspectJAutoProxy注解告诉spring开始启用aop注解进行配置

2.5.3. JUnit测试

public class aopTest {

    public static void main(String[] args) {
//        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//        AccountService accountService = ac.getBean("accountService", AccountService.class);
//        // 由于切入点表达式中只配置了saveAccount方法与advice结合,因此只能完成saveAccount方法能够执行前置通知
//        accountService.saveAccount();

        // 使用纯注解运行AOP
        ApplicationContext ac1 = new AnnotationConfigApplicationContext(Logger.class);
        AccountService accountService1 = ac1.getBean("accountService", AccountService.class);
        accountService1.saveAccount();
    }
}

打印输出结果:
请添加图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

从现在开始壹并超

你的鼓励,我们就是hxd

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值