MyBatis系列八 MyBatis-Spring(上)
本书主要讲解的是MyBatis,所以对Spring的一些技术,例如,IOC (反转控制)和 AOP (面向切面编程),只是点到一些基础和书中需要使用的部分。
Spring框架已经成为Java世界最为流行的IOC和AOP框架。通过Spring框架我们可 以使用IOC的依赖注入,即插即拔功能;通过AOP框架,数据库事务可以委托给Spring 处理,消除掉很大一部分的事务代码。在目前Java互联网技术中,Spring MVC大行其道, 它配合MyBatis的高度灵活、可配置、可优化SQL等特性,完全可以构建高性能的大型网 站。毫无疑问,MyBatis和Spring两大框架已经成了 Java互联网技术的主流框架组合之一,它们经受住了大数据量和大批量请求的考验,在大型网站系统中得到了大量的应用。使用MyBatis-Spring使得业务层和模型层得到了更好的分离,与此同时在Spring环境中使用 MyBatis也更加简单,节省了不少的代码。我们甚至不需要显式的使用SqlSessionFactory、 SqlSession等对象。因为MyBatis-Spring为我们封装了它们。
MyBatis提供了和Spring无缝对接的功能,它主要通过mybatis-spring-x.x.x.jar实现。 本书使用的 MyBatis-Spring 版本为 1.2.3。我们可以在网上(http://mvnrepository.com/artifact/ org. mybatis/mybatis-spring) 下载它。
一、Spring的基础知识
为了更好地讨论MyBatis在Spring项目中的应用,我们先谈谈Spring的基础知识。Spring技术主要由两个基础的功能I0C和A0P构成。MyBatis-Spring项目提供了一些基础 的类,使得Spring能和MyBatis结合起来用,这个项目需要使用mybatis-spring-x.x.x.jar实现。不过在此之前我们先了解一下Spring的一些基础知识,其中包括Spring IOC、Spring AOP 和Spring关于数据库事务的一些知识。
1、Spring IOC 基础
在Java基础教程中,我们往往使用创建关键字来完成对服务对象的创建。举个例子, 我们有很多的U盘,它们都能够存储计算机的数据,但是它们可能来自不同的品牌,有金士顿(KingstonUSBDisk)的、闪迪(SanUSBDisk)的,或者其他满足 U 盘接口 (USBDisk)规范的。如果我们用new方法,那么就意味着我们的接口只能用于某种特定品牌的U盘。
USBDisk usbDisk = new KingstonUSBDisk ();
通过上面的操作,USBDisk和KingstonUSBDisk就形成了耦合。换句话说,如果想用闪迪的U盘我需要修改源码才行。如果未来有更先进的U盘,那就要修改源码了,大型系统的资源多达成百上千,如果都采用这样的方式,系统会造成严重的耦合,不利于维护和扩展。
这个时候IOC理念来了,首先它不是一种技术,而是一种理念。假设我们不釆用new 方法,而是使用一种描述的方式,每一个U盘都有一段自己的描述,通过接口我们可以读 入这些信息,根据这些信息注入对应的U盘,这样我们在维护源码的时候只需要去描述这 些信息并且提供对应的服务即可,不需要去改动源码了。
仍以U盘为例,如果用的是闪迪U盘,那么在信息描述段给出的是闪迪U盘,系统就会根据这个信息去匹配对应的实现类,而无需用new方法去生成实现类。同样,如果用的是金士顿U盘,那么在信息描述段给出的就是金士顿,系统也会自动生成对应的服务注入到我们的系统中,而我们只需要通过描述就能获得资源,无需自己用new方法去创建资源和服务。
从上面的描述可以知道,我们往Spring中注入资源往往是通过描述来实现的,在Spring 中往往是注解或者是XML描述。Spring中的IOC注入方式分为下面这几种。
•构造方法注入。
• setter 注入。
•接口注入。
构造方法注入是依靠类的构造去实现的,对于一些参数较少的对象可以使用这个方式注入。比如角色类(Trole),它的构造方法中包含三个属性:编号(id)、角色名称(roleName) 和备注(note)O我们需要进行如代码清单8.1所示的操作来构建它。
这样我们就描述了一个TRole,它可以注入到其他的资源中。但是如果构造方法多, 显然构造注入不是一个很好的方法,而Spring更加推荐使用setter注入。假设上例角色类 还有一个没有参数的构造方法,它的三个属性,编号(id)、角色名称(roleName)和备注 (note)都有setter方法,那么我们可以使用setter注入,如代码清单8.2所示。
使用setter注入更加灵活,因为使用构造方法,会受到构造方法的参数个数、顺序这 些因素干扰。侵入更加少,所以这是Spring首选的注入方式。
Spring的接口注入方式。它是一种注入其他服务的接口,比如JNDI数据源的注入,在 Tomcat或者其他的服务器中往往配置了 JNDI数据源,那么就可以使用接口注入我们需要 的资源,如代码清单8-3所示。
它允许你从一个远程服务中注入一些服务到本地调用O
上面讨论了注入的几种方式,在大型系统中,我们往往还会使用注解注入的方式来描 述系统服务之间的关系,这也是Spring所推荐的方式。
2、Spring AOP 基础
Spring IOC相对而言还是比较容易理解的,如果你懂得了第6章的反射技术,就知道它是用反射技术实现的,而Spring AOP就不是了。在MyBatis-Spring技术中,它最大的用 处是事务的控制,这是一个最麻烦也最难理解的东西。
Spring AOP是通过动态代理来实现的。首先在传统的MVC构架中,业务层一般都夹 带着数据库的事务管理,例如,插入一个角色,它是使用RoleService接口的实现类 RoleServicelmp 1去实现的,如代码清单8・4所示。
当程序进入到insertRole方法的时候,Spring就会读取配置的传播行为进行设置,这里 的配置为Propagation.REQUIRED,它的意思是当前方法如果有事务则加入当前事务,否则就 创建新的事务。这样这个insertRole方法就在事务内调用了,那么它是怎么实现的呢?
在传统Spring的书籍中,我们一般会涉及到一些抽象的概念,如切面、连接点、通知、 切入点、目标对象、AOP代理等等极其抽象的概念。
这些内容论述的时候太过于抽象,所以我们不长篇大论地罗列一些晦涩的概念去讨论 它们,而是使用原理去讨论它们,下面我们通过原理来分析它们执行的过程。
Spring AOP实际上就是一个动态代理的典范。不熟悉动态代理的读者可以翻阅本书 第6章的内容,请务必掌握它们,这是阅读下面分析的基础,否则你读下面的文字便会像 读天书一样。
现在以角色服务类(RoleServicelmpl)为例。
首先Spring可以生成代理对象,这样调度insertRole方法的时候就进入了一个invoke 方法里面。Spring会判断到底要不要拦截这个方法,这是一个切入点的配置问题,它是通 过正则式匹配的,比如我们在正则式配置insert*这样的统配,那么Spring就会拦截这个 insertRole。方法,否则就不拦截,直接通过invoke方法反射调用这个方法,就结束了。这 便是切入点的概念,很简单吧。
其次就是切面。切面是干什么的?它是插入角色的,里面包含事务,而事务就是整个 方法的一个切面,可能你的方法会很复杂,包含业务、财务和日志等多方面,而它们都受 到同一事务管辖,那么事务就是这方法的一个切面。这个时候Spring就会根据我们配置的 信息,知道这个方法需要事务,釆用传播行为Propagation.REQUIRED运行方法,这就是 Spring的切面。
再次就是连接点。连接点是在程序运行中根据不同的通知来实现的程序段。由于Spring使用动态代理,我们在反射原始的方法之前可以做一些事情,于是有了前置通知(Beforeadvice),也可以在反射之后做一些事情,那便是后置通知(After advice),反射原来的方法可能正确返回,也可能因此抛出异常,所以还有正常返回后通知(After return advice)和产 生异常的抛出异常后通知(After throwing advice)o也有可能需要用自定义方法取代原有的 方法,就如MyBatis的插件一样,不釆用原有的invoke方法而是使用自定义的方法,所以 还有环绕通知(Around advice),怎么样用动态代理的原理来分析是不是比单独讲概念要清 晰得多呢?
代理目标,就是哪个类的对象被代理了。这里显然就是RoleServicelmpl对象被代理了。
AOP代理(AOP Proxy)就是指釆用何种方式进行代理,我们知道JDK的代理需要使 用接口,而CGLIB则不需要,因此在默认的情况下Spring釆用这样的规则。当Spring的 服务包含接口描述时釆用JDK动态代理,否则采用CGLIB代理。当然你可以通过配置修 改它们。
基于上面的论述,我们清楚了 AOP的大致情况,这些在理解动态代理的基础上是相对 简单的。如图8.1所示,Spring AOP在动态代理下运行的流程。
这里执行方法的逻辑有点复杂,笔者另外给图解释它,如图8.2所示。这便是在Spring AOP动态代理下做的判断和运行的流程图,表面上看起来有点复杂,实际在理解了动态代 理后结合Java基础,就可以十分容易地理解它们了,也可以使用代码去实现它们。
3、Spring事务管理
Spring事务管理是通过Spring AOP去实现的,在8.1.2节中我们讨论了 Spring AOP的 执行过程和基础概念,默认的情况下Spring在执行的方法抛出异常后,引发事务回滚,当 然你可以用拦截器或者配置去改变它们,我们这里只讨论默认的情况,不讨论其他复杂的 情况。我们首先讨论一下Spring的隔离级别和传播行为,这是很容易犯错的地方。
(1)事务隔离级别
数据库和程序一样,也有并发的问题,在同时存在两个或者两个以上的数据库事务环 境中,同一条记录甚至是不同记录都会由于SQL在不同时刻的执行产生不同的结果,甚至 产生错误。于是便有了隔离级别这样的数据库的概念,按照数据库的概念分为脏读、读写 提交、可重复读、序列化4种。我们来讨论一下它们。
脏读是指一个事务能够读取另外一个事务未提交的数据,如表8.1所示。
这里我们发现事务B读取了事务A未提交的数据,而最后事务A将回滚,这是十分危 险的。为了避免这个问题,我们往往使用读写提交的隔离级别,如表8.2所示。
这里我们用了读写提交完成了这些逻辑,但是读写提交依旧会产生一些问题,让我们 看看这样的场景,如表8.3所示。
这里我们看到,对于余额而言,在T4时刻买单失败了。因为在T3时刻老婆提交了消 费800元的事务,这时老公可要出洋相了。为了避免这个问题,我们可以使用可重复读的 策略,这样就消除了老公无钱买单的尴尬场景。
但是可重复读是针对于同一条记录而言的,对于不同的记录会发生下面这样的场景, 如表8-4所示。
我们看到老婆在查询之后,老公启动了消费,并先于老婆之前打印账单记录,所以在 T4时刻,打印了 1800元11条记录,这个时候老婆就会去质疑这800元是不是幻读的。上 面和不可重复读很接近,但是我们需要注意的是,不可重复读是针对同一条记录,而幻读 是针对删除和插入记录的。
为了避免服这个问题我们可以釆用序列化的隔离层。序列化就意味着所有的操作都会按顺序执行,不会出现脏读、不可重读和幻读的情况,如表8.5所示。
这就是数据库隔离层的情况,上面我们只讨论了在多并发环境下数据安全性的问题,而没有讨论它们之间的性能。一般而言,性能从脏读一读写提交一可重复读一序列化是直 线下降的,更多的时候我们使用读写提交便可以了,也不是所有的数据库支持所有的隔离级别,比如Oracle数据库只支持读写提交和序列化,它的默认隔离级别为读写提交,而 MySQL数据库的默认隔离级别为可重复读。
(2)传播行为
传播行为,是指方法之间的调用问题。在大部分的情况下,我们认为事务都应该是一 次性全部成功或者全部失败的。例如,业务做成功了,但是财务没有合乎规范,被财务部 否决了,这个时候就需要回滚所有的事务。但是也会有特殊的场景,比如信用卡还款,在 还款过程中,我们有一个总的程序代码,循环调用一个repayCreditCard的还款方法,进行 还款处理,但是我们发现其中的一张卡发生了异常,这时我们不能把所有执行过的信用卡 数据回滚,,而只能回滚出现异常的这张卡。如果将所有执行过还款操作的信用卡回滚, 那么就意味着之前按时还款的用户也被认为是不按时还款的,这显然不合理。换句话说, 我们在做每一张卡操作的时候都希望有一个独立的事务管控它,使得每一张卡的还款互不 干扰。
在Spring中定义了 7种传播行为,如表8.6所示。
我们应该注意自调用的问题,什么是自调用呢?比如说我们的角色服务类有两个方法,分 别是 insertRoleList 方法和 insertRole 方法,而 insertRole 方法注解为 PROPAGATION. REQUIRES_NEW,如代码清单8-5所示。
所谓自调用就是自己的类方法调用其他方法的过程。如insertRoleList调用了 insertRole 方法,而这里注解insertRole为REQUIRES_NEW,每次调用方法的时候,会生成独立事务。 但是请读者务必注意,这实际上是不生效的,为什么呢?
我们回顾一下之前讲解的Spring AOP的动态代理运行的过程,Spring的数据库事务是在动态代理进入到一个invoke方法里面的,然后判断是否需要拦截方法,需要的时候才根 据注解和配置生成数据库事务切面上下文,而这里的自调用是没有代理对象的,是原始 对象的调用,所以根本就没有invoke方法去解析注解和配置生成数据库切面的上下文, 独立事务也无从谈起,Spring只会延续使用insertRoleList的上下文信息,所以这个注解 是无效的。你需要这样的功能,你只能独立写一个类,再去调用insertRole方法,因为在另外一个类里面,你得到的是RoleServicelmpl的代理类,进入它的invoke方法的时候它 会去解析注解,知道你需要一个独立事务。在使用的时候请读者务必小心这个问题,避 免落入陷阱。
4、Spring MVC 基础
Spring MVC是当前最为流行的互联网MVC框架,Spring MVC对框架进行了比较简易的封装,各个层级都是比较清晰的。它的核心是DispatcherServlet, Servlet将根据拦截的配置去拦截一些请求,它的作用是做一个转发在它接收了转发后就需要跳转到其他的地方。例如,在web.xml中这样配置Spring MVC,如代码清单8-6所示。
这样凡是以.do结尾的请求都会被DispatcherServlet所拦截,它拦截后一般需要进一步 跳转,一般描述跳转的有XML描述或者注解描述,现在更多得使用注解方式,所以让我们这里主要用注解的方式。一旦在Spring MVC项目中为一个类注解了©Controller,那么它就可以是一个跳转的地方,它在MVC框架中起到一个控制器的作用,如代码清单8-7所示。
代码清单 8-7: Spring MVC Controller 的伪代码
@Controller//标识为控制器. public class RoleController ( @RequestMapping (n/role/getRolen) //DispatcherServlet 匹配路径时,进入方法
@ResponseBody //标注把结果转化为JSON public RoleBean getRole (@RequestParam ("id” ) int id) //标注参数对应关系 RoleBean role = this.roleService.getRole(id); long end = System.currentTimeMillis();
return role; } }
一旦类给了©Controller标注,那么Spring MVC就认为它是一个控制层,而会根据 @RequestMapping所配置的路径跳转到对应的控制器和方法中去。参数的名称和参数的映 射关系靠@RequestParam注解对应,也比较简单。
这个方法只是返回了一个角色对象,它并不会自己变为JSON,因此我们需要处理视图 解析器,那么我们需要配置视图解析器以拦截请求的结果,如代码清单8.8所示。
我们加入 @ResponeBody 标注视图解析器 MappingJacksonHttpMessageConverter 就会拦截这个请求,然后把结果转化为JSON数据,返回给视图层。Spring MVC的跳转流程,如图8-3所示。我们真正的开发主要集中在控制器上。Spring MVC还有许多内容,但是本书是以MyBatis为主,就不介绍太多了。
二、MyBatis-Spring 应用
1、概述
在Spring发布3.0版本的时候是支持iBatis2项目的,MyBatis项目组也想将MyBatis3 版本的支持添加到Spring3.0中,而不幸的事情发生了,在MyBatis3未完成的时候,Spring3.0 就已经开发完成并发布了。Spring团队可不想发布一个基于非发布版的MyBatis的整合支 持,那么Spring官方的支持就不得不继续等待了。在这个时候MyBatis社区自己编写出了 MyBatis-Spring 项目,使得 MyBatis3 能在 Spring 中使用。
在Spring中,配置的方法较多,可以通过XML进行配置,也可以通过注解配置,但是通过注解配置巳经成为了主流,所以本书主要讲解通过注解配置MyBatis-SpringO配置MyBatis-Spring分为下面几个部分。
- 配置数据源。
- 配置 SqlSessionFactory。
- 配置 SqlSessionTemplate。 •
- 配置 Mapper 。
- 事务处理。
在 MyBatis 中要构建 SqlSessionFactory 对象,让它来产生 SqlSession,而在 MyBatis- Spring项目中SqlSession的使用是通过SqlSessionTemplate来实现的,它提供了对SqlSession 操作的封装。所以通过SqlSessionTemplate可以得到Mappero下面让我们开始在Spring环 境下配置MyBatis项目。
2、配置 SqISessionFactory
我们需要构建SqISessionFactory,它的作用是生成SqlSession,所以这节的目的很明确 就是构建 SqISessionFactory。MyBatis-Spring 项目提供了 org.mybatis.spring.SqlSession FactoryBean类给我们去配置。一般而言,我们需要给出两个参数,一个是数据源,另一个 是MyBatis的配置文件路径,这样Spring IOC容器就会初始化这个SqISessionFactory Bean, 解析MyBatis配置文件并连同数据源一同保存在Spring Bean里面。让我们看看配置的方法, 如代码清单8.9所示。
首先,配置一个数据源,它只要实现javax.sql.DataSource接口便可以了,所以它可以 是任意第三方的数据源,也可以是通过JNDI从容器中获得的数据源。然后,构建 SqlSessionFactoryBean对象。我们注入一个数据源和配置文件,classpath的写法说明它在.class 目录文件里面。这样Spring在初始化IOC容器的时候去初始化SqlSessionFactoryBean,它通过 解析配置文件得到MyBatis运行所需的上下文。
由于这里Spring已经帮助我们初始化了数据源,MyBatis的配置文件就无需再配置 关于数据库environments的节点信息了。所以配置文件就可以配置成类似代码清单8-10这 样的了。
当然我们也可以根据需要去修改配置文件。这样就成功地配置了 SqlSessionFactory, 接着我们需要创建SqlSessionTemplate。
严格地说,SqlSessionFactoryBean已经可以通过Spring IOC配置了,我们完全可以通 过Spring IOC来代替原来的配置,它为我们提供了以下属性。
在大部分情况下,我们无需全部配置,只需要配置其中几项便可以了。如果遇到复杂 的配置,笔者建议大家使用MyBatis配置文件,因为它更便于管理,且可读性高。
3、配置 SqlSessionTemplate
SqlSessionTemplate (org.mybatis.spring.SqlSessionTemplate)是一个模板类,通过调用 SqlSession来完成工作,所以在MyBatis-Spring项目中它是一个核心类。但是在Spring中 构建它是件相当容易的事情,它有两种构建方法,一种是只有一个SqlSessionFactory作为 参数的;另外一种是有两个参数的,一个是SqlSessionFactory,另一个是执行器类型,它 是一个枚举类(org.apache.ibatis.session.ExecutorType)o 弄清楚了这些,让我们看看在 Spring 中是如何构建它们的。
构建方法一,使用SqlSessionFactory参数构建,如代码清单8-11所示。
构建方法二,使用两个参数构建,如代码清单8.12所示。
这里ExecutorType的取值范围是:SIMPLE、REUSE、BATCH,对应的是我们在第6 章讨论过的三种执行器的类型。当然大部分情况下我们都不这么配置,而是在配置文件中 配置执行器的类型便可。
通过这些配置就意味着Spring会把我们之前配置的SqlSessionFactory设置到 SqlSessionTemplate 中。如果我们同时设置了 SqlSessionFactory 和 SqlSessionTemplate,那么 系统就只会用使用SqlSessionTemplate去覆盖掉SqlSessionFactory。
在iBatis时代SqlSessionTemplate可以执行许多功能,同样的在MyBatis中也是可以的。 但是在目前MyBatis的编程中用得不多,因为我们完全可以直接通过映射器擦除它,这样更 易于理解。但是在定制化编程中SqlSessionTemplate是一个很有用的类,尤其是在特殊的场 景(比如你需要Spring的编程事务的时候),或在大型Web应用中,它往往应用于DAO层, 所以我们将它注入到某个DAO中,自己编写一个公共的DAO基类也是可以的,然后Spring 中通过依赖注入相关的资源来初始化。现在,我们写一个基类,如代码清单8.13所示。
如果要做一些增删查改用户信息的工作,那么我们就需要一个用户DAO的接口,如代码清单8.14所示。
然后,我们给出实现类,如代码清单8.15所示。
方法较为简单,但是要用到它,我们还要先在Spring中配置它,如代码清单8-16所示。
这样就在Spring中注入了 SqISessionTemplate到DAO层。我们就可以使用它做我们想 要做的事情,它的作用就等同于SqlSession对象。让我们可以做想要做的事情。
这和iBatis时代的编程方式是一样的,我们不建议使用这样的编程方式,原因有两个。
-
- SqISessionTemplate是MyBatis的类,我们要使用一个id,去标出我们将会调用的那 条SQL,这对于编程来说是困难的,有侵入框架之嫌,可读性较低。
- 我们在编写代码时不能保证id的正确性。因为IDE无法识别id是否正确,需要运 行的时候才能知道。
因此笔者建议大家釆用Mapper接口的编程方式。
4、配置Mapper
在代码中,大部分场景都不建议使用SqlSessionTemplate或者SqlSession的方式,这里 我们釆用Mapper接口编程的方式,让SqlSessionTemplate在开发过程中“消失”,这样更 符合面向对象的编程,也更利于我们理解。
(1) MapperFactoryBean
在MyBatis中,Mapper只需要是一个接口,而不是一个实现类,它是由MyBatis体系 通过动态代理的形式生成代理对象去运行的,所以Spring也没有办法为其生成实现类。为 了处理这个问题,MyBatis-Spring团队提供了一个MapperFactoryBean类作为中介,我们可以通过配置它来实现我们想要的Mapper。配置 MapperFactoryBean 有 3 个参数,mapperinterface、SqlSessionFactory 和 SqlSessionTemplate 。
- mapperinterface,用来制定接口 ,当我们的接口继承了配置的接口,那么MyBatis就认为它是一个Mapper。
- SqlSessionFactory,当 SqlSessiomTemplate 属性不被配置的时候,MyBatis-Spring 才会去设置它。
- SqlSessionTemplate,当它被设置的时候,SqlSessionFactroy将被作废,如代码清单 8.17所示。
这样我们就可以使用这个接口进行编程了,它的效果等同于sqlSession.getMapper (UserDAO.class)o不过在Spring中,它更为优雅。
(2)MapperScannerConfigurer
一个复杂的系统存在许许多多的DAO,比如我们往往不单单有UserDAO,还有角色 的RoleDAO.产品的ProducDAO,如果需要一个个配置,那么工作量会很大,不过 MyBatis-Spring团队已经处理了这种场景,它釆用自动扫描的形式来配置我们的映射器, 这样我们就可以在很少的代码情况下完成对映射器的配置,大大提高了效率。
在MyBatis-Spring项目中,我们釆用的是MapperScannerConfigurer。我们可以配置这 么几个属性。
- basePackage,指定让Spring自动扫描什么包,它会逐层深入扫描。
- annotationClass,表示如果类被这个注解标识的时候,才进行扫描。
- sqlSessionFactoryBeanName,指定在 Spring 中定义 sqlSessionFactory 的 bean 名称。 如果它被定义,sqlSessionFactory将不起作用。
- sqlSessionTemplateBeanName,指定在 Spring 中定义 sqlSessionTemplate 的 bean 的名 称。如果它被定义,sqlSessionFactoryBeanName将不起作用。
- markerinterface,指定是实现了什么接口就认为它是Mapper。我们需要提供一个公 共的接口去标记。在Spring配置前需要给DAO 一个注解,在Spring中往往是使用 注解©Repository表示DAO层的,让我们对UserDAO进行改造,如代码清单8-18 所示。
现在我们在Spring中配置它,如代码清单8-19所示。
这样Spring上下文就会自动扫描com.leam.dao从而找到标注了 Repository的接口,自 动生成Mapper,而无需多余的配置,大大方便了我们的运用。
5、配置事务
MyBatis和Spring结合后是使用Spring AOP去管理事务的,使用Spring AOP是相当简 单的,它分为声明式事务和编程式事务两种。大部分场景下使用声明式事务便可以了。使 用声明式事务更方便,所以目前是主流方向,本书也保持一致,如代码清单8.20所示。
这个时候我们往往需要业务层,业务层既是处理业务的地方,又是管理数据库事务的 地方,我们可以使用Spring的注解Service来表示哪个类为业务层的类。同样,我们也可 以通过自动扫描的方法读取Service对象到Spring上下文中,配置如代码清单8-21所示。
这样Spring就会自动扫描这些Service对象的bean读取到上下文中。MyBatis的事务就 交由Spring去控制了。我们只需要在Spring中通过注解注入即可,如代码清单8.22所示。