SpringAOP总结

Spring第2天之AOP

目录

  • 注解的IoC

    • 注册bean的注解:@Component("name"), @Controller, @Service, @Repository

    • 配置bean的注解:@Scope, @PostConstruct, @PreDestroy

    • 依赖注入的注解:

      • @Autowired:byType注入
      • @Autowired + @Qualifer:byName注入
      • @Resource(name=""):byName注入
      • @Value():注入简单值
    • 在xml里开启组件扫描

      <context:component-scan base-package="com.guang"/>
      
  • 纯注解的IoC:

    • 主要是代替掉xml文件
    • jar包里的类,要注册bean:
    @Configuration
    @ComponentScan("com.guang")
    @PropertySource("classpath:jdbc.properties")
    @Import({DaoConfig.class, ServiceConfig.class})
    public class AppConfig{
        
        @Value("${jdbc.driver}")
        private String driver;
        
        @Value("${jdbc.url}")
        private String url;
        
        @Value("${jdbc.username}")
        private String username;
        
        @Value("${jdbc.password}")
        private String password;
        
        @Bean("ds")
        public DataSource dataSource(){
            ComboPooledDataSource ds = new ComboPooledDataSource();
            ds.setDrvierClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
            return ds;
        }
        
        @Bean
        public QueryRunner runner(DataSource dataSource){
            return new QueryRunner(dataSource);
        }
    }
    

一、银行转账功能实现【理解】

转账功能实现

需求描述

  • tom给jerry转账1000,使用事务保证转账业务的功能正常
  • 本功能的目的,是为了说明AOP的作用和原理

功能分析

实现流程

实现步骤
  1. 准备工作:

    创建Maven项目,导入依赖

  2. 准备事务管理的工具类

  3. 准备dao、Service层代码

    1. 在测试类中,调用Service的转账方法transfer(转账人,收款人,转账金额)
    2. 在Service中:
    try{
    	====开启事务:获取连接开启事务,把连接绑定到当前线程上====
        1. 查询得到转账人信息Account对象:fromAccount
        2. 查询得到收款人信息Account对象:toAccount
        3. 修改转账人的金额:-转账金额; 修改收款人的金额:+转账金额
        4. 调用dao,更新转账人信息;
        5. 调用dao,更新收款人信息
    	====提交事务:获取当前线程上的连接,提交事务====
    }catch(Exception e){
    	====回滚事务:获取当前线程上的连接,回滚事务====
    }finally{
    	====关闭连接:获取当前线程上的连接,关闭连接,取消连接和线程绑定====
    }
    
    1. 在dao中:执行SQL语句所使用的连接对象Connection,必须是Service中开启事务的连接
    public void edit(Account account){
    	===从当前线程上获取连接===
    	使用此连接执行SQL语句:update account set ......
    }
    
  4. 提供Spring的配置文件

  5. 功能测试

需求实现

1. 准备工作

创建Maven项目,导入依赖

<dependencies>
    <!--MySql驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <!--c3p0-->
    <dependency>
        <groupId>com.mchange</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.5.4</version>
    </dependency>
    <!--dbutils-->
    <dependency>
        <groupId>commons-dbutils</groupId>
        <artifactId>commons-dbutils</artifactId>
        <version>1.7</version>
    </dependency>
    <!--spring-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--spring-test-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--junit-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>
2. 准备事务管理的工具类
  • TransactionManager:事务管理的工具类,提供了开启事务、提交事务、回滚事务等方法
    • 注意:需要给这个工具类注入连接池对象dataSource,不要忘记提供一个连接池对象
@Component
public class TransactionManager {
    private ThreadLocal<Connection> tl = new ThreadLocal<>();

    //需要注入一个连接池
    @Autowired
    private DataSource dataSource;

    /**
     * 开启事务
     */
    public void startTransaction() throws Exception{
        //1.创建连接对象
        Connection connection = dataSource.getConnection();
        //2.开启事务
        connection.setAutoCommit(false);
        //3.使用ThreadLocal对象,把连接绑定到当前线程上
        tl.set(connection);
    }

    /**
     * 从当前线程上获取绑定的那个连接对象
     */
    public Connection getConnection(){
        return tl.get();
    }

    /**
     * 提交事务并关闭连接
     */
    public void commitAndClose(){
        try {
            //从当前线程上获取连接
            Connection connection = tl.get();
            //提交事务
            connection.commit();
            //关闭连接
            connection.close();
            //把连接从当前线程上清理掉
            tl.remove();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 回滚事务并关闭连接
     */
    public void rollbackAndClose(){
        try {
            //从当前线程上获取连接
            Connection connection = tl.get();
            //回滚事务
            connection.rollback();
            //关闭连接
            connection.close();
            //把连接从当前线程上清理掉
            tl.remove();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
3. 准备dao、Service层代码
  • AccountDaoAccountDaoImpl
public interface AccountDao {
    void edit(Account account) throws SQLException;

    Account findByName(String name) throws SQLException;
}
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private QueryRunner runner;
    @Autowired
    private TransactionManager txManager;

    @Override
    public Account findByName(String name) throws SQLException {
        return runner.query(txManager.getConnection(), "select * from account where name = ?", new BeanHandler<>(Account.class), name);
    }

    @Override
    public void edit(Account account) throws SQLException {
        runner.update(txManager.getConnection(),"update account set name=?,money=? where id=?", account.getName(), account.getMoney(), account.getId());
    }
}
  • AccountServiceAccountServiceImpl
public interface AccountService {
    void transfer(String out, String in, Float money);
}
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;
    @Autowired
    private TransactionManager txManager;

    @Override
    public void transfer(String out, String in, Float money) {
        try {
            txManager.startTransaction();

            Account outAccount = accountDao.findByName(out);
            Account inAccount = accountDao.findByName(in);

            outAccount.setMoney(outAccount.getMoney() - money);
            inAccount.setMoney(inAccount.getMoney() + money);

            accountDao.edit(outAccount);
            int i = 1/0;
            accountDao.edit(inAccount);

            txManager.commitAndClose();
        } catch (Exception e) {
            e.printStackTrace();
            txManager.rollbackAndClose();
        }
    }
}
4. 提供Spring的配置文件
  • applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!--开启组件扫描-->
    <context:component-scan base-package="com.guang"/>

    <!--配置连接池-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql:///spring"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>

    <!--配置QueryRunner-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
        <constructor-arg name="ds" ref="dataSource"/>
    </bean>
</beans>
5. 功能测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TransferTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() throws SQLException {
        accountService.transfer("tom","jerry", 1000f);
    }
}
存在的问题
  • 事务管理的代码,和Service层的业务功能代码耦合到了一起,不易维护
    • 如果有很多方法都需要事务,就需要对代码进行大量修改
  • 要达到的目标:
    • service层,只要负责业务功能的实现,不要在Service层出现非业务功能相关的代码
    • 在service层源码不变的情况下,要对Service层的功能进行增强:增加事务管理的功能

使用动态代理优化转账功能

需求描述

  • 实现银行转账,并且事务管理代码 和 转账功能代码解耦
  • 不修改银行转账的功能代码,又增加事务管理的功能

需求分析

功能分析
  • AccountServiceImpl.transfer()方法中,只保留银行转账的业务功能代码

  • 获取一个AccountServiceImpl的代理对象

    通过动态代理的方式,对AccountServiceImpl.transfer方法的功能进行增强:增加事务控制的代码

  • 测试类中调用代理对象,实现转账+事务控制

实现步骤
  1. 修改AccountServiceImpl

    去掉所有事务管理相关的代码

  2. 创建一个代理工厂类AccountServiceProxyFactory,把工厂声明为一个bean

    提供一个非静态方法,用于生产AccountServiceImpl的代理对象,在代理对象中增加事务控制的代码

  3. 修改applicationContext.xml

    使用非静态工厂的方式,把工厂对象生产的代理对象,定义为一个bean

  4. 测试类中,调用代理对象进行转账

需求实现

1. 修改AccountServiceImpl
  • 去掉所有事务管理相关的代码
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Override
    public void transfer(String out, String in, Float money) throws SQLException {
        Account outAccount = accountDao.findByName(out);
        Account inAccount = accountDao.findByName(in);

        outAccount.setMoney(outAccount.getMoney() - money);
        inAccount.setMoney(inAccount.getMoney() + money);

        accountDao.edit(outAccount);
        //int i = 1 / 0;
        accountDao.edit(inAccount);
    }
}
2. 创建代理工厂类AccountServiceProxyFactory
  • 用于生产AccountServiceImpl的代理对象,在代理对象中增加事务控制的代码
@Component("accountServiceProxyFactory")
public class AccountServiceProxyFactory {

