1、Spring简介
1.Spring 是最受欢迎的企业级 Java 应用程序开发框架,数以百万的来自世界各地的开发人员使用
2.Spring 框架来创建性能好、易于测试、可重用的代码。
3.Spring 框架是一个开源的 Java 平台,它最初是由 Rod Johnson 编写的,并且于 2003 年 6 月首
次在 Apache 2.0 许可下发布。
4.Spring 是轻量级的框架,其基础版本只有 2 MB 左右的大小。
5.Spring 框架的核心特性是可以用于开发任何 Java 应用程序,但是在 Java EE 平台上构建 web 应 用程序是需要扩展的。 Spring 框架的目标是使 J2EE 开发变得更容易使用,通过启用基于POJO 编程模型来促进良好的编程实践。
1.2、Spring Framework
Spring
基础框架,可以视为
Spring
基础设施,基本上任何其他
Spring
项目都是以
Spring Framework 为基础的。
1.2.1
、
Spring Framework
特性
- 非侵入式:使用 Spring Framework 开发应用程序时,Spring 对应用程序本身的结构影响非常 小。对领域模型可以做到零污染;对功能性组件也只需要使用几个简单的注解进行标记,完全不会破坏原有结构,反而能将组件结构进一步简化。这就使得基于 Spring Framework 开发应用程序 时结构清晰、简洁优雅
-
控制反转: IOC——Inversion of Control ,翻转资源获取方向。把自己创建资源、向环境索取资源变成环境将资源准备好,我们享受资源注入。
-
面向切面编程:AOP——Aspect Oriented Programming,在不修改源代码的基础上增强代码 功能
-
容器:Spring IOC 是一个容器,因为它包含并且管理组件对象的生命周期。组件享受到了容器化 的管理,替程序员屏蔽了组件创建过程中的大量细节,极大的降低了使用门槛,大幅度提高了开发效率。
-
组件化: Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML和 Java 注解组合这些对象。这使得我们可以基于一个个功能明确、边界清晰的组件有条不紊的搭建超大型复杂应用系统。
-
声明式:很多以前需要编写代码才能实现的功能,现在只需要声明需求即可由框架代为实现。
-
一站式:在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库。而且Spring 旗下的项目已经覆盖了广泛领域,很多方面的功能性需求可以在 Spring Framework 的基础上全部使用 Spring 来实现。
1.2.2
、
Spring Framework
五大功能模块
功能模块
|
功能介绍
|
Core Container
|
核心容器,在
Spring
环境下使用任何功能都必须基于
IOC
容器。
|
AOP&Aspects
|
面向切面编程
|
Testing
|
提供了对
junit
或
TestNG
测试框架的整合。
|
Data Access/Integration
|
提供了对数据访问
/
集成的功能。
|
Spring MVC
|
Spring MVC提供了面向Web
应用程序的集成功能。
|
2、IOC
2.1.1、IOC思想
IOC
:
Inversion of Control
,翻译过来是
反转控制
。
①获取资源的传统方式
在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的模式下开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率。
②反转控制方式获取资源
反转控制的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向
——
改由容器主动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可,极大的降低了学习成本,提高了开发的效率。这种行为也称为查找的被动
形式。
③
DI
DI
:
Dependency Injection
,翻译过来是
依赖注入
。
DI
是
IOC
的另一种表述方式:即组件以一些预先定义好的方式(例如:
setter
方法)接受来自于容器的资源注入。相对于IOC
而言,这种表述更直接。
所以结论是:
IOC
就是一种反转控制的思想, 而
DI
是对
IOC
的一种具体实现。
2.1.2、IOC容器在Spring中的实现
Spring
的
IOC
容器就是
IOC
思想的一个落地的产品实现。
IOC
容器中管理的组件也叫做
bean
。在创建bean 之前,首先需要创建
IOC
容器。
Spring
提供了
IOC
容器的两种实现方式:
①
BeanFactory
这是
IOC
容器的基本实现,是
Spring
内部使用的接口。面向
Spring
本身,不提供给开发人员使用。
②
ApplicationContext
BeanFactory
的子接口,提供了更多高级特性。面向
Spring
的使用者,几乎所有场合都使用
ApplicationContext
而不是底层的
BeanFactory
③
ApplicationContext
的主要实现类
类型名
|
简介
|
ClassPathXmlApplicationContext
|
通过读取类路径下的
XML
格式的配置文件创建
IOC
容器对象
|
FileSystemXmlApplicationContex
|
通过文件系统路径读取
XML
格式的配置文件创建
IOC
容器对象
|
ConfigurableApplicationContext
|
ApplicationContext
的子接口,包含一些扩展方法refresh() 和
close()
,让
ApplicationContext
具有启动、关闭和刷新上下文的能力。
|
WebApplicationContext
|
专门为
Web
应用准备,基于
Web
环境创建
IOC
容器对象,并将对象引入存入 ServletContext
域中。
|
2.2、基于XML管理bean
2.2.1、实验一:入门案例
①创建Maven Module
②引入依赖
<dependencies>
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.22</version>
</dependency>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
③创建类
HelloWorld
public class HelloWorld {
public void sayHello(){
System.out.println("helloWorld");
}
}
④在resources下创建
Spring
的配置文件
⑤在
Spring
的配置文件中配置
bean
<!--配置HelloWorld所对应的bean,即将HelloWorld的对象交给Spring的IOC容器管理
通过bean标签配置IOC容器所管理的bean 属性:id:设置bean的唯一标识
class:设置bean所对应类型的全类名
-->
<bean id="helloworld" class="com.lwh.pojo.HelloWorld"></bean>
⑥创建测试类测试
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
HelloWorld helloworld = (HelloWorld) context.getBean("helloworld");
helloworld.sayHello();
}
}
⑦思路
⑧注意
Spring
底层默认通过反射技术调用组件类的无参构造器来创建组件对象,这一点需要注意。如果在需要 无参构造器时,没有无参构造器,则会抛出下面的异常:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name
'helloworld' defined in class path resource [applicationContext.xml]: Instantiation of bean
failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed
to instantiate [com.atguigu.spring.bean.HelloWorld]: No default constructor found; nested
exception is java.lang.NoSuchMethodException: com.atguigu.spring.bean.HelloWorld.<init>
()
2.2.2、实验二:获取bean
①方式一:根据
id
获取
由于
id
属性指定了
bean
的唯一标识,所以根据
bean
标签的
id
属性可以精确获取到一个组件对象。上个实验中我们使用的就是这种方式。
②方式二:根据类型获取
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
HelloWorld helloworld = context.getBean(HelloWorld.class);
helloworld.sayHello();
}
③方式三:根据
id
和类型
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
HelloWorld helloworld = context.getBean("helloworld",HelloWorld.class);
helloworld.sayHello();
}
④注意
当根据类型获取
bean
时,要求
IOC
容器中指定类型的
bean
有且只能有一个
当
IOC
容器中一共配置了两个:
<bean id="helloworldOne" class="com.lwh.pojo.HelloWorld"></bean>
<bean id="helloworldTwo" class="com.lwh.pojo.HelloWorld"></bean>
根据类型获取时会抛出异常:
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean
of type 'com.atguigu.spring.bean.HelloWorld' available: expected single matching bean but
found 2: helloworldOne,helloworldTwo
⑤扩展
如果组件类实现了接口,根据接口类型可以获取 bean,前提是bean唯一
如果一个接口有多个实现类,这些实现类都配置了
bean
,根据接口类型不可以获取
bean,因为bean不唯一
⑥结论
根据类型来获取
bean
时,在满足
bean
唯一性的前提下,其实只是看:『对象
instanceof
指定的类
型』的返回结果,只要返回的是
true
就可以认定为和类型匹配,能够获取到。
2.2.3、实验三:依赖注入之setter注入
①创建学生类Student
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student1{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
②配置bean时为属性赋值
<bean id="studentOne" class="com.lwh.pojo.Student">
<!-- property标签:通过组件类的setXxx()方法给组件对象设置属性 -->
<!-- name属性:指定属性名(这个属性名是getXxx()、setXxx()方法定义的,和成员变量无关) -->
<!-- value属性:指定属性值 -->
<property name="name" value="lwh"/>
<property name="age" value="18" />
</bean>
2.2.4、实验四:依赖注入之构造器注入
①在
Student
类中添加有参构造
public Student(String name, int age) {
this.name = name;
this.age = age;
}
②配置
bean
<bean id="studentTwo" class="com.lwh.pojo.Student">
<constructor-arg value="lwh"></constructor-arg>
<constructor-arg value="18"></constructor-arg>
</bean>
注意:
constructor-arg
标签还有两个属性可以进一步描述构造器参数:
index
属性:指定参数所在位置的索引(从
0
开始)
name
属性:指定参数名
2.2.5、实验五:特殊值处理
①字面量赋值
什么是字面量?
int a = 10;
声明一个变量
a
,初始化为
10
,此时
a
就不代表字母
a
了,而是作为一个变量的名字。当我们引用
a
的时候,我们实际上拿到的值是
10
。
而如果
a
是带引号的:
'a'
,那么它现在不是一个变量,它就是代表
a
这个字母本身,这就是字面
量。所以字面量没有引申含义,就是我们看到的这个数据本身。
<!-- 使用value属性给bean的属性赋值时,Spring会把value属性的值看做字面量 -->
<property name="name" value="lwh"/>
②
null
值
<property name="name">
<null />
</property>
③
xml
实体
<!-- 小于号在XML文档中用来定义标签的开始,不能随便使用 -->
<!-- 解决方案一:使用XML实体来代替 -->
<property name="expression" value="a < b"/>
④
CDATA
节
<property name="expression">
<!-- 解决方案二:使用CDATA节 -->
<!-- CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据 -->
<!-- XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析 -->
<!-- 所以CDATA节中写什么符号都随意 -->
<value><![CDATA[a < b]]></value>
</property>
2.2.6、实验六:为类类型属性赋值
①创建班级类
Clazz
public class Clazz {
private String cid;
private String cname;
public Clazz() {
}
public Clazz(String cid, String cname) {
this.cid = cid;
this.cname = cname;
}
public String getCid() {
return cid;
}
public void setCid(String cid) {
this.cid = cid;
}
public String getCname() {
return cname;
}
public void setCname(String cname) {
this.cname = cname;
}
@Override
public String toString() {
return "Clazz{" +
"cid='" + cid + '\'' +
", cname='" + cname + '\'' +
'}';
}
}
②修改
Student
类
在
Student
类中添加以下代码:
private Clazz clazz;
public Clazz getClazz() {
return clazz;
}
public void setClazz(Clazz clazz) {
this.clazz = clazz;
}
③方式一:引用外部已声明的
bean
配置
Clazz
类型的
bean
:
<bean id="clazz" class="com.lwh.pojo.Clazz">
<property name="cid" value="1" />
<property name="cname" value="远大前程" />
</bean>
为
Student
中的
clazz
属性赋值:
<bean id="student" class="com.lwh.pojo.Student">
<property name="name" value="lwh"/>
<property name="age" value="18" />
<!-- ref属性:引用IOC容器中某个bean的id,将所对应的bean为属性赋值 -->
<property name="clazz" ref="clazz" />
</bean>
如果错把ref属性写成了value属性,会抛出异常: Caused by: java.lang.IllegalStateException:
Cannot convert value of type 'java.lang.String' to required type
'com.atguigu.spring.bean.Clazz' for property 'clazz': no matching editors or conversion
strategy found
意思是不能把String类型转换成我们要的Clazz类型,说明我们使用value属性时,Spring只把这个
属性看做一个普通的字符串,不会认为这是一个bean的id,更不会根据它去找到bean来赋值
④方式二:内部
bean
<bean id="studentFour" class="com.lwh.pojo.Student">
<property name="name" value="lwh"></property>
<property name="age" value="18"></property>
<property name="clazz">
<!-- 在一个bean中再声明一个bean就是内部bean -->
<!-- 内部bean只能用于给属性赋值,不能在外部通过IOC容器获取,因此可以省略id属性 -->
<bean id="clazzInner" class="com.lwh.pojo.Clazz">
<property name="cid" value="1"></property>
<property name="cname" value="远大前程班"></property>
</bean>
</property>
</bean>
2.2.7、实验七:为数组类型属性赋值
①修改
Student
类
在
Student
类中添加以下代码:
private String[] books;
public String[] getBooks() {
return books;
}
public void setBooks(String[] books) {
this.books = books;
}
②配置
bean
<bean id="studentFour" class="com.lwh.pojo.Student">
<property name="name" value="lwh"></property>
<property name="age" value="18"></property>
<property name="clazz">
<bean id="clazzInner" class="com.lwh.pojo.Clazz">
<property name="cid" value="1"></property>
<property name="cname" value="远大前程班"></property>
</bean>
</property>
<property name="books">
<array>
<value>语文</value>
<value>数学</value>
<value>英语</value>
</array>
</property>
</bean>
2.2.8、实验八:为集合类型属性赋值
①为
List
集合类型属性赋值
在Student
类中添加以下代码:
private List<String> hobbys;
public List<String> getHobbys() {
return hobbys;
}
public void setHobbys(List<String> hobbys) {
this.hobbys = hobbys;
}
配置
bean
:
<property name="hobbys" ref="hobbyList" />
<util:list id="hobbyList">
<value>打台球</value>
<value>打羽毛球</value>
<value>打篮球</value>
</util:list>
②为
Map
集合类型属性赋值
在Student
类中添加以下代码:
private Map<String,String> card;
public Map<String, String> getCard() {
return card;
}
public void setCard(Map<String, String> card) {
this.card = card;
}
配置
bean
:
<property name="card" ref="cardMap" />
<util:map id="cardMap">
<entry key="身份证" value="112345545134515464" />
<entry key="银行卡" value="124654844444414454164" />
</util:map>
使用
util:list
、
util:map
标签必须引入相应的命名空间,可以通过
idea
的提示功能选择
2.2.9、实验九:p命名空间
引入p命名空间后,可以通过以下方式为bean的各个属性赋值
<bean id="studentFive" class="com.lwh.pojo.Student" p:name="lwh" p:age="18" p:clazz-ref="clazz"></bean>
2.2.10
、实验十:引入外部属性文件
①加入依赖
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.11</version>
</dependency>
②创建外部属性文件
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456
③引入属性文件
<!-- 引入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
④配置bean
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
2.2.11、实验十一:bean的作用域
在
Spring
中可以通过配置
bean
标签的
scope
属性来指定
bean
的作用域范围,各取值含义参加下表:
取值
|
含义
|
创建对象的时机
|
singleton
(默认)
|
在
IOC
容器中,这个
bean
的对象始终为单实例
|
IOC
容器初始化时
|
prototype
|
这个
bean
在
IOC
容器中有多个实例
|
获取
bean
时
|
如果是在
WebApplicationContext
环境下还会有另外两个作用域(但不常用):
取值
|
含义
|
request
|
在一个请求范围内有效
|
session
| 在一个会话范围内有效 |
2.2.12
、实验十二:
bean
的生命周期
①具体的生命周期过程
- bean对象创建(调用无参构造器)
- 给bean对象设置属性
- bean对象初始化之前操作(由bean的后置处理器负责)
- bean对象初始化(需在配置bean时指定初始化方法)
- bean对象初始化之后操作(由bean的后置处理器负责)
- bean对象就绪可以使用
- bean对象销毁(需在配置bean时指定销毁方法)
- IOC容器关闭
2.2.13、实验十三:FactoryBean
FactoryBean
是
Spring
提供的一种整合第三方框架的常用机制。和普通的
bean
不同,配置一个
FactoryBean类型的
bean
,在获取
bean
的时候得到的并不是
class
属性中配置的这个类的对象,而是getObject()方法的返回值。通过这种机制,
Spring
可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。
将来我们整合
Mybatis
时,
Spring
就是通过
FactoryBean
机制来帮我们创建
SqlSessionFactory
对象的。
2.2.14、实验十四:基于xml的自动装配
自动装配:
根据指定的策略,在
IOC
容器中匹配某一个
bean
,自动为指定的
bean
中所依赖的类类型或接口类
型属性赋值
①场景模拟
创建类
UserController
public class UserController {
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
public void saveUser(){
userService.saveUser();
}
}
创建接口
UserService
public interface UserService {
void saveUser();
}
创建类
UserServiceImpl
实现接口
UserService
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void saveUser() {
userDao.saveUser();
}
}
创建接口
UserDao
public interface UserDao {
void saveUser();
}
创建类
UserDaoImpl
实现接口
UserDao
public class UserDaoImpl implements UserDao {
@Override
public void saveUser() {
System.out.println("保存成功");
}
}
②配置
bean
使用
bean
标签的
autowire
属性设置自动装配效果
自动装配方式:
byType
byType
:根据类型匹配
IOC
容器中的某个兼容类型的
bean
,为属性自动赋值
若在
IOC
中,没有任何一个兼容类型的
bean
能够为属性赋值,则该属性不装配,即值为默认值
null
若在
IOC
中,有多个兼容类型的
bean
能够为属性赋值,则抛出异常
NoUniqueBeanDefinitionException
<bean id="userController" class="com.lwh.controller.UserController" autowire="byType" />
<bean id="userService" class="com.lwh.service.impl.UserServiceImpl" autowire="byType" />
<bean id="userDao" class="com.lwh.dao.impl.UserDaoImpl" />
自动装配方式:
byName
byName
:将自动装配的属性的属性名,作为
bean
的
id
在
IOC
容器中匹配相对应的
bean
进行赋值
<bean id="userController" class="com.lwh.controller.UserController" autowire="byName" />
<bean id="userService" class="com.lwh.service.impl.UserServiceImpl" autowire="byName" />
<bean id="userServiceImpl" class="com.lwh.service.impl.UserServiceImpl" autowire="byName" />
<bean id="userDao" class="com.lwh.dao.impl.UserDaoImpl" />
<bean id="userDaoImpl" class="com.lwh.dao.impl.UserDaoImpl" />
2.3、基于注解管理bean
2.3.1、实验一:标记与扫描
①注解
和
XML
配置文件一样,注解本身并不能执行,注解本身仅仅只是做一个标记,具体的功能是框架检测到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作。
本质上:所有一切的操作都是
Java
代码来完成的,
XML
和注解只是告诉框架中的
Java
代码如何执行。
举例:元旦联欢会要布置教室,蓝色的地方贴上元旦快乐四个字,红色的地方贴上拉花,黄色的地方贴上气球。
班长做了所有标记,同学们来完成具体工作。墙上的标记相当于我们在代码中使用的注解,后面同学们做的工作,相当于框架的具体操作。
②扫描
Spring
为了知道程序员在哪些地方标记了什么注解,就需要通过扫描的方式,来进行检测。然后根据注解进行后续操作。
③标识组件的常用注解
@Component:将类标识为普通组件
@Controller:将类标识为控制层组件
@Service:将类标识为业务层组件
@Repository:将类标识为持久层组件
问:以上四个注解有什么关系和区别?
通过查看源码我们得知, @Controller 、 @Service 、 @Repository 这三个注解只是在 @Component 注解的基础上起了三个新的名字。对于 Spring 使用 IOC 容器管理这些组件来说没有区别。所以 @Controller 、 @Service 、 @Repository 这三个注解只是给开发人员看的,让我们能够便于分辨组件的作用。注意:虽然它们本质上一样,但是为了代码的可读性,为了程序结构严谨我们肯定不能随便胡乱标记。
⑥创建组件
创建控制层组件
@Controller
public class UserController {
}
创建接口
UserService
public interface UserService {
}
创建业务层组件
UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
}
创建接口
UserDao
public interface UserDao {
}
创建持久层组件
UserDaoImpl
@Repository
public class UserDaoImpl implements UserDao {
}
⑦扫描组件
情况一:最基本的扫描方式
<context:component-scan base-package="com.lwh" />
情况二:指定要排除的组件
<context:component-scan base-package="com.atguigu">
<!-- context:exclude-filter标签:指定排除规则 -->
<!--type:设置排除或包含的依据 type="annotation",根据注解排除,expression中设置要排除的注解的全类名 type="assignable",根据类型排除,expression中设置要排除的类型的全类名 -->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<!--<context:exclude-filter type="assignable"expression="com.lwh.controller.UserController"/>-->
</context:component-scan>
情况三:仅扫描指定组件
<context:component-scan base-package="com.atguigu" use-default-filters="false">
<!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 -->
<!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
<!-- 此时必须设置use-default-filters="false",因为默认规则即扫描指定包下所有类 -->
<!--type:设置排除或包含的依据 type="annotation",根据注解排除,expression中设置要排除的注解的全类名 type="assignable",根据类型排除,expression中设置要排除的类型的全类名 -->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<!--<context:include-filter type="assignable" expression="com.atguigu.controller.UserController"/>-->
</context:component-scan>
⑧组件所对应的
bean
的
id
在我们使用
XML
方式管理
bean
的时候,每个
bean
都有一个唯一标识,便于在其他地方引用。现在使用
注解后,每个组件仍然应该有一个唯一标识。
默认情况
类名首字母小写就是
bean
的
id
。例如:
UserController
类对应的
bean
的
id
就是
userController
。
自定义
bean
的
id
可通过标识组件的注解的
value
属性设置自定义的
bean
的
id
@Service("userService")//
默认为
userServiceImpl public class UserServiceImpl implements
UserService {}
2.3.2
、实验二:基于注解的自动装配
①场景模拟
参考基于
xml
的自动装配
在
UserController
中声明
UserService
对象
在
UserServiceImpl
中声明
UserDao
对象
②
@Autowired
注解
在成员变量上直接标记
@Autowired
注解即可完成自动装配,不需要提供
setXxx()
方法。以后我们在项目中的正式用法就是这样。
@Controller
public class UserController {
@Autowired
private UserService userService;
public void saveUser(){
userService.saveUser();
}
}
public interface UserService {
void saveUser();
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void saveUser() {
userDao.saveUser();
}
}
public interface UserDao {
void saveUser();
}
@Repository
public class UserDaoImpl implements UserDao {
@Override
public void saveUser() {
System.out.println("保存成功");
}
}
@Autowired注解可以标记在构造器和set方法上
③@Autowired
工作流程
- 首先根据所需要的组件类型到IOC容器中查找
- 能够找到唯一的bean:直接执行装配
- 如果完全找不到匹配这个类型的bean:装配失败
- 和所需类型匹配的bean不止一个
- 没有@Qualifier注解:根据@Autowired标记位置成员变量的变量名作为bean的id进行匹配
- 能够找到:执行装配
- 找不到:装配失败
- 使用@Qualifier注解:根据@Qualifier注解中指定的名称作为bean的id进行匹配
- 能够找到:执行装配
- 找不到:装配失败
@Controller
public class UserController {
@Autowired
@Qualifier("userServiceImpl")
private UserService userService;
public void saveUser(){
userService.saveUser();
}
}
@Autowired
中有属性
required
,默认值为
true
,因此在自动装配无法找到相应的
bean
时,会装
配失败
可以将属性
required
的值设置为
true
,则表示能装就装,装不上就不装,此时自动装配的属性为
默认值
但是实际开发时,基本上所有需要装配组件的地方都是必须装配的,用不上这个属性。
3、AOP
3.1、场景模拟
3.1.1、声明接口
声明计算器接口
Calculator
,包含加减乘除的抽象方法
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
3.1.2、创建实现类
public class CalculatorImpl implements Calculator{
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
3.1.3、创建带日志功能的实现类
public class CalculatorLogImpl implements Calculator{
@Override
public int add(int i, int j) {
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
int result = i + j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] add 方法结束了,结果是:" + result);
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
int result = i - j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] sub 方法结束了,结果是:" + result);
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);
int result = i * j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] mul 方法结束了,结果是:" + result);
return result;
}
@Override
public int div(int i, int j) {
System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);
int result = i / j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] div 方法结束了,结果是:" + result);
return result;
}
}
3.1.4、提出问题
①现有代码缺陷
针对带日志功能的实现类,我们发现有如下缺陷:
- 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
- 附加功能分散在各个业务功能方法中,不利于统一维护
②解决思路
解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
③困难
解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。
3.2、代理模式
3.2.1、概念
①介绍
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接
调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——
解耦
。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
使用代理后:
②生活中的代理
- 广告商找大明星拍广告需要经过经纪人
- 合作伙伴找大老板谈合作要约见面时间需要经过秘书
- 房产中介是买卖双方的代理
③相关术语
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
- 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。
3.2.2、静态代理
创建静态代理类:
public class CalculatorStaticProxy implements Calculator{
// 将被代理的目标对象声明为成员变量
private Calculator target;
public CalculatorStaticProxy() {
}
public CalculatorStaticProxy(Calculator target) {
this.target = target;
}
@Override
public int add(int i, int j) {
// 附加功能由代理类中的代理方法来实现
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int addResult = target.add(i,j);
System.out.println("[日志] add 方法结束了,结果是:" + addResult);
return addResult;
}
@Override
public int sub(int i, int j) {
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int subResult = target.sub(i,j);
System.out.println("[日志] add 方法结束了,结果是:" + subResult);
return subResult;
}
@Override
public int mul(int i, int j) {
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int mulResult = target.mul(i,j);
System.out.println("[日志] add 方法结束了,结果是:" + mulResult);
return mulResult;
}
@Override
public int div(int i, int j) {
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int divResult = target.div(i,j);
System.out.println("[日志] add 方法结束了,结果是:" + divResult);
return divResult;
}
}
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。
3.2.3、动态代理
生产代理对象的工厂类:
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxy(){
/** newProxyInstance():创建一个代理实例 * 其中有三个参数:
* 1、classLoader:加载动态生成的代理类的类加载器
* 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
* 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接 口中的抽象方法
*/
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/** proxy:代理对象
* method:代理对象需要实现的方法,即其中需要重写的方法
* args:method所对应方法的参数
*/
Object result = null;
try {
System.out.println("[动态代理][日志]" + method.getName() + ",参数" + Arrays.toString(args));
result = method.invoke(target, args);
System.out.println("[动态代理][日志] "+method.getName()+",结 果:"+ result);
} catch (Exception e) {
e.printStackTrace();
System.out.println("[动态代理][日志] "+method.getName()+",异 常:"+e.getMessage());
} finally {
System.out.println("[动态代理][日志] "+method.getName()+",方法 执行完毕");
}
return result;
}
};
return Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
}
}
3.2.4、测试
@Test
public void test() {
ProxyFactory factory = new ProxyFactory(new CalculatorImpl());
Calculator proxy = (Calculator) factory.getProxy();
proxy.add(1,2);
// proxy.div(1,0);
}
3.3、AOP概念及相关术语
3.3.1、概述
AOP
(
Aspect Oriented Programming
)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。
3.3.2、相关术语
①横切关注点
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
②通知
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行
- 异常通知:在被代理的目标方法异常结束后执行
- 后置通知:在被代理的目标方法最终结束后执行
- 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
③切面
封装通知方法的类。
④目标
被代理的目标对象。
⑤代理
向目标对象应用通知之后创建的代理对象。
⑥连接点
这也是一个纯逻辑概念,不是语法定义的。
把方法排成一排,每一个横切位置看成
x
轴方向,把方法从上到下执行的顺序看成
y
轴,
x
轴和
y
轴的交叉点就是连接点。
⑦切入点
- 定位连接点的方式。
- 每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
- 如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
- Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
- 切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
3.3.3、作用
- 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
- 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。
3.4、基于注解的AOP
3.4.1、技术说明
- 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
- cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
- AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
3.4.2、准备工作
①添加依赖
在
IOC
所需依赖基础上再加入下面依赖即可:
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.22</version>
</dependency>
②准备被代理的目标资源
接口:
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
实现类:
@Component
public class CalculatorPureImpl implements Calculator{
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
3.4.3、创建切面类并配置
// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
public class LogAspect {
@Before("execution(public int com.lwh.CalculatorPureImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
String name = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger --> 前置通知,方法名:"+ name + ",参数:" + args);
}
@After("execution(public int com.lwh.CalculatorPureImpl.*(..))")
public void afterMethod(JoinPoint joinPoint) {
String name = joinPoint.getSignature().getName();
System.out.println("Logger --> 后置通知,方法名:" + name);
}
@AfterReturning(value = "execution(public int com.lwh.CalculatorPureImpl.*(..))",returning = "result")
public void afterRunningMethod(JoinPoint joinPoint, Object result) {
String name = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名:"+name+",结 果:"+result);
}
@AfterThrowing(value = "execution(public int com.lwh.CalculatorPureImpl.*(..))",throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
String name = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:"+name+",异常:"+ex);
}
@Around("execution(public int com.lwh.CalculatorPureImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
Object result = null;
try {
System.out.println("环绕通知--->前置通知");
result = joinPoint.proceed();
System.out.println("环绕通知--->返回通知");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知--->异常通知");
} finally {
System.out.println("环绕通知--->后置通知");
}
return result;
}
}
在
Spring
的配置文件中配置:
<!--基于注解的AOP的实现:
1、将目标对象和切面交给IOC容器管理(注解+扫描)
2、开启AspectJ的自动代理,为目标对象自动生成代理
3、将切面类通过注解@Aspect标识
-->
<context:component-scan base-package="com.lwh"/>
<aop:aspectj-autoproxy/>
3.4.4、各种通知
- 前置通知:使用@Before注解标识,在被代理的目标方法前执行
- 返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行
- 异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行
- 后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行
- 环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
各种通知的执行顺序:Spring 版本 5.3.x 以前:
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
Spring 版本 5.3.x 以后:
- 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
3.4.5、切入点表达式语法
①作用
②语法细节
- 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
- 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
- 例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello
- 在包名的部分,使用“*..”表示包名任意、包的层次深度任意
- 在类名的部分,类名部分整体用*号代替,表示类名任意
- 在类名的部分,可以使用*号代替类名的一部分
- 例如:*Service匹配所有名称以Service结尾的类或接口
- 在方法名部分,可以使用*号表示方法名任意
- 在方法名部分,可以使用*号代替方法名的一部分
- 例如:*Operation匹配所有方法名以Operation结尾的方法
- 在方法参数列表部分,使用(..)表示参数列表任意
- 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
- 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
- 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
- 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
- 例如:execution(public int ..Service.*(.., int)) 正确
- 例如:execution(* int ..Service.*(.., int))
3.4.6、重用切入点表达式
①声明
@Pointcut("execution(* com.lwh.*.*(..))")
public void pointCut(){}
②在同一个切面中使用
@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
}
3.4.7、切面的优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的
内外嵌套
顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
使用
@Order
注解可以控制切面的优先级:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低
3.5、基于XML的AOP
<context:component-scan base-package="com.lwh.aop.xml" />
<aop:config>
<!--配置切面类-->
<aop:aspect ref="loggerAspect">
<aop:pointcut id="pointCut" expression="execution(* com.atguigu.aop.xml.CalculatorImpl.*(..))"/>
<aop:before method="beforeMethod" pointcut-ref="pointCut" />
<aop:after method="afterMethod" pointcut-ref="pointCut" />
<aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointCut"/>
<aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut- ref="pointCut"/>
<aop:around method="aroundMethod" pointcut-ref="pointCut"/>
</aop:aspect>
<aop:aspect ref="validateAspect" order="1">
<aop:before method="validateBeforeMethod" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>
4、声明式事务
4.1、JdbcTemplate
4.1.1、简介
Spring
框架对
JDBC
进行封装,使用
JdbcTemplate
方便实现对数据库操作
4.1.2、准备工作
①加入依赖
<dependencies>
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.22</version>
</dependency>
<!-- Spring 持久化层支持jar包 -->
<!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个 jar包 -->
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.22</version>
</dependency>
<!-- Spring 测试相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.22</version>
</dependency>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.11</version>
</dependency>
</dependencies>
②创建
jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=true jdbc.user=root jdbc.password=123456
③配置Spring的配置文件
<!-- 导入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置 JdbcTemplate -->
<bean class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 装配数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
4.1.3、测试
①在测试类装配
JdbcTemplate
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring-jdbc.xml")
public class JdbcTemplateTest {
@Autowired
private JdbcTemplate jdbcTemplate;
}
②测试增删改功能
@Test
public void test() {
String sql = "insert into t_user values(null,?,?,?,?,?)";
jdbcTemplate.update(sql,"lwh","888",20,"男","123456@qq.com");
}
③查询一条数据为实体类对象
@Test
public void test1() {
String sql = "select * from t_user where id = ?";
User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 5);
System.out.println(user);
}
④查询多条数据为一个
list
集合
@Test
public void test2() {
String sql = "select * from t_user";
List<User> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
list.forEach(System.out::println);
}
⑤查询单行单列的值
@Test
public void test3() {
String sql = "select count(*) from t_user";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println(count);
}
4.2、声明式事务概念
4.2.1、编程式事务
事务功能的相关操作全部通过自己编写代码来实现:
Connection conn = ...;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
// 释放数据库连接
conn.close();
}
编程式的实现方式存在缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
4.2.2、声明式事务
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
- 好处1:提高开发效率
- 好处2:消除了冗余的代码
- 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化
所以,我们可以总结下面两个概念:
- 编程式:自己写代码实现功能
- 声明式:通过配置让框架实现功能
4.3、基于注解的声明式事务
4.3.1、准备工作
①加入依赖
<dependencies>
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.22</version>
</dependency>
<!-- Spring 持久化层支持jar包 -->
<!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个 jar包 -->
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.22</version>
</dependency>
<!-- Spring 测试相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.22</version>
</dependency>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.11</version>
</dependency>
</dependencies>
②创建
jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=true jdbc.user=root jdbc.password=123456
③配置Spring的配置文件
<!-- 导入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置 JdbcTemplate -->
<bean class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 装配数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
④创建表
CREATE TABLE `t_book` (`book_id` int ( 11 ) NOT NULL AUTO_INCREMENT COMMENT ' 主键 ' ,`book_name` varchar ( 20 ) DEFAULT NULL COMMENT ' 图书名称 ' ,`price` int ( 11 ) DEFAULT NULL COMMENT ' 价格 ' ,`stock` int ( 10 ) unsigned DEFAULT NULL COMMENT ' 库存(无符号) ' ,PRIMARY KEY (`book_id`)) ENGINE=InnoDB AUTO_INCREMENT= 3 DEFAULT CHARSET=utf8;insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values ( 1 , ' 斗破苍穹 ' , 80 , 100 ),( 2 , ' 斗罗大陆 ' , 50 , 100 );CREATE TABLE `t_user` (`user_id` int ( 11 ) NOT NULL AUTO_INCREMENT COMMENT ' 主键 ' ,`username` varchar ( 20 ) DEFAULT NULL COMMENT ' 用户名 ' ,`balance` int ( 10 ) unsigned DEFAULT NULL COMMENT ' 余额(无符号) ' ,PRIMARY KEY (`user_id`)) ENGINE=InnoDB AUTO_INCREMENT= 2 DEFAULT CHARSET=utf8;insert into `t_user`(`user_id`,`username`,`balance`) values ( 1 , 'admin' , 50 );
⑤创建组件
创建
BookController
:
@Controller
public class BookController {
@Autowired
private BookService bookService;
public void buyBook(Integer userId, Integer bookId) {
bookService.buyBook(userId,bookId);
}
}
创建接口
BookService
:
public interface BookService {
void buyBook(Integer userId, Integer bookId);
}
创建实现类
BookServiceImpl
:
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
@Transactional
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
// 更新图书的库存
bookDao.updateStock(bookId);
// 更新用户的余额
bookDao.updateBalance(userId, price);
}
}
创建接口
BookDao
:
public interface BookDao {
Integer getPriceByBookId(Integer bookId);
void updateStock(Integer bookId);
void updateBalance(Integer userId, Integer price);
}
创建实现类
BookDaoImpl
:
@Repository
public class BookDaoImpl implements BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Integer getPriceByBookId(Integer bookId) {
String sql = "select price from t_book where book_id = ?";
return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
}
@Override
public void updateStock(Integer bookId) {
String sql = "update t_book set stock = stock - 1 where book_id = ?";
jdbcTemplate.update(sql,bookId);
}
@Override
public void updateBalance(Integer userId, Integer price) {
String sql = "update t_users set balance = balance - ? where user_id = ?";
jdbcTemplate.update(sql,price,userId);
}
}
4.3.2、测试无事务情况
①创建测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:tx-annotation.xml")
public class TxByAnnotationTest {
@Autowired
private BookController bookController;
@Test
public void test() {
bookController.buyBook(1,1);
}
}
②模拟场景
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
假设用户
id
为
1
的用户,购买
id
为
1
的图书
用户余额为
50
,而图书价格为
80
购买图书之后,用户的余额为
-30
,数据库中余额字段设置了无符号,因此无法将
-30
插入到余额字段
此时执行
sql
语句会抛出
SQLException
③观察结果
因为没有添加事务,图书的库存更新了,但是用户的余额没有更新
显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败
4.3.3、加入事务
①添加事务配置
在
Spring
的配置文件中添加配置:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--开启事务的注解驱动 通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务 -->
<!-- transaction-manager属性的默认值是transactionManager,
如果事务管理器bean的id正好就 是这个默认值,则可以省略这个属性
-->
<tx:annotation-driven transaction-manager="transactionManager"/>
注意:导入的名称空间需要 tx 结尾的那个。
②添加事务注解
因为
service
层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在
service
层处理
在
BookServiceImpl
的
buybook()
添加注解
@Transactional
③观察结果
由于使用了
Spring
的声明式事务,更新库存和更新余额都没有执行
4.3.4、@Transactional注解标识的位置
@Transactional
标识在方法上,咋只会影响该方法
@Transactional
标识的类上,咋会影响类中所有的方法
4.3.5、事务属性:只读
①介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
②使用方式
@Transactional(readOnly = true)
③注意
对增删改操作设置只读会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification
are not allowed
4.3.6、事务属性:超时
①介绍
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java
程序或
MySQL
数据库或网络连接等等)。
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。
概括来说就是一句话:超时回滚,释放资源。
②使用方式
@Transactional(timeout = 3)
③观察结果
执行过程中抛出异常:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out:
deadline was Fri Jun 04 16:25:39 CST 2022
4.3.7、事务属性:回滚策略
①介绍
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
可以通过
@Transactional
中相关属性设置回滚策略
- rollbackFor属性:需要设置一个Class类型的对象
- rollbackForClassName属性:需要设置一个字符串类型的全类名
- noRollbackFor属性:需要设置一个Class类型的对象
- rollbackFor属性:需要设置一个字符串类型的全类名
②使用方式
@Transactional(noRollbackFor = ArithmeticException.class)
@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
③观察结果
虽然购买图书功能中出现了数学运算异常(
ArithmeticException
),但是我们设置的回滚策略是,当出现ArithmeticException
不发生回滚,因此购买图书的操作正常执行
4.3.8、事务属性:事务隔离级别
①介绍
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL
标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
- 读未提交:READ UNCOMMITTED 允许Transaction01读取Transaction02未提交的修改。
- 读已提交:READ COMMITTED、 要求Transaction01只能读取Transaction02已提交的修改。
- 可重复读:REPEATABLE READ 确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。
- 串行化:SERIALIZABLE 确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力见下表:
隔离级别
|
脏读
|
不可重复读
|
幻读
|
READ UNCOMMITTED
| 有 | 有 | 有 |
READ COMMITTED
| 无 | 有 | 有 |
REPEATABLE READ
| 无 | 无 | 有 |
SERIALIZABLE
| 无 | 无 | 无 |
各种数据库产品对事务隔离级别的支持程度:
隔离级别
|
Oracle
|
MySQL
|
READ UNCOMMITTED
|
×
|
√
|
READ COMMITTED
|
√
(
默认
)
|
√
|
REPEATABLE READ
|
×
|
√(默认)
|
SERIALIZABLE
| √ |
√
|
②使用方式
@Transactional ( isolation = Isolation . DEFAULT ) // 使用数据库默认的隔离级别@Transactional ( isolation = Isolation . READ_UNCOMMITTED ) // 读未提交@Transactional ( isolation = Isolation . READ_COMMITTED ) // 读已提交@Transactional ( isolation = Isolation . REPEATABLE_READ ) // 可重复读@Transactional ( isolation = Isolation . SERIALIZABLE ) // 串行化
4.3.9、事务属性:事务传播行为
①介绍
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
②测试
创建接口
CheckoutService
:
public interface CheckoutService {
void checkout(Integer[] bookIds, Integer userId);
}
创建实现类
CheckoutServiceImpl
:
@Service
public class CheckoutServiceImpl implements CheckoutService {
@Autowired
private BookService bookService;
@Override
@Transactional
//一次购买多本图书
public void checkout(Integer[] bookIds, Integer userId) {
for (Integer bookId : bookIds) {
bookService.buyBook(bookId, userId);
}
}
}
在
BookController
中添加方法:
@Autowired
private CheckoutService checkoutService;
public void checkout(Integer[] bookIds, Integer userId){
checkoutService.checkout(bookIds, userId);
}
在数据库中将用户的余额修改为
100
元
③观察结果
可以通过
@Transactional
中的
propagation
属性设置事务传播行为
修改
BookServiceImpl
中
buyBook()
上,注解
@Transactional
的
propagation
属性
@Transactional(propagation = Propagation.REQUIRED)
,默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。经过观察,购买图书的方法buyBook()
在
checkout()
中被调用,checkout()
上有事务注解,因此在此事务中执行。所购买的两本图书的价格为
80
和
50
,而用户的余额为100
,因此在购买第二本图书时余额不足失败,导致整个
checkout()
回滚,即只要有一本书买不了,就都买不了
@Transactional(propagation = Propagation.REQUIRES_NEW)
,表示不管当前线程上是否有已经开启的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()
的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()
中回滚,购买第一本图书不受影响,即能买几本就买几本
4.4、基于XML的声明式事务
4.4.1、场景模拟
参考基于注解的声明式事务
4.4.2、修改Spring配置文件
将
Spring
配置文件中去掉
tx:annotation-driven
标签,并添加配置:
<aop:config>
<!-- 配置事务通知和切入点表达式 -->
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.atguigu.spring.tx.xml.service.impl.*.*(..))"></aop:advisor>
</aop:config>
<!-- tx:advice标签:配置事务通知 -->
<!-- id属性:给事务通知标签设置唯一标识,便于引用 -->
<!-- transaction-manager属性:关联事务管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- tx:method标签:配置具体的事务方法 -->
<!-- name属性:指定方法名,可以使用星号代表多个字符 -->
<tx:method name="get*" read-only="true"/>
<tx:method name="query*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<!-- read-only属性:设置只读属性 -->
<!-- rollback-for属性:设置回滚的异常 -->
<!-- no-rollback-for属性:设置不回滚的异常 -->
<!-- isolation属性:设置事务的隔离级别 -->
<!-- timeout属性:设置事务的超时属性 -->
<!-- propagation属性:设置事务的传播行为 -->
<tx:method name="save*" read-only="false" rollback- for="java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="update*" read-only="false" rollback- for="java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="delete*" read-only="false" rollback- for="java.lang.Exception" propagation="REQUIRES_NEW"/>
</tx:attributes>
</tx:advice>
注意:基于xml实现的声明式事务,必须引入aspectJ的依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.22</version>
</dependency>