返回介绍

8.1 Spring 的基础知识

发布于 2025-04-26 13:08:35 字数 11565 浏览 0 评论 0 收藏

为了更好地讨论 MyBatis 在 Spring 项目中的应用,我们先谈谈 Spring 的基础知识。Spring 技术主要由两个基础的功能 IOC 和 AOP 构成。MyBatis-Spring 项目提供了一些基础的类,使得 Spring 能和 MyBatis 结合起来用,这个项目需要使用 mybatis-spring-x.x.x.jar 实现。不过在此之前我们先了解一下 Spring 的一些基础知识,其中包括 Spring IOC、Spring AOP 和 Spring 关于数据库事务的一些知识。

8.1.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)。我们需要进行如代码清单 8-1 所示的操作来构建它。

代码清单 8-1:用 Spring IOC 生成实例

<bean id= role  class= com.learn.mybatis.chapter8.pojo.TRole >
        <constructor-arg index="0" value="1" />  
        <constructor-arg index="1" value="CEO" />  
        <constructor-arg index="2" value="公司老大" />  
</bean>

这样我们就描述了一个 TRole,它可以注入到其他的资源中。但是如果构造方法多,显然构造注入不是一个很好的方法,而 Spring 更加推荐使用 setter 注入。假设上例角色类还有一个没有参数的构造方法,它的三个属性,编号(id)、角色名称(roleName)和备注(note)都有 setter 方法,那么我们可以使用 setter 注入,如代码清单 8-2 所示。

代码清单 8-2:使用 Spring 的 setter 注入

<bean id= role  class= com.learn.mybatis.chapter8.pojo.TRole >
        <property name="id" value="1"/>  
        <property name="roleName" value="CEO" />  
        <property name="note" value="公司老大" />  
</bean>

使用 setter 注入更加灵活,因为使用构造方法,会受到构造方法的参数个数、顺序这些因素干扰。侵入更加少,所以这是 Spring 首选的注入方式。

Spring 的接口注入方式。它是一种注入其他服务的接口,比如 JNDI 数据源的注入,在 Tomcat 或者其他的服务器中往往配置了 JNDI 数据源,那么就可以使用接口注入我们需要的资源,如代码清单 8-3 所示。

代码清单 8-3:使用 Spring 接口注入数据源

  <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName">
            <value>java:comp/env/jdbc/mybatis</value>
        </property>
    </bean>  

它允许你从一个远程服务中注入一些服务到本地调用。

上面讨论了注入的几种方式,在大型系统中,我们往往还会使用注解注入的方式来描述系统服务之间的关系,这也是 Spring 所推荐的方式。

8.1.2 Spring AOP 基础

Spring IOC 相对而言还是比较容易理解的,如果你懂得了第 6 章的反射技术,就知道它是用反射技术实现的,而 Spring AOP 就不是了。在 MyBatis-Spring 技术中,它最大的用处是事务的控制,这是一个最麻烦也最难理解的东西。

Spring AOP 是通过动态代理来实现的。首先在传统的 MVC 构架中,业务层一般都夹带着数据库的事务管理,例如,插入一个角色,它是使用 RoleService 接口的实现类 RoleServiceImpl 去实现的,如代码清单 8-4 所示。

代码清单 8-4:插入角色

@Service
public class RoleServiceImpl implements RoleService {
......

    @Autowired
private RoleDAO roleDAO = null;

@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int insertRole(Role role) {
    return roleDAO.insertRole(role);
} 
......
}

当程序进入到 insertRole 方法的时候,Spring 就会读取配置的传播行为进行设置,这里的配置为 Propagation.REQUIRED,它的意思是当前方法如果有事务则加入当前事务,否则就创建新的事务。这样这个 insertRole 方法就在事务内调用了,那么它是怎么实现的呢?

在传统 Spring 的书籍中,我们一般会涉及到一些抽象的概念,如切面、连接点、通知、切入点、目标对象、AOP 代理等等极其抽象的概念。

这些内容论述的时候太过于抽象,所以我们不长篇大论地罗列一些晦涩的概念去讨论它们,而是使用原理去讨论它们,下面我们通过原理来分析它们执行的过程。

Spring AOP 实际上就是一个动态代理的典范。不熟悉动态代理的读者可以翻阅本书第 6 章的内容,请务必掌握它们,这是阅读下面分析的基础,否则你读下面的文字便会像读天书一样。

现在以角色服务类(RoleServiceImpl)为例。

首先 Spring 可以生成代理对象,这样调度 insertRole 方法的时候就进入了一个 invoke 方法里面。Spring 会判断到底要不要拦截这个方法,这是一个切入点的配置问题,它是通过正则式匹配的,比如我们在正则式配置 insert*这样的统配,那么 Spring 就会拦截这个 insertRole() 方法,否则就不拦截,直接通过 invoke 方法反射调用这个方法,就结束了。这便是切入点的概念,很简单吧。