    @Autowired
    private AccountService accountService;

    @Autowired
    private TransactionManager txManager;

    public AccountService createProxy(){
        return (AccountService) Proxy.newProxyInstance(
                accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object result = null;

                        try {
                            //开启事务
                            txManager.startTransaction();
                            //调用目标对象的方法:执行转账
                            result = method.invoke(accountService, args);
                            //提交事务
                            txManager.commitAndClose();
                        } catch (Exception e) {
                            e.printStackTrace();
                            //回滚事务
                            txManager.rollbackAndClose();
                        }
                        return result;
                    }
                }
        );
    }
}
3. 修改applicationContext.xml
  1. 把工厂类AccountServiceProxyFactory配置成为一个bean,id是:accountProxyFactory
  2. 配置使用工厂类生产一个AccountService的代理对象
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!--开启组件扫描-->
    <context:component-scan base-package="com.guang"/>

    <!--配置连接池-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql:///spring"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>

    <!--配置QueryRunner-->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
        <constructor-arg name="ds" ref="dataSource"/>
    </bean>

    <!--配置accountService的代理对象:增加了事务控制-->
    <bean id="accountServiceProxy" factory-bean="accountServiceProxyFactory" factory-method="createProxy"></bean>
</beans>
4. 功能测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TransferTest {

    @Autowired
    @Qualifier("accountServiceProxy")
    private AccountService accountService;

    @Test
    public void testTransfer() throws SQLException {
        accountService.transfer("tom","jerry", 1000f);
    }
}

总结

  • 达到了目标:

    • 对银行转账方法进行了增强:事务控制
    • 方法功能增强了,但是没有修改转账功能源码
  • 其中:

    • 被增强的目标对象:AccountServiceImpl
    • 被增强的目标方法:transfer
    • 功能增强的类:TransactionManager
  • 如何增强的:使用了动态代理技术

    • 实际使用的是目标对象的代理对象,在代理对象里进行了功能增强
  • 存在的问题:

    • 我们还需要自己编写代码,生成代理对象
    • 还不能控制增强哪些功能
  • 问题的解决:

    • 使用Spring的AOP思想:Spring会帮我们生成代理对象
    • 我们只要进行配置:对哪些方法,进行哪些增强

二、AOP简介

什么是AOP

  • AOP:Aspect Oriented Programming,面向切面编程。是通过预编译方式(aspectj)或者运行期动态代理(Spring)实现程序功能的统一维护的技术。
  • AOP是OOP(面向对象编程)的技术延续,是软件开发中的一个热点,也是Spring中的一个重要内容。利用AOP可以实现对业务逻辑各个部分之间的隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时提高了开发效率。

AOP的作用

  • 作用:不修改源码的情况下,进行功能增强,通过动态代理实现的
  • 优势:减少重复代码,提高开发效率,方便维护
  • 比如:给功能增加日志输出, 事务管理的功能

AOP的底层实现

​ 实际上,Spring的AOP,底层是通过动态代理实现的。在运行期间,通过代理技术动态生成代理对象,代理对象方法执行时进行功能的增强介入,再去调用目标方法,从而完成功能增强。

  • 常用的动态代理技术有:

    • JDK的动态代理:基于接口实现的
    • cglib的动态代理:基于子类实现的
  • Spring的AOP采用了哪种代理方式?

    • 如果目标对象有接口,就采用JDK的动态代理技术
    • 如果目标对象没有接口,就采用cglib技术

小结

  • AOP是:在不修改源码的情况下,进行功能增强
  • AOP的本质是:动态代理

三、Spring的AOP【重点】

AOP相关的概念

AOP相关概念

  • 目标对象(Target):要代理的/要增强的目标对象。

  • 代理对象(Proxy):目标对象被AOP织入增强后,就得到一个代理对象

  • 连接点(JoinPoint):能够被拦截到的点,在Spring里指的是方法

    目标类里,所有能够进行增强的方法,都是连接点

  • 切入点(PointCut):要对哪些连接点进行拦截的定义

    已经增强的连接点,叫切入点

  • 通知/增强(Advice):拦截到连接点之后要做的事情

    对目标对象的方法,进行功能增强的代码

  • 切面(Aspect):是切入点和通知的结合

  • 织入(Weaving):把增强/通知 应用到 目标对象来创建代理对象的过程。Spring采用动态代理技术织入,而AspectJ采用编译期织入和装载期织入

AOP开发前要明确的事项

我们要做的事情:
  • 编写核心业务代码(Target目标类的目标方法)
  • 编写通知类,通知类中有通知方法(Advice增强功能方法)
  • 在配置文件中,配置织入关系,即将哪些通知与哪些切入点 结合,形成切面
Spring的AOP做的事情:
  • 生成动态代理的过程(把通知织入到切入点的过程),是由Spring来实现的
  • Spring会监控切入点方法的执行,一旦发现切入点方法执行,使用代理机制动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

小结

  • AOP相关的概念/术语
    • 目标类Target:要对哪个类进行增强
    • 代理对象Proxy:对目标类增强后的那个代理对象
    • 连接点JoinPoint:目标类里可增强的方法
    • 切入点PointCut:要增强的方法
    • 通知Advice:功能增强的代码
    • 切面Aspect:切入点 + 通知
    • 织入Weaving:把切入点 和 通知 进行结合,生成代理对象的过程
  • 使用AOP,我们要做的事情:
    • 编写目标类,自己的业务代码
    • 编写通知类
    • 配置切面
  • 使用AOP,Spring做的事情
    • 根据我们配置的切面,进行织入生成代理对象

