本文介绍Spring全家桶(Spring、SpringMVC、Spring Boot、Spring Cloud)中最基础的框架——Spring。
1. Spring概述
Spring框架主要负责三层架构中的业务逻辑层。该框架出现在2002年左右,解决企业级项目开发的难度。减轻对项目模块之间的管理、类和类之间的管理,帮助开发人员创建对象,管理对象之间的关系。
因为企业级项目比较庞大,功能以及对象的模块比较多,这样单纯的手工管理模块以及模块的对应关系比较复杂,因此出现了该框架。Spring框架中两个最核心的技术是IoC和AOP。能够实现模块之间、类之间的解耦合,使得程序之间修改变得相对容易。
在Spring官网上可以看到Spring全家桶,Spring框架指的就是Spring Frame这个项目。点击该项目进入到详细页面。
可以看到Spring框架中的特点以及关键技术等等,其中依赖注入指的就是IoC,ORM指的是数据访问,可以和MyBatis集成。
在“LEARN”按钮部分,Reference Doc.指的是该版本的所有模块的参考文档,API Doc.指的是具体API方法的帮助文档,日常使用中主要是参考这两个文档。
1.1 Spring优点
Spring是一个框架,是一个半成品的软件。有20多个模块组成,它是一个容器管理对象,而容器是装东西的,Spring容器不装文本、数字等等,装的是对象,即Spring是存储对象的容器。具有以下优点:
轻量
Spring框架使用的jar包都比较小,框架运行占用的资源少,效率较高,不依赖其他jar。
针对接口编程,解耦合
Spring提供了IoC控制反转,由容器管理对象、对象的依赖关系。原来在代码中的对象创建方式,现在由容器完成,对象之间的依赖解耦合。
AOP编程的支持
通过Spring提供的AOP功能,方便进行面向切面编程,许多不容易用传统OOP实现的功能可以通过AOP轻松应付。在Spring中,开发人员可以从繁杂的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。
方便集成各种优秀框架
Spring可以集成各种框架,降低各种框架的使用难度。
1.2 Spring体系结构
Spring框架的体系结构如下所示:
其中左上方是数据集成模块,可以用JDBC,也可以使用ORM来集成MyBatis框架来访问数据。右上方是Web开发,集成SpringMVC框架来实现。
中间是AOP、Aspects,右侧两个是集成功能,如消息、邮件等等。
下面是核心容器(主要涉及到IoC)。最下面是test模块,类似maven中的junit。
总体来看分为以下几个模块:
- 数据访问模块
- Web应用模块
- AOP模块
- 集成功能模块(略过)
- 核心容器模块
- 测试模块(略过)
本文自底向上介绍Spring框架的各个模块。
2. IoC控制反转
2.1 IoC概述
控制反转(IoC,Inversion of Control)是一个概念,是一种思想。指的是将对象的创建、赋值以及管理工作都交给代码之外的容器实现,也就是对象的创建等工作不再是由我们自己编写代码来实现,而是由其他外部资源来完成的。即对象的控制权不再是程序,而是其他资源,这就是控制反转。
进一步讲,控制反转分为两部分:控制和反转。
控制:指的是创建对象,对象的属性赋值以及对象之间的关系管理。
反转:即把原来的权限转移给代码之外的容器来实现,由容器来代替开发人员进行操作。
(正转:由开发人员在代码中,使用new构造方法创建对象,开发人员主动管理对象)
那么为什么要这样做呢?这样做的优点是什么:
它的目的是:在减少对代码改动的基础上,也能实现不同的功能。也就是说,将分离出的代码部分以其他方式实现,而剩余核心部分则是以代码的形式展现,对于分离出的“代码部分”,即使修改,也不会参与编译(类似配置文件),这样就可以通过修改分离的部分来应对不同的业务,然后直接运行程序,不再需要编译。
个人理解:感觉和MyBatis类似,MyBatis是将数据库的相关操作交给了其他资源,使得程序和数据操作分离。
而这里则是将对象的基本管理操作交给代码之外的其他资源,使得程序更加注重逻辑功能,相当于对程序的分工进一步细化,更加保持核心功能。
并且这个分离出的功能在不修改源代码的基础上,修改该文件来实现该功能。
目前为止,Java创建对象的方式主要有以下几种:
- 构造方法,
new Student()
- 反射
- 序列化
- 克隆
- IoC:容器创建对象
- 动态代理
注意,IoC和以上其他方法都不一样,IoC不需要我们手动创建对象。目前为止,学过的技术中,体现IoC的一个机制就是Servlet:
- 创建类继承HttpServlet
- 在web.xml文件中注册servlet,使用<servlet-name>以及<servlet-class>注册
- 但是我们并没有自己 new Servlet对象
- 而是Servlet是Web容器(如Tomcat服务器)帮助创建的,并且它自己实现了调用,也就是说,这个web工作的流程已经设计好了,我们只需要完成具体的功能即可。Tomcat也被称为Web容器,就是因为里面存放了Servlet对象、监听器、过滤器对象等等。
那么Spring也有同样的容器作用,帮助我们创建对象,供我们使用,我们只需要在配置文件中声明该类即可。
IoC作为一种思想,必然有针对该思想的技术实现,具体的技术实现就是依赖注入(DI,Dependency Injection):我们只需要在程序中提供要使用的对象名称就可以,至于对象如何在程序中创建、赋值、查找等都由容器内部实现。
Spring就是使用DI实现了IoC的功能,底层创建对象仍然使用的是反射机制。
注意,Spring是一个容器,用来管理对象以及给对象属性赋值,底层是通过反射创建对象。
2.2 Spring的第一个程序
注意,Spring只是容器,用来管理对象等操作,并不是web项目容器。因此可以采用普通的maven工程即可。
步骤如下所示:
创建maven项目
这个不用多说,直接maven项目即可
加入maven依赖:
Spring依赖
spring-context和spring-webmvc是Spring中的两个模块。
- spring-context:是ioc功能,用来创建对象。
- spring-webmvc:做web开发会使用,是servlet的升级,当然这也会用到spring-context中创建对象的功能。
1
2
3
4
5
6
7<!-- Spring -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.16</version>
</dependency>单元测试junit依赖(方便测试)
1
2
3
4
5
6
7<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
创建类(接口和它的实现类)
和没有使用框架一样,就是普通的类。注意,Spring只是帮助创建对象,但是其类仍然需要自己定义。创建的接口和类如下所示:
1
2
3public interface SomeService {
void doSome();
}1
2
3
4
5
6public class SomeServiceImpl implements SomeService {
public void doSome() {
System.out.println("执行了SomeService中的doSome()方法");
}
}创建Spring需要使用的配置文件
声明类的信息,这些类由Spring创建和管理。即将上述类的信息,交给Spring,即类似Servlet的配置文件来注册类。IDEA提供了创建Spring配置文件的快捷方式。我们在resource目录下创建即可,名字自定义(一个规范是ApplicationContext.xml),这里起名为
beans.xml
:配置文件如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<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">
<!--
底层就是类似这样: SomeService ss = new org.hianian.service.impl.SomeServiceImpl();
将对象创建好后,放入到Map中,springMap.put(id值, 对象)。
之后,仅需要id值就可以获取到创建好的对象了。
-->
<bean id="someService" class="org.hianian.service.impl.SomeServiceImpl"></bean>
</beans>
<!--
Spring配置文件:
1. beans:是根标签,Spring把Java对象称为Bean。所以根便签中必定有若干个<bean>子标签
2. 上面的 .xsd 文件约束文件,和MyBatis中的 .dtd 文件类似,用来限制本文件中出现的标签和属性的。可以直接在浏览器中打开查看。
3. <bean id="" class="" /> 标签用来告诉Spring创建对象
这个标签的意义是 声明bean,就是告诉Spring要创建某个类的对象
1. id属性是对象的自定义名称,唯一值,符合Java命名规则即可。后续Spring通过这个定位对象,类似Servlet中的<servlet-name>中的值。
2. class是类的全限定名称,不能是接口,只能是类,因为是通过反射机制创建对象,Spring通过这个创建对象,
dicen j
4. 注意,一个bean标签只能声明一个对象。
-->测试Spring创建对象。
下面是Spring框架和传统方法的对比:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class TestSomeService {
public void testDoSome(){
// 不用Spring框架时的写法
SomeService ss = new SomeServiceImpl();
ss.doSome();
}
public void testSpringDoSome(){
// 用Spring框架时的写法
// 1. 指定Spring配置文件的名称
String config = "beans.xml";
// 2. 创建表示Spring容器的对象:ApplicationContext,
// 这个ApplicationContext就是表示Spring容器,可以通过该容器获取到对象
// ApplicationContext是一个接口,其常用的实现类有:
// FileSystemXmlApplicationContext 是从其他盘符中读取配置文件(较少使用)
// ClassPathXmlApplicationContext 是从 类路径 中读取配置文件。
// 读取配置文件信息,当读取到bean标签的时候,就会完成创建对应对象的工作,将对象放入map中。
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 从容器中获取某个对象,ApplicationContext.getBean(),有多个重载方法,一般用id来获取.
// 返回类型是Object,这里需要强转,才能使用对象的方法以及后续的对象操作等等。
SomeService someService = (SomeService) ac.getBean("someService");
someService.doSome();
}
}
注意,Spring默认创建对象的时间:在创建Spring容器ApplicationContext时,会创建配置文件中的所有的bean标签对象。
注意,创建对象,默认调用的是无参构造方法。
另外,似乎一个容器对应一个配置文件,那么也就是说,我们可以创建多个配置文件,并据此创建多个容器。
2.2.1 获取Spring容器中的对象信息
方法名 | 描述 |
---|---|
ac.getBeanDefinitionCount() | 获取容器中对象的数量 |
ac.getBeanDefinitionNames() | 获取容器中对象的名称 |
1 |
|
2.2.2 创建非自定义对象
我们知道,通知Spring创建对象需要在配置文件中声明对象。此时,如果想要创建已有的类(Java内置类,比如String、ArrayList等等),和前面一样,只需要在声明的时候填写全限定名称即可,如
1 | <bean id="myDate" class="java.util.Date"></bean> |
1 |
|
获取到对象之后,接下来就是给对象属性赋值了(DI),主要有下面两种方法实现:基于配置文件和基于注解。
DI有两种语法:
- set注入(设值注入):使用Spring调用 该类的set方法,在set方法中可以实现属性的赋值,大部分是这种用法。
- 构造注入:使用Spring调用 该类的有参构造方法,创建对象。在构造方法中完成赋值。
注意,Spring中规定Java的基本数据类型(以及其包装类)和String数据类型为简单数据类型。
2.3 基于XML的DI
这种方式是在Spring配置文件中,使用标签和属性给Java对象的属性赋值。
2.3.1 set注入
简单数据类型的注入
1
2
3
4
5<bean id="" class="">
<!-- 注意,一个property只能给一个属性赋值 -->
<!-- 底层在创建完对象后,会调用set方法进行赋值,可以在set方法输出进行测试 -->
<property name="属性名字" value="要赋的值"></property>
</bean>经过测试:set方法名必须符合命名规范,即
set属性名
,否则就会找不到该方法,报异常:NotWritablePropertyException
,Bean property 'name' is not writable or has an invalid setter method
。另外,Spring只是负责调用set方法,至于方法的内部实现,没有约束。并且,仅仅是根据name的值来进行set拼接找到对应的set方法,不会检查到底有没有name这个属性。那么此时,可以利用set方法的命名规则,来执行对应的方法,而无需考虑到底有没有该属性。
还有,value值一定要双引号括起来,无论是什么数据类型,统一为字符串类型,这是xml文件的规则。
案例如下所示:
1
2
3
4<bean id="myStudent01" class="org.hianian.ba02.Student">
<property name="name" value="张三"></property>
<property name="age" value="23"></property>
</bean>引用数据类型的注入
1
2
3
4
5
6<bean id="" class="">
<!-- 我们知道,引用数据类型就是指向的是对象,所以引用数据类型的注入,需要有该对象,而该对象可以用
<bean>标签创建,所以利用ref属性将对象传入即可,而bean标签的id可以唯一标识对象。
-->
<property name="属性名字" ref="bean的id"></property>
</bean>案例如下所示:
1
2
3
4
5
6
7
8
9
10<bean id="mySchool" class="org.hianian.ba02.School">
<property name="name" value="动力节点"></property>
<property name="address" value="北京市大兴区亦庄"></property>
</bean>
<bean id="myStudent" class="org.hianian.ba02.Student">
<property name="name" value="张三"></property>
<property name="age" value="24"></property>
<property name="school" ref="mySchool"></property>
</bean>
2.3.2 构造注入
构造注入则是调用构造方法在创建对象的时候来设置属性值。默认情况下,Spring调用无参构造方法创建对象,需要使用<constructor-arg>标签来设置使用构造方法,这个标签和<property>标签类似。
- name属性表示构造方法的形参名
- index表示构造方法的参数的位置,从左到右依次是0,1,2的顺序(可以替代name属性)
- value表示如果构造方法的形参是简单类型,则使用value赋值
- ref表示如果构造方法的形参是引用类型,则使用ref赋值
案例如下所示:
1 | <bean id="myStudent" class="org.hianian.ba03.Student"> |
2.3.3 引用数据类型属性的自动注入
上述两种方法,无论是基本数据类型还是引用数据类型,均是从配置文件中通过标签来注入属性。由于在大型项目中,类的属性可能涉及到很多个引用数据类型,因此就需要多个<bean>标签,代码比较繁琐,因此Spring允许自动注入引用数据类型,即自动注入。
自动注入指的是:Spring框架根据某些规则可以给引用类型赋值(注意,只针对引用类型)。常用的语法有如下两种:
byName
即按名称注入。Java类中引用类型的属性名和Spring容器中(配置文件)<property>标签的id名称一样,且数据类型一致的,这样的容器中的bean,Spring能够赋值给引用类型。
就是说,因为需要给引用类型的属性赋值,所以必须要有该对象,即在配置文件中必定有<bean>标签声明该对象。此时,如果需要给该属性赋值,我们可以用byName来赋值,将id为x的<bean>对象赋值给属性x,而不需要<property>标签了。
所以,bean标签的id必须要和类属性名一致。而且该方法对所有引用数据类型有效。即会检查该类中所有的引用数据类型属性,然后在配置文件中找对应的bean标签,最后赋值。
语法规则如下:
1
2
3
4<bean id="" class="" aotuwire="byName">
<!--简单类型属性赋值-->
<property name="" value="">
<\bean>案例如下所示:
1
2
3
4
5
6
7
8
9
10<bean id="myStudent" class="org.hianian.ba04.Student" autowire="byName">
<property name="name" value="张三"></property>
<property name="age" value="24"></property>
</bean>
<!-- 注意,id名和属性名一致 -->
<bean id="school" class="org.hianian.ba04.School">
<property name="name" value="清华大学"></property>
<property name="address" value="北京市大兴区亦庄plus"></property>
</bean>byType
即按类型注入。Java类中引用类型的属性名和Spring容器中(配置文件)<bean>标签的class属性是同源关系的,这样的bean能够赋值给引用类型。同源指的是同一类,有以下三类:
- Java类中引用类型的数据类型和bean的class的值是一样的。
- java类中引用类型的数据类型(父)和bean的class的值(子)是父子类关系。
- Java类中引用类型的数据类型(接口)和bean的class的值(实现类)是接口和实现类关系。
语法规则如下:
1
2
3
4<bean id="" class="" aotuwire="byType">
<!--简单类型属性赋值-->
<property name="" value="">
<\bean>案例如下所示:
1
2
3
4
5
6
7
8
9<bean id="myStudent" class="org.hianian.ba05.Student" autowire="byType">
<property name="name" value="张三"></property>
<property name="age" value="24"></property>
</bean>
<bean id="mySchool" class="org.hianian.ba05.School">
<property name="name" value="人民大学plus"></property>
<property name="address" value="北京市大兴区亦庄plus"></property>
</bean>在注入的时候,首先创建对象,对基本数据类型赋值,然后发现是byType,所以查找对象中的引用数据类型的类型,然后在配置文件中找对应的同源对象,并对其赋值。
针对其他类型这里不再测试。因为同源类型有三种,那么如果这三种同时出现会怎么样呢?Spring有优先级吗?
答案是没有的,如果出现多个同源关系,会报错。
oUniqueBeanDefinitionException: No qualifying bean of type 'org.hianian.ba05.School' available: expected single matching bean but found 2: mySchool,myPrimarySchool
2.4 基于注解的DI(掌握)
这种方式是使用注解完成属性赋值。使用注解的步骤如下:
加入maven的依赖spring-context。实际上注解需要使用spring-aop依赖,而在加入spring-context的同时,会间接自动加入spring-aop的依赖。
在类中加入spring的注解(spring提供了多个不同功能的注解)
在spring的配置文件中,加入一个组件扫描器的标签,指明注解在项目中的位置。组件扫描器的语法如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 声明组件扫描器 属性base-package用来指定注解在项目中的包名,实际上就是指明在哪里扫描组件-->
<!-- 这里的组件指的就是Java对象 -->
<!--
component-scan工作方式:spring会扫描遍历base-package指定的包,把包和子包中的所有类扫描,找到类中的注解
按照注解的功能进行操作,比如创建对象或者给属性赋值等等。
-->
<context:component-scan base-package="org.hianian.ba01"></context:component-scan>
</beans>注意,在加入组件扫描标签后,上面自动新增了约束文件:spring-context.xsd,并起了一个命名空间的名称:xmlns:context=”http://www.springframework.org/schema/context"。
另外,有时候不只有一个包,所以,需要指定扫描多个包,有如下三种方式:
1
2
3
4
5
6
7
8
9<!-- 使用多个组件扫描器 -->
<context:component-scan base-package="org.hianian.ba01"></context:component-scan>
<context:component-scan base-package="org.hianian.ba02"></context:component-scan>
<!-- 使用分隔符分号或者逗号分隔多个包名 -->
<context:component-scan base-package="org.hianian.ba01; org.hianian.ba02"></context:component-scan>
<!-- 指定这些多个包的父包 -->
<context:component-scan base-package="org.hianian"></context:component-scan>
主要有以下几个注解:
@Component
该注解用于创建对象,相当于<bean>标签。有一个属性value,表示对象的名称,相当于bean标签的id值,是唯一的。这个注解用于修饰类,只能在类上面写,相当于创建某类的对象。注意,一个注解创建的对象,在整个Spring容器中就一个。
@Repository
该注解也是用于创建对象,只不过一般放在DAO的实现类上面,即用于持久层类对象。使用语法和Component一样,用来访问数据库。
@Service
该注解也是用于创建对象,只不过一般放在Service的实现类上面,用于业务层类对象。使用语法和Component一样,用来处理业务逻辑。
@Controller
该注解也是用于创建对象,只不过一般放在控制器的实现类上面,用于控制层类对象。使用语法和Component一样,用于接收请求,显示处理结果。
@Value
用于给简单数据类型属性赋值。该注解有一个属性value,是String类型的,表示简单类型的属性值。有两个位置用法,一个是在属性定义的上面,无需set方法(推荐使用);第二个是在set方法上面使用。注意,即使自己定义了set方法,但是对于第一种用法来说,并不会调用自定义的set方法。
@Autowired
用于给引用类型属性赋值。该注解底层是Spring的自动注入原理。支持byType(默认)和byName。有两个位置用于,一个是在属性定义的上面,无序set方法(推荐使用);第二个是在set方法上面使用。注意,在使用之前,一定要先用创建对应的对象(注解方式或配置文件方式均可,都会在容器中保存)。此时spring才会根据自动注入原理找同源类型对象,否则会找不到赋值为空。
如果想要采用byName方式,则除了添加@Autowired之外,还需要添加@Qualifier(value=”bean的id”)注解,表示使用指定名称的bean完成赋值。(注意,两个注解没有先后顺序。)
@Autowired注解有一个required属性,该属性是布尔类型,默认为true,表示当引用类型属性赋值失败时(比如同源类型对象找不到),程序就报错,终止执行。如果设为false,则表示找不到就赋值为null,程序正常执行。
@Resource
该注解和Autowired注解的作用一致,语法和Resource类似,只不过该注解是JDK提供的,Spring提供了对该注解的支持。也是采用自动注入的原理,支持byName、byType。默认是byName。但是,该注解默认情况下@Resource,当使用byName赋值失败时,会使用byType进行尝试赋值。可以采用@Resource(name=”bean的id值”)来限制只采用byName。
注意,上述@Repository、@Service、@Controller除了创建对象之外,还有对应的扩展角色功能,实现了对项目的对象分层处理。所以说,如果该类不是上述三类,就采用@Component注解修饰创建对象。
案例如下所示:
1 | // 采用注解的方式创建对象,并赋值 |
1 | // 采用注解的方式创建对象,并赋值 |
1 | // 测试类 |
2.5 补充
什么样的对象放入容器中?
DAO类、service类、controller类、工具类。Spring中的对象默认都是单例的,即在容器中叫这个名称的对象只有一个。
不放入Spring容器中的对象?
实体类对象,因为实体类对象的数据(属性值)来自数据库,而数据库是需要访问查询才能获得,只能是在程序运行过程中创建。
servlet、监听器、过滤器等,因为这些对象是web容器(如Tomcat)管理的,而不是spring容器来管理。
使用多配置文件
一方面,大型项目肯定涉及到很多个类对象,此时如果将类对象都声明再一个配置文件中,不方便管理。
另外,文件的大小也会随着bean标签的增多而变大。读取效率也会降低。
并且,只有一个文件,那么多人协同开发,就可能会影响文件的版本更新问题,不同人之间的修改可能会覆盖。
多文件的分配方式如下:
- 按功能模块,一个模块一个配置文件
- 按类的功能,数据库相关的配置采用一个配置文件,做事务的相关配置采用一个配置文件,做service功能的采用一个配置文件。
那么,如果某个文件中的对象,用到另一个文件中的对象该怎么办呢?比如前面的引用类型赋值。
此时,除了上述的分模块配置文件,我们还设置一个主配置文件,用于将各模块配置文件关联起来。即import,这样,就相当于所有的配置文件都处于一个总的配置文件中。语法规则如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
<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">
<!-- 主配置文件,用来包含其他模块的配置文件,也就是将各个文件关联起来。一般情况下,本文件中不定义任何对象。 -->
<import resource="classpath:ba06/sprint-student.xml"></import>
<import resource="classpath:ba06/sprint-school.xml"></import>
<!-- 或者使用通配符 -->
<import resources="classpath:ba06/*.xml"></import>
</beans>
基于配置文件和基础注解的依赖注入有什么优缺点?
配置文件的优点:
- 和代码分离,修改配置文件不需要重新编译源码。
配置文件的缺点:
- 文件比较多,需要编写xml代码,而且不容易看清对象的内部逻辑。
注解的优点:
- 和代码在一起,容器看清对象的逻辑关系。
注解的缺点:
- 和代码在一起,修改注解需要重新编译源码。
总体来说,注解适合不经常修改对象属性的情况。配置文件适合经常修改对象属性的情况。一般情况下,主要采用注解的方式,辅助采用配置文件的方式。
总体来看,IoC能够实现业务对象之间的解耦合。例如Service层和dao对象之间的解耦合。二者对象的赋值可以通过配置文件进行修改,对象之间没有必然的联系,使得程序更加松散。
3. AOP面向切面编程
AOP涉及到动态代理,而动态代理有JDK(内置类)和CGLIB(第三方库)两种方式,JDK要求除了有目标类之外还必须有接口。而CGLIB则不要求有接口,但是要求类和方法不能是final修饰,主要通过继承目标类实现代理和功能增强。CGLIB经常被应用在框架中,如Spring等,效率比JDK高。这里不再展开叙述,可参考前面的文章。总之,动态代理,一个是代理,进行了功能增强;一个是动态,代码比较灵活,减少代码量,可以在不修改源码的基础上进行代理其他功能。
总体来说动态代理有如下作用:
- 在目标类源码不改变的情况下,增加功能
- 减少代码的重复
- 专注业务逻辑代码
- 解耦合,让业务功能和日志、事务等非事务功能分离
什么时候考虑使用AOP技术?
- 当要给一个系统中存在的类增加功能,但是不能修改源码,可以使用AOP增加功能。
- 给项目中的多个类增加一个相同的功能,可以使用AOP。
- 给业务方法增加事务,如日志输出。
AOP也被称为面向切面编程(Aspect Orient Programming),基于动态代理。本质上说就是动态代理的规范化,把动态代理的实现步骤以及方式都定义好了,让开发人员用一种统一的方式使用动态代理。即既使用动态代理,也使得步骤变得统一,易于多个开发人员统一开发,易于维护。(因为动态代理本身比较灵活,可以任意开发,但是对于多人开发来说,必须形成统一规范,这样才方面协同)。
一般情况下,切面指的是给目标类增加的功能(即功能增强)。切面一般是非业务方法,可以独立使用,不影响主体业务功能。
面向切面编程和面向对象编程类似,以切面为核心,分析
- 什么样的功能可以以切面的形式使用?
- 什么时候使用?(连接点/切入点,执行时间)
- 给谁用?(目标对象)
AOP的术语以下几个:
- Aspect:切面,表示增强的功能,就是一堆代码,完成某个功能(非业务功能)。常见的切面有:日志,事务,统计信息,参数检查,权限验证。
- JoinPoint:连接点,连接业务方法和切面的位置。实际上就是某类中的业务方法。(比如在该方法执行前计时,在该方法执行后打日志,这里这个方法就是连接点,计时和打日志就是切面。本质上就是通过该方法将切面和整体业务逻辑连接起来)
- Pointcut:切入点,指多个连接点方法的集合(即多个连接点的统称,比如这些方法都要接入切面,那么这些方法就被称为切入点)。
- 目标对象:给哪个类的方法增加功能,这个类就是目标对象。
- Advice:通知(或者说增强),表示切面功能执行的时间。
一个切面有三个关键要素:
- 切面的功能代码,切面干什么
- 切面的执行位置,使用Pointcut表示切面执行的位置
- 切面的执行时间,使用Advice表示时间,如在目标方法之前,还是目标方法之后使用。
AOP的技术实现框架有以下几个:
- Spring框架,主要在事务处理时使用AOP,这是它自己实现的,比较笨重繁琐,我们在项目开发时很少使用自带的AOP。
- aspectJ框架,这是一个开源的专门做AOP的框架,在业界比较权威,在实际开发中用的比较多,而且Spring框架集成了该框架。官网。另外,aspectJ框架实现AOP有两种方式:xml配置文件和注解。在开发过程中一般主要采用注解的形式,对于xml配置文件的方式主要是做事务时使用。
3.1 aspectJ框架
我们知道切面
主要有三要素:
功能代码,这个没什么可说的,就是代码实现。
切面的执行时间,也被称为Advice(翻译为通知、增强),在aspectJ框架中使用注解或xml配置文件来表示。主要的注解有以下五种:
- @Before(前置通知,掌握)
- 在目标方法之前执行
- 不会改变目标方法的执行结果
- 不会影响目标方法的执行
- @AfterReturning(后置通知,掌握)
- 在目标方法之后执行
- 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能。
- 因为能够获取到该值,所以可以“修改”返回值。
- @Around(环绕通知,掌握)
- @AfterThrowing(异常通知,了解)
- @After(最终通知,了解)
- @PointCut(定义切入点)
- @Before(前置通知,掌握)
切面的执行位置(切入点),aspectJ框架使用的是切入点表达式来表示。我们知道切入点就是目标类方法(连接点),因此,需要指明该方法(权限修饰符、返回值类型、所在包名类名、方法名、参数列表(主需要参数类型,形参名不需要)、抛出异常等等),注意,方法返回值和方法声明(参数)这两部分是必须的。
因此,总体来说,首先知道切入点,即在哪里插入切面(切入点表达式)。然后要知道切面的执行时间,即在什么时候(注解、xml配置文件)。最后还要实现切面的具体功能(代码)。
3.2 aspectJ框架的第一个程序
在现有的某个类的方法基础上,在不改变原来的类的代码的基础上,进行功能增强,比如在某个方法执行之前输出时间。使用aspectJ实现AOP的基本步骤:
新建Maven项目
加入Spring和aspectJ依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14<!-- Spring -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.16</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.15</version>
</dependency>创建目标类:接口和实现类。(本程序的目标是给类中的方法增加功能)
1
2
3
4
5package org.hianian.ba01;
// 接口
public interface SomeService {
void doSome(String name, Integer age);
}1
2
3
4
5
6
7
8
9
10
11package org.hianian.ba01.impl;
// 目标类以及业务方法
public class SomeServiceImpl implements SomeService {
public void doSome(String name, Integer age) {
// 给doSome方法增加一个功能,在doSome()执行之前,输出方法的执行时间
// 业务方法
System.out.println("=====目标方法doSome()=====");
}
}创建切面类(其实就是普通类,目的就是将普通类和实现类中的方法关联起来)
在类的上面加入注解:@Aspect
在类中定义方法(增强方法、通知方法),方法就是切面要执行的功能代码。在方法的上面加入aspectJ中的通知注解,例如@Before。另外还需要指定切入点表达式execution()。
注意,方法的定义有如下要求:
- 公共方法public
- 方法没有返回值
- 方法名称自定义
- 方法可以有参数,也可以没有参数。(如果有参数,参数不是自定义的,只能使用特定的参数类型如JointPoint、Object等等。)
- 该参数表示目标方法。
- 通过该方法我们可以获取目标方法执行时的信息,例如方法名称、方法实参等等。
- 如果在通知方法中需要使用上述信息,可以加入JointPoint参数。
- 这个参数是由框架赋值的,要求必须在第一个参数位置。
@Before注解的属性value的值为切面表达式,即指明切面的执行位置。Before本身指定了执行时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package org.hianian.ba01.acpect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.Date;
// 切面类
public class MyAspect {
// 定义实现切面功能的方法,注意方法的定义要求
// 注意,切入点表达式,可以简化。Before注解表示在方法执行前执行本增强方法。
public void myBefore(){
System.out.println("前置通知,切面功能:在目标方法之前输出执行时间:" + new Date());
}
}在IDEA中可以看到,此时会自动提示该方法为增强方法(Navigate To Advised Methods)。
测试一下方法参数JointPoint。
1
2
3
4
5
6
7
public void myBefore(JoinPoint jp){
System.out.println("目标方法的方法定义:" + jp.getSignature());
System.out.println("目标方法的方法名:" + jp.getSignature().getName());
System.out.println("前置通知,切面功能:在目标方法之前输出执行时间:" + new Date());
}结果如下所示:
1
2
3
4
5目标方法的方法定义:void org.hianian.ba01.SomeService.doSome(String,Integer)
目标方法的方法名:doSome
前置通知,切面功能:在目标方法之前输出执行时间:Tue Mar 29 11:28:30 CST 2022
=====目标方法doSome()=====
代理对象的类型:com.sun.proxy.$Proxy8创建Spring的配置文件,声明对象,把对象交给容器统一管理。
- 声明目标对象
- 声明切面类对象
- 声明aspectJ框架中的自动代理生成器标签(
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
)。自动代理生成器对象是用来完成代理对象的自动创建功能的,不再需要手动创建代理类对象了。- 注意,自动代理生成器使用的是aspectJ框架内部的功能,来创建目标对象的代理对象。创建代理对象是在内存中实现的,修改目标对象的内存中的结构,将目标对象修改为代理对象。所以此时目标对象就是修改后的代理对象(本质上似乎并没有创建新对象,猜一下,是将切面类对象作为一个属性加入到目标类对象中,在执行目标类对象目标方法的时候,调用切面类对象的增强方法)。
- 自动代理生成器对象会将Spring容器中的所有的符合条件(切入点表达式)的目标对象,一次性都生成代理对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<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
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 把对象交给Spring容器,由Spring容器统一创建,管理对象。 -->
<!-- 声明目标对象 -->
<bean id="someService" class="org.hianian.ba01.impl.SomeServiceImpl"></bean>
<!-- 声明切面对象 -->
<bean id="myAspect" class="org.hianian.ba01.acpect.MyAspect"></bean>
<!-- 注意,自动代理生成器使用的是aspectJ框架内部的功能,来创建目标对象的代理对象。
创建代理对象是在内存中实现的,修改目标对象的内存中的结构,
将目标对象修改为代理对象。所以此时目标对象就是修改后的代理对象(本质上似乎并没有创建新对象)。 -->
<!-- 同样,在加入该标签后,IDEA自动添加了若干个约束文件,如上xmlns所示 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>创建测试类,从Spring容器中获取目标对象(实际上就是代理对象),通过代理执行方法,实现AOP的功能增强。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class Test01 {
public void testProxy(){
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 从容器中获取目标对象(注意,此时已经经过了自动代理生成器对象在内存中转换,所以,获取到的目标对象就是代理对象)
SomeService proxy = (SomeService) ac.getBean("someService");
// 通过代理对象执行目标方法,实现增强功能以及原有功能。
proxy.doSome("张三", 23);
// 因为目标类实现了接口,所以aspectJ采用的是JDK的方式实现动态代理
// com.sun.proxy.$Proxy8
System.out.println("代理对象的类型:" + proxy.getClass().getName());
}
}
从上面的步骤来看,Spring主要做的是控制反转,创建容器来管理对象,以及支持AOP框架。实现AOP功能则需要有切面类以及AOP框架的自动代理生成器对象。该对象通过搜索spring容器中对象,通过@Aspect注解来找到切面类,进而找到其增强方法,根据方法上面的注解找到什么时候执行以及在哪里执行(切入点表达式,找到目标类)。
3.3 @AfterReturning
上面的案例中讲解了前置通知注解,这里讲解一下后置通知,顾名思义就是在目标方法执行之后执行此通知方法(增强方法)。其属性主要有两个:
- value:即切入点表达式
- returning:自定义的变量,表示目标方法的返回值,注意变量名必须和增强方法的参数名一致。
后置通知修饰的方法和前置通知类似,因为是后置通知,所以必须获得目标方法的执行结果,因此,通知方法中的形参可以表示目标方法的执行结果返回值,类型规定为Object。
因此,后置通知主要有以下几个特点:
- 在目标方法之后执行
- 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能。
- 因为能够获取到该值,所以可以“修改”返回值。(注意,经过测试,对于引用数据类型来说,是可以修改其值的,并且会影响最终方法获得的结果。)即执行顺序如下所示:
- Object res = doOther();
- myAfterReturning(res); // 如果是引用数据类型,则会修改值,影响下面的返回结果。
- return res;
案例如下所示(为了测试引用数据类型传参,采用了引用数据类型):
1 | // 接口 |
1 | // 目标类以及业务方法 |
1 | // 切面类 |
1 | public class Test01 { |
3.4 @Around
环绕通知是最强的通知。对修饰的方法有如下要求:
- public
- 必须有一个返回值,推荐使用Object类型
- 方法名称自定义
- 方法有参数,固定的参数:ProceedingJoinPoint类型(注意,该类型继承了JoinPoint,所以有JoinPoint的一些方法调用)
环绕通知的注解属性有value,值仍然是切入点表达式。环绕通知注解有以下特点:
- 它是功能最强的通知(主要用来做事务)
- 在目标方法的执行前后都能增强功能
- 控制目标方法是否被调用执行(因为手动调用,所以可控制执行条件)
- 可以修改目标方法的执行结果,影响最后的调用结果。(和引用数据类型修改不一样,这个似乎是将增强方法的返回结果返回给了调用者)
实际上,环绕通知等同于JDK动态代理的InvocationHandler接口的invoke方法。增强方法的参数ProceedingJoinPoint就等同于invoke方法中的Method参数,用于执行目标方法。增强方法的返回值就是目标方法的执行结果,可以被修改。总体上说,环绕通知就是使得我们可以更加灵活地手动调用目标方法,以及调整增强功能的位置。
测试案例如下所示,其他代码和前面类似,这里不再展示:
1 | // 切面类 |
3.5 @AfterThrowing
异常通知,主要用于目标方法在抛出异常时执行。异常通知修饰的方法要求如下:
- public
- 没有返回值
- 方法名称自定义
- 方法有一个Exception类型参数(表示目标方法抛出的异常),如果还有就是JoinPoint
该注解的属性有两个:
- value,即切入点表达式
- throwing,自定义的变量,表示目标方法抛出的异常对象,变量名必须和增强方法的参数名一样。
特点是:
- 在目标方法抛出异常时执行的
- 可以做异常的监控程序,监控目标方法执行时是不是有异常,如果有异常,可以进行相关操作等等。
该注解用的较少,简单测试一下:
1 | // 目标类以及业务方法 |
1 | // 切面类 |
相当于try{}catch(){}中的catch。
3.6 @After
最终通知修饰的方法有如下要求:
- public
- 没有返回值
- 方法名称自定义
- 方法没有参数,如果有就是JoinPoint
最终通知注解只有一个属性value,值为切入点表达式。修饰位置在方法的上面,特点是总是会执行(即使目标方法出现异常,增强方法也会执行),在目标方法之后执行。一般用来做资源清除工作。相当于try{}catch(){}finally{}中的finally。
1 | // 切面类 |
3.7 @PointCut
该注解是作为一个辅助出现的,用来定义和管理切入点。如果项目中有多个切入点表达式是重复的,可以复用的,可以使用PointCut。属性有一个value,值为切入点表达式,注解的使用位置是在自定义的方法上面。
特点:
- 当使用@PointCut定义在一个方法的上面,此时这个方法的名称就是切入点表达式的别名。在其他的通知中,value属性就可以使用这个方法名称(需要加括号),代替切入点表达式了。
因为有时候,针对一个目标方法,有时候会做多个增强,或者前置和后置通知等等,此时就需要多个增强方法,而就会写多个同样的切入点表达式。这时候,就可以使用@PointCut注解来定义别名。
似乎对该注解修饰的方法没有过多的要求。代码如下所示:
1 | // 切面类 |
3.8 JDK和CGLIB
前面测试过,如果目标类实现了接口,那么aspectJ默认采用的就是JDK的方式实现动态代理。可以进行测试,如果没有实现接口,那么就采用的是CGLIB的方法实现动态代理。如下所示,可以看到CGLIB的字眼。
org.hianian.ba07.impl.SomeServiceImpl$$EnhancerBySpringCGLIB$$8c0c98aa
实际上,即使目标类实现了接口,仍然可以采用CGLIB方式实现动态代理。需要在配置文件中的自动代理生成器标签中加入proxy-target-class="true"
属性值。即:
1 | <aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy> |
4. Spring集成MyBatis
前面讲解过MyBatis,将SQL语句和程序分离,使得耦合度降低。而Spring依据IoC和AOP进一步解耦合。这是两个不同应用功能的框架,MyBatis更加具体,负责数据库,而Spring则是负责整体业务逻辑层面。很明显,MyBatis相当于一个子集。
可以把MyBatis集成到Spring中,像一个框架那样使用。IoC是管理对象的,而AOP则是负责业务逻辑的。因此可以用IoC来管理MyBatis框架的相关对象,比如SqlSessionFactory对象等等,这样开发人员可以从Spring容器中获取对象,这样开发人员就不用同时面对多个框架了,只需面对Spring框架即可。
我们知道,MyBatis框架的使用,有以下几个步骤:
- 定义DAO接口,
- 创建SQL映射文件,xml
- 创建MyBatis主配置文件
- 根据动态代理,利用SqlSession.getMapper()获取到DAO接口的代理对象
- 根据代理对象执行数据库操作
本质上主要用到的对象有:SqlSession、SqlSessionFactory(需要读取主配置文件)。而主配置文件主要有数据库连接信息以及SQL映射文件信息。
在MyBatis中,采用自带的数据库连接池来进行连接对象,而该连接池性能较弱。因此在Spring中采用独立的连接池(如阿里的druid)来连接数据库。
因此,通过以上说明,我们需要让Spring创建以下对象(以目前掌握的知识来看,只能采用xml配置文件来创建,因为没有源码,所以无法采用注解方式创建):
- 独立的连接池类的对象
- SqlSessionFactory对象
- DAO对象
4.1 案例
Spring集成MyBatis有如下步骤:
新建Maven项目
加入Maven依赖
- spring依赖
- mybatis依赖
- mysql驱动
- spring的事务依赖
- mybatis和spring集成的依赖。该依赖是MyBatis官方提供的,用来在Spring项目中创建MyBatis的SqlSessionFactory、dao对象的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75<dependencies>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- Spring核心 IoC -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.16</version>
</dependency>
<!-- AspectJ 外部 AOP -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.15</version>
</dependency>
<!-- Spring事务 -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.14</version>
</dependency>
<!-- Spring事务(访问数据库) -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.14</version>
</dependency>
<!-- mybatis依赖 -->
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<!-- mybatis和spring集成的依赖 -->
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<!-- mysql驱动依赖 -->
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!-- 阿里提供的数据库连接池 -->
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>创建实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59public class User {
private Integer no;
private String login;
private String password;
private String name;
public User() {
}
public User(Integer no, String login, String password, String name) {
this.no = no;
this.login = login;
this.password = password;
this.name = name;
}
public Integer getNo() {
return no;
}
public void setNo(Integer no) {
this.no = no;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
return "user{" +
"no=" + no +
", login='" + login + '\'' +
", password='" + password + '\'' +
", name='" + name + '\'' +
'}';
}
}创建dao接口和SQL映射文件(mapper文件)
1
2
3
4
5public interface UserDao {
int insertUser(User user);
List<User> selectUser();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<mapper namespace="org.hianian.dao.UserDao">
<insert id="insertUser">
insert into t_user values(#{no}, #{login}, #{password}, #{name})
</insert>
<select id="selectUser" resultType="org.hianian.entity.User">
select no, login, password, name from t_user order by no desc
</select>
</mapper>创建mybatis主配置文件
对于主配置文件,由于连接池采用第三方工具,因此<environments>等数据连接标签就不再需要了(这些对象交给druid第三方工具以及Spring框架来创建)。主配置文件只需要设置一些别名、日志以及映射文件即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<configuration>
<!-- 指定properties文件的位置,从类路径根下开始找 -->
<properties resource="jdbc.properties"></properties>
<!-- 控制MyBatis的全局行为 -->
<!-- 在主配置文件的configuration标签内,加入如下标签 -->
<settings>
<!-- 控制输出日志,输出为标准控制台输出 -->
<setting name="logImpl" value="STDOUT_LOGGING"></setting>
</settings>
<mappers>
<mapper resource="org/hianian/dao/UserDao.xml"/>
</mappers>
</configuration>创建Sevice接口和实现类,属性是dao。(dao接口对象本来是由Service对象来调用,因此这里创建Service接口及实现类。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class UserServiceImpl implements UserService {
private UserDao userDao;
// 采用set注入的方式给属性赋值
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
// 为了简单起见,这里的Service方法只是简单地调用Dao接口对象中的方法,并没有其他复杂操作。
public List<User> queryUser() {
List<User> users = userDao.selectUser();
return users;
}
public int addUser(User user) {
int result = userDao.insertUser(user);
return result;
}
}创建spring的配置文件:声明mybatis的对象讲给spring创建
数据源(datasource,就是上面的连接池)
采用第三方工具alibaba/druid: 阿里云计算平台DataWorks(https://help.aliyun.com/document_detail/137663.html) 团队出品,为监控而生的数据库连接池 (github.com),可以参考官方文档说明。如下所示(Spring中创建DruidDataSource对象):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31<!-- 构造方法执行之后,会调用init初始化方法。然后在销毁对象的时候,会调用close方法 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 给对象的各种属性赋值, 通常配置url、username、password这三项即可-->
<!-- 另外也可手动配置maxActive最大连接数量 -->
<!-- 注意,druid会根据url来识别数据库驱动类名,因此不需要提供数据库驱动名 -->
<!-- 如果连接的数据库非常见数据库,可以配置属性driverClassName -->
<!-- 其他的属性可以不用写 -->
<property name="url" value="${jdbc_url}" />
<property name="username" value="${jdbc_user}" />
<property name="password" value="${jdbc_password}" />
<property name="filters" value="stat" />
<property name="maxActive" value="20" />
<property name="initialSize" value="1" />
<property name="maxWait" value="6000" />
<property name="minIdle" value="1" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="poolPreparedStatements" value="true" />
<property name="maxOpenPreparedStatements" value="20" />
<property name="asyncInit" value="true" />
</bean>SqlSessionFactory对象
dao对象
声明自定义的Service对象
上面的配置信息如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 将数据库的配置信息写在一个独立的配置文件中,将该配置文件引入到本配置文件中用于数据域对象读取 -->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!-- 声明数据源DataSource对象以及连接对象Connection,作用是连接数据库,代替MyBatis主配置文件中的数据库连接 -->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="maxActive" value="${jdbc.maxActive}"></property>
</bean>
<!-- 声明MyBatis所提供的SqlSessionFactoryBean类,这个类内部会创建SqlSessionFactory对象。 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 通过之前的MyBatis主配置文件知道,创建SqlSessionFactory对象需要数据库连接信息以及主配置文件 -->
<!-- 连接数据库就是上面的myDataSource,即下面的属性 -->
<property name="dataSource" ref="myDataSource"></property>
<!-- 主配置文件就是resources/MyBatis.xml -->
<property name="configLocation" value="classpath:MyBatis.xml"></property>
</bean>
<!-- 声明Dao对象,使用SqlSession的getMapper(.class)方法来创建对象 -->
<!-- 这个声明类对象不需要id(不需要手动调用),MapperScannerConfigurer会在内部调用getMapper()生成每个dao接口的代理对象。 -->
<!-- 因为需要SqlSession对象,以及接口的class名。所以需要将这两个信息赋给属性 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 指定SqlSessionFactory对象的id,会自动通过SqlSessionFactory对象创建SqlSession对象 -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
<!-- 指定代理对象的接口的class (只需要指定dao接口所在包名即可,会自动扫描接口类型)-->
<!-- MapperScannerConfigurer会扫描指定包中所有的接口,把每个接口都执行一次getMapper()方法,得到每个接口生成的dao对象 -->
<!-- 创建好的dao对象放入到Spring容器中。对象名称就是Dao接口的首字母小写 -->
<!-- 如果是多个包,那么就用逗号分隔 -->
<property name="basePackage" value="org.hianian.dao"></property>
</bean>
<!-- 声明service对象,为用户提供服务 -->
<bean id="userService" class="org.hianian.Service.impl.UserServiceImpl">
<!-- 给属性赋值,注意,因为是引用数据类型,采用ref属性,值就是上面内部创建好的Dao对象 -->
<property name="userDao" ref="userDao"></property>
</bean>
</beans>经过上述步骤,可以在测试类中测试一下Spring中有没有SqlSessionFactory对象。
创建测试类,获取Service对象,通过Service调用dao对象完成数据库的访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66public class MyTest {
public void testMB() throws IOException {
// SqlSession sqlSession = MyBatisUtils.getSqlSession();
//
// UserDao userDao = sqlSession.getMapper(UserDao.class);
//
// List<User> result = userDao.selectUser();
//
// for(User user: result) {
// System.out.println(user);
// }
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) ac.getBean("sqlSessionFactory");
//
// System.out.println(sqlSessionFactory);
String[] names = ac.getBeanDefinitionNames();
for(String name: names) {
System.out.println(name + "=====" + ac.getBean(name));
}
}
public void testMB3() throws IOException {
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 通过Service对象为用户提供服务(不只有访问数据库,这里为了简单起见,Service接口只调用了数据库操作。)
UserService userService = (UserService) ac.getBean("userService");
List<User> users = userService.queryUser();
for(User user: users) {
System.out.println(user);
}
}
public void testMB4() throws IOException {
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 通过Service对象为用户提供服务(不只有访问数据库,这里为了简单起见,Service接口只调用了数据库操作。)
UserService userService = (UserService) ac.getBean("userService");
User user = new User(16, "hh", "hh132", "huhuhu");
int i = userService.addUser(user);
System.out.println(i);
}
}
可以看到,前面的几个步骤仍然是MyBatis的步骤,只是后面的创建对象步骤交给Spring框架来做。注意,MyBatis集成到Spring框架中,事务是自动提交的,无需执行SqlSession.commit()。
另外,本质上仍然是三部分:实体类、Dao类、Service类。只不过其中这些对象的创建放到了Spring容器中自动创建,我们只需要获取对象即可。
5. Spring事务
在讲解Spring事务之前,先看下面几个问题:
什么是事务?
最早出现在数据库中,指的是一系列SQL语句的集合(就是多个SQL操作),这些操作完成最终的任务,这个任务就被称为事务。
在什么时候想到使用事务?
当我们希望这个任务完成或者不完成,但是这个任务分为多个SQL操作,所以说,这些操作这么全部完成,要么都不完成,不能只完成一部分。此时就需要使用事务,最经典的就是转账操作,涉及到多个表,从A账户中减少,从B账户中增加数据。这两个操作需要都完成,这个转账操作才算完成。
那么在Java代码中,为了方便控制事务,会在Service类的业务方法中采用事务,因为业务方法会调用多个dao方法,执行多个SQL语句。
通常使用JDBC访问数据库以及MyBatis访问数据库,怎么处理事务?
- JDBC中的
Connection.commit()
和Connection.rollback()
- MyBatis中的
SqlSession.commit()
和SqlSession.rollback()
问题3中事务的处理方式,有什么不足?
- 不同的数据库访问技术,处理事务的对象、方法不同。需要了解不同数据库访问技术使用事务的原理。
- 掌握多种数据库事务的处理逻辑。什么时候提交事务,什么时候回滚事务。
- 处理事务的多种方法。
总之就是:多种数据库的访问技术,有着不同的事务处理的机制、对象和方法。
怎么解决上述不足?
Spring提供了一种处理事务的统一模型,能使用统一步骤,完成多种不同数据库访问技术的事务处理。比如使用Spring的事务处理机制,可以完成MyBatis、Hibernate等访问数据库的事务处理。
Spring采用的是声明式事务,即把事务相关的资源和内容都提供给Spring,Spring就能处理事务提交、回滚。
处理事务,需要怎么做,做什么?
Spring处理事务的模型,使用的步骤都是固定的,我们只需要把使用的信息提供给Spring就可以了。主要有以下内容。
事务内部提交、回滚事务,使用的是事务管理器对象,代替程序员手动commit、rollback。事务管理器指的是一个接口和他的众多实现类(了解)。
- 接口:PlatformTransactionManager,定义了事务的重要方法,比如commit()、rollback()。
- 实现类:Spring把每一种数据库访问技术都创建了对应的事务处理类。比如MyBatis对应的事务处理类就是DataSourceTransactionManager,Hibernate对应的事务处理类就是HibernateTransactionManager。
因此,我们只需要告诉Spring采用哪种数据库访问技术即可,Spring就会创建对应的事务管理对象进行事务的提交和回滚。那么这么告诉Spring呢?在主配置文件中声明对应的事务处理对象即可。
业务方法需要什么样的事务,需要说明具体的事务类型。主要有以下方面内容:
事务的隔离级别:
- 读未提交(ISOLATION_READ_UNCOMMITTED):为解决任何并发问题
- 读已提交(ISOLATION_READ_COMMITTED):解决脏读,存在不可重复读与幻读
- 可重复度(ISOLATION_REPEATABLE_READ):解决脏读、不可重复读,存在幻读
- 串行化(ISOLATION_SERIALIZABLE):不存在任何并发问题
事务的超时时间(一般情况下不需要考虑,因为时间影响因素太多,比如网络状况等。)
表示一个方法最长的执行时间(单位为秒),如果方法执行时超过了这个时间,事务就回滚。
事务的传播行为(重要,掌握)
控制业务方法是不是有事务,是什么样的事务。有以下七个传播行为,表示业务方法在调用时,事务在方法之间是如何使用的:
传播行为 描述 PROPAGATION_REQUIRED(掌握) 指定的方法必须在事务内部执行。若当前调用处存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事物。这是最常见的传播行为,也是Spring默认的。 PROPAFATION_REQUIRES_NEW(掌握) 总是新建一个事务,若当前调用处存在事务,就将当前事务挂起,直到新事务执行完毕。 PROPAGATION_SUPPORTS(掌握) 指定的方法支持调用处的当前事务,如果没有事务,也可以以非事务方式执行。(比如查询操作) PROPAGATION_MANDATORY PROPAGATION_NESTED PROPAGATION_NEVER PROPAGATION_NOT_SUPPORTED 提交事务、回滚事务的时机
- 当业务方法执行成功,没有异常抛出,当方法执行完毕,Spring在方法执行后自动提交事务,即调用事务管理器的
commit()
方法。- 当业务方法抛出运行时异常或ERROR,Spring自动执行回滚,即调用事务管理器的
rollback()
方法。- 当业务方法抛出运行时异常,默认也会自动提交事务。
总体来说,Spring处理事务需要我们告诉它:
- 指定要使用的事务管理器实现类
- 指定哪些类,哪些方法需要加入事务的功能
- 指定方法需要的隔离级别、传播行为、超时等等。
5.1 具体案例
购买商品trans_sale项目
本例要实现购买商品,模拟用户下订单,向订单中添加销售记录,从商品表中减少库存(注意,首先要查询余量是否满足需求)。
即购买商品涉及到多个SQL语句,将其看成一个事务。
步骤如下面所示。
5.1.1 创建数据库表
1 | drop table if exists t_goods; |
1 | drop table if exists t_sales; |
5.1.2 创建maven项目
这个不必多说,注意添加依赖即可。
5.1.3 创建实体类
有两张表,则创建两个实体类。
1 | public class Goods { |
1 | public class Sales { |
5.1.4 定义dao接口
有两张表,所以也要定义两个用于访问该表的接口。
1 | public interface GoodsDao { |
1 | public interface SalesDao { |
5.1.5 定义dao接口对应的SQL映射文件
1 |
|
1 |
|
5.1.6 定义异常类
前面提到过,Spring中的事务,面对运行时异常以及ERROR时,会自动回滚。为了方便测试,这里定义一个异常类,用于后续方法中手动抛出。
1 | package org.hianian.excep; |
5.1.7 定义Service接口
给了实现业务方法,这里定义接口。
1 | public interface UserService { |
5.1.8 定义Service的实现类
为了方便对比,可以先不管事务,直接按照流程来做,如下所示。
1 | public class UserServiceImpl implements UserService { |
可以看到,如果出现异常,那么就会导致两张表的更新不一致,即事务“只完成了一部分”。此时就应该添加事务,保证SQL语句要么都执行成功,要么都执行失败。那么怎么加入事务呢?给谁加入事务呢?当然是给该方法加入事务,事务作为一个附加功能,和主体业务逻辑没有太大的关系,因此可以采用AOP的方式加入事务。
具体的描述如5.1.11补充所示,代码如下所示:
1 | public class UserServiceImpl implements UserService { |
5.1.9 修改Spring配置文件内容
5.1.9.1 Spring自带的事务注解
- 声明事务管理器对象
- 开启事务注解驱动
1 | <!-- 使用Spring的事务管理 --> |
在要添加事务的方法上添加注解:
1 | // 在方法上加入事务注解(可以看到,都是默认值,所以可以省略属性,直接@Transactional即可) |
5.1.9.2 AspectJ中的事务注解
1 | <!-- AspectJ框架进行事务管理 --> |
可以看到本方法不需要设计代码修改,使得配置文件和代码完全分离。
5.1.10 定义测试类
注意,观察获得的service对象是不是代理对象。
1 | public class MyTest { |
5.1.11 补充
所以说,事务原本是数据库中的概念,在Dao层。但一般情况下,需要将事务提升到业务层,即Service层。这样做是为了能够使用事务的特定来管理具体的业务。在Spring中通常可以通过以下两种方式来实现对事务的管理。
使用Spring的事务注解管理事务。(适合中小项目)
该方法使用的是Spring自带的注解@Transactional,修饰public方法,实现事务管理。其属性有以下几种:
propagation
用于设置事务传播属性,该属性类型为Propagation枚举,默认值为Propagation.REQUIRED。
isolation
用于设置事务的隔离级别,该属性类型为Isolation枚举,默认值为Isolation.DEFAULT。
readOnly
用于设置所修饰的方法对数据库的操作是否是只读的,该属性类型为Boolean,默认值为false。
timeout(一般不用设置)
用于设置本操作与数据库连接的超时时限。单位为秒,类型为int,默认值为-1,即没有时限。
rollbackFor
指定需要回滚的异常类,即抛什么异常时会回滚。类型为Class[],默认值为空数组。如果只有一个异常类时,可以不用数组。
注意,Spring框架会首先检查方法抛出的异常是不是在数组中,如果在,则一定回滚。如果不在,那么会判断异常是不是运行时异常,如果是,则一定回滚。
rollbackForClassName
指定需要回滚的异常类类名。(和上面类似,只不过本属性类型为数组)类型为String[]。
noRollbackFor
指定不需要回滚的异常类类名。类型为Class[]。
noRollbackForClassName
指定不需要回滚的异常类类名。类型为String[]。
使用该注解方式添加事务的步骤:
需要声明事务管理器对象
开启事务注解驱动,即告诉Spring框架,使用注解的方式管理事务。
Spring内部会使用AOP机制,创建@Transactional所在的类的代理对象,给修饰的方法加入事务的功能。Spring给业务方法加入事务:在业务方法执行之前,先开启事务,在业务方法之后提交或回滚事务,使用AOP的环绕通知,即try{}catch(){},如果发生异常,则调用rollback,否则commit()。
在方法的上面加入注解@Transactional。
案例可以参考上面5.1.9.1部分,前两步在配置文件中声明,第三步在代码中声明。注意,这个注解内部自己实现了环绕通知,所以无需我们自己手动使用AOP采用环绕通知。
使用AspectJ的AOP配置管理事务。(适合大型项目)
上面方法存在一些不足,如果目标类较多,就会一一添加注解。可以使用AspectJ框架,在Spring配置文件中声明类、方法需要的事务,这种声明式方法使得业务方法和事务配置完全分离。步骤如下所示:
- 加入AspectJ依赖
- 声明事务管理器对象
- 声明方法需要的事务类型(配置方法的事务属性:隔离级别、传播行为、超时)
- 配置AOP:指定哪些类要创建代理。
案例可以参考上面5.1.9.2部分。这种方法完全在配置文件中使用,不涉及到代码部分。
6. Spring与Web
这部分讲解如何在Web项目中使用Spring框架,首先要解决在web层(Servlet)中获取到Spring容器的问题。只要在Web层获取到了Spring容器,就可以从容器中获取到Service对象。
通过上面的例子可以知道,我们通过在 main方法/测试方法 中创建容器对象,从而可以获取到容器中的各种对象。而Servlet中,我们只是在doGet()等方法中编写代码,Web容器自动调用对象的doGet方法。那么我们应该在doGet()方法中手动创建Spring容器对象吗?答案是否定的,因为用户每请求一次,就会调用一次方法,就会创建一个Spring容器对象,并且容器也会创建内部所有的对象,这肯定是不合理的。
实际上,应该创建一份对象就好了,并且在每个Servlet对象中都可以使用。所以应该将该Spring容器对象放入到全局作用域ServletContext中。所以可以用监听器,当全局作用域对象被创建时,创建Spring容器,并将其放入到ServletContext中。
监听器的作用主要有两个:一是创建容器对象,而是将容器对象放入到ServletContext中。可以自己手写监听器实现上述步骤,也可以采用已经实现好的ContextLoaderListener,自动监听,实现将Spring容器创建并放入到ServletContext中,需要添加spring-web依赖。
注意,ContextLoaderListener会创建WebApplicationContext对象,该对象继承了ApplicationContext类。放入到ServletContext中,key为WebApplicationContext.ROOT_WEB_APPLICATION__CONTEXT_ATTRIBUTE
。
因此,我们既可以通过ServletContext对象来获取ApplicationContext对象,也可以通过WebApplicationContextUtils工具类中的getRequiredWebApplicationContext(ServletContext sc)方法来获取。
1 | <!-- Spring相关配置 --> |
7. 总结
8. 备注
参考B站《动力节点》。