其次就是切面。切面是干什么的?它是插入角色的,里面包含事务,而事务就是整个方法的一个切面,可能你的方法会很复杂,包含业务、财务和日志等多方面,而它们都受到同一事务管辖,那么事务就是这方法的一个切面。这个时候 Spring 就会根据我们配置的信息,知道这个方法需要事务,采用传播行为 Propagation.REQUIRED 运行方法,这就是 Spring 的切面。

再次就是连接点。连接点是在程序运行中根据不同的通知来实现的程序段。由于 Spring 使用动态代理,我们在反射原始的方法之前可以做一些事情,于是有了前置通知(Before advice),也可以在反射之后做一些事情,那便是后置通知(After advice),反射原来的方法可能正确返回,也可能因此抛出异常,所以还有正常返回后通知(After return advice)和产生异常的抛出异常后通知(After throwing advice)。也有可能需要用自定义方法取代原有的方法,就如 MyBatis 的插件一样,不采用原有的 invoke 方法而是使用自定义的方法,所以还有环绕通知(Around advice),怎么样用动态代理的原理来分析是不是比单独讲概念要清晰得多呢?

代理目标,就是哪个类的对象被代理了。这里显然就是 RoleServiceImpl 对象被代理了。

AOP 代理(AOP Proxy)就是指采用何种方式进行代理,我们知道 JDK 的代理需要使用接口,而 CGLIB 则不需要,因此在默认的情况下 Spring 采用这样的规则。当 Spring 的服务包含接口描述时采用 JDK 动态代理,否则采用 CGLIB 代理。当然你可以通过配置修改它们。

基于上面的论述,我们清楚了 AOP 的大致情况,这些在理解动态代理的基础上是相对简单的。如图 8-1 所示,Spring AOP 在动态代理下运行的流程。

183-1

图 8-1 Spring AOP 在动态代理下运行的流程

这里执行方法的逻辑有点复杂,笔者另外给图解释它,如图 8-2 所示。这便是在 Spring AOP 动态代理下做的判断和运行的流程图,表面上看起来有点复杂,实际在理解了动态代理后结合 Java 基础,就可以十分容易地理解它们了,也可以使用代码去实现它们。

8.1.3 Spring 事务管理

Spring 事务管理是通过 Spring AOP 去实现的,在 8.1.2 节中我们讨论了 Spring AOP 的执行过程和基础概念,默认的情况下 Spring 在执行的方法抛出异常后,引发事务回滚,当然你可以用拦截器或者配置去改变它们,我们这里只讨论默认的情况,不讨论其他复杂的情况。我们首先讨论一下 Spring 的隔离级别和传播行为,这是很容易犯错的地方。

184-1

图 8-2 Spring AOP 动态代理下消息执行过程

8.1.3.1 事务隔离级别

数据库和程序一样,也有并发的问题,在同时存在两个或者两个以上的数据库事务环境中,同一条记录甚至是不同记录都会由于 SQL 在不同时刻的执行产生不同的结果,甚至产生错误。于是便有了隔离级别这样的数据库的概念,按照数据库的概念分为脏读、读写提交、可重复读、序列化 4 种。我们来讨论一下它们。

脏读是指一个事务能够读取另外一个事务未提交的数据,如表 8-1 所示。

这里我们发现事务 B 读取了事务 A 未提交的数据,而最后事务 A 将回滚,这是十分危险的。为了避免这个问题,我们往往使用读写提交的隔离级别,如表 8-2 所示。

表 8-1 脏读

185-1

表 8-2 读写提交

185-2

这里我们用了读写提交完成了这些逻辑,但是读写提交依旧会产生一些问题,让我们看看这样的场景,如表 8-3 所示。

表 8-3 读写提交产生的问题

185-3

这里我们看到,对于余额而言,在 T4 时刻买单失败了。因为在 T3 时刻老婆提交了消费 800 元的事务,这时老公可要出洋相了。为了避免这个问题,我们可以使用可重复读的策略,这样就消除了老公无钱买单的尴尬场景。

但是可重复读是针对于同一条记录而言的,对于不同的记录会发生下面这样的场景,如表 8-4 所示。

表 8-4 不同的记录会发生的场景

186-1

我们看到老婆在查询之后,老公启动了消费,并先于老婆之前打印账单记录,所以在 T4 时刻,打印了 1800 元 11 条记录,这个时候老婆就会去质疑这 800 元是不是幻读的。上面和不可重复读很接近,但是我们需要注意的是,不可重复读是针对同一条记录,而幻读是针对删除和插入记录的。

为了避免服这个问题我们可以采用序列化的隔离层。序列化就意味着所有的操作都会按顺序执行,不会出现脏读、不可重读和幻读的情况,如表 8-5 所示。

表 8-5 序列化的隔离层

186-2

这就是数据库隔离层的情况,上面我们只讨论了在多并发环境下数据安全性的问题,而没有讨论它们之间的性能。一般而言,性能从脏读→读写提交→可重复读→序列化是直线下降的,更多的时候我们使用读写提交便可以了,也不是所有的数据库支持所有的隔离级别,比如 Oracle 数据库只支持读写提交和序列化,它的默认隔离级别为读写提交,而 MySQL 数据库的默认隔离级别为可重复读。