基于XML的AOP【重点】

快速入门

1) 需求描述
  • 有目标类UserServiceImpl,有通知类MyAdvice
  • 使用XML方式AOP,对目标类UserServiceImpl的方法进行增强
2) 步骤分析
  1. 创建maven项目,导入AOP相关的依赖坐标
  2. 创建目标类(要增强的类,内部有切入点),创建通知类(内部有增强的方法代码)
  3. 修改配置文件:
    1. 把目标类和通知类都配置成为bean对象
    2. 配置切入点和通知方法(增强方法)的织入关系:配置切面
  4. 测试代码
3) 入门实现
1. 创建maven项目,导入坐标
<dependencies>
    <!--Spring上下文核心包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--AOP的实现包-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.9</version>
    </dependency>
    <!--Spring和单元测试集成-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--单元测试-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>
2. 创建目标类和通知类
  • 目标类:com.guang.aop.UserServiceImpl
public class UserService{
    void save();
}
public class UserServiceImpl {
    public void save(){
        System.out.println("UserServiceImpl.save......");
    }
}
  • 通知类:com.guang.aop.MyAdvice
public class MyAdvice {
    public void before(){
        System.out.println("前置通知...");
    }
}
3. 修改配置文件
  1. 把目标类和通知类都配置到Spring配置文件中
  2. 配置切入和通知方法(增强方法)的织入关系
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置要增强的目标对象-->
    <bean id="userService" class="com.guang.service.impl.UserServiceImpl"/>
    <!--配置切面对象-->
    <bean id="myAdvice" class="com.guang.aop.MyAdvice"/>
    <!--配置AOP-->
    <aop:config>
        <!--配置切面。切面:通知方法+切入点-->
        <aop:aspect ref="myAdvice">
            <!--配置通知-->
            <aop:before method="before" pointcut="execution(void com.guang.service.impl.UserServiceImpl.save())"/>
        </aop:aspect>
    </aop:config>
</beans>

注意:在xml中增加了aop的名称空间如下:

4. 测试代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AopTest {
    @Autowired
    private UserService userService;

    @Test
    public void testQuickStart(){
        userService.save();
    }
}
4) 步骤小结
  1. 导入jar包:spring-context, aspectjweaver
  2. 编写目标类、编写通知类
  3. 配置切面
<aop:config>
	<aop:aspect ref="通知对象">
        <aop:before method="通知对象里的通知方法" pointcut="切入点表达式"/>
    </aop:aspect>
</aop:config>

AOP详解

1) 切点表达式的写法
语法:
execution([权限修饰符] 返回值类型 包名.类名.方法名(参数列表))
  • 修饰符:可以省略
  • 返回值类型:
    • 可以指定类型。比如String (如果类型有歧义,就写全限定类名,比如:java.util.Date
    • *,表示任意字符。比如Str*,或者*
  • 包名:
    • 可以写.:表示当前包下的类或者子包。比如com.guang.service
    • 可以写..:表示当前包里所有后代类、后代包。比如com..service
    • *:表示任意字符。比如:com.it*, com.*
  • 类名:
    • 可以指定类名。比如:UserServiceImpl
    • * 表示任意字符。比如:*ServiceImpl*
  • 方法名:
    • 可以指定方法名
    • * 表示任意字符。比如:save**
  • 参数列表:
    • 可以指定类型。比如:String,Integer表示第一个参数是String,第二个参数是Integer类型
    • *表示任意字符。比如:
      • String, * 表示第一个参数是String,第二个参数是任意类型
      • Str*, Integer表示第一个参数类型Str开头,第二个参数是Integer类型
    • 可以使用..表示任意个数、任意类型的参数
示例
execution(public void com.guang.dao.impl.UserDao.save())
execution(void com.guang.dao.impl.UserDao.*(..))
execution(* com.guang.dao.impl.*.*(..))
execution(* com.guang.dao..*.*(..))
execution(* *..*.*(..)) --不建议使用
2) 通知的种类
通知的语法
<aop:通知类型 method="通知中的方法" pointcut="切点表达式"></aop:通知类型>
通知的类型
名称 标签 说明
前置通知 <aop:before> 通知方法在切入点方法之前执行
后置通知 <aop:after-returning> 在切入点方法正常执行之后,执行通知方法
异常通知 <aop:after-throwing> 在切入点方法抛出异常时,执行通知方法
最终通知 <aop:after> 无论切入点方法是否有异常,最终都执行通知方法
环绕通知 <aop:around> 通知方法在切入点方法之前、之后都执行
通知示例

注意:通知方法的名称随意,我们这里是为了方便理解,才起名称为:before, after等等

  • 前置通知

    • 通知方法定义MyAdvicebefore方法:
    public void before(){
        System.out.println("前置通知");
    }
    
    • xml配置
    <aop:before method="before" 
                pointcut="execution(void com.guang.service..*.save())"/>
    
  • 后置通知

    • 通知方法定义
    public void afterReturning(){
        System.out.println("后置通知");
    }
    
    • xml配置
    <aop:after-returning method="afterReturning" 
                         pointcut="execution(void com.guang.service..*.save())"/>
    
  • 环绕通知

    • 通知方法定义
    /**
     * @param pjp ProceedingJoinPoint:正在执行的切入点方法对象
     * @return 切入点方法的返回值
     */
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("环绕:前置通知...");
    
        Object[] args = pjp.getArgs();
        //切入点方法执行
        Object proceed = pjp.proceed(args);
    
        System.out.println("环绕:后置通知...");
    
        return proceed;
    }
    
    • xml配置
    <aop:around method="around" 
                pointcut="execution(void com.guang.service..*.save())"/>
    
  • 异常抛出通知

    • 通知方法定义
    public void afterThrowing(){
        System.out.println("抛出异常通知");
    }
    
    • xml配置
    <aop:after-throwing method="afterThrowing" 
                        pointcut="execution(void com.guang.service..*.save())"/>
    
  • 最终通知

    • 通知方法定义
    public void after(){
        System.out.println("最终通知");
    }
    
    • xml配置
    <aop:after method="after" 
               pointcut="execution(void com.guang.service..*.save())/>
    
3) 切点表达式的抽取
  • 当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取;在增强中使用pointcut-ref代替pointcut,来引入切入点表达式。

  • 示例:

    <!--配置AOP-->
    <aop:config>
        <!--配置切入点表达式-->
        <aop:pointcut id="myPointCut" 
                      expression="execution(void com.guang.service..*.save())"/>
        <!--配置切面-->
        <aop:aspect ref="myAdvice">
            <!--前置通知-->
            <aop:before method="before" pointcut-ref="myPointCut"/>
            <!--后置通知-->
            <aop:after-returning method="afterReturning" pointcut-ref="myPointCut"/>
            <!--异常通知-->
            <aop:after-throwing method="afterThrowing" pointcut-ref="myPointCut"/>
            <!--最终通知-->
            <aop:after method="after" pointcut-ref="myPointCut"/>
        </aop:aspect>
    </aop:config>
    
