Spring知识点复习二

    科技2026-02-04  3

    1.一个简单的转账案例

    我们以一个最简单的转账的例子开始,就是像前面学JAVA Web时用到的转账,然后结合事务的操作来引入Spring的AOP。

    在这里其他代码都跟前一天的数据库增删改查大差不差,所以在这里只展示AccountServiceImpl中的transfer方法,即转账功能:

    public void transfer(String sourceName, String targetName, Float money) { //1.根据名称查询转出账户 Account source = accountDao.findAccountByName(sourceName); //2.根据名称查询转入账户 Account target = accountDao.findAccountByName(targetName); //3.转出账户减钱 source.setMoney(source.getMoney()-money); //4.转入账户加钱 target.setMoney(target.getMoney()+money); //5.更新转出账户 accountDao.updateAccount(source); //int a = 1/0; //6.更新转入账户 accountDao.updateAccount(target); }

    这个时候程序可以实现基本的转账功能,但是事务的回滚会有困难,当我们把a=1/0的注释给拿掉时,就会出现原账户的钱减少了,但目标账户的钱却没有改变。所以这个时候我们开始考虑事务的问题,毫无疑问这中间原先就是有事务支持的,但是就是可能每次与数据库交互都是独立的事务,都有独立的Connection,所以每次事务执行完之后都会进行提交。所以我们需要这些操作只有一个Connection,这个时候我们就需要用到ThreadLocal对象把Connection和当前线程绑定,使一个线程中只有一个Connection来控制事务。所以要对代码进行一定的调整,首先是在业务层实现对事务的控制。我们新建一个叫ConnectionUtils的类,其作用是从数据源获取一个连接并实现和线程的绑定和解绑。

    package com.itheima.utils; import javax.sql.DataSource; import java.sql.Connection; /** * 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定 */ public class ConnectionUtils { private ThreadLocal<Connection> tl = new ThreadLocal<Connection>(); private DataSource dataSource; /** *提供一个set方法,让spring给我们注入 * @param dataSource */ public void setDataSource(DataSource dataSource){ this.dataSource = dataSource; } /** * 获取当前线程上的连接 * @return */ public Connection getThreadConnection(){ try { //1.先从ThreadLocal上获取 Connection conn = tl.get(); //2.判断当前线程上是否有连接 if(conn == null){ //3.从数据源中获取一个连接,并且存入ThreadLocal中 conn = dataSource.getConnection(); tl.set(conn); } //4.返回当前线程上的连接 return conn; }catch (Exception e){ throw new RuntimeException(e); } } /** * 当我们把连接关闭,线程还回池中时,下次再获取这个线程时,线程上还是有连接的,但是这个连接这个时候已经不能用了 * 所以我们需要把链接和线程解绑 */ public void removeConnection(){ tl.remove(); } }

    其次我们需要写一个事务管理相关的工具类TransactionManager

    package com.itheima.utils; import java.sql.SQLException; public class TransactionManager { private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } /** * 事务开启 */ public void begin(){ try { connectionUtils.getThreadConnection().setAutoCommit(false); } catch (SQLException e) { e.printStackTrace(); } } /** * 事务提交 */ public void commit(){ try { connectionUtils.getThreadConnection().commit(); } catch (SQLException e) { e.printStackTrace(); } } /** * 事务回滚 */ public void rollback(){ try { connectionUtils.getThreadConnection().rollback(); } catch (SQLException e) { e.printStackTrace(); } } /** * 关闭连接 */ public void release(){ try { connectionUtils.getThreadConnection().close(); connectionUtils.removeConnection(); } catch (SQLException e) { e.printStackTrace(); } } }

    写完以上两个工具类之后我们就能对一个事务进行管理了,所以最后我们的transfer代码变成了如下的模样:

    public void transfer(String sourceName, String targetName, Float money) { try { txManager.begin(); //1.根据名称查询转出账户 Account source = accountDao.findAccountByName(sourceName); //2.根据名称查询转入账户 Account target = accountDao.findAccountByName(targetName); //3.转出账户减钱 source.setMoney(source.getMoney()-money); //4.转入账户加钱 target.setMoney(target.getMoney()+money); //5.更新转出账户 accountDao.updateAccount(source); //int a = 1/0; //6.更新转入账户 accountDao.updateAccount(target); txManager.commit(); }catch (Exception e){ txManager.rollback(); e.printStackTrace(); }finally { txManager.release(); } }

    这些写完之后就差配置IOC了,我们还是以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"> <!--把对象的创建交给spring来管理--> <!-- 配置Dao对象 --> <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"> <!-- 注入dao --> <property name="accountDao" ref="accountDao"></property> <!-- 注入txManager --> <property name="txManager" ref="txManager"></property> </bean> <!-- 配置Dao对象 --> <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl"> <!-- 注入QueryRunner --> <property name="runner" ref="runner"></property> <!-- 注入ConnectionUtils --> <property name="connectionUtils" ref="connectionUtils"></property> </bean> <!-- 配置QueryRunner --> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean> <!-- 配置数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!-- 连接数据库的必备信息 --> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring_day02"></property> <property name="user" value="root"></property> <property name="password" value="root"></property> </bean> <!-- 配置Connection的工具类 ConnectionUtils --> <bean id="connectionUtils" class="com.itheima.utils.ConnectionUtils"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务管理器 --> <bean id="txManager" class="com.itheima.utils.TransactionManager"> <property name="connectionUtils" ref="connectionUtils"></property> </bean> </beans>

    然后整个就可以实现在出错的时候实现事务的回滚。

    但是反观我们上面的一系列实现,代码的耦合性还是太强了,特别是增加了两个工具类之后配置变得比较繁琐,这个暂且不提,我们来看我们的AccountServiceImpl由于增加了事务管理的代码,虽然我只展示了transfer这一个方法,但是在其他比如新增、更新等方法中也有类似的事务管理的代码,所以变得异常复杂。既然这个代码都是冗余的,我们肯定可以把这一部分重复代码给抽取出来放在一个地方。这就用到了我们的动态代理的知识了。

    2.动态代理的两种实现方法

    基于接口的动态代理

    这种代理方法一般的主体有三类:接口、原类、代理对象。接口中有两种方法,一个是卖电脑,一个是售后。原类实现了这个接口并重写了两个方法,代理对象的实质就是在不对原类进行更改的前提下,实现对原类中方法的增强。具体的就看下面的程序:

    接口IProducer

    package com.itheima.proxy; public interface IProducer { public void saleProduct(float money); public void afterSale(float money); }

    原类Producer

    package com.itheima.proxy; public class Producer implements IProducer{ public void saleProduct(float money){ System.out.println("卖掉一台电脑,收获"+money+"元"); } public void afterSale(float money){ System.out.println("进行售后服务,收获"+money+"元"); } }

    代理对象及客户测试Client:

    package com.itheima.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class Client { public static void main(String[] args) { final Producer producer = new Producer(); /** * 动态代理: * 特点:字节码随用随创建,随用随加载 * 作用:不修改源码的基础上对方法增强 * 分类: * 基于接口的动态代理 * 基于子类的动态代理 * 基于接口的动态代理: * 涉及的类:Proxy * 提供者:JDK官方 * 如何撞见代理对象 * 使用Proxy类中的newProxyInstance方法 * 创建代理对象的要求 * 被代理类最少实现一个接口,如果没有则不能使用 * newProxyInstance方法的参数: * ClassLoader:类加载器 * 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器 * class[] * 它是用于让代理对象和被代理对象有相同方法。固定写法 * InvocationHandler:用于提供增强的代码 * 它是让我们写如何代理。我们一般都是写一个改接口的实现类,通常情况下都是匿名内部类,但不是必须的 */ IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() { /** * @param proxy 代理对象的引用,一般不用 * @param method 当前执行的方法 * @param args 当前执行方法所需的参数 * @return 和被代理对象有相同的返回值 * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //提供增强的代码 Object returnValue = null; //1.获取方法执行的参数 Float money = (Float)args[0]; if("saleProduct".equals(method.getName())){ returnValue = method.invoke(producer,money*0.8f); } return returnValue; } }); proxyProducer.saleProduct(10000f); } }

    这样很明显最后输出的就是8000元的电脑。

    基于子类的动态代理

    这个代理方案和前面这个其实差不多,唯一的区别是不需要有接口存在了,取而代之的要求是需要被增强或者被代理的类不是最终类。

    package com.itheima.cglib; import com.itheima.proxy.IProducer; import net.sf.cglib.proxy.Callback; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class Client { public static void main(String[] args) { final Producer producer = new Producer(); /** * 动态代理: * 特点:字节码随用随创建,随用随加载 * 作用:不修改源码的基础上对方法增强 * 分类: * 基于接口的动态代理 * 基于子类的动态代理 * 基于接口的动态代理: * 涉及的类:Enhancer * 提供者:第三方cglib库 * 如何撞见代理对象 * 使用Enhancer类中的create方法 * 创建代理对象的要求 * 被代理类最少实现一个接口,如果没有则不能使用 * create方法的参数: * ClassLoader:类加载器 * 它是用于指定被代理对象的字节码 * Callback:用于提供增强的代码 * 它是让我们写如何代理。我们一般都是写一个改接口的实现类,通常情况下都是匿名内部类,但不是必须的 * 此接口一般是谁用谁写 * 我们一般写的都是改接口的子接口实现类:MethodInterceptor */ Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() { @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { //提供增强的代码 Object returnValue = null; //1.获取方法执行的参数 Float money = (Float)args[0]; if("saleProduct".equals(method.getName())){ returnValue = method.invoke(producer,money*0.8f); } return returnValue; } }); cglibProducer.saleProduct(10000f); } }

    这里有一点需要注意的是,匿名内部类在调用外部成员时需要用final修饰。 

    知道了这两种实现方式之后,我们就开始可以用动态代理的方式来实现事务的管理了。我们实现的思路就是创建一个工厂类来专门产生AccountServiceImpl的代理对象,并在代理对象中实现对其中的方法增强,即加入事务管理。所以我们就有了如下:

    package com.itheima.factory; import com.itheima.service.IAccountService; import com.itheima.utils.TransactionManager; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class BeanFactory { private IAccountService accountService; private TransactionManager txManager; /** * 用于依赖注入 * @param txManager */ public final void setTxManager(TransactionManager txManager) { this.txManager = txManager; } /** * 用于注入 * @param accountService */ public final void setAccountService(IAccountService accountService) { this.accountService = accountService; } public IAccountService getAccountService(){ //匿名内部类使用外部变量时,必须在外部变量前加final return (IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object returnValue = null; try { //1.开启事务 txManager.begin(); //2.执行操作 returnValue = method.invoke(accountService,args); //3.提交事务 txManager.commit(); //4.返回结果 return returnValue; } catch (Exception e) { //5.回滚事务 txManager.rollback(); throw new RuntimeException(e); } finally { //6.关闭连接 txManager.release(); } } }); } }

    然后在最后测试的时候我们直接调用代理对象中的方法就可以了,简单而方便,还能实现事务的管理。

    3.Spring的AOP

    其实刚刚看的这里就是SpringAOP的基本思想,那就是通过配置来实现对方法的增强,而不用自己去写动态代理了。但是AOP和IOC有所不同,它的一些理论概念还是比较多的,需要先把一些书面概念给弄清楚。

    Joinpoint( 连接点 ): 所谓连接点是指那些被拦截到的点。在 spring , 这些点指的是方法 , 因为 spring 只支持方法类型的 连接点。 Pointcut( 切入点 ): 所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。切入点一定是连接点,反之不一定。 Advice( 通知 / 增强 ): 所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。 通知的类型:前置通知 , 后置通知 , 异常通知 , 最终通知 , 环绕通知。 Target( 目标对象 ): 代理的目标对象。 Weaving( 织入 ): 是指把增强应用到目标对象来创建新的代理对象的过程。 spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。 Proxy (代理) : 一个类被 AOP 织入增强后,就产生一个结果代理类。

    基本的概念了解这么多就差不多了,接下来就是通过一个简单的例子来了解AOP。 

    首先我们需要导入所需要的包:

    我们有一个比较简单的接口和类,那就是IAccountService和AccountServiceImpl,然后我们就是需要通过AOP来对其中的方法进行增强,原先我们需要写一个代理类,现在不需要了,只需要转们写一个通知类,里面就是用于增强的一些方法,接着我们只需要在bean.xml中进行配置就可以了。

    AccountServiceImpl如下;

    package com.itheima.service.impl; import com.itheima.service.IAccountService; public class AccountServiceImpl implements IAccountService { public void saveAccount() { System.out.println("执行了保存"); } public void updateAccount(int i) { System.out.println("执行了更新"+i); } public int deleteAccount() { System.out.println("执行了删除"); return 0; } }

    通知类Logger如下:

    package com.itheima.utils; /** * 用于提供公共代码的工具类,用于记录日志 */ public class Logger { public void printLog(){ System.out.println("Logger类中的printLog方法开始打印日志了"); } }

    配置的bean.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.itheima.service.impl.AccountServiceImpl"></bean> <!-- spring中基于XML的aop配置步骤 1.把通知的Bean也交给spring来管理 2.使用aop:config标签表明开始AOP的配置 3.使用aop:aspect标签表明配置切面 id属性:是给切面提供一个唯一标识 ref属性:是指定通知类bean的Id 4.在aop:aspect标签的内部使用对应标签来配置通知的类型 示例是让printLog方法在切入点方法执行之前,所以是前置通知 aop:before:表示配置前置通知 method属性:用于指定Logger类中那个方法是前置通知 pointcut属性:用于指定切入点表达式,该表达式的含义是对业务层中哪些方法进行增强 切入点表达式的写法: 关键字:execution(表达式) 表达式: 访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表) 标准的表达式写法: public void com.itheima.service.impl.AccountServiceImpl.saveAccount() --> <bean id="logger" class="com.itheima.utils.Logger"></bean> <!-- 配置AOP --> <aop:config> <!-- 配置切面 --> <aop:aspect id="loggerAdvice" ref="logger"> <!-- 配置通知的类型,并且建立通知方法和切入点方法的关联 --> <aop:before method="printLog" pointcut="execution(* com.itheima.service.impl.*(..))"></aop:before> </aop:aspect> </aop:config> </beans>

    这样就相当于配置了一条前置通知。

    这就是实现了AOP的简单示例,接下来具体探讨一下通知的类型:

    一共有五种类型:前置通知、后置通知、异常通知、最终通知、环绕通知。其中环绕通知相当于把前面几种通知都给写里头,所以方法的执行也在通知里面,这里是现在的通知类:

    package com.itheima.utils; import org.aspectj.lang.ProceedingJoinPoint; /** * 用于提供公共代码的工具类,用于记录日志 */ public class Logger { /** * 前置通知 */ public void beforePrintLog(){ System.out.println("前置通知打印日志"); } /** * 后置通知 */ public void afterReturningPrintLog(){ System.out.println("后置通知打印日志"); } /** * 最终通知 */ public void afterPrintLog(){ System.out.println("最终通知打印日志"); } /** * 异常通知 */ public void afterThrowingLog(){ System.out.println("异常通知打印日志"); } public Object aroundPringLog(ProceedingJoinPoint pjp){ Object rtValue = null; Object[] args = pjp.getArgs(); try { System.out.println("前置"); rtValue = pjp.proceed(args); System.out.println("后置"); return rtValue; } catch (Throwable throwable) { System.out.println("异常"); throw new RuntimeException(throwable); }finally { System.out.println("最终"); } } }

    底下是配置:

    <?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"> <!-- 配置srping的Ioc,把service对象配置进来--> <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean> <!-- 配置Logger类 --> <bean id="logger" class="com.itheima.utils.Logger"></bean> <!--配置AOP--> <aop:config> <!-- 配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容 此标签写在aop:aspect标签内部只能当前切面使用。 它还可以写在aop:aspect外面,此时就变成了所有切面可用 --> <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut> <!--配置切面 --> <aop:aspect id="logAdvice" ref="logger"> <!-- 配置前置通知:在切入点方法执行之前执行 <aop:before method="beforePrintLog" pointcut-ref="pt1" ></aop:before>--> <!-- 配置后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个 <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>--> <!-- 配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个 <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>--> <!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行 <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>--> <!-- 配置环绕通知 详细的注释请看Logger类中--> <aop:around method="aroundPringLog" pointcut-ref="pt1"></aop:around> </aop:aspect> </aop:config> </beans>

    说完了这种xml配置,接下来我们看一看注解配置,注解配置的好处是比较简洁。

    这个时候xml中只做两件事,即定义需要扫描的包和开启注解AOP的支持。

    <!-- 配置spring创建容器时要导入的包 --> <context:component-scan base-package="com.itheima" ></context:component-scan> <!-- 配置spring开启注解AOP的支持 --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

    然后就是在通知类中主要就是注明这是一个通知类、配置切入点表达式,配置通知类型这三样。

    Logger

    package com.itheima.utils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; /** * 用于提供公共代码的工具类,用于记录日志 */ @Component("logger") @Aspect//表示当前是一个切面类 public class Logger { @Pointcut("execution(* com.itheima.service.impl.*.*(..))") private void pt1(){} /* *//** * 前置通知 *//* @Before("pt1()") public void beforePrintLog(){ System.out.println("前置通知打印日志"); } *//** * 后置通知 *//* @AfterReturning("pt1()") public void afterReturningPrintLog(){ System.out.println("后置通知打印日志"); } *//** * 最终通知 *//* @After("pt1()") public void afterPrintLog(){ System.out.println("最终通知打印日志"); } *//** * 异常通知 *//* @AfterThrowing("pt1()") public void afterThrowingLog(){ System.out.println("异常通知打印日志"); }*/ /** * 采用注解进行配置时AOP时推荐使用环绕通知,因为xml在执行顺序上会产生问题,最终会在后置之前执行 * @param pjp * @return */ @Around("pt1()") public Object aroundPringLog(ProceedingJoinPoint pjp){ Object rtValue = null; Object[] args = pjp.getArgs(); try { System.out.println("前置"); rtValue = pjp.proceed(args); System.out.println("后置"); return rtValue; } catch (Throwable throwable) { System.out.println("异常"); throw new RuntimeException(throwable); }finally { System.out.println("最终"); } } }

    4.JDBC Template

    以前用的比较多,这里主要就说两点,一个是配置,一个是JdbcDaoSupport。

    配置很简单,如下:

    <?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"> <bean id="accountDaoImpl" class="com.itheima.dao.impl.AccountDaoImpl"> <property name="jdbcTemplate" ref="jdbcTemplate"></property> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql://localhost:3306/spring_day02"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </bean> </beans>

    然后我们在AccountDaoImpl中是这样使用template的

    public class AccountDaoImpl implements AccountDao { private JdbcTemplate jdbcTemplate; public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void updateAccount(Account account) { jdbcTemplate.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId()); } @Override public Account findById(Integer id) { List<Account> accounts = jdbcTemplate.query("select * from account where id=?", new BeanPropertyRowMapper<Account>(Account.class), id); return accounts.isEmpty()?null:accounts.get(0); } @Override public Account findByName(String name) { List<Account> accounts = jdbcTemplate.query("select * from account where name=?", new BeanPropertyRowMapper<Account>(Account.class), name); if(accounts.isEmpty()){ return null; } if(accounts.size()>1){ throw new RuntimeException("结果不唯一"); } return accounts.get(0); } }

    和我们原先的使用方式几乎一致,但是这里Spring框架为了给我进一步简化这个步骤,可以去掉注入时的set方法,所以在配置时就直接把DataSource注入到AccountDaoImpl中,然后只需要继承一个类即JdbcDaoSupport也可以同样使用Template。最后的使用情况如下:

    package com.itheima.dao.impl; import com.itheima.dao.AccountDao; import com.itheima.domain.Account; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.support.JdbcDaoSupport; import java.util.List; public class AccountDaoImpl2 extends JdbcDaoSupport implements AccountDao { @Override public void updateAccount(Account account) { getJdbcTemplate().update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId()); } @Override public Account findById(Integer id) { List<Account> accounts = getJdbcTemplate().query("select * from account where id=?", new BeanPropertyRowMapper<Account>(Account.class), id); return accounts.isEmpty()?null:accounts.get(0); } @Override public Account findByName(String name) { List<Account> accounts = getJdbcTemplate().query("select * from account where name=?", new BeanPropertyRowMapper<Account>(Account.class), name); if(accounts.isEmpty()){ return null; } if(accounts.size()>1){ throw new RuntimeException("结果不唯一"); } return accounts.get(0); } }

    这里的getJdbcTemplate()你可以看成前面有一个super.,这样可能更便于理解。这两种方式的区别也比较明显,前者比较适合注解的方法,后者你想要在template上面加注解是不可能的。

    5.使用AOP进行事务管理

    前面进行事务管理,都是通过动态代理完成的,这个时候我们学习了AOP,就分别采用xml和注解两种方式来对这个任务进行一个实现。

    第一个就主要修改一下xml文件,配置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来管理--> <!-- 配置Service对象 --> <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"> <!-- 注入dao --> <property name="accountDao" ref="accountDao"></property> </bean> <!-- 配置Dao对象 --> <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl"> <!-- 注入QueryRunner --> <property name="runner" ref="runner"></property> <!-- 注入ConnectionUtils --> <property name="connectionUtils" ref="connectionUtils"></property> </bean> <!-- 配置QueryRunner --> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean> <!-- 配置数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!-- 连接数据库的必备信息 --> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring_day02"></property> <property name="user" value="root"></property> <property name="password" value="root"></property> </bean> <!-- 配置Connection的工具类 ConnectionUtils --> <bean id="connectionUtils" class="com.itheima.utils.ConnectionUtils"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务管理器 --> <bean id="txManager" class="com.itheima.utils.TransactionManager"> <property name="connectionUtils" ref="connectionUtils"></property> </bean> <aop:config> <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/> <aop:aspect id="txAdvice" ref="txManager"> <!-- 配置前置事务 --> <aop:before method="begin" pointcut-ref="pt1"></aop:before> <aop:after-returning method="commit" pointcut-ref="pt1"></aop:after-returning> <aop:after-throwing method="rollback" pointcut-ref="pt1"></aop:after-throwing> <aop:after method="release" pointcut-ref="pt1"></aop:after> </aop:aspect> </aop:config> </beans>

    至于注解配置,我们就配一个环绕通知了,其他就不配了。改主要还是该TransactionManager类,在bean.xml中我们保持对数据源的配置、runner的配置,最后加上对AOP的支持开启。

    package com.itheima.utils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.sql.SQLException; @Component("txManager") @Aspect public class TransactionManager { @Autowired private ConnectionUtils connectionUtils; @Pointcut("execution(* com.itheima.service.impl.*.*(..))") private void pt1(){} /** * 事务开启 */ public void begin(){ try { connectionUtils.getThreadConnection().setAutoCommit(false); } catch (SQLException e) { e.printStackTrace(); } } /** * 事务提交 */ public void commit(){ try { connectionUtils.getThreadConnection().commit(); } catch (SQLException e) { e.printStackTrace(); } } /** * 事务回滚 */ public void rollback(){ try { connectionUtils.getThreadConnection().rollback(); } catch (SQLException e) { e.printStackTrace(); } } /** * 关闭连接 */ public void release(){ try { connectionUtils.getThreadConnection().close(); connectionUtils.removeConnection(); } catch (SQLException e) { e.printStackTrace(); } } @Around("pt1()") public Object autoAdvice(ProceedingJoinPoint pjp){ Object returnValue = null; try { //1.获取参数 Object[] args = pjp.getArgs(); //2.开启事务 this.begin(); //3.执行操作 Object proceed = pjp.proceed(args); //4.提交事务 this.commit(); return proceed; }catch (Throwable e){ //5.回滚事务 this.rollback(); throw new RuntimeException(e); }finally { //6.释放资源 this.release(); } } }

    其实对事务管理这个任务,我们根本就没有必要自己去写工具类,去绑定线程这么复杂,Spring中早就有一套体系来对这些进行实现了,直接配置事务通知就行了,直接看bean.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" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 配置业务层--> <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"></property> </bean> <!-- 配置账户的持久层--> <bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置数据源--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql://localhost:3306/spring_day02"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </bean> <!-- Spring中基于声明的事务管理器 --> <!-- 1.配置事务管理器 2.配置事务的通知 此时我们先导入事务的约束 tx名称空间和约束,同时也需要aop的 使用tx:advice标签配置事务通知 属性: id:给事务通知一个唯一标识 transaction-manager:给石武同志一个事务管理器引用 3.配置AOP的通知切入点表达式 4.建立切入点表达式和事务通知的对应关系 5.配置事务的属性 --> <!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事务的通知 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <!-- 配置事务的属性 isolation:用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别。 propagation:用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS。 read-only:用于指定事务是否只读。只有查询方法才能设置为true。默认值是false,表示读写。 timeout:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。 rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值。表示任何异常都回滚。 no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚。没有默认值。表示任何异常都回滚。 --> <tx:attributes> <tx:method name="*" propagation="REQUIRED" read-only="false"/> <tx:method name="find*" propagation="SUPPORTS" read-only="true"/> </tx:attributes> </tx:advice> <!-- 配置aop --> <aop:config> <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/> <!-- 建立切入点表达式和事务通知的对应关系 --> <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor> </aop:config> </beans>

    别看事务的属性多,其实配起来常常就只用propagation和read-only。

    当然也有基于注解的配置:

    AccountServiceImpl

    package com.itheima.service.impl; import com.itheima.dao.IAccountDao; import com.itheima.domain.Account; import com.itheima.service.IAccountService; import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; /** * 账户的业务层实现类 * * 事务控制应该都是在业务层 */ @Service("accountService") @Transactional(propagation = Propagation.SUPPORTS,readOnly=true) public class AccountServiceImpl implements IAccountService{ @Autowired private IAccountDao accountDao; @Override public Account findAccountById(Integer accountId) { return accountDao.findAccountById(accountId); } //需要的是读写型事务配置 @Transactional(propagation = Propagation.REQUIRED,readOnly=false) @Override public void transfer(String sourceName, String targetName, Float money) { System.out.println("transfer...."); //2.1根据名称查询转出账户 Account source = accountDao.findAccountByName(sourceName); //2.2根据名称查询转入账户 Account target = accountDao.findAccountByName(targetName); //2.3转出账户减钱 source.setMoney(source.getMoney()-money); //2.4转入账户加钱 target.setMoney(target.getMoney()+money); //2.5更新转出账户 accountDao.updateAccount(source); //int i=1/0; //2.6更新转入账户 accountDao.updateAccount(target); } }

    这个需要注意的是,你如果有多种类型的数据库操作,有的需要只读,有的不需要,那就可以在类前面先配一个,在个别方法前再配一个。

    bean.xml中保留需要扫描的包,JdbcTemplate,数据源,事务管理器

    <!-- 配置spring创建容器时要扫描的包--> <context:component-scan base-package="com.itheima"></context:component-scan> <!-- 配置JdbcTemplate--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置数据源--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql://localhost:3306/spring_day02"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </bean> <!-- spring中基于注解的声明式事务控制配置步骤 1、配置事务管理器 2、开启Spring对事务的支持 3、在需要事务支持的地方使用@Transaction注解 --> <!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean>

    最后实现一个纯注解的配置实现,其他都一样,就需要我们新建一个config包,然后在一个properties文件中写数据源的相关配置。config包中有一个主配置类,一个JDBC相关配置信息和一个事务管理器相关配置:

    主配置类:

    @Configuration @ComponentScan("com.itheima") @Import({JdbcConfiguration.class,TransactionConfig.class}) @PropertySource("jdbcConfiguration.properties") @EnableTransactionManagement public class SpringConfiguration { }

    JDBC配置类和原先一样:

    package com.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DriverManagerDataSource; import javax.sql.DataSource; public class JdbcConfiguration { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; /** * 创建JdbcTemplate类 */ @Bean(name = "jdbcTemplate") public JdbcTemplate createJdbcTemplate(DataSource ds){ return new JdbcTemplate(ds); } /** * 创建数据源 */ @Bean(name="dataSource") public DataSource createDataSource(){ DriverManagerDataSource ds = new DriverManagerDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); return ds; } }

    事务管理器配置类:

    package com.config; import org.springframework.context.annotation.Bean; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; /** * 和事务相关的配置类 */ public class TransactionConfig { /** * 用于创建事务管理器对象 * @param dataSource * @return */ @Bean(name="transactionManager") public PlatformTransactionManager createTransactionManager(DataSource dataSource){ return new DataSourceTransactionManager(dataSource); } }

     

    Processed: 0.014, SQL: 9