8.1.3.2 传播行为

传播行为,是指方法之间的调用问题。在大部分的情况下,我们认为事务都应该是一次性全部成功或者全部失败的。例如,业务做成功了,但是财务没有合乎规范,被财务部否决了,这个时候就需要回滚所有的事务。但是也会有特殊的场景,比如信用卡还款,在还款过程中,我们有一个总的程序代码,循环调用一个 repayCreditCard 的还款方法,进行还款处理,但是我们发现其中的一张卡发生了异常,这时我们不能把所有执行过的信用卡数据回滚,而只能回滚出现异常的这张卡。如果将所有执行过还款操作的信用卡回滚,那么就意味着之前按时还款的用户也被认为是不按时还款的,这显然不合理。换句话说,我们在做每一张卡操作的时候都希望有一个独立的事务管控它,使得每一张卡的还款互不干扰。

在 Spring 中定义了 7 种传播行为,如表 8-6 所示。

表 8-6 7 种传播行为

187-1

我们应该注意自调用的问题,什么是自调用呢?比如说我们的角色服务类有两个方法,分别是 insertRoleList 方法和 insertRole 方法,而 insertRole 方法注解为 PROPAGATION_REQUIRES_NEW,如代码清单 8-5 所示。

代码清单 8-5:无效的传播行为

@Service
public class RoleServiceImpl {
     @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int insertRoleList(List<Role> roleList) {
    for (Role role : roleList) {
        this.insertRole(role);//insertRole 的注解失效
    }
}
......
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW)
public int insertRole(Role role) {
    try {
        return roleDAO.insertRole(role)
    } catch(Exception ex) {
        ex.printStackTrace();
    }
    return 0;
}
}

所谓自调用就是自己的类方法调用其他方法的过程。如 insertRoleList 调用了 insertRole 方法,而这里注解 insertRole 为 REQUIRES_NEW,每次调用方法的时候,会生成独立事务。但是请读者务必注意,这实际上是不生效的,为什么呢?

我们回顾一下之前讲解的 Spring AOP 的动态代理运行的过程,Spring 的数据库事务是在动态代理进入到一个 invoke 方法里面的,然后判断是否需要拦截方法,需要的时候才根据注解和配置生成数据库事务切面上下文,而这里的自调用是没有代理对象的,是原始对象的调用,所以根本就没有 invoke 方法去解析注解和配置生成数据库切面的上下文,独立事务也无从谈起,Spring 只会延续使用 insertRoleList 的上下文信息,所以这个注解是无效的。你需要这样的功能,你只能独立写一个类,再去调用 insertRole 方法,因为在另外一个类里面,你得到的是 RoleServiceImpl 的代理类,进入它的 invoke 方法的时候它会去解析注解,知道你需要一个独立事务。在使用的时候请读者务必小心这个问题,避免落入陷阱。

8.1.4 Spring MVC 基础

Spring MVC 是当前最为流行的互联网 MVC 框架,Spring MVC 对框架进行了比较简易的封装,各个层级都是比较清晰的。它的核心是 DispatcherServlet,Servlet 将根据拦截的配置去拦截一些请求,它的作用是做一个转发在它接收了转发后就需要跳转到其他的地方。例如,在 web.xml 中这样配置 Spring MVC,如代码清单 8-6 所示。

代码清单 8-6:web.xml 中配置 Spring MVC

<servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>2</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>

这样凡是以.do 结尾的请求都会被 DispatcherServlet 所拦截,它拦截后一般需要进一步跳转,一般描述跳转的有 XML 描述或者注解描述,现在更多得使用注解方式,所以让我们这里主要用注解的方式。一旦在 Spring MVC 项目中为一个类注解了 @Controller,那么它就可以是一个跳转的地方,它在 MVC 框架中起到一个控制器的作用,如代码清单 8-7 所示。

代码清单 8-7:Spring MVC Controller 的伪代码

@Controller//标识为控制器.
public class RoleController {
    @RequestMapping("/role/getRole")//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 所示。

代码清单 8-8:配置视图解析器

<bean class="org.springframework.web.servlet.mvc.annotation. AnnotationMethodHandlerAdapter">      
        <property name="messageConverters">      
            <list >      
                <ref bean="mappingJacksonHttpMessageConverter" />      
            </list>      
        </property>      
    </bean>  

    <bean id="mappingJacksonHttpMessageConverter" class="org.springframework. http.converter.json.MappingJacksonHttpMessageConverter">    
        <property name="supportedMediaTypes">    
            <list>    
                <value>application/json;charset=UTF-8</value>    
            </list>    
        </property>  
    </bean>

我们加入 @ResponeBody 标注视图解析器 MappingJacksonHttpMessageConverter 就会拦截这个请求,然后把结果转化为 JSON 数据,返回给视图层。

Spring MVC 的跳转流程,如图 8-3 所示。

191_0001

图 8-3 Spring MVC 构架图

我们真正的开发主要集中在控制器上。Spring MVC 还有许多内容,但是本书是以 MyBatis 为主,就不介绍太多了。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。