4) 小结
  • 需要我们编写的内容:
    • 编写目标类,编写通知类
    • 配置切面
<aop:config>
	<aop:pointcut id="xxx" expression="切入点表达式"/>
    <aop:aspect ref="通知对象">
        <aop:before method="通知对象里的通知方法" pointcut-ref="xxx"/>
        <aop:after-returning method="通知对象里的通知方法" pointcut-ref="xxx"/>
        <aop:after-throwing method="通知对象里的通知方法" pointcut-ref="xxx"/>
        <aop:after method="通知对象里的通知方法" pointcut-ref="xxx"/>
        
        <aop:around method="通知对象里的通知方法" pointcut-ref="xxx"/>
    </aop:aspect>
</aop:config>
  • 注意环绕通知的方法
public Object aroundMethod(ProceedingJoinPoint pjp){
	Object reuslt = null;
    
    try{
        //写前置通知代码
        
        //调用目标对象的方法
    	result = pjp.proceed(pjp.getArgs());
        
        //写后置通知代码
    }catch(Throwable t){
        //写异常通知代码
    }finally{
        //写最终通知代码
    }
}

基于注解的AOP【重点】

快速入门

1) 需求描述
  • 有目标类UserServiceImpl,有通知类MyAdvice
  • 使用注解方式的AOP对目标类UserServiceImpl的方法进行增强
2) 步骤分析
  1. 创建maven项目,导入AOP需要的依赖坐标
  2. 创建目标类,创建通知类
    1. 使用注解@Component标注两个类,配置成为bean对象
    2. 在通知类中,使用注解配置织入关系
  3. 在配置文件中,开启组件扫描和AOP的自动代理(自动装配)
  4. 测试
3) 入门实现
1. 创建maven项目,导入坐标
  • 注意:需要增加AOP的实现包:aspectjweaver
<dependencies>
    <!--Spring上下文核心包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--AOP的实现包-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.9</version>
    </dependency>
    <!--Spring和单元测试集成-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--单元测试-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>
2. 创建目标类,创建通知类
  1. 使用注解标注两个类,配置成为bean对象

    • 实际开发中,使用@Repository, @Service, @Controller注解,按照分层进行配置
  2. 在通知类中,使用注解配置织入关系

    • 目标类com.guang.aop.Target
    public class UserService{
        void save();
    }
    
    @Service("userService")
    public class UserServiceImpl {
        public void save(){
            System.out.println("UserServiceImpl.save......");
        }
    }
    
    • 通知类com.guang.aop.MyAdvice
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    //声明当前类是切面类:把切入点和通知,在这个类里进行织入,当前类就成为了一个切面类
    @Aspect 
    @Component("myAdvice")
    public class MyAdvice {
    
        @Before("execution(void com.guang.impl..*.save())")
        public void before(){
            System.out.println("前置通知...");
        }
    
        @AfterReturning("execution(void com.guang.impl..*.save()))")
        public void afterReturning(){
            System.out.println("后置通知");
        }
    
        @After("execution(void com.guang.impl..*.save())")
        public void after(){
            System.out.println("最终通知");
        }
    
        @AfterThrowing("execution(void com.guang.impl..*.save())")
        public void afterThrowing(){
            System.out.println("抛出异常通知");
        }
    
        /**
         * @param pjp ProceedingJoinPoint:正在执行的切入点方法对象
         * @return 切入点方法的返回值
         */
        @Around("execution(void com.guang.impl..*.save())")
        public Object around(ProceedingJoinPoint pjp) throws Throwable {
            System.out.println("环绕:前置通知...");
    
            //切入点方法执行
            Object proceed = pjp.proceed();
    
            System.out.println("环绕:后置通知...");
    
            return proceed;
        }
    }
    
4. 开启组件扫描和AOP自动代理
  • applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--开启组件扫描-->
    <context:component-scan base-package="com.guang"/>

    <!--开启AOP自动代理-->
    <aop:aspectj-autoproxy/>
</beans>

如果要使用纯注解开发,可以使用配置类代替applicationContext.xml,配置类如下:

@Configuration //标记当前类是:配置类
@ComponentScan(basePackage="com.guang") //配置注解扫描
@EnableAspectJAutoProxy //开启AOP自动代理
public class AppConfig{   
}
5. 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AopTest {

    @Autowired
    private UserService userService;

    @Test
    public void testQuickStart(){
        userService.save()
    }
}
4) 步骤小结

AOP详解

1) 通知的种类
通知的语法
@通知注解("切入点表达式")
通知的类型
名称 注解 说明
前置通知 @Before 通知方法在切入点方法之前执行
后置通知 @AfterRuturning 通知方法在切入点方法之后执行
异常通知 @AfterThrowing 通知方法在抛出异常时执行
最终通知 @After 通知方法无论是否有异常,最终都执行
环绕通知 @Around 通知方法在切入点方法之前、之后都执行
  • 注意:
    • 注解方式配置的通知,执行顺序是:前置->最终->后置/异常
    • 如果想要指定执行的顺序,就使用环绕通知
2) 切点表达式的抽取
  • 同xml的AOP一样,当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取;
  • 抽取方法是:
    • 在增强类(切面类,即被@Aspect标的类)上增加方法,在方法上使用@Pointcut注解定义切入点表达式,
    • 在增强注解中引用切入点表达式所在的方法
  • 示例:
@Aspect
@Component("myAdvice1")
public class MyAdvice1 {

    //定义切入点表达式
    @Pointcut("execution(void com.guang.service..*.save())")
    public void myPointcut(){}

    //引用切入点表达式
    //完整写法:com.guang.aop.MyAdvice.myPointcut()
    //简单写法:myPointcut(), 引入当前类里定义的表达式,可以省略包类和类名不写
    @Before("myPointcut()")
    public void before(){
        System.out.println("前置通知...");
    }
   
    @AfterReturning("myPointcut()")
    public void afterReturning(){
        System.out.println("后置通知");
    }

    @After("myPointcut()")
    public void after(){
        System.out.println("最终通知");
    }

    @AfterThrowing("myPointcut()")
    public void afterThrowing(){
        System.out.println("抛出异常通知");
    }

    /*@Around("myPointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("前置通知...");

        //切入点方法执行
        Object proceed = pjp.proceed();

        System.out.println("后置通知...");

        return proceed;
    }*/
}
3) 小结
  1. 在通知类上加注解@Aspect,声明成一个切面
  2. 在通知类里方法上加注解@Before/@AfterReturning/@AfterThrowing/@After/@Around,配置切入点表达式
  3. 在xml里开启aop的自动代理:<aop:aspectj-autoproxy/>

纯注解的AOP

  • 主要是把XML的配置,放到核心配置类上
@Configuration
@ComponentScan(basePackages="com.guang")//开启组件扫描
@EnableAspectJAutoProxy //开启AOP的自动代理
public class AppConfig{
    
}
@Aspect
@Component("myAdvice2")
public class MyAdvice2 {

    //定义切入点表达式
    @Pointcut("execution(void com.guang.service..*.save())")
    public void myPointcut(){}

    @Before("myPointcut()")
    public void before(){
        System.out.println("前置通知...");
    }
   
    @AfterReturning("myPointcut()")
    public void afterReturning(){
        System.out.println("后置通知");
    }

    @After("myPointcut()")
    public void after(){
        System.out.println("最终通知");
    }

    @AfterThrowing("myPointcut()")
    public void afterThrowing(){
        System.out.println("异常通知");
    }
}

四、配置第三方连接池

准备环境

  • 创建Maven项目,导入jar依赖
<dependencies>
    <!--MySql驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <!--c3p0连接池-->
    <dependency>
        <groupId>c3p0</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.1.2</version>
    </dependency>
    <!--druid连接池-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.19</version>
    </dependency>
    <!--Spring的jdbc支持-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>

    <!--Spring-context-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--Spring整合Junit-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--单元测试Junit-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

配置连接池

  • 创建applicationContext.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--c3p0的连接池-->
    <bean id="c3p0Ds" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql:///spring99"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>

    <!--druid连接池-->
    <bean id="druidDs" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql:///spring99"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>

    <!--Spring内置的数据源(没有实现连接池技术)-->
    <bean id="springDs" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql:///spring99"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>
</beans>

功能测试

  • 创建单元测试类
/**
 * @author liuyp
 * @date 2020/04/07
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class DataSourceTest {
    @Autowired
    private DataSource c3p0Ds;

    @Autowired
    private DataSource druidDs;

    @Autowired
    private DataSource springDs;

    @Test
    public void testC3p0() throws SQLException {
        Connection connection = c3p0Ds.getConnection();
        System.out.println(connection);
        connection.close();
    }

    @Test
    public void testDruid() throws SQLException {
        Connection connection = druidDs.getConnection();
        System.out.println(connection);
        connection.close();
    }

    @Test
    public void testSpring() throws SQLException {
        Connection connection = springDs.getConnection();
        System.out.println(connection);
        connection.close();
    }
}

五、JdbcTemplate

1. 在Spring里使用JdbcTemplate

JdbcTemplate介绍

JdbcTemplate简介
  • JdbcTemplate是Spring提供的dao层技术,它对JDBC进行了薄薄的封装,用于简化JDBC操作的步骤
    • 和DBUtils的作用一样的
  • Spring通过JdbcTemplate,实现了声明式事务管理
  • JdbcTemplate需要导入jar包
JdbcTemplate的API
  • JdbcTemplate的API

在Spring里配置JdbcTemplate

  1. 在pom.xml里添加依赖
<!--Spring的jdbc支持-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
  1. applicationContext.xml里配置JdbcTemplate
<!--配置JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
</bean>

使用示例

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class DemoJdbcTemplateTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void testInsert(){
        jdbcTemplate.update("insert into account (id,name,money) values (?,?,?)", null, "lisi", 2000f);
    }

    @Test
    public void testUpdate(){
        jdbcTemplate.update("update account set name = ?, money = ? where id = ?", "zhangsan", 2000f, 6);
    }

    @Test
    public void testDelete(){
        jdbcTemplate.update("delete from account where id = ?", 6);
    }

    @Test
    public void testQueryOne(){
        Account account = jdbcTemplate.queryForObject("select * from account where id = ?", new BeanPropertyRowMapper<>(Account.class), 4);
        System.out.println(account);
    }

    @Test
    public void testQueryMany(){
        List<Account> accounts = jdbcTemplate.query("select * from account", new BeanPropertyRowMapper<>(Account.class));
        for (Account account : accounts) {
            System.out.println(account);
        }
    }

    @Test
    public void testQueryMap(){
        Map<String, Object> map = jdbcTemplate.queryForMap("select * from account where id = ?", 5);
        System.out.println(map);
    }

    @Test
    public void testQueryList(){
        List<Map<String, Object>> mapList = jdbcTemplate.queryForList("select * from account");
        for (Map<String, Object> map : mapList) {
            System.out.println(map);
        }
    }

    @Test
    public void testTotalCount(){
        Integer count = jdbcTemplate.queryForObject("select count(*) from account", Integer.class);
        System.out.println(count);
    }
}

小结

2. 在dao中使用JdbcTemplate

  • 有dao接口如下:
public interface AccountDao {
    List<Account> queryAll();

    Account findById(Integer id);
}

在dao中定义JdbcTemplate并XML注入

  • AccountDaoImpl_1
public class AccountDaoImpl_1 implements AccountDao {
    private JdbcTemplate jdbcTemplate;
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public List<Account> queryAll() {
        return jdbcTemplate.query("select * from account", new BeanPropertyRowMapper<>(Account.class));
    }

    @Override
    public Account findById(Integer id) {
        return jdbcTemplate.queryForObject("select * from account where id = ?", new BeanPropertyRowMapper<>(Account.class), id);
    }
}
  • applicationContext.xml
<bean id="accountDao1" class="com.guang.dao.impl.AccountDaoImpl_1">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>

dao继承JdbcDaoSupport并XML注入

  • AccountDaoImpl_2
public class AccountDaoImpl_2 extends JdbcDaoSupport implements AccountDao {
    @Override
    public List<Account> queryAll() {
        return getJdbcTemplate().query("select * from account", new BeanPropertyRowMapper<>(Account.class));
    }

    @Override
    public Account findById(Integer id) {
        Account account = getJdbcTemplate().queryForObject("select * from account where id = ?", new BeanPropertyRowMapper<>(Account.class), id);
        return account;
    }
}
  • applicationContext.xml
<bean id="accountDao2" class="com.guang.dao.impl.AccountDaoImpl_2">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
  • 参考JdbcDaoSupport类的源码如下:
public abstract class JdbcDaoSupport extends DaoSupport {

	@Nullable
	private JdbcTemplate jdbcTemplate;

	public final void setJdbcTemplate(@Nullable JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
		initTemplateConfig();
	}

	@Nullable
	public final JdbcTemplate getJdbcTemplate() {
		return this.jdbcTemplate;
	}

    //............其它代码略................
}

在dao里定义JdbcTemplate注解注入

@Repository("accountDao")
public class AccountDaoImpl implements AccountDao{
    @Autowired
    private JdbcTemplate jdbcTemplate;
}

六、Spring的事务管理

  • 事务的作用:保证事务里多个操作,要么全部成功,要么全部失败
  • 目标:
    • 了解事务管理的API
    • 理解事务的传播行为

1. 编程式事务管理【了解】

  • 所谓事务管理,即:按照给定的事务规则,来执行提交或回滚操作。其中:
    • "给定的事务规则":用TransactionDefinition表示
    • "按照..来执行提交或回滚操作":用PlatformTransactionManager来完成
    • TransactionStatus用于表示一个运行着的事务的状态

关于编程式事务的说明

  • 编程式事务管理:通过编写代码的方式实现事务管理

    • 编程式事务管理,因事务管理与业务功能耦合性太强,不方便维护,目前已经基本不用

      spring 2.0 就已经提供了 xml配置的声明式事务管理的支持

    • 如果想要了解Spring的编程式事务,可参考《资料/spring02_transaction_program》

  • 以下API仅做介绍了解,用于了解Spring事务相关的API,并回顾事务相关的概念

PlatformTransactionManager

  • 是Spring提供的事务管理器接口,它提供了我们常用的操作事务的方法:开启事务、提交事务等
  • 注意:PlatformTransactionManager是接口类型,不同的dao层技术有不同的实现,例如:
    • dao层是jdbcTemplate或Mybatis时,实现类是:DataSourceTransactionManager
    • dao层是Hibernate时,实现类是:HibernateTransactionManager
方法 返回值 说明
getTransaction(TransactionDefinition td) TransactionStatus 开启事务,并得到事务状态
commit(TransactionStatus status) 提交事务
rollback(TransactionStatus status) 回滚事务

TransactionDefinition

  • 事务的定义信息对象,提供了以下常用方法:
方法 参数 返回值 说明
getIsolationLevel() int 获取事务的隔离级别
getPropogationBehavior() int 获取事务的传播行为
getTimeout() int 获取超时时间
isReadOnly() boolean 是否只读的事务
事务的隔离级别:
  • ISOLATION_DEFAULT:默认事务隔离级别
    • MySql默认隔离级别:repeatable read
    • Oracle默认隔离级别:read committed
  • ISOLATION_READ_UNCOMMITTED:读未提交--存在脏读、不可重复读、幻读
  • ISOLATION_READ_COMMITTED:读已提交--存在不可重复读、幻读
  • ISOLATION_REPEATABLE_READ:重复读--存在幻读
  • ISOLATION_SERIALIZABLE:串行化--没有并发问题
事务的传播行为:

用于解决业务方法调用业务方法时,事务的统一性问题的

以下三个,是要当前事务的

  • PROPAGATION_REQUIRED需要有事务。默认
    • 如果有事务,就使用这个事务
    • 如果没有事务,就创建事务。
  • PROPAGATION_SUPPORTS:支持事务
    • 如果有事务,就使用当前事务,
    • 如果没有事务,就以非事务方式执行(没有事务)
  • PROPAGATION_MANDATORY:强制的
    • 如果有事务,就使用当前事务
    • 如果没有事务,就抛异常

以下三个,是不要当前事务的

  • PROPAGATION_REQUIRES_NEW:新建的
    • 如果有事务,就把事务挂起,再新建事务
    • 如果没有事务,新建事务
  • PROPAGATION_NOT_SUPPORTED:不支持的
    • 如果有事务,就把事务挂起,以非事务方式执行
    • 如果没有事务,就以非事务方式执行
  • PROPAGATION_NEVER:非事务的
    • 如果有事务,就抛异常
    • 如果没有事务,就以非事务方式执行

最后一个,是特殊的

  • PROPAGATION_NESTED:嵌套的
    • 如果有事务,就在事务里再嵌套一个事务执行
    • 如果没有事务,就是类似REQUIRED的操作
事务运行的超时时间:

超时后事务自动回滚

  • 默认值-1,表示没有超时限制
  • 如果有,可以以秒为单位进行设置
是否只读:
  • 如果设置为只读,那么方法只能查询,不能增删改
  • 通常是查询方法设置为只读

TransactionStatus

  • 提供了查询事务具体运行状态的方法,常用方法如下:
方法 返回值 说明
hasSavePoint() boolean 事务是否有回滚点
isCompleted() boolean 事务是否已经完成
isNewTransaction() boolean 是否是新事务
isRollbackOnly() boolean 事务是否是 要回滚的状态

小结

  • PlatformTransactionManager接口:
    • 如果dao层用的是Mybatis、JdbcTemplate:用DataSourceTransactionManager
    • 如果dao层用的是Hibernate:用HibernateTransactionManager
  • 事务定义信息:
    • 事务的隔离级别:通常使用默认ISOLATION_DEFAULT
    • 事务的传播行为:通常使用默认PROPAGATION_REQUIRED
    • 事务的超时时间:如果事务执行超时,会回滚。单位是秒。值为-1表示永不超时
    • 事务是否是只读:如果只读,事务里只能执行查询操作,不能增删改

2. 声明式事务管理【重点】

转账功能的环境准备

  • tom给jerry转账,不带事务的功能实现,为后边的事务控制做准备
1) 创建Maven项目,导入依赖坐标
  • 这里的连接池使用Spring内置的DriverManagerDataSource(可以用任意连接池技术)
  • dao层技术要使用JdbcTemplate,不能使用dbutils
<dependencies>
    <!--MySql驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <!--c3p0连接池-->
    <dependency>
        <groupId>c3p0</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.1.2</version>
    </dependency>
    <!--Spring上下文-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--Spring的JDBC和事务支持-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--Aspect-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.9</version>
    </dependency>
    <!--Spring整合Junit-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--Junit-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>
2) 创建JavaBean,dao,Service
  • JavaBean:
public class Account {
    private Integer id;
    private String name;
    private Double money;

	//get/set...
    //toString...
}
  • AccountDaoAccountDaoImpl
public interface AccountDao {
    void edit(Account account) throws SQLException;
    Account findByName(String name) throws SQLException;
}
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void edit(Account account) throws SQLException {
        jdbcTemplate.update("update account set name=?, money=? where id=?", account.getName(), account.getMoney(), account.getId());
    }

    @Override
    public Account findByName(String name) throws SQLException {
        return jdbcTemplate.queryForObject("select * from account where name = ?", new BeanPropertyRowMapper<>(Account.class), name);
    }
}
  • AccountServiceAccountServiceImpl
public interface AccountService {
    void transfer(String from, String to, Float money) throws SQLException;
}
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Override
    public void transfer(String from, String to, Float money) throws SQLException {
        //1.查询帐号信息
        Account fromAccount = accountDao.findByName(from);
        Account toAccount = accountDao.findByName(to);
        //2.修改帐号金额
        fromAccount.setMoney(fromAccount.getMoney() - money);
        toAccount.setMoney(toAccount.getMoney() + money);
        //3.把数据更新到数据库
        accountDao.edit(fromAccount);
        //int i = 1/0;
        accountDao.edit(toAccount);
    }
}
3) 配置bean和依赖注入
  • applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!--开启组件扫描-->
    <context:component-scan base-package="com.guang"/>

    <!--配置连接池-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql:///spring"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>

    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg name="dataSource" ref="dataSource"/>
    </bean>
</beans>
4) 功能测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TransferTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void transfer() throws SQLException {
        accountService.transfer("tom", "jerry", 1000f);
    }
}

什么是声明式事务控制

  • 介绍:
    • 声明式事务控制,是采用声明的方式进行事务管理。所谓的声明,指的就是在配置文件中进行配置。
    • 通过声明式(配置)的方式来处理事务,代替编码式事务控制
  • 作用:
    • 事务管理不入侵开发的组件,松耦合
      • 业务逻辑代码中,没有事务的代码,甚至不会意识到正在事务当中。
      • 事实上也应该如此,业务逻辑代码只处理业务功能,事务控制是属于系统层面的服务;如果想要更改事务,只需要在配置文件中重新配置即可
    • 能以模板的方式使用
      • Spring的声明式事务以AOP为基础,但是几乎是固定的配置模板,即使不懂AOP,也可以配置实现事务管理
    • 易维护。
      • 在不需要事务管理的时候,只需要在配置文件中进行修改,即可把事务管理移除掉,而不需要修改源码,方便维护
  • 注意:Spring的声明式事务,底层就是AOP

基于XML的声明式事务控制

1) 需要明确的事项
  • 谁是目标类?
  • 谁是切入点?
  • 谁是通知?
  • dao层技术是JdbcTemplate,使用DataSourceTransactionManager
2) 快速入门
需求描述
  • 通过Spring的xml配置,对银行转账功能,进行事务控制
实现步骤
  • 只需要修改applicationContext.xml即可:
    1. 在配置文件中增加aop和tx的名称空间
    2. 配置事务的通知(增强)
    3. 配置切面,把事务通知织入到转账方法中
功能实现
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       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
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--开启组件扫描-->
    <context:component-scan base-package="com.guang"/>

    <!--配置连接池-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql:///spring"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>

    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--配置事务通知-->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    <!--配置切面-->
    <aop:config>
        <aop:advisor advice-ref="txAdvice" 
                     pointcut="execution(* com.guang.service..*.*(..))"/>
    </aop:config>
</beans>
3) 配置详解
aop:config:切面配置
<aop:config>
    <aop:advisor advice-ref="txAdvice"  
                 pointcut="execution(* com.guang.service.impl..*.*(..))"/>
</aop:config>
  • aop:config:aop提供的用于配置切面的标签
  • aop:advisor:Spring提供的专门用于配置事务的,作用类似于aop:aspect
    • advice-ref:要引入的通知配置,必须要引用<tx:advice>所配置的事务通知
    • pointcut:切入点表达式
tx:advice:事务通知配置
<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
        <!-- transfer方法:隔离级别是repeatable-read,事务传播特性是required,非只读 -->
        <tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>    
        
        <!-- save开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
        <tx:method name="save*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
        
        <!-- edit开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
        <tx:method name="edit*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
        
        <!-- delete开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
        <tx:method name="delete*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
        
        <!-- query开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
        <tx:method name="query*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>
        
        <!-- find开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
        <tx:method name="find*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>
    </tx:attributes>
</tx:advice>
  • tx:advice

    • id属性:唯一标识

    • transaction-manager属性:配置一个事务管理器,即PlatformTransactionManager的实现类对象

      类似于我们的自己编写的事务管理器,里边提供了事务管理的方法,例如:提交、回滚事务的方法等等

  • tx:attributes:在标签内部设置事务的属性信息(事务定义信息,TransactionDefinition)

  • tx:method:要进行事务控制的方法配置,表示 要对哪些方法,进行什么样的事务控制

    • name属性:要进行事务控制方法名称,可以使用通配符*
    • isolation属性:事务的隔离级别设置
    • propagation属性:事务传播特性
    • read-only属性:是否只读
    • timeout属性:超时时间。默认-1表示不限制,如果设置的话,单位是秒
4) 小结
  • service里的方法,不需要有任何事务管理相关的代码
  • 只需要在xml里配置即可
<!-- 配置事务管理器 -->
<bean id="txManager" class="DataSourceTransactionManager全限定类名">
	<property name="dataSource" ref="连接池"/>
</bean>

<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
	<tx:attributes>
    	<tx:method name="*"/>
    </tx:attributes>
</tx:advice>

<!-- 配置事务切面 -->
<aop:config>
	<aop:advisor advice-ref="txAdvice" pointcut="切入点表达式"/>
</aop:config>

基于注解的声明式事务控制

1) 快速入门
需求描述
  • 通过Spring的注解配置,对银行转账功能,进行事务控制
实现步骤
  1. 在需要事务控制的方法/类上增加注解@Transactional
  2. 在配置文件applicationContext.xml中修改配置
    • 配置事务管理器
    • 开启事务的注解驱动
功能实现
  • 修改银行转账的Service类:AccountServiceImpl
public class AccountServiceImpl implements AccountService {

    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    @Transactional(isolation = Isolation.DEFAULT, propagation = Propagation.REQUIRED, readOnly = false)
    public void transfer(String from, String to, Double money) {
        //1.查询帐号信息
        Account fromAccount = accountDao.findByName(from);
        Account toAccount = accountDao.findByName(to);
        //2.修改帐号金额
        fromAccount.setMoney(fromAccount.getMoney() - money);
        toAccount.setMoney(toAccount.getMoney() + money);
        //3.把数据更新到数据库
        accountDao.edit(fromAccount);
        //int i = 1/0;
        accountDao.edit(toAccount);
    }
}
  • 修改配置文件applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       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
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--开启组件扫描-->
    <context:component-scan base-package="com.guang"/>

    <!--配置连接池-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql:///spring"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>

    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--开启事务的注解驱动-->
    <tx:annotation-driven transaction-manager="txManager"/>
</beans>
2) 配置详解
注解@Transactional
  • 加在 需要进行事务控制的方法/类上,用于代替xml配置中的tx:advice和事务切面的aop:config
  • isolation属性:设置事务的隔离级别,从枚举Isolation中取值
  • propagation属性:设置事务的传播特性,从枚举Propagation中取值
  • readOnly属性:设置是否是只读的
  • timeout属性:设置超时时间,单位秒。-1表示不限制
开启事务的注解驱动

XML方式

  • 使用注解进行事务管理,必须要在applicationContext.xml中开启 事务的注解驱动,否则无效
<!-- 开启事务的注解驱动。`transaction-manager`属性:指定事务管理器 -->
<tx:annotation-driven transaction-manager="txManager"/>
<!-- 开启事务的注解驱动。默认注入名称为transactionManager的事务管理器 -->
<tx:annotation-driver/>

纯注解方式

  • 如果是纯注解,开启事务的注解驱动,需要在核心配置类上增加注解:@EnableTransactionManagement

  • 配置示例

@Configuration
@ComponentScan("com.guang")
@EnableTransactionManagement //自动byType注入事务管理器
public class AppConfig{
    
    //连接池
    @Bean
    public DataSource dataSource() throws PropertyVetoException {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setDriverClass("com.mysql.jdbc.Driver");
        dataSource.setJdbcUrl("jdbc:mysql:///spring96");
        dataSource.setUser("root");
        dataSource.setPassword("root");
        return dataSource;
    }

    //JdbcTemplate
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }
    
    //事务管理器
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }
}
3) 小结
  • 在xml文件里
<!-- 配置事务管理器 -->
<bean id="txManager" class="DataSourceTransactionManager全限定类名">
	<property name="dataSource" ref="连接池"/>
</bean>

<!-- 开启事务的注解驱动 -->
<tx:annotation-driven transaction-manager="txManager"/>
<context:component-scan base-package="com.guang"/>
  • 哪个方法需要事务管理,就在哪个方法上加注解:@Transactional

拓展了解

通知中获取切入点对象

介绍

如果想要在通知方法中,获取切入点对象。可以在通知方法里直接增加以下参数:

  • Spring提供的运行时连接点/切入点对象:
类名 介绍
org.aspectj.lang.JoinPoint 切入点对象,
用于前置、后置、异常、最终通知,作为通知方法的形参
org.aspectj.lang.ProceedingJoinPoint 切入点对象,是JoinPoint的子接口
用于环绕通知,作为通知方法的参数
  • org.aspectj.lang.JoinPoint的常用方法
返回值 方法名 说明
java.lang.Object[] getArgs() 连接点的实参值.
Signature getSignature() 连接点方法签名
java.lang.Object getTarget() Returns the target object.
java.lang.Object getThis() Returns the currently executing object.
java.lang.String toLongString() Returns an extended string representation of the join point.
java.lang.String toShortString() Returns an abbreviated string representation of the join point.
java.lang.String toString()
JoinPoint.StaticPart getStaticPart() Returns an object that encapsulates the static parts of this join point.
java.lang.String getKind() Returns a String representing the kind of join point.
  • ProceedingJoinPointJoinPoint的子接口,它除了上述方法,还有
返回值 方法名 说明
java.lang.Object proceed() 执行下一个通知;
如果后边没有通知了,调用目标方法
java.lang.Object proceed(Object[] args) 执行下一个通知;
如果后边没有通知了,调用目标方法

示例

public class MyAdvice {

    public void before(JoinPoint jp) {
        System.out.println("前置:" + jp.getSignature());
    }

    public void afterReturning(JoinPoint jp){
        System.out.println("后置:" + jp.getSignature());
    }

    public void afterThrowing(JoinPoint jp){
        System.out.println("异常:" + jp.getSignature());
    }

    public void after(JoinPoint jp){
        System.out.println("最终:" + jp.getSignature());
    }

    public Object around(ProceedingJoinPoint pjp){
        Object result = null;
        try {
            System.out.println("==环绕:前置通知==");


            //调用对象的方法,返回方法执行结果
            result = pjp.proceed(pjp.getArgs());
            
            System.out.println("==环绕:后置通知==");
        } catch (Throwable throwable) {
            System.out.println("==环绕:异常通知==");
            throwable.printStackTrace();
        } finally {
            System.out.println("==环绕:最终通知==");
        }
        return result;
    }
}

通知中绑定参数

  • 不同类型的通知,可以绑定的参数是不同的

前置通知

  • 在通知中,可以绑定参数:获取切入点方法的实参
  • 通知方法:
public void before(JoinPoint jp, Object params){
    System.out.println("==前置通知==");
    System.out.println("连接点:" + jp.getSignature());
    System.out.println("实参:" + params);
}
  • 切入点表达式:
<aop:before method="before" 
            pointcut="execution(* com.guang..*.*(..)) and args(params)"/>

后置通知

  • 在通知中,可以绑定参数:获取切入点方法的实参和返回值
  • 通知方法:
public void afterReturning(JoinPoint jp, Object params, Object result){
    System.out.println("==后置通知==");
    System.out.println("方法参数:" + params);
    System.out.println("返回值:" + result);
}
  • 切入点表达式:
<aop:after-returning method="afterReturning" 
                     pointcut="execution(* com.guang..*.*(..)) and args(params)" 
                     returning="result"/>

异常通知

  • 在通知中,可以绑定参数:获取切入点方法的实参,和异常信息对象
  • 通知方法:
public void afterThrowing(Exception ex, Object params){
    System.out.println("==异常通知==");
    System.out.println("方法实参:" + params);
    System.out.println("异常:" + ex);
}
  • 切入点表达式:
<aop:after-throwing method="afterThrowing" 
                    pointcut="execution(* com.guang..*.*(..)) and args(params)" 
                    throwing="ex"/>

最终通知

  • 在通知中,可以绑定参数:获取方法的实参
  • 通知方法:
public void after(Object params){
    System.out.println("==最终通知==");
    System.out.println("方法实参:" + params);
}
  • 切入点表达式:
<aop:after method="after" 
           pointcut="execution(* com.guang..*.*(..)) and args(params)"/>

小结

  • 什么是AOP?面向切面编程,本质是动态代理,用于在不修改源码的情况下进行功能增强

  • AOP相关的术语:

    • 目标对象Target:要增强的那个对象
    • 代理对象Proxy:增强后的那个代理对象
    • 连接点JoinPoint:可以增强的方法
    • 切入点PointCut:要增强的方法
    • 通知Advice:功能增强的代码
    • 切面Aspect:切入点 + 通知
    • 织入Weaving:把切入点 和通知进行结合,生成代理对象的过程
  • XML方式的AOP

    1. 编写目标类, 编写通知
    2. 在xml里配置切面
    <bean id="" class="目标类"></bean>
    <bean id="通知对象" class="通知类"></bean>
    
    <aop:config>
    	<aop:pointcut id="xxx" expression="切入点表达式"/>
        
        <aop:aspect ref="通知对象">
        	<aop:before method="前置通知方法" pointcut-ref="xxx"/>
        	<aop:after-returning method="后置通知方法" pointcut-ref="xxx"/>        
        	<aop:after-throwing method="异常通知方法" pointcut-ref="xxx"/>        
        	<aop:after method="最终通知方法" pointcut-ref="xxx"/>     
            
            <aop:around method="环绕通知方法" pointcut-ref="xxx"/>
        </aop:aspect>
    </aop:config>
    
    • 环绕通知方法
    public Object aroundMethod(ProceedingJoinPoint pjp){
    	Object result = null;
        try{
            //写前置增强
            
            //调用目标对象的方法
        	result = pjp.proceed(pjp.getArgs()); 
            
            //写后置增强
        }catch(Throwable t){
            //写异常处理
        }finally{
            //写最终处理
        }
    	return result;
    }
    
  • 注解的AOP

    1. 编写目标类,编写通知类
    2. 在通知类里配置切面
      • 前置通知:@Before
      • 后置通知:@AfterReturning
      • 异常通知:@AfterThrowing
      • 最终通知:@After
      • 环绕通知:@Around
    @Aspect
    @Component
    public class MyAdvice{
    	@Pointcut("execution(* com.guang.service..*.*(..))")
        public void pc(){}
        
        @Before("pc()")
        public void beforeMethod(){
            System.out.println("==前置增强");
        }
        
        @Around("pc()")
        public Object aroundMethod(ProceedingJoinPoint pjp){
            Object result = null;
            try{
                //写前置增强
    
                //调用目标对象的方法
                result = pjp.proceed(pjp.getArgs()); 
    
                //写后置增强
            }catch(Throwable t){
                //写异常处理
            }finally{
                //写最终处理
            }
            return result;
        }
    }
    
    1. 在xml里开启AOP的自动代理
    <aop:aspectj-autoproxy/>
    
posted @ 2022-02-21 11:15  写的代码很烂  阅读(164)  评论(0编辑  收藏  举报