Java - Spring Framework
Spring 一般是说 Spring 这个产品的统称,而不是指具体细分的哪个框架。
Spring Framework 是后面 SpringMVC、SpringBoot 框架的基石,它们都是基于 Spring 进行开发的,所以第一个优先搞懂 Spring 的用法及基本原理。
Spring Framework 6.1.x 要求 JDK 17-23 范围内,这些信息都可以在官方文档中查阅到:
- https://spring.io/projects/spring-framework#overview,查看现状
- https://docs.spring.io/spring-framework/reference/index.html,文档说明目前版本 6.1.10
- https://docs.spring.io/spring-framework/docs/current/javadoc-api,API 文档
目录
- 目录
- 1 IoC
1 IoC
Inversion of Control 是一种设计思想。
在 Spring 里可以帮我们自动创建 bean 对象,还可以对 bean 对象注入其他对象,这样两个对象就有了依赖关系。
这个创建出来的对象是放在 IoC 容器(也叫 Spring 容器),类似于一个 Map 的内容中。
IoC 只是一个思路,具体实现有很多种方式,在 Spring 中对 IoC 的实现是 DI(Dependency Injection)依赖注入,对 bean 做注入只有两种方式,一是调 setter 方法,二是调构造方法。
引入 Spring-Context 依赖练习 Spring 自动创建对象。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.10</version>
</dependency>
再创建一个 bean,test.java。
package com.raingray.spring.bean;
public class test {
private String testAttr;
public test() {
System.out.prinln("com.raingray.spring.bean.test 被实例化了");
}
public test(String testAttr) {
this.testAttr = testAttr;
}
public String getTestAttr() {
return testAttr;
}
public void setTestAttr(String testAttr) {
this.testAttr = testAttr;
}
@Override
public String toString() {
return "test{" +
"testAttr='" + testAttr + '\'' +
'}';
}
}
在 Resource 目录创建 Spring 配置文件,文件名没有规定说写死,这里我叫 SpringConf.xml。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
向 <beans>
中添加 <bean>
标签,配置元数据。id 属性是这个 bean 的标识符需要唯一,class 属性是 bean 全限定名,到时候 Spring 容器创建对象方便找。需要注意的是 id 属性如果不写,默认值是 class 属性指定的类名首字母小写,比如类名是 TestAxxUxx,那么默认 id 是 testAxxUxx。
<bean id="testBean" class="com.raingray.spring.bean.test"/>
创建 Spring 容器是用 ClassPathXmlApplicationContext 方法来读取类跟路径下 Spring 配置文件,可以读取一个或者多个配置文件,最终返回应用上下文对象。这个上下文对象需要类型接收啊,这里使用 ApplicationContext,因为它继承了 BeanFactory 容器接口,使用 ApplicationContext 作为类型是因为它做了一些其他扩展后续可能要用到,当然你不想用其他的内容单纯用 BeanFactory 作为类型也是没问题的。
package com.raingray.spring;
import com.raingray.spring.bean.test;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestObjectInstance {
@Test
void testContainerObjectInstance() {
// 创建容器和对象
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
}
}
new ClassPathXmlApplicationContext("spring.xml");
运行后容器创建完毕,也自动调用创建 bean 无参构造方法创建对象,详细点来说是解析了 xml 文件通过反射来创建对应对象。因此只要一个类有无参构造方法,Spring 容器就能为你创建出对象。
com.raingray.spring.bean.test 被实例化了
创建完容器,通过 BeanFactory 接口 getBean 方法,根据 bean 的 id 获取具体对象。默认获取到的是 Object,因此在获取的时候可以传递对应 bean 类型,避免类型转换。
// getBean 获取创建的对象,需要向下转型
test testObjectInstance = (test) context.getBean("testBean");
testObjectInstance.setTestAttr("test attr value1");
System.out.println(testObjectInstance);
// getBean 获取创建的对象,直接传入类型无需向下转型
test testObjectInstance1 = context.getBean("testBean", test.class);
testObjectInstance1.setTestAttr("test attr value2");
System.out.println(testObjectInstance);
new ClassPathXmlApplicationContext("spring.xml");
运行后,test 是被被调用无参构造方法创建出来的,底层是解析了 xml 文件通过反射来创建对应对象。只要一个类有无参构造方法,Spring 容器就能为你创建出对象。
com.raingray.spring.bean.test 被实例化了
test{testAttr='test attr value1'}
test{testAttr='test attr value2'}
1.1 XML 的方式配置-Setter 方法注入
前面说了还可以针对 bean 注入其他对象,这里就给 test 注入一个其他 Bean,先把 test 添加一个属性 injection,把对应 toString/getter/setter/构造方法都更新下。
package com.raingray.spring.bean;
import com.raingray.spring.bean.injection;
public class test {
private String testAttr;
// 要注入的属性
private injection injection;
public test() {
System.out.println( "com.raingray.spring.bean.test 无参构造方法被调用");
}
public test(String testAttr, injection injection) {
this.testAttr = testAttr;
this.injection = injection;
}
public String getTestAttr() {
return testAttr;
}
public void setTestAttr(String testAttr) {
this.testAttr = testAttr;
}
public com.raingray.spring.bean.injection getInjection() {
return injection;
}
public void setInjection(com.raingray.spring.bean.injection injection) {
this.injection = injection;
System.out.println("com.raingray.spring.bean.test 的 setInjection 方法被调用了");
}
@Override
public String toString() {
return "test{" +
"testAttr='" + testAttr + '\'' +
", injection=" + injection +
'}';
}
}
紧接着创建 injection.java。
package com.raingray.spring.bean;
public class injection {
private String injectionAttr;
public injection() {
System.out.println("com.raingray.spring.bean.injection 无参构造方法被调用");
}
public injection(String injectionAttr) {
this.injectionAttr = injectionAttr;
}
public String getInjectionAttr() {
return injectionAttr;
}
public void setInjectionAttr(String injectionAttr) {
this.injectionAttr = injectionAttr;
}
@Override
public String toString() {
return "injection{" +
"injectionAttr='" + injectionAttr + '\'' +
'}';
}
}
注入使用 property
标签,属性 name 是要注入时要调用的 Setter 方法,这里写的 injection 就对应方法 setInjection, ref 属性是 bean 标签 id,用来指明要注入的目标,这里是 injectionBen 对应 com.raingray.spring.bean.injection 这个对象。
<bean id="testBean" class="com.raingray.spring.bean.test">
<property name="injection" ref="injectionBean" />
</bean>
<bean id="injectionBean" class="com.raingray.spring.bean.injection"/>
尝试调用注入的依赖对象。
// 创建容器和对象
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
// 获取 testBean 对象
test testBean = context.getBean("testBean", test.class);
// 调用 testBean 的 getter 方法获取注入的依赖对象 com.raingray.spring.bean.injection
System.out.println(testBean.getInjection());
testBean.getInjection().setInjectionAttr("123");
System.out.println(testBean.getInjection());
从运行日志来看,前两行是自动创建的 Bean 对象,第三行就是调 test.setInjection 方法向 test.injection 属性注入已经创建好的对象 com.raingray.spring.bean.injection,所以注入依赖整个顺序是先创建创建容器和 Bean 对象,最后注入依赖,注入完成我们看后两行日志成功验证 test 对象能获取到 com.raingray.spring.bean.injection 对象,再调用 setter 和 getter 方法成功设置它的属性。
com.raingray.spring.bean.test 无参构造方法被调用
com.raingray.spring.bean.injection 无参构造方法被调用
com.raingray.spring.bean.test 的 setInjection 方法被调用了
injection{injectionAttr='null'}
injection{injectionAttr='123'}
1.1.1 简单类型注入
前面使用 property 的 ref 属性注入引用类型对象,这次再看看其他类型对象如何注入,首先是简单类型,到底在 Spring Bean 里哪些是简单类型是由 org.springframework.beans.BeanUtils.isSimpleValueType) 静态方法进行判断,它认为下面都是是简单类型:
- Primitive Or Wrapper
- java.lang.String
- java.lang.Enum
- java.lang.CharSequence
- java.lang.Number
- java.util.Date
- java.time.temporal.Temporal
- java.time.ZoneId
- java.util.TimeZone
- java.io.File
- java.nio.file.Path
- java.nio.charset.Charset
- java.util.Currency
- java.net.InetAddress
- java.net.URI
- java.net.URL
- java.util.UUID
- java.util.Locale
- java.util.regex.Pattern
- java.lang.Class
怎么注入?这是通过property 的 value 属性进行注入简单类型的对象。
先创建一个测试类 simpleTypeBean.java,里面就放 setter 和 toString 方法,setter 一会儿 Spring 赋值用,toString 我们输出看看有没赋值成功用。
package com.raingray.spring.bean;
public class simpleTypeBean {
private String strings;
private int ints;
private Double doubles;
public void setStrings(String strings) {
this.strings = strings;
}
public void setInts(int ints) {
this.ints = ints;
}
public void setDoubles(Double doubles) {
this.doubles = doubles;
}
@Override
public String toString() {
return "simpleTypeBean{" +
"strings='" + strings + '\'' +
", ints=" + ints +
", doubles=" + doubles +
'}';
}
}
编写配置文件 spring-di-simpleType.xml。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="simpleTypeBean1" class="com.raingray.spring.bean.simpleTypeBean">
<!-- 简单类型写法一 -->
<property name="strings" value="123124asdasd"/>
<property name="ints" value="123124"/>
<property name="doubles" value="123.0123123123"/>
</bean>
<bean id="simpleTypeBean2" class="com.raingray.spring.bean.simpleTypeBean">
<!-- 简单类型写法二 -->
<property name="strings">
<value>123124asdasd"</value>
</property>
<property name="ints">
<value>1231234</value>
</property>
<property name="doubles">
<value>1234124124.1231231203</value>
</property>
</bean>
</beans>
这里我写了两个 bean,首先看第一个 simpleTypeBean1,给属性赋值的时候使用 value 属性,这也是最推荐的写法,第二个 simpleTypeBean2,这是另一种写法,通过 property 子标签 value 赋值。
编写测试代码。
@Test
void testDISimpleType() {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-di-simpleType.xml");
simpleTypeBean otherObj1 = classPathXmlApplicationContext.getBean("simpleTypeBean1", simpleTypeBean.class);
System.out.println(otherObj1);
simpleTypeBean otherObj2 = classPathXmlApplicationContext.getBean("simpleTypeBean2", simpleTypeBean.class);
System.out.println(otherObj2);
}
运行输出,从结果可以看到两种方式都能成功赋值。
simpleTypeBean{strings='123124asdasd', ints=123124, doubles=123.0123123123}
simpleTypeBean{strings='123124asdasd"', ints=1231234, doubles=1.2341241241231232E9}
1.1.2 数组/集合注入
先创建 mapTypeBean.java。
package com.raingray.spring.bean;
import java.util.*;
public class mapTypeBean {
private Integer[] ints;
private simpleTypeBean[] simpleTypes;
private List<String> doubles;
private Set<String> sets;
private Map<simpleTypeBean, other> refMaps;
private Properties properties;
private Map<String, Integer> simpleMaps;
public void setInts(Integer[] ints) {
this.ints = ints;
}
public void setSimpleTypes(simpleTypeBean[] simpleTypes) {
this.simpleTypes = simpleTypes;
}
public void setDoubles(List<String> doubles) {
this.doubles = doubles;
}
public void setSets(Set<String> sets) {
this.sets = sets;
}
public void setRefMaps(Map<simpleTypeBean, other> refMaps) {
this.refMaps = refMaps;
}
public void setSimpleMaps(Map<String, Integer> simpleMaps) {
this.simpleMaps = simpleMaps;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
@Override
public String toString() {
return "mapTypeBean{" +
"ints=" + Arrays.toString(ints) +
", simpleTypes=" + Arrays.toString(simpleTypes) +
", doubles=" + doubles +
", sets=" + sets +
", refMaps=" + refMaps +
", properties=" + properties +
", simpleMaps=" + simpleMaps +
'}';
}
}
把其他引用也创建出来。
simpeTypeBean.java
package com.raingray.spring.bean;
public class simpleTypeBean {
private String strings;
private int ints;
private Double doubles;
public void setStrings(String strings) {
this.strings = strings;
}
public void setInts(int ints) {
this.ints = ints;
}
public void setDoubles(Double doubles) {
this.doubles = doubles;
}
@Override
public String toString() {
return "simpleTypeBean{" +
"strings='" + strings + '\'' +
", ints=" + ints +
", doubles=" + doubles +
'}';
}
}
other.java
package com.raingray.spring.bean;
import java.util.List;
public class other {
private String test;
private List<String> arr;
public other(String test, List arr) {
this.test = test;
this.arr = arr;
System.out.println("other(String test, ArrayList arr) 构造方法被调用");
}
public void setArr(List arr) {
this.arr = arr;
}
public other() {
System.out.println("other 无参构造方法被调用");
}
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
@Override
public String toString() {
return "other{" +
"test='" + test + '\'' +
", arr=" + arr +
'}';
}
}
创建 spring-di-mapType.xml。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="mapTypeBean" class="com.raingray.spring.bean.mapTypeBean">
<!-- 简单类型数组 -->
<property name="ints">
<array>
<value>1</value>
<value>2</value>
<value>3</value>
</array>
</property>
<!-- 引用类型数组 -->
<property name="simpleTypes">
<array>
<ref bean="simpleTypeBean"/>
<ref bean="simpleTypeBean"/>
<ref bean="simpleTypeBean"/>
</array>
</property>
<!-- List 集合 -->
<property name="doubles">
<list>
<value>112.0</value>
<value>113</value>
<value>3.123.11112</value>
</list>
</property>
<!-- Set 集合 -->
<property name="sets">
<set>
<value>1231abc</value>
<value>1231abc</value>
<value>abaszc</value>
</set>
</property>
<!-- Map 集合,引用类型 -->
<property name="refMaps">
<map>
<entry key-ref="simpleTypeBean" value-ref="otherBean"/>
<entry key-ref="simpleTypeBean2" value-ref="otherBean"/>
</map>
</property>
<!-- Map 集合,简单类型 -->
<property name="simpleMaps">
<map>
<entry key="key1" value="199117"/>
<entry key="key2" value="199481"/>
</map>
</property>
<!-- Map 集合,简单类型-->
<property name="properties">
<props>
<prop key="keyName1">keyValue1</prop>
<prop key="keyName2">keyValue2</prop>
<prop key="keyName3">keyValue3</prop>
</props>
</property>
</bean>
<bean id="simpleTypeBean" class="com.raingray.spring.bean.simpleTypeBean">
<property name="doubles" value="1212.111"/>
<property name="ints" value="12312313"/>
<property name="strings" value="ajflsdjfzlsdf啊啊啥的"/>
</bean>
<bean id="simpleTypeBean2" class="com.raingray.spring.bean.simpleTypeBean">
<property name="doubles" value="1212.111"/>
<property name="ints" value="12312313"/>
<property name="strings" value="ajflsdjfzlsdf啊啊啥的"/>
</bean>
<bean id="otherBean" class="com.raingray.spring.bean.other">
<property name="test" value="1212.111"/>
<property name="arr">
<list>
<value>1111str</value>
<value>zsdztr</value>
</list>
</property>
</bean>
</beans>
这里我们一次性注入了最常用到的 4 种数据类型:
- 简单类型数组
引用类型数组 - List 集合
- Set 集合
- 引用类型 Map 集合
简单类型 Map 集合
简单来说,简单类型用 value 标签,引用类型用 ref 标签。
比如注入数组要用到 array 子标签,值是基本类型使用 value 标签来指定,值是引用类型则是 ref 标签,list 和 set 都是一样的。
Map 的话稍有不同,要用 entry 子标签,基本类型用 key 指定键,value 指定值,引用类型则在属性后面添加 -ref
即可。
最后 properties 注入时要用子标签 props,值要用 prop 写。需要注意 Properties 也是 Map 集合,因为父类是 Hashtable,它实现了 Map,只是 Properties 只能存 String,。
运行测试程序。
@Test
void testMapTypeBean() {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-di-mapType.xml");
mapTypeBean mapTypeBeanObj = classPathXmlApplicationContext.getBean("mapTypeBean", mapTypeBean.class);
System.out.println(mapTypeBeanObj);
}
输出日志。
mapTypeBean{
ints=[
1,
2,
3
],
simpleTypes=[
simpleTypeBean{strings='ajflsdjfzlsdf啊啊啥的', ints=12312313, doubles=1212.111},
simpleTypeBean{strings='ajflsdjfzlsdf啊啊啥的', ints=12312313, doubles=1212.111},
simpleTypeBean{strings='ajflsdjfzlsdf啊啊啥的', ints=12312313, doubles=1212.111}
],
doubles=[112.0,
113,
3.123.11112
],
sets=[
1231abc,
abaszc
],
refMaps={
simpleTypeBean{strings='ajflsdjfzlsdf啊啊啥的', ints=12312313, doubles=1212.111}=other{test='1212.111', arr=[1111str, zsdztr]},
simpleTypeBean{strings='ajflsdjfzlsdf啊啊啥的', ints=12312313, doubles=1212.111}=other{test='1212.111', arr=[1111str, zsdztr]}
},
properties={
keyName1=keyValue1,
keyName2=keyValue2,
keyName3=keyValue3
},
simpleMaps={
key1=199117,
key2=199481
}
}
1.1.3 NULL 和空字符串注入
手动赋值为空字符串。
<property name="yyy">
<!-- 方式一,使用 value 单标签 -->
<array>
<value/>
</array>
</property>
<property name="yyy">
<!-- 方式二,使用 value 完整标签 -->
<array>
<value><value/>
</array>
</property>
<!-- 方式三,使用 value 属性赋空值 -->
<property name="yyy" value=""/>
注入 NULL 可以手动使用标签,或者不为这个属性赋值也是 NULL。
<property name="yyy">
<!-- 方式一,使用 null 单标签 -->
<array>
<null/>
</array>
</property>
<property name="yyy">
<!-- 方式二,使用 null 完整标签 -->
<array>
<null/>
</array>
</property>
参数使用了特殊字符 XML 解析器会出现语法错误。第一个方案是转换成 HTML 实体名称,第二个方案是使用 CDATA 标签不解析指定内容 <![CDATA[这里的字符不被解析]]>
。
1.1.4 p 命令空间注入
前面注入我们需要配置一大堆子标签,p 命令空间的写法可以简化配置,使用前需要先在 beans 添加下面属性。
xmlns:p="http://www.springframework.org/schema/p"
先创建 nameSpaceBean.java
package com.raingray.spring.bean;
public class nameSpaceBean {
private String spaceName;
private other other;
public void setSpaceName(String spaceName) {
this.spaceName = spaceName;
}
public void setOther(com.raingray.spring.bean.other other) {
this.other = other;
}
@Override
public String toString() {
return "nameSpaceBean{" +
"spaceName='" + spaceName + '\'' +
", other=" + other +
'}';
}
}
原先的写法是 property 子标签来表示具体值。
<!-- 原来的配置方式 -->
<bean id="nameSPaceBean" class="com.raingray.spring.bean.nameSpaceBean">
<property name="spaceName" value="this is name"/>
<property name="other" ref="other"/>
</bean>
<bean id="other" class="com.raingray.spring.bean.other">
<property name="arr">
<list>
<value>a</value>
<value>b</value>
<value>c</value>
</list>
</property>
<property name="test" value="just string"/>
</bean>
换成 p 命令空间写法,是在 bean 标签内用 p:
开头后面跟上属性名称,直接等号赋值,如果是要注入其他 bean,属性名后面要加上 -ref
,值填写对应 bean 的 id。
<bean id="other" class="com.raingray.spring.bean.other">
<property name="arr">
<list>
<value>a</value>
<value>b</value>
<value>c</value>
</list>
</property>
<property name="test" value="just string"/>
</bean>
<!-- 使用 p 命令空间注入-->
<bean id="nameSpaceBean2" class="com.raingray.spring.bean.nameSpaceBean"
p:spaceName="name" p:other-ref="other"/>
1.1.5 自动装配
自动装配就是自动进行注入,只是官方叫 Autowiring。
先创建个 bean,后面用来测试自动装配。
autowiri1.java
package com.raingray.spring.bean;
public class autowiri1 {
private simpleTypeBean name;
public autowiri1() {
System.out.println("autowiri1 无参构造方法被执行");
}
public void setName(simpleTypeBean name) {
System.out.println("setName(simpleTypeBean name) 方法被执行");
this.name = name;
}
@Override
public String toString() {
return "autowiri1{" +
"name=" + name +
'}';
}
}
1.根据名称注入
以前对某个属性注入一个对象需要用到 ref 属性,用来指定 bean 的 id。
<bean id="simpleTypeBean" class="com.raingray.spring.bean.simpleTypeBean">
<property name="strings" value="stinabc"/>
<property name="ints" value="123456"/>
<property name="doubles" value="1.111"/>
</bean>
<bean id="manualBean" class="com.raingray.spring.bean.autowiri1">
<property name="name" ref="simpleTypeBean"/>
</bean>
现如今 bean 标签只需要开启 autowire="byName"
,就能自动根据对象的属性名注入。原理是 byName 会自动找到 autowiri1 对象里所有属性名,在当前 Spring 配置文件里跟所有 bean 标签的 id 进行匹配,一旦匹配成功就调 autowiri1 对象 setter 方法传入 bean 对象进行赋值。
<!--基于名称自动 setter 注入-->
<bean id="name" class="com.raingray.spring.bean.simpleTypeBean">
<property name="strings" value="stinabc"/>
<property name="ints" value="123456"/>
<property name="doubles" value="1.111"/>
</bean>
<bean id="autowiriBean" class="com.raingray.spring.bean.autowiri1" autowire="byName"/>
详细点来说,autowiri1 只有一个属性 name,类型是 simpleTypeBean,启动自动装配后就会遍历当前 Spring 配置文件中所有 bean 标签,去比较它的 id,发现有一个是 name,就会调用 autowiri1.setName 方法,把 com.raingray.spring.bean.simpleTypeBean 对象传入完成赋值操作。
如果 <bean ...>
标签类型一个都匹配不上,或者是匹配上了但 autowiri1 没有对应 setter 方法,那 name 属性就是 null。
运行测试程序。
@Test
void testAutowiring() {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-autowiring.xml");
autowiri1 beanObj2 = classPathXmlApplicationContext.getBean("autowiriBean", autowiri1.class);
System.out.println(beanObj2);
}
日志输出
autowiri1 无参构造方法被执行
setName(simpleTypeBean name) 方法被执行
autowiri1{name=simpleTypeBean{strings='stinabc', ints=123456, doubles=1.111}}
2.根据类型注入
下面 id 为 autowiriBean 的 bean 开启了基于类型的自动装配。原理是先获取到 com.raingray.spring.bean.autowiri1 里面所有属性和对应类型,挨个取出属性和对应的类型,扫描当前配置文件中所有 bean 的 class 进行匹配,一旦匹配上,就调对应 setter 方法进行注入。
<!--基于类型自动 setter 注入-->
<bean class="com.raingray.spring.bean.simpleTypeBean">
<property name="strings" value="stinabc"/>
<property name="ints" value="778899"/>
<property name="doubles" value="2.2222"/>
</bean>
<bean id="autowiriBean" class="com.raingray.spring.bean.autowiri1" autowire="byType"/>
根据当前配置来讲,com.raingray.spring.bean.autowiri1 里只有一个属性 private simpleTypeBean name;
,那就拿着 name 属性对应的类型 simpleTypeBean 在当前配置文件里注册的 bean 中找,找到后调用 simpleTypeBean.setName 方法把 bena 对象传入赋值。
运行测试程序。
@Test
void testAutowiring() {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-autowiring.xml");
autowiri1 beanObj2 = classPathXmlApplicationContext.getBean("autowiriBean", autowiri1.class);
System.out.println(beanObj2);
}
测试代码输出。
autowiri1 无参构造方法被执行
setName(simpleTypeBean name) 方法被执行
autowiri1{name=simpleTypeBean{strings='stinabc', ints=778899, doubles=2.2222}}
根据类型匹配,如果匹配到了多个相同类型的 bean 会抛异常,这要求 beans 里面只能注册一个相同类型的 bena。
<bean class="com.raingray.spring.bean.simpleTypeBean">
<property name="strings" value="stinabc"/>
<property name="ints" value="123456"/>
<property name="doubles" value="1.111"/>
</bean>
<bean id="testBean" class="com.raingray.spring.bean.simpleTypeBean">
<property name="strings" value="stinabc"/>
<property name="ints" value="123456"/>
<property name="doubles" value="1.111"/>
</bean>
<bean id="autowiriBean" class="com.raingray.spring.bean.autowiri1" autowire="byType"/>
匹配到多个 bean 对象异常信息。
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'autowiriBean' defined in class path resource [spring-autowiring.xml]: Unsatisfied dependency expressed through bean property 'name': No qualifying bean of type 'com.raingray.spring.bean.simpleTypeBean' available: expected single matching bean but found 2: com.raingray.spring.bean.simpleTypeBean#0,testBean
1.2 XML 方式的配置-构造方法注入
这回不调 setter 方法注入对象,可以直接调构造方法注入。
创建 test.java 这个 POJO 类来测试构造方法注入。
package com.raingray.spring.bean;
import java.util.ArrayList;
public class other {
private String test;
private ArrayList arr;
public other(String test, ArrayList arr) {
this.test = test;
this.arr = arr;
System.out.println("other(String test, ArrayList arr) 构造方法被调用");
}
public other() {
System.out.println("other 无参构造方法被调用");
}
@Override
public String toString() {
return "other{" +
"test='" + test + '\'' +
", arr=" + arr +
'}';
}
}
构造参数注入使用 constructor-arg
标签,index 属性是按照构造参数位置注入,这里 0 是 String test,1 是 ArrayList arr。至于先写 0 还是先写 1,这个顺序不重要。
<bean id="charBean" class="java.lang.String"/>
<bean id="arrayBean" class="java.util.ArrayList"/>
<bean id="constructorInjection" class="com.raingray.spring.bean.other">
<constructor-arg index="1" ref="arrayBean" />
<constructor-arg index="0" ref="charBean" />
</bean>
也可以根据构造参数名称来注入,一定要按照形参定义的顺序写,不然无法传递到指定位置报异常。
<bean id="charBean" class="java.lang.String"/>
<bean id="arrayBean" class="java.util.ArrayList"/>
<bean id="constructorInjection" class="com.raingray.spring.bean.other">
<constructor-arg name="test" ref="charBean" />
<constructor-arg name="arr" ref="arrayBean" />
</bean>
运行测试。
@Test
void testConstructorInjection() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-di.xml");
other testBean = context.getBean("constructorInjection", other.class);
System.out.println(testBean);
}
运行日志证明了是调用指定参数构造方法来创建的对象。
other(String test, ArrayList arr) 构造方法被调用
other{test='', arr=[]}
1.2.1 c 命名空间注入
创建 bean,cNameSpaceBean.java。
package com.raingray.spring.bean;
public class cNameSpaceBean {
private String name;
private other otherObj;
public cNameSpaceBean() {
System.out.println("cNameSpaceBean() 无参构造方法被执行");
}
public cNameSpaceBean(String name, other o) {
System.out.println("cNameSpaceBean(String name, other o) 有参构造方法被执行");
this.name = name;
this.otherObj = o;
}
@Override
public String toString() {
return "cNameSpaceBean{" +
"name='" + name + '\'' +
", otherObj=" + otherObj +
'}';
}
}
编写配置文件 spring-di-cTag.xml。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
xmlns:c="http://www.springframework.org/schema/c">
<bean id="other" class="com.raingray.spring.bean.other">
<property name="arr">
<list>
<value>a</value>
<value>b</value>
<value>c</value>
</list>
</property>
<property name="test" value="just string"/>
</bean>
<!-- 使用 c 命令空间注入,下标方式-->
<bean id="cNameSpaceBean1" class="com.raingray.spring.bean.cNameSpaceBean"
c:_0="this is name"
c:_1-ref="other"/>
<!-- 使用 c 命令空间注入,命名方式-->
<bean id="cNameSpaceBean2" class="com.raingray.spring.bean.cNameSpaceBean"
c:name="this is name"
c:o-ref="other"/>
</beans>
要使用 c 命名空间需要在 beans 标签添加属性。
xmlns:c="http://www.springframework.org/schema/c"
注入时使用 c 命令空间注入,c:_
后面可以跟参数下标,从 0 开始为第一个参数,要是引用类型还可以在参数下标或者名称后面添加 -ref
后缀,值填 bean 的 id。也可以 c:
跟参数名称,通过具体参数名来注入。
<--下标注入-->
<bean id="..." class="...." c:name="this is name" c:o-ref="other"/>
<!--参数名称-->
<bean id="..." class="...." c:="this is name" c:_1-ref="other"/>
运行测试参数。
@Test
void testCNameSpaceDI() {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-di-cTag.xml");
cNameSpaceBean beanObj1 = classPathXmlApplicationContext.getBean("cNameSpaceBean1", cNameSpaceBean.class);
System.out.println(beanObj1);
cNameSpaceBean beanObj2 = classPathXmlApplicationContext.getBean("cNameSpaceBean2", cNameSpaceBean.class);
System.out.println(beanObj2);
}
两种方式都使用有参构造方法注入成功。
cNameSpaceBean(String name, other o) 有参构造方法被执行
cNameSpaceBean(String name, other o) 有参构造方法被执行
cNameSpaceBean{name='this is name', otherObj=other{test='just string', arr=[a, b, c]}}
cNameSpaceBean{name='this is name', otherObj=other{test='just string', arr=[a, b, c]}}
1.2.2 util 命名空间复用配置🔨
beans 标签添加属性
xmlns:util="http://www.springframework.org/schema/util"
对 xsi:schemaLocation 属性新添加标签约束链接
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
变成
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
后面通过 util 标签来创建可以复用的内容,定义完成使用 id 作为表示引用。
<util:properties id="唯一标识">
</util:properties>
<property name="" ref="唯一标识">
1.3 context 命名空间加载配置文件
1.启用 context 命名空间
对 beans 标签添加属性。
xmlns:context="http://www.springframework.org/schema/context"
对 <beans>
标签 xsi:schemaLocation
属性添加值。
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
2.加载配置文件
先创建一个配置文件 test.properties。
test.key=keyName -> value
在 beans 标签内使用 context 标签加载文件,默认是从类根路径开始。
<context:property-placeholder location="test.properties"/>
3.读取配置文件值
通过 ${key-name}
读取 key-name 的值。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns: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
http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="test.properties"/>
<bean id="stringBean" class="java.lang.String">
<!-- 读取 test.key 的值 -->
<constructor-arg name="original" value="${test.key}"/>
</bean>
</beans>
运行测试程序。
@Test
void testContextTag() {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-contextTag.xml");
String beanObj = classPathXmlApplicationContext.getBean("stringBean", java.lang.String.class);
System.out.println(beanObj);
}
成功读取到 test.key 的值。
keyName -> value
这个 key 的值不要和环境变量名冲突,如果一样就优先读环境变量的值。
1.4 Bean 作用域
Bean 作用域是说 Bean 在什么是时候创建出来,目前阶段我们只有 singleton 和 prototype 两种,往后学到 SpringMVC 还会有 request、session、application、websocket 这几种。
先创建个 bean 用来测试。beanScoop.java
package com.raingray.spring.bean;
public class beanScoop {
private String scoopName;
public beanScoop() {
System.out.println("beanScoop() 无数构造方法被执行");
}
}
1.singleton
在 IoC 容器创建后就创建这个 Bean 对象。用法也很简单,只需要在 bean 标签 scoop 属性添加对应值就行,不写 scoop 属性 singleton 也是默认值。
<bean id="benaScoop" class="com.raingray.spring.bean.beanScoop" scope="singleton"/>
运行测试代码。
@Test
void testScoopAttr() {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
}
发现确实是 IoC 容器创建后就自动创建的 Bean 对象。
beanScoop() 无数构造方法被执行
而且反复通过 getBean 获取结果为同一个对象,并不是每次获取都重新创建。
beanScoop beanObj = classPathXmlApplicationContext.getBean("benaScoop", beanScoop.class);
System.out.println(beanObj);
beanScoop beanObj1 = classPathXmlApplicationContext.getBean("benaScoop", beanScoop.class);
System.out.println(beanObj1);
运行输出。
beanScoop() 无数构造方法被执行
com.raingray.spring.bean.beanScoop@618c5d94
com.raingray.spring.bean.beanScoop@618c5d94
2.prototype
prototype 则是在调用 getBean 方法时再创建这个 Bean 对象。
<bean id="benaScoop" class="com.raingray.spring.bean.beanScoop" scope="prototype"/>
此时运行不会创建对象。
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
只有调用 getBean 方法时创建新对象,而且是每次调用都创建新的。
beanScoop beanObj = classPathXmlApplicationContext.getBean("benaScoop", beanScoop.class);
System.out.println(beanObj);
beanScoop beanObj1 = classPathXmlApplicationContext.getBean("benaScoop", beanScoop.class);
System.out.println(beanObj1);
运行输出
beanScoop() 无数构造方法被执行
com.raingray.spring.bean.beanScoop@160ac7fb
beanScoop() 无数构造方法被执行
com.raingray.spring.bean.beanScoop@12bfd80d
1.5 工厂模式创建 Bean 对象
由于要使用工厂模式,这属于设计模式创建型中的一种,站在使用者的角度看,只需要给出要名称,自动就得到对象。
1.简单工厂(又叫静态工厂)
这里就以一个兵工厂为例,让它帮我们创建出枪支,这个枪要求能击发即可。
这里涉及三个角色:
- 枪模。每个枪支需要设计成模具
- 枪。根据枪模原型完成具体枪支形状,相当做出各个部位的零件,甚至还可以根据实战要求增加或改良枪械
- 兵工厂。由工厂组装出最终能用的枪支。
先创建出枪的模板 Gun.java,由于还不知道要造什么枪,这里仅把枪的通用功能做出来,这里是开枪。
package com.raingray.spring.bean;
public abstract class Gun {
public Gun() {
System.out.println("Gun 产品类无参构造方法被执行");
}
public abstract void shot();
}
再根据枪模板去细化我到底要做什么枪,这里我做一把步枪 M14 和手枪 M1911。
M14.java
package com.raingray.spring.bean;
public class M14 extends Gun {
public M14() {
System.out.println("M14 无参构造方法被执行");
}
@Override
public void shot() {
System.out.println("M14 开枪");
}
public void inspectionArms() {
System.out.println("M14 验枪完毕,没有任何问题");
}
}
M1911.java
package com.raingray.spring.bean;
public class M1911 extends Gun {
public M1911() {
System.out.println("M1911 无参构造方法被执行");
}
@Override
public void shot() {
System.out.println("M1911 开枪");
}
}
最后通过兵工厂 MilitaryFactory.java 组装出我们真正能用的枪。
package com.raingray.spring.bean;
public class MilitaryFactory {
public MilitaryFactory() {
System.out.println("兵工厂无参构造方法被执行");
}
public static Gun getGun(String gunName) {
System.out.println("兵工厂造枪方法被执行");
return switch (gunName) {
case "M14" -> new M14();
case "M1911" -> new M1911();
default -> throw new RuntimeException("名称错误");
};
}
}
这里就创建 testStaticFactoryPattern 方法,模拟用户从兵工厂取枪和实弹射击。
@Test
public void testStaticFactoryPattern() {
Gun m1911 = MilitaryFactory.getGun("M1911");
m1911.shot();
System.out.println();
Gun gun = MilitaryFactory.getGun("M14");
if (gun instanceof M14) {
M14 m14 = (M14) gun;
m14.shot();
m14.inspectionArms();
}
}
从运行结果来看,我们只需要向兵工厂传递我们需要什么枪,工厂自动帮我们创建出所有枪的对象,后续就拿着枪用即可,如果要用到枪独有的内容需要向下转型,这里是验证功能。
兵工厂造枪方法被执行
Gun 产品类无参构造方法被执行
M1911 无参构造方法被执行
M1911 开枪
兵工厂造枪方法被执行
Gun 产品类无参构造方法被执行
M14 无参构造方法被执行
M14 开枪
M14 验枪完毕,没有任何问题
简单工厂有个缺点,往后要是还需添加其他枪呢?必须得改动兵工厂的代码,不符合 OCP 原则。
2.工厂方法
还是兵工厂的例子,简单工厂是一个兵工厂里创建出所有枪的对象,为了规避工厂坏了就无法创建枪的风险,这次用工厂方法模式优化它。工厂方法比简单工厂好在哪?,工厂方法是每个工厂它自身只专门生产一个对象,这就不会在新增的时候违背 OCP,也不会因为工厂坏了导致所有生产中断掉,但是工厂方法也有缺点,一旦要新增的东西过多,会导致类不断增加,因为增加一个对象就要配套新建一个工厂进行生产。
这里涉及四个角色:
- 枪模。每个枪支需要设计成模具
- 枪。根据枪模原型完成具体枪支形状,相当做出各个部位的零件,甚至还可以根据实战要求增加或改良枪械
- 兵工厂模板。所有工厂的建造模板,都按这个模子来建。
- 兵工厂。由工厂组装出最终能用的枪支。
枪模 Gun.java
package com.raingray.spring.bean;
public abstract class Gun {
public Gun() {
System.out.println("Gun 产品类无参构造方法被执行");
}
public abstract void shot();
}
枪 M14.java
package com.raingray.spring.bean;
public class M14 extends Gun {
public M14() {
System.out.println("M14 无参构造方法被执行");
}
@Override
public void shot() {
System.out.println("M14 开枪");
}
public void inspectionArms() {
System.out.println("M14 验枪完毕,没有任何问题");
}
}
枪 M1911.java
package com.raingray.spring.bean;
public class M1911 extends Gun {
public M1911() {
System.out.println("M1911 无参构造方法被执行");
}
@Override
public void shot() {
System.out.println("M1911 开枪");
}
}
兵工厂模板。
package com.raingray.spring.bean;
public abstract class FactoryTemplate {
public FactoryTemplate() {
System.out.println("兵工厂模板 FactoryTemplate 无参构造方法被执行");
}
public abstract Gun getGun();
}
再从兵工厂模板建造各个枪独立的兵工厂。
M14Factory.java
package com.raingray.spring.bean;
public class M14Factory extends FactoryTemplate {
public M14Factory() {
System.out.println("M14Factory 兵工厂无参构造方法被执行");
}
@Override
public M14 getGun() {
System.out.println("M14Factory 兵工厂造枪方法被执行");
return new M14();
}
}
M1911Factory.java
package com.raingray.spring.bean;
public class M1911Factory extends FactoryTemplate {
public M1911Factory() {
System.out.println("M1911Factory 兵工厂无参构造方法被执行");
}
@Override
public M1911 getGun() {
System.out.println("M1911Factory 兵工厂造枪方法被执行");
return new M1911();
}
}
用户从各个枪单独的兵工厂创建出对象,然后使用功能都正常。
@Test
public void testFactoryMethodPattern() {
M1911 m1911 = new M1911Factory().getGun();
m1911.shot();
System.out.println();
M14 m14 = new M14Factory().getGun();
m14.shot();
m14.inspectionArms();
}
运行输出。
兵工厂模板 FactoryTemplate 无参构造方法被执行
M1911Factory 兵工厂无参构造方法被执行
M1911Factory 兵工厂造枪方法被执行
Gun 产品类无参构造方法被执行
M1911 无参构造方法被执行
M1911 开枪
兵工厂模板 FactoryTemplate 无参构造方法被执行
M14Factory 兵工厂无参构造方法被执行
M14Factory 兵工厂造枪方法被执行
Gun 产品类无参构造方法被执行
M14 无参构造方法被执行
M14 开枪
M14 验枪完毕,没有任何问题
1.5.1 简单工厂模式(Simple Factory Pattern)
创建产品模板 Parent.java。
package com.raingray.spring.bean;
public abstract class Parent {
public Parent() {
System.out.println("Parent 无参构造方法被执行");
}
public abstract void say();
}
创建产品 A.java。
package com.raingray.spring.bean;
public class A extends Parent {
public A() {
System.out.println("A 无参构造方法被执行");
}
@Override
public void say() {
System.out.println("A Say ......");
}
}
创建产品 B.java。
package com.raingray.spring.bean;
public class B extends Parent {
public B() {
System.out.println("B 无参构造方法被执行");
}
@Override
public void say() {
System.out.println("B Say ......");
}
}
创建工厂 SimpleFactory.java。
package com.raingray.spring.bean;
public class SimpleFactory {
public SimpleFactory() {
System.out.println("SimpleFactory 无参构造方法被执行");
}
public static Object get(String name) {
System.out.println("SimpleFactory 工厂静态方法被执行,正在创建对象中");
if(name.equals("a")) {
return new A();
} else if(name.equals("b")) {
return new B();
} else {
throw new RuntimeException("name error please check");
}
}
}
注册 Bean 通过 class 来指定静态工厂是那个类,用 factory-method 指定工厂类静态方法是哪个,这里会调 SimpleFactory.get,如果这个 get 静态方法要传参,可以用 constructor-arg 标签来传。这里通过简单工厂创建了两个 A 和 B 对象。
<bean id="simpleFactoryBean" class="com.raingray.spring.bean.SimpleFactory" factory-method="get" >
<!--只有一个参数时可以省略 name-->
<constructor-arg value="a"/>
<!-- 也可以写参数名称 -->
<!-- <constructor-arg name="gunName" value="a"/> -->
</bean>
<bean id="simpleFactoryBean1" class="com.raingray.spring.bean.SimpleFactory" factory-method="get" >
<constructor-arg value="b"/>
</bean>
运行测试方法。
@Test
void testSpringFactoryBeanCreate() {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-simpleFactory.xml");
A beanObj = classPathXmlApplicationContext.getBean("simpleFactoryBean", A.class);
beanObj.say();
System.out.println();
B beanObj1 = classPathXmlApplicationContext.getBean("simpleFactoryBean1", B.class);
beanObj1.say();
}
输出日志。从结果来看,Spring 创建完 IoC 容器后就自动调工厂静态方法创建好对象,后续只需要用 getBean 获取对象使用就好。
SimpleFactory 工厂静态方法被执行,正在创建对象中
Parent 无参构造方法被执行
A 无参构造方法被执行
SimpleFactory 工厂静态方法被执行,正在创建对象中
Parent 无参构造方法被执行
B 无参构造方法被执行
A Say ......
B Say ......
1.5.2 工厂方法模式(Factory Method Pattern)
产品类 M14Factory.java
package com.raingray.spring.bean;
public class M14Factory extends FactoryTemplate {
public M14Factory() {
System.out.println("M14Factory 兵工厂无参构造方法被执行");
}
@Override
public M14 getGun() {
System.out.println("M14Factory 兵工厂造枪方法被执行");
return new M14();
}
}
产品对应的工厂类 M1911Factory.java
package com.raingray.spring.bean;
public class M1911Factory extends FactoryTemplate {
public M1911Factory() {
System.out.println("M1911Factory 兵工厂无参构造方法被执行");
}
@Override
public M1911 getGun() {
System.out.println("M1911Factory 兵工厂造枪方法被执行");
return new M1911();
}
}
配置 Bean。先创建出工厂对象 M1911Factory,再通过 factory-bean 属性确认调哪个工厂对象,factory-method 属性用来确认调工厂对象哪个的实例方法。
<!-- 工厂方法模式 -->
<!-- 先创建出工厂对象 -->
<bean id="m1911FactoryBean" class="com.raingray.spring.bean.M1911Factory"/>
<!--通过工厂对象 M1911Factory 调用对应实例方法 getGun 获取 M1911 对象-->
<bean id="factoryMethod" factory-bean="m1911FactoryBean" factory-method="getGun"/>
运行测试程序。
@Test
void testSpringFactoryMethod() {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-FactoryMethod.xml");
M1911 beanObj = classPathXmlApplicationContext.getBean("factoryMethod", M1911.class);
System.out.println(beanObj);
beanObj.shot();
}
输出日志。
兵工厂模板 FactoryTemplate 无参构造方法被执行
M1911Factory 兵工厂无参构造方法被执行
M1911Factory 兵工厂造枪方法被执行
Gun 产品类无参构造方法被执行
M1911 无参构造方法被执行
com.raingray.spring.bean.M1911@56c9bbd8
M1911 开枪
1.5.3 FactoryBean 创建实例
FactoryBean 接口可以帮助 Spring 容器创建 Bean。很像上面 1.5.2 工厂方法模式。
这个 FactoryBean 接口有三个方法,getObject() 是用来创建这个 Bean 对象的,后面通过 BeanFactory 接口的 getBean 方法会调用到它,getObjectType 是返回这个 Bean 的类型,通过 BeanFactory 接口的 getType() 方法获取,isSingleton() 默认方法不用实现,它用于控制这个 Bean 的 Scoop,如果为 false 是多例每次 getBean 创建新对象,为 true 是单例只有第一次获取是创建新对象,后面重复获取则仍然使用第一次创建的对象。
package org.springframework.beans.factory;
import org.springframework.lang.Nullable;
public interface FactoryBean<T> {
String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
@Nullable
T getObject() throws Exception;
@Nullable
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}
比如一个实现了 FactoryBean 接口的类 testFactoryBean.java,它可以在里面的 getObject 方法中创建 Book 对象并返回,在创建的过程中可以做各种复杂的处理,比如创建完 Book 对象后在某个条件下对 Book 对象中某个属性进行修改,或者是创建 Book 对象之间做其他处理。
创建 Book.java 这个 Bean。
package com.raingray.spring.bean;
public class Book {
public Book() {
System.out.println("Book constructor");
}
}
创建 BookFactoryBean.java 这个工厂 Bean。
package com.raingray.spring.bean;
import org.springframework.beans.factory.FactoryBean;
public class BookFactoryBean implements FactoryBean<Book> {
public BookFactoryBean() {
System.out.println("BookFactoryBean 无参构造方法被执行");
}
@Override
public Book getObject() throws Exception {
System.out.println("BookFactoryBean 对象的 getObject() 方法被执行");
return new Book();
}
@Override
public Class<?> getObjectType() {
return Book.class;
}
@Override
public boolean isSingleton() {
return false;
}
}
注册工厂 Bean。
<bean id="bookFactoryBean" class="com.raingray.spring.bean.BookFactoryBean"/>
运行测试程序。
@Test
void testSpringFactoryBeanInterface() {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-factoryBean.xml");
System.out.println();
System.out.println(classPathXmlApplicationContext.getBean("bookFactoryBean", Book.class));
System.out.println();
System.out.println(classPathXmlApplicationContext.getBean("bookFactoryBean", Book.class));
System.out.println();
System.out.println(classPathXmlApplicationContext.getType("bookFactoryBean"));
}
运行输出结果。
BookFactoryBean 无参构造方法被执行
BookFactoryBean 对象的 getObject() 方法被执行
Book 无参构造方法被执行
com.raingray.spring.bean.Book@64dafeed
BookFactoryBean 对象的 getObject() 方法被执行
Book 无参构造方法被执行
com.raingray.spring.bean.Book@388ba540
class com.raingray.spring.bean.Book
创建完 IoC 容器后会自动创建 BookFactoryBean 工厂 Bean 对象,因此调用了无参构造方法,后面执行 getBean 会调用 BookFactoryBean.getObject 实例方法创建 Book 对象,由于 BookFactoryBean.isSingleton() 的作用域我设置为多例,所以每次调 getBean 都回创建新的 Book 对象,最后通过 getType() 获取到了 Book 的 Class 对象。
这整个流程和前面的工厂方法执行流程一样,都是先创建工厂对象,再调工厂对象的方法创建出具体对象。
1.6 Bean 生命周期
从 Bean 创建到销毁整个过程就是 Bean 的生命周期,比如在创建或者销毁 Bean 时我们可以控制执行指定的方法。
Bean 是作用域是单例情况下(scope="singleton") 整个完整生命周期如下:
1.调用对象 Bean 无参或有参构造方法创建对象
2.调用通过有参或者 setter 方法对属性赋值
3.执行 BeanPostProcessor 接口实现类对象的 postProcessBeforeInitialization) 方法进行
4.Bean 对象的 init-method 方法被执行
5.执行 BeanPostProcessor 接口实现类对象的 postProcessAfterInitialization) 方法
6.使用 Bean 对象
7.调用 ConfigurableApplicationContext 接口实现类对象 close() 方法关闭 IoC 容器后,Bean 对象的 destroy-method 方法被执行。Bean 作用域被配置成 property 多例的情况下,不会执行 destroy-method 指定的方法。
创建一个测试 Bean,BeanLifeCycle.java。
package com.raingray.spring.bean;
public class BeanLifeCycle {
private String beanName;
public BeanLifeCycle() {
System.out.println("1. BeanLifeCycle 无参构造方法被执行");
}
public BeanLifeCycle(String beanName) {
System.out.println("1. BeanLifeCycle 有参构造方法被执行");
this.beanName = beanName;
System.out.println("2. BeanLifeCycle 对象 name 属性被赋值");
}
public void setBeanName(String beanName) {
this.beanName = beanName;
System.out.println("2. BeanLifeCycle 对象 name 属性被赋值");
}
private void initMethod() {
System.out.println("4. BeanLifeCycle initMethod 方法被执行");
}
private void destroyMethod() {
System.out.println("7. BeanLifeCycle destroyMethod 方法被执行");
}
}
TestBeanPostProcessor.java。
package com.raingray.spring.bean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class TestBeanPostProcessor implements BeanPostProcessor {
public TestBeanPostProcessor() {
System.out.println("1. TestBeanPostProcessor 无参构造方法被执行");
}
/**
* @param bean 具体 bean 对象
* @param beanName ,beanName 是。
* @return
* @throws BeansException
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("3. BeanPostProcessor 接口实现类 " + beanName + " 的 postProcessBeforeInitialization 方法被执行");
return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("5. BeanPostProcessor 接口实现类 " + beanName + " 的 postProcessAfterInitialization 方法被执行");
return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}
}
注册 Bean。这里的 init-method 属性是第四步会执行的方法,destroy-method 属性是关闭 IoC 容器后要执行的方法。注册 TestBeanPostProcessor 是为了在对象赋完值后,抢在 init-method 方法执行前和执行后去运行一些内容。
<bean id="beanLifeCycle" class="com.raingray.spring.bean.BeanLifeCycle"
init-method="initMethod"
destroy-method="destroyMethod">
<!--<constructor-arg name="beanName" value="this is default name"/>-->
<property name="beanName" value="this is new name"/>
</bean>
<bean class="com.raingray.spring.bean.TestBeanPostProcessor"/>
运行测试代码。
@Test
public void testBeanLifeCycle() {
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-lifecycle.xml");
System.out.println("6. 正在使用 Bean:" + classPathXmlApplicationContext.getBean("beanLifeCycle"));
classPathXmlApplicationContext.close();
}
日志输出。
1. TestBeanPostProcessor 无参构造方法被执行
1. BeanLifeCycle 无参构造方法被执行
2. BeanLifeCycle 对象 name 属性被赋值
3. BeanPostProcessor 接口实现类 beanLifeCycle 的 postProcessBeforeInitialization 方法被执行
4. BeanLifeCycle initMethod 方法被执行
5. BeanPostProcessor 接口实现类 beanLifeCycle 的 postProcessAfterInitialization 方法被执行
6. 正在使用 Bean:com.raingray.spring.bean.BeanLifeCycle@54227100
7. BeanLifeCycle destroyMethod 方法被执行
其实不止只这些,还有一些接口也是参与生命周期的,只是这里没细讲,以后用到再扩展。
- BeanNameAware
BeanClassLoaderAware
BeanFactoryAware - InitializingBean
- DisposableBean
1.6 注解的方式配置
在一个类上添加 @Component 表示这个类普通组件,后续就可以根据类自动创建对象出来。除了 @Component 以外还有三个注解是根据三层架构来来定义的注解,本质上它们都是基于 @Component 来创建出来。
- @Service,业务类
- @Repository,数据类
- @Controller,控制器
使用这些注解要先引入依赖 spring-aop.jar 包,由于这里我引用了 spring-context.jar,它自己依赖于 spring-aop 就无需主动引入,可以通过依赖传递自己解决。
创建一个测试类 Bean.java,在类上使用注解 @Component,元素 value 是 Bean 的 ID,不写默认 ID 是类名首字母小写。这里我没有取名,因此名称为 bean。
package com.raingray;
import org.springframework.stereotype.Component;
@Component
public class Bean {
private String name;
public Bean() {
System.out.println("Bean 无参数构造方法被执行");
}
}
怎么就通过注解来自动创建对象呢?这里需要用到 context 的包扫描功能,只要类上有对应组件,那就创建对象。
要想使用需先开启 Spring 配置文件 context 功能。先在 beans 标签添加属性。
xmlns:context="http://www.springframework.org/schema/context"
对 beans 标签 xsi:schemaLocation 属性添加值,启用 context 命名空间。
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
Spring 扫描包内哪个类使用了注解就自动创建成对象放入 Map,扫描多个包可以用逗号、分号、空格、制表符、换行符隔开。
<!--扫描单个包-->
<context:component-scan base-package="com.powernode.spring6.bean"/>
<!--扫描多个包-->
<context:component-scan base-package="com.powernode.spring6.bean, com.powernode.spring6.DAO"/>
这里只扫描 com.raingray 下的 .class 上有没注解 @Component、@Service、@Repository、@Controller。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns: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
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.raingray"/>
</beans>
运行测试程序。
@Test
public void test() {
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-annotation.xml");
System.out.println(classPathXmlApplicationContext.getBean("bean"));
}
成功注册 bean。
Bean 无参数构造方法被执行
com.raingray.Bean@7cb502c
通过 XML 配置文件去指定要扫描的包还是很麻烦,注解提供不用配置文件的方式扫描。创建 ConfigurationBean.java,在类上使用 @Configuration 表示是 Spring 类是用来做 Bean 配置,@ComponentScan 指定要扫描的包名,一个包可以直接写字符串不指定 value 元素名称,多个用花括号 {"包名1", "包名2"}。
package com.raingray;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("com.raingray")
public class ConfigurationBean {
public ConfigurationBean() {
System.out.println("ConfigurationBean 无参数构造方法被执行");
}
}
创建全注解测试程序。
@Test
public void testAllAnnotationConf() {
// 方式一,手动注册和刷新
// AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
// annotationConfigApplicationContext.register(ConfigurationBean.class);
// annotationConfigApplicationContext.refresh();
// 完成注册和刷新后才能使用 bean
// System.out.println(annotationConfigApplicationContext.getBean("bean", Bean.class));
// 方式二,自动完成手动注册和刷新
AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(ConfigurationBean.class);
System.out.println(annotationConfigApplicationContext.getBean("bean", Bean.class));
}
````
成功使用 bean
ConfigurationBean 无参数构造方法被执行
Bean 无参数构造方法被执行
com.raingray.Bean@51c693d
有时候类上启用了不同的注解 @Component、@Service、@Repository、@Controller,如果只想写了指定注解 @Controller 才创建为对象怎么设置,这里依旧提供 XML 和注解两种配置方式。
1.启用白名单控制哪些注解才能创建对象
`<context:component-scan>` 标签 use-default-filter="false" 所有类上有使用注解注册对象都失效,再通过 `<context:include-filter ...>` 只有指定注解 @Controller 的类才注册对象。
<context:component-scan base-package="com.raingray" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
那能不能不写 use-default-filter="false" 呢?不写默认是 true 则会扫描 @Component、@Service、@Repository、@Controller 这四个注解,只要有就会进行注册,哪怕你在里面限制了只允许 @Controller 注册也不行,所以使用白名单时千万要禁用默认扫描。
<context:component-scan base-package="com.raingray">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
如果想只扫描 @Component 注解,千万不能直扫描它,因为 @Service、@Repository、@Controller 这三个注解都是 @Componenet 的别名,访问它们本质上等同于使用 @Componenet,最好通过第二种方式的黑名单来完成这种需求。
<context:component-scan base-package="com.raingray">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
</context:component-scan>
设置 @ComponentScan 注解的 useDefaultFilters 元素为 false 禁止扫描包内的类,通过 includeFilters 元素来指定扫描包内使用 @Service 和 @Controller 注解的类。
package com.raingray;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
@Configuration
@ComponentScan(
basePackages = "com.raingray",
includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Service.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class),
},
useDefaultFilters = false
)
public class ConfigurationBean {
public ConfigurationBean() {
System.out.println("ConfigurationBean 无参数构造方法被执行");
}
}
2.启用黑名单控制哪些注解才能创建对象
先启用所有注册 use-default-filter="true"(这也是默认值无需主动配置为 true),再通过 exclude 进行排除,相当于拉黑名单,被排除的注解无法注册成对象,哪不在名单里的就能被扫描到创建对象。
<context:component-scan base-package="com.raingray">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
</context:component-scan>
通过注解的方式也是一样,控制默认扫描包内所有类的元素是 useDefaultFilters,默认为 true,也就不用写了,再通过 excludeFilters 元素排除要扫描的注解 @Service 和 @Controller,剩下的有设置 @Component、@Repository 就能成功注册成对象。
@ComponentScan(
basePackages = "com.raingray",
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Service.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class),
}
)
#### 1.6.1 @Value 简单类型注入
@Value 可以用在成员变量、方法、参数、注解这几个地方。
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
1.在成员变量上使用
在 Bean的成员变量上使用,之后就会通过反射给这个字段赋值,而不是调 Setter 方法赋值。
@Value("1234abc")
private String name;
@Value("1")
private int age;
但直接写值违背 OCP 原则,为了灵活,还可以注入属性配置文件的值。先引入配置文件。
XML 加载方式。用 Context 命名空间加载配置。
<context:property-placeholder location="application.properties"/>
注解加载方式。在全注解的情况下在类上添加 @PropertySource 加载配置。
@PropertySource("application.properties")
通过SPEL(Spring EL)表达式 `${}` 来读取属性配置文件中对应键的值。
@Value("${keyname}")
2.在构造方法形参上使用
sdsfs(@Value("this is name") String name, @Value("18") int age)
相当于给形参赋值。但形参如果有多个,不要出现只给某一个参数赋值,一定要全部用 @value 赋值,不然报错。
在使用的时候不能主动定义无参构造方法,不然一直就调无参,不然才调有参构造方法。
3.在单个参数的实例方法上使用
@Value(1)
public void thisFunction(String name)
这个值最终被传递到形参 name 上。要是这个方法的形参有多个,那 Spring 就不知道传给哪个参数,会抛异常。
在测试过程中发现在实例方法上使用优先级最高,其次为字段上使用,最后才是构造方法形参,下面代码最终 name 赋值为属性配置文件 app.name 的值,age 设置为 88888。
@Value("1234abc")
private String name;
@Value("1")
private int age;
@Value("${app.name}")
public void setXXX(String name) {
this.name = name;
}
@Value("88888")
public void setLAK(int age) {
this.age = age;
}
public Bean(@Value("222333") String name, @Value("2221111") int age) {
this.name = name;
this.age = age;
}
#### 1.6.2 @Autowired 和 @Qualifier 自动装配
@Autowired 就是根据类型进行自动装配,可以在构造方法、实例和静态方法、参数、成员变量和注解上使用。
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Qualifier 是根据名称进行装配必须和 @Autowired 一起使用除了不能在构造方法上用意外,其他的一样。
1.成员变量上使用
下面代码 obj 是 injectionBean 引用类型,在创建对象时发现有这个注解后会去 IoC 容器里找有没这个类型的对象,有的话就自动调无参构造方法创建出对象给 obj 成员变量赋值,可跟 Setter 方法不沾边,不是调的 Setter 方法。
@Autowired
private injectionBean obj;
有一种情况会赋值失败,就是发现多个 injectionBean 类型此时就会被抛异常。这种场景下 obj 的类型是指向接口,但是 IoC 容器里这个接口的实现类有很多 Spring 不知道选哪个,因此出现错误。
这种情况要配合使用 @Qualifier 注解指定一个名称,告诉 Spring 具体要注入 Bean 对象的 ID。
@Autowired
@Qualifier("injectTestImpl")
private injectionBean obj;
发现没有注入成功也是很常见的情形,很可能是你忘了把对应类添加上 @Component、@Service、@Repository、@Controller 的注解,所以找不到对象。
2.在单个参数的实例方法上使用
@Autowired
@Qualifier("injectTestImpl")
public void setTest(testfather obj) {
this.obj = obj;
}
原理一致不在赘述,和前面 @Value 在实例方法上使用注意事项一样,自动注入的方法只能有一个参数。
3.在构造方法上使用
构造方法注入构造方法只适用于只有一个参数的时候,也可能是没学到位,不会操作。
@Autowired
public Bean(@Qualifier("injectTestImpl") testfather obj) {
this.obj = obj;
}
假设目前有构造方法,有一个简单类型变量 name 和一个引用类型 obj,怎么进行赋值?
public Bean(String name, testfather obj) {
this.name = name;
this.obj = obj;
}
下面写法肯定不行,因为构造方法在 @Autowired 看到 name 形参首先会自动基于类型找 String,找不到就抛异常。
@Autowired
public Bean(String name, @Qualifier("injectTestImpl") testfather obj) {
this.name = name;
this.obj = obj;
}
所以正确写法是也给 name 赋上值,就不会出错。
@Autowired
public Bean(@Value("thisname") String name, @Qualifier("injectTestImpl") testfather obj) {
this.name = name;
this.obj = obj;
}
4.在参数上使用
先看怎么在构造方法参数上用,首先这个类构造方法必须只有一个,如果有多个类型还需要指定 Bean 的名称。
public Bean(@Autowired @Qualifier("injectTestImpl") testfather obj) {
this.obj = obj;
}
#### 1.6.3 @Resource 自动装配
@Resource 是 JDK 扩展中的注解,在默认的 JDK 中是不带的,要使用这个注解需要引入对应依赖 annotation,[不同版本](https://projects.eclipse.org/projects/ee4j.ca)要求的 Java 版本不同,[3.0](https://jakarta.ee/zh/specifications/annotations/3.0/) 就要求 11 及以上,而且在不同 JDK 版本中默认的命名空间也有改变,比如 Spring5 中就是引入 javax,Spring6 引入 jakarta。
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>3.0.0</version>
@Resource 的使用对象,只能对 Setter 方法和成员变量进行注入对象。默认是根据 name 元素提供的值到 IoC 容器中找到对应 Bean 的 ID 进行装配。
比如下面成员变量 obj,通过 @Resource 注入 ID 为 testinje 的 Bean。
@Resource(name = "testinje")
private testfather obj;
还有另一种用法就是不写 Bean ID 的情况,这默认会取成员变量名称作为 ID 在 IoC 容器里寻找对应 Bean,这里找的是 obj。要是名称也不一样就会通过类型进行装配,具体点来将是扫描你指定的包里这些类型为 testfather 的类创建对象进行注入,如果有多个依然会报错很正常,所以要求只能找到一个。
@Resource
private testfather obj;
在成员变量 obj 的 Setter 上使用 @Resource 规则也是一样,提供了 name 就注入对应 ID 为 的 Bean,
@Resource(name = "testinje")
public void setObj(testfather obj) {
System.out.println("注入的对象是:" + obj);
this.obj = obj;
}
没提供就用形参名称 obj 作为 Bean 的 ID,去 IoC 找 Bean 对象。根据名称找不到就根据形参的类型扫描包里的类,匹配上就创建对象进行注入,如果找到多个依然会报错。
@Resource
public void setObj(testfather obj) {
System.out.println("注入的对象是:" + obj);
this.obj = obj;
}
### 1.7 Spring 配置文件拆分和导入
有时候项目太大了,在单个配置文件中写最后内容比较多容易出错,这时候可以按功能或者三层架构拆分为多个文件,最后在一个主配置文件中导入。
导入单个配置文件。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<import resource="test1.xml"/>
<import resource="test2.xml"/>
<import resource="test3.xml"/>
批量导入 test 开头的 XML 配置文件。
## 2 AOP
AOP(Aspect-Oriented Programming)全称面相切面编程,是一种编程的思想,Spring 中 AOP 实现底层是动态代理设计模式。为了理解这个 AOP 原理最好把代理设计模式使用一遍,我这为了快速熟悉操作就没学。
有了 AOP 可专心编写核心业务逻辑代码,把与业务无关的代码,但是又必须要用到的功能,比如日志、安全,单独建立成模块,在核心业务功能运行的时候自动调用。
这个 AOP 思想有点像 Python 装饰器。
### 2.1 AspectJ 框架注解方式使用 AOP
#### 2.1.1 引入依赖
Spring 也有通过动态代理来实现 AOP,但是现在用的更多的是依赖 AspectJ 框架完成 AOP 操作。
因此要引用 spring-aop 和 aspectj 两个 jar 包,由于 spring-aop 包通过 spring-context 自动引入,所以只需要 spring-context 和 aspectj。
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.10</version>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.1.10</version>
#### 2.1.2 创建 Spring 配置类
通过全注解的方式进行配置。@EnableAspectJAutoPorxy 是启用 Aspect 代理功能,相当于 AspectJ 框架的功能开关。
package com.raingray;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan({"com.raingray.aspect", "com.raingray.service"})
@EnableAspectJAutoProxy
public class SpringConfiguration {
}
#### 2.1.3 实现业务功能类
创建一个简单的测试类 UserService.java。
package com.raingray.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void login() {
System.out.println("UserService: 用户登录中......");
}
}
#### 2.1.4 创建切面类
切面类是指你要给业务类实现的增强功能,使用 @Aspect 注解表示当前是个切面类,切面类也需要自动创建成对象被 IoC 管理,由于它没有什么分层的思路所以一般用 @Component。
package com.raingray.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogAspect {
......
}
切面类具体增强的功能叫通知,这个通知是说这个增强方法在什么时候执行,一共有五种对应注解如下:
- 前置通知,@Before,目标方法之前执行。这个目标方法只能是实例方法,不能在构造方法、静态方法上使用,而且这个目标方法还有个官方术语叫切入点(PointCut),这里就不说专业术语了不方便理解
- 后置通知,@AfterReturnning,目标方法之后执行
- 异常通知,@AfterThrowing,如果目标方法出现异常则执行,因为出现了异常,后置通知肯定不会运行
- 最终通知,@After,永远在后置通知和异常通知后面执行,简单来说不管目标方法运行的咋样反正永远在最后执行
- 环绕通知,@Around,环绕是围绕着前置通知之前运行一次,并且在后置通知之后也运行一次(出现异常则不执行)
知道了这五个通知注解在目标方法上的执行顺序,还需要告诉它上哪儿找目标方法,怎么指定是 [AspectJ 切入点表达式](https://docs.spring.io/spring-framework/reference/core/aop/ataspectj/pointcuts.html)来找,它有专门的一套语法来做筛选。
| 语法名称 | 值填写要求 |
| ------------------------ | ------------------------------------------------------------ |
| [方法访问控制权限修饰符] | 值选填,表示要找的方法是什么权限,写 public 只筛选出 public 的方法,不写则 public、private、protected、default 都筛。 |
| 方法返回值类型 | 值必填,值是星号代表方法可以返回任意类型, |
| [方法所在类的路径] | 值选填,用来指定方法所在类,要写类全限定名,写成 `com.raingray..` 就代表匹配 com.raingray 包下所有类,写成 `*` 匹配任意包名,比如 com 包下有 com.a 和 com.b,那么`com.*` 就等同于匹配 com.a 和 com.b 包,或者 `*..` 结合起来就是任何包下包括所有子包的所有类都匹配。不写值默认匹配所有类。 |
| 方法名 | 值必填,值是方法名称,可以用星号,比如值是 `set*` 就匹配所有 set 开头的方法,或者直接写星号 `*` 表示匹配所有方法。 |
| 参数列表 | 值必填,值是参数名,比如 `()` 标识方法没有参数,`(..)` 表示可以有任意数量参数且不限制类型,`(*)` 表示方法只有一个参数不限制类型,`(*, String)` 表示有两个参数,第一个是任意类型,第二个是 String类型。 |
| [参数异常] | 值选填,不写则匹配任意异常类型。 |
比如下面 `execution()` 是固定写法,当中 `public` 是方法访问控制权限修饰符,`int` 是方法返回值类型,`com.raingray` 是类全限定名,test 是类中的方法名,`(..)` 是参数列表,`throws IllegalArgumentException` 是参数异常类型。简单来说是要匹配 com.raingray.test 类中的 public int test 方法,这个方法有任意数量形参,还往外抛 IllegalArgumentException 异常。
execution(public int com.raingray.test(..) throws IllegalArgumentException))
通知方法编写。需要注意的是每个通知的方法上都可以使用 `JoinPoint joinPoint` 参数获取到目标方法信息,对于异常通知 @AfterThrowing 还可获得异常对象信息,后置通知 @AfterReturning 可以获得目标方法返回的对象信息,但是这些参数都是可选项,不是必须要写。
package com.raingray.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect
@Component
public class LogAspect {
/**
* @param joinPoint 这个 JoinPoint 对象有 getArgs 方法可以获取到切入点参数,
* getSignature() 方法可以获取到切入点签名。
*/
@Before("execution(public Object com.raingray.service.UserService.login())")
public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
System.out.println("@Before 前置通知执行:切入点信息" +
signature.getDeclaringTypeName() + "#" + signature.getName() + Arrays.toString(joinPoint.getArgs()));
}
/**
* @param joinPoint 目标方法
* @param returnObj 切入点返回值对象,在注解上使用元素 returning 定义返回值参数名称,
* 最后在方法上也要定义一个相同参数名称来接收,这样可以获取到返回值。
*/
@AfterReturning(value = "execution(public Object com.raingray.service.UserService.login())", returning = "returnObj")
public void afterReturning(JoinPoint joinPoint, Object returnObj) {
System.out.println("@AfterReturning 后置通知执行:切入点返回值是 " + returnObj);
}
/**
* @param joinPoint
* @param e 在 @AfterThrowing 元素 throwing 上定义一个异常参数名称,在方法上也取相同名称就能获取到目标方法上产生的异常对象。
*/
@AfterThrowing(value = "execution(public Object com.raingray.service.UserService.login())", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("@AfterThrowing 异常通知:" + joinPoint.getSignature().getName() + " 出现异常 " + e);
}
@After("execution(public Object com.raingray.service.UserService.login())")
public void after(JoinPoint joinPoint) {
System.out.println("@After 最终通知:......");
}
/**
* @param joinPoint ProceedingJoinPoint 接口是目标方法
* @throws Throwable ProceedingJoinPoint 接口的 proceed() 方法要求 向外抛出异常
*/
@Around("execution(public Object com.raingray.service.UserService.login())")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("@Around 前环绕通知");
// 执行目标方法
joinPoint.proceed();
System.out.println("@Around 后环绕通知");
// 第二种处理方式
/*try {
System.out.println("@Around 前环绕通知");
// 调用目标方法。
joinPoint.proceed();
} catch (Throwable e) {
// 这里也可以处理异常,不会影响 @AfterThrowing 运行。
System.out.println("环绕处理异常:");
e.printStackTrace();
} finally {
System.out.println("@Around 后环绕通知");
}*/
}
}
运行测试程序 TestLogAspect.java
import com.raingray.SpringConfiguration;
import com.raingray.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class TestLogAspect {
@Test
public void testAopWithAnnotation() {
AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(SpringConfiguration.class);
UserService userService = annotationConfigApplicationContext.getBean("userService", UserService.class);
userService.login();
}
}
目标方法没有异常的情况下运行结果。
@Around 前环绕通知
@Before 前置通知执行:切入点信息com.raingray.service.UserService#login[]
UserService: 用户登录中......
@AfterReturning 后置通知执行:切入点返回值是 Sun Jul 28 16:41:06 CST 2024
@After 最终通知:......
@Around 后环绕通知
如果目标方法出现异常,这里就用 `System.out.println(1 / 0);` 作为演示。
@Around 前环绕通知
@Before 前置通知执行:切入点信息com.raingray.service.UserService#login[]
UserService: 用户登录中......
@AfterThrowing 异常通知:login 出现异常 java.lang.ArithmeticException: / by zero
@After 最终通知:......
java.lang.ArithmeticException: / by zero
at com.raingray.service.UserService.login(UserService.java:11)
......
每个通知方法如果切入点表达式都是一样的,可以单独在切面类中用 @Pointcut 注解给一个空的方法定义成切面表达式。
@Pointcut("execution(public Object com.raingray.service.UserService.login())")
private void pointCut() {}
@Pointcut("execution(public Object com.raingray.service.UserService.forgetPassword())")
private void pointCut2() {}
后续直接在对应通知方法上调用,如果是其他切面类中使用当前切类的切面表达式,需要写清楚完整路路径。
@After("pointCut()")
public void before { ...... }
@AfterReturning("pointCut()")
public void afterReturning(JoinPoint joinPoint, Object returnObj) {...... }
@Around("pointCut2()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable { ...... }
// 在其他切面类中引用,需要写清楚完整路径
// @After("com.raingray.xxx.pointCut()")
如果有多个切面类都作用到业务类上,它们执行的顺序是不明确的,有要有先后排序的话需要用到 @Order 注解来调整优先级,数值越小优先级越高。
@Aspect
@Component
@Order(2)
public class LogAspect { ...... }
@Aspect
@Component
@Order(1)
public class SecurityAspect { ...... }
运行结果符合预期。有点像洋葱一样芯是目标方法,优先级最高的 SecurityAspect 包裹在最表层,LogAspect 则是在里面。
@Around 前环绕通知:安全
@Before 前置通知执行:安全
@Around 前环绕通知
@Before 前置通知执行:切入点信息com.raingray.service.UserService#login[]
UserService: 用户登录中......
@AfterReturning 后置通知执行:切入点返回值是 Sun Jul 28 16:57:05 CST 2024
@After 最终通知:......
@Around 后环绕通知
@AfterReturning 后置通知执行:安全
@After 最终通知:安全
@Around 后环绕通知:安全
### 2.2 AspectJ 框架 XML 方式使用 AOP
#### 2.1.1 创建业务功能类
UserService.java
package com.raingray.service;
import java.util.Date;
public class UserService {
public Object login() {
System.out.println("UserService: 用户登录中......");
// System.out.println(1 / 0);
return new Date();
}
}
#### 2.1.2 创建切面类
LogAspect.java。
package com.raingray.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Arrays;
public class LogAspect {
@Pointcut("execution(public Object com.raingray.service.UserService.login())")
private void pointCut() {}
@Pointcut("execution(public Object com.raingray.service.UserService.login())")
private void pointCut2() {}
/**
* @param joinPoint 这个 JoinPoint 对象有 getArgs 方法可以获取到切入点参数,
* getSignature() 方法可以获取到切入点签名。
*/
// @Before("execution(public Object com.raingray.service.UserService.login())")
public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
System.out.println("@Before 前置通知执行:切入点信息" +
signature.getDeclaringTypeName() + "#" + signature.getName() + Arrays.toString(joinPoint.getArgs()));
}
/**
* @param joinPoint 目标方法
* @param returnObj 切入点返回值对象,在注解上使用元素 returning 定义返回值参数名称,
* 最后在方法上也要定义一个相同参数名称来接收,这样可以获取到返回值。
*/
public void afterReturning(JoinPoint joinPoint, Object returnObj) {
System.out.println("@AfterReturning 后置通知执行:切入点返回值是 " + returnObj);
}
/**
* @param joinPoint
* @param e 在 @AfterThrowing 元素 throwing 上定义一个异常参数名称,在方法上也取相同名称就能获取到目标方法上产生的异常对象。
*/
public void afterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("@AfterThrowing 异常通知:" + joinPoint.getSignature().getName() + " 出现异常 " + e);
}
public void after(JoinPoint joinPoint) {
System.out.println("@After 最终通知:......");
}
/**
* @param joinPoint ProceedingJoinPoint 接口是目标方法
* @throws Throwable ProceedingJoinPoint 接口的 proceed() 方法要求 向外抛出异常
*/
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("@Around 前环绕通知");
// 执行目标方法
joinPoint.proceed();
System.out.println("@Around 后环绕通知");
// 第二种处理方式
/*try {
System.out.println("@Around 前环绕通知");
// 调用目标方法。
joinPoint.proceed();
} catch (Throwable e) {
// 这里也可以处理异常,不会影响 @AfterThrowing 运行。
System.out.println("环绕处理异常:");
e.printStackTrace();
} finally {
System.out.println("@Around 后环绕通知");
}*/
}
}
SecurityAspect.java
package com.raingray.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Arrays;
public class SecurityAspect {
private void pointCut() {}
/**
* @param joinPoint 这个 JoinPoint 对象有 getArgs 方法可以获取到切入点参数,
* getSignature() 方法可以获取到切入点签名。
*/
// @Before("execution(public Object com.raingray.service.UserService.login())")
public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
System.out.println("@Before 前置通知执行:安全");
}
/**
* @param joinPoint 目标方法
* @param returnObj 切入点返回值对象,在注解上使用元素 returning 定义返回值参数名称,
* 最后在方法上也要定义一个相同参数名称来接收,这样可以获取到返回值。
*/
public void afterReturning(JoinPoint joinPoint, Object returnObj) {
System.out.println("@AfterReturning 后置通知执行:安全");
}
/**
* @param joinPoint
* @param e 在 @AfterThrowing 元素 throwing 上定义一个异常参数名称,在方法上也取相同名称就能获取到目标方法上产生的异常对象。
*/
public void afterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("@AfterThrowing 异常通知:安全");
}
public void after(JoinPoint joinPoint) {
System.out.println("@After 最终通知:安全");
}
/**
* @param joinPoint ProceedingJoinPoint 接口是目标方法
* @throws Throwable ProceedingJoinPoint 接口的 proceed() 方法要求 向外抛出异常
*/
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("@Around 前环绕通知:安全");
// 执行目标方法
joinPoint.proceed();
System.out.println("@Around 后环绕通知:安全");
// 第二种处理方式
/*try {
System.out.println("@Around 前环绕通知");
// 调用目标方法。
joinPoint.proceed();
} catch (Throwable e) {
// 这里也可以处理异常,不会影响 @AfterThrowing 运行。
System.out.println("环绕处理异常:");
e.printStackTrace();
} finally {
System.out.println("@Around 后环绕通知");
}*/
}
}
#### 2.2.3 创建 Spring 配置文件
要 aop 命名空间,后面配置切面类用。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
这在默认配置文件中是不带的。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
由于没添加 context 命名空间无法使用组件扫描,这里就用 `<bean>` 最原始的创建对象的方式。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 创建对象-->
<bean id="userService" class="com.raingray.service.UserService"/>
<bean id="logAspect" class="com.raingray.aspect.LogAspect"/>
<bean id="securityAspect" class="com.raingray.aspect.SecurityAspect"/>
<!-- 开启 AspectJ 功能,等同于注解 @EnableAspectJAutoProxy-->
<aop:aspectj-autoproxy/>
<!-- 配置切面类 -->
<aop:config>
<!-- <aop:pointcut> 配置切入点表达式,等同于 @Pointcut,或者是通知注解中的 value 元素 -->
<aop:pointcut id="pointcut" expression="execution(public Object com.raingray.service.UserService.login())"/>
<!-- <aop:aspect> 声明哪个类是切面类
ref 值是 Bean ID,等同于 @Aspect
order 用来定义多个切面的情况下运行优先级 -->
<aop:aspect ref="logAspect" order="2">
<!-- 配置前置通知
method 是指切面类中那个方法作为通知
pointcut 用来指定这个方法切入点表达式
pointcut-ref 引入 <aop:pointcut> ID 为 pointcut 的切入表达式,相当于共用切入点表达式-->
<aop:before method="before" pointcut-ref="pointcut"/>
<!-- 配置后置通知
returning 是指定返回对象的参数名,要和方法中参数名一致-->
<aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="returnObj"/>
<!-- 配置异常通知
throwing 是目标方法返回的异常对象参数名,要和方法中参数名一致-->
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="e"/>
<!-- 配置最终通知 -->
<aop:after method="after" pointcut-ref="pointcut"/>
<!-- 配置环绕通知 -->
<aop:around method="around" pointcut-ref="pointcut"/>
</aop:aspect>
<aop:aspect ref="securityAspect" order="1">
<aop:before method="before" pointcut-ref="pointcut"/>
<aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="returnObj"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="e"/>
<aop:after method="after" pointcut-ref="pointcut"/>
<aop:around method="around" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
运行测试程序
import com.raingray.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestLogAspect {
@Test
public void testAopWithXML() {
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("SpringConfiguration.xml");
UserService userService = classPathXmlApplicationContext.getBean("userService", UserService.class);
userService.login();
}
}
结果和注解异常,正常运行。
@Before 前置通知执行:安全
@Around 前环绕通知:安全
@Before 前置通知执行:切入点信息com.raingray.service.UserService#login[]
@Around 前环绕通知
UserService: 用户登录中......
@Around 后环绕通知
@After 最终通知:......
@AfterReturning 后置通知执行:切入点返回值是 null
@Around 后环绕通知:安全
@After 最终通知:安全
@AfterReturning 后置通知执行:安全
## 3 事务
事务是 DML 语句的集合,执行结果要么一起成功要么一起失败。在哪儿用事务?具体场景是 Service 方法中调多个 Dao 方法。
在 JDBC 和 Mybatis 开启、提交、回滚事务的类和对应方法都不同,因此 Spring 中就定义了个接口 [PlatformTransactionManager](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/PlatformTransactionManager.html),它叫事务管理器,接口里面规范了事务提交和会滚方法,不同连接数据库的 ORM 框架或者 JDBC 库对应实现类 Spring 也帮写好了,不需要我们手动实现。
比如 Mybatis 用 DataSourceTransactionManager,Hibaernate 用 HibernateTransactionManager。
- [DataSourceTransactionManager](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/datasource/DataSourceTransactionManager.html),给 Mybatis 框架,JDBC 原生类、Spring jdbcTemplate 对 JDBC 包装工具类使用
- [HibernateTransactionManager](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/orm/hibernate5/HibernateTransactionManager.html),Hibernate 框架使用
- JtaTransactionManager,分布式事务
在 Spring 使用事务分两种,一种是编程式事务,需要手动管理开启事务、提交事务、回滚事务,另一种是声明式事务,只需要配置一下哪些方法要用到事务就能自动管理。
在 Spring 中最需要掌握的是声明式事务,它有 Annotation 和 XML 两种配置方式。简单来说只需要把对应事务管理器对象通过交给 IoC 管理,通过 AOP 自动通过动态代理调事务管理器的环绕通知对应方法(而里面最终就会找到你使用的访问数据库的技术,JDBC、MyBatis......)就可以自动实现提交和回滚。
### 3.1 声明式事务注解使用方式
中小型项目直接在方法上写注解比较方便。
#### 3.1.1 创建事务管理器对象
引入 spring-jdbc 依赖,后面创建 DataSourceTransactionManager 对象要用。
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.1.10</version>
创建 DataSourceTransactionManager 对象,给里面传递 Druid 数据源,这样才能获取到 JDBC 连接控制事务。这里 `<context:property>` 需要引用 context 命名空间,就不写了,文章中 [1.3 小节](#content-13-context-%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4%E5%8A%A0%E8%BD%BD%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6)讲过。
<context:property-placeholder location="jdbc.properties"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!--数据源还有很多配置项可以参考
https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE#1-%E9%80%9A%E7%94%A8%E9%85%8D%E7%BD%AE-->
<property name="dataSource" ref="druidSource"/>
#### 3.1.2 开启注解事务管理器
在 `<beans>` 标签添加属性。
xmlns:tx="http://www.springframework.org/schema/tx"
在 `<beans>` 标签 `xsi:schemaLocation` 属性添加值。
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
之后就可以用 `<tx:annotation-driven>` 标签开启事务,其中 transaction-manager 是指定事务管理器 Bean 的 ID,如果 ID 是 transactionManager 可以不写 transaction-manager,这是它的默认值。
<tx:annotation-driven transaction-manager="dataSourceTransactionManageer"/>
前面创建创建事务管理器对象和开启注解事务管理器还是用到了 XML,纯注解怎么开启?直接创建 Spring 配置类使用注解即可。
package com.raingray;
import com.alibaba.druid.pool.DruidDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
//import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.io.IOException;
@Configuration
@ComponentScan("com.raingray.service.impl")
//@PropertySource("classpath:jdbc.properties") // 从类根路径加载文件
@EnableTransactionManagement // 启用注解事务管理器
public class SpringConfig {
// @Value("${jdbc.driver}")
// private String driverClassName;
//
// @Value("${jdbc.url}")
// private String url;
//
// @Value("${jdbc.username}")
// private String username;
//
// @Value("${jdbc.password}")
// private String password;
/**
* 创建 Druid 数据源 Bean 对象
* @return 返回 DruidDataSource 对象,自动放入 IoC 容器管理,Bean id 为方法名 druidDataSource
*/
@Bean
public DruidDataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
/**
* @Bean 和 @Value 在运行时 @Bean 先运行,所以这时候 @Value 还没向成员变量注入值呢,所以一定是 null
* https://blog.csdn.net/qq_22339269/article/details/108615235
*/
/* druidDataSource.setDriverClassName(driverClassName);
druidDataSource.setUrl(url);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
System.out.println(this.driverClassName);*/
druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql://localhost:3306/mybatis");
druidDataSource.setUsername("root");
druidDataSource.setPassword("");
return druidDataSource;
}
/**
* 创建事务管理器 Bean 对象
* @param dataSource 自动根据类型在 IoC 找 id 为 DruidDataSource 的对象注入到 dataSource 参数
* 这样就能自动注入 Druid 数据源。
* @return 返回 DataSourceTransactionManager 对象,自动放入 IoC 容器管理,Bean id 为方法名 dataSourceTransactionManager
*/
@Bean
public DataSourceTransactionManager dataSourceTransactionManager(DruidDataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
在 Spring 配置类上添加 [@EnableTransactionManagement](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/EnableTransactionManagement.html) 就是开启注解。
光有注解无法获取数据库连接没法用事务,数据源怎么引用?可以通过设置一个实例方法,在方法里手动创建对应数据源对象,再通过 @Bean 自动将 return 的对象放入 IoC 容器管理,这个 Bean 的 id 就是方法名称(想要自定义 id 名可以使用 name 元素)。
在上面创建数据源的方法是 druidDataSource,在里面和 XML 一样直接调 setter 方法设置对应属性,最后直接返回对象,这个 Bean 的 ID 就是 druidDataSource。你可能会问这些属性为什么不用 @PropertySource 读取配置文件配合 @Value 注入到成员变量呢?因为 @Bean 和 @Value 在运行时有优先级问题,@Bean 会优先运行,此时 @Value 还没对成员变量注入值,直接读取成员变量会是 null,Spring 中怎么解决见代码注释 URL。
紧接着创建 DataSourceTransactionManager 事务管理器对象,这里使用用 dataSourceTransactionManager 方法创建,里面的数据源是通过形参引用,那么这个形参是谁传来的?这点我们不需要手动操作,使用了 @Bean 的方法所有形参会自动根据形参类型注入无需手动传入。
#### 3.1.3 配置哪些方法使用事务
那要怎么使用注解自动处理事务呢?这里要用到 [@Transactional](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html),它可以在在类和方法上使用,在类上使用表示类里面所有方法都启用事务,在具体方法上使用此注解,代表这个方法启用事务。
既然是自动处理事务,那么它会自动开启事务,默认情况下在代码出现 RuntimeException 异常时自动回滚(可以自定义其他异常),没有异常则在方法执行完自动提交事务。
#### 3.1.4 指定事务属性
知道具体在哪些方法上使用 @Transactional 启用事务,有些时候还需要对事务进行更细致的配置,而 @Transactional 注解有很多元素可以做配置,下面就来看看哪些元素需要掌握。
##### 3.1.4.1 事务传播行为
什么是事务传播行为?有时候我们对还会在 a 方法里面调用方法 b,它两都开启事务的情况下,或者只有一方开启事务,它两是怎么相互影响,这是传播行为要搞明白的事。
public void aSerivce() {
...
bService();
}
有哪些事务?
- `Propagation.REQUIRED`,这是所有使用 @Transactional 的默认值。当前方法事务设置为 REQUIRED 被调用时,如果对方有事务就融入对方的事务(相当于这两方法都用同一个数据库连接,因此事务也是同一个事务),没有就自己创建新事务运行。
- `Propagation.REQUIRES_NEW`,当前方法事务被设置为 REQUIRES_NEW,被调用时如果对方没有事务就自己创建新事务运行,如果对方有事务执行到我们这儿就用同步机制先把对方事务暂时挂起,自己创建一个新事务,等我们执行完毕后再恢复运行对方的事务。
- `Propagation.SUPPORTS`,当前方法事务设置成 SUPPORTS,被调用时如果对方有那么就使用融入对方事务运行,如果对方没有事务那我就不用事务运行。
- `Propagation.NEVER`,当前方法事务设置为 NEVE,被调用后如果对方有事务则抛异常,没有事务我就不启用事务运行。
- `Propagation.NOT_SUPPORTED`,当前方法事务设置为 NOT_SUPPORTED,表示当前方法不以事务运行,被调用时如果对方有事务则先将对方挂起,等当前方法以非事务的方式运行完成后,再恢复对方事务运行。
- `Propagation.MANDATORY`,如果当前方法事务设置成 MANDATORY,说明当前方法必须在事务中运行,被调用时对方没有事务则报错,对方有事务就融入对方事务。
- `Propagation.NESTED` 嵌套事务,用的少,每个数据库对嵌套事务的支持不一样,用到时再学。
怎么配置传播行为?在注解 @Transactional 添加元素 proppageation,值是 Propagation 枚举类。
@Transactional(propageation = Propagation.REQUIRED)
Propagation.REQUIRES_NEW。
##### 3.1.4.2 事务隔离级别
- ISOLATION_READ_UNCOMMITTED
- ISOLATION_READ_COMMITTED
- ISOLATION_REPEATABLE_READ
- ISOLATION_SERIALIZABLE
默认值是 DEFAULT 表示使用数据库默认的隔离级别。
@Transactional(isolation = Isolation.DEFAULT)
Oracle 和 MySQL 对 DEFAULT 支持不同。
| 隔离级别 | Oracle | MySQL |
| -------------------------- | --------- | --------- |
| ISOLATION_READ_UNCOMMITTED | ❌ | ✅ |
| ISOLATION_READ_COMMITTED | ✅默认支持 | ✅ |
| ISOLATION_REPEATABLE_READ | ❌ | ✅默认支持 |
| ISOLATION_SERIALIZABLE | ✅ | ✅ |
##### 3.1.4.3 事务只读属性
事务只读表示是否只允许执行查询 DQL 语句,默认值是 false,此时执行 DQL 语句就抛异常,要想成功执行 DQL 语句需要设置为 true。
@Transactional(readOnly = true)
##### 3.1.4.4 事务超时属性
事务执行超过多少秒就抛异常进行回滚,默认值是 -1 表示永远不超时。
@Transactional(timeout = 3)
这样做的好处是程序卡死,或者长查询就抛异常,避免占用资源。
##### 3.1.4.5 事务回滚策略属性
默认情况下只有出现 RuntimeException 才会进行回滚,但是我们可以灵活设置除了 RuntimeException 异常以外哪些异常产生时也进行回滚。
// 遇到这些异常的类进行回滚,这是多个的情况
@Transactional(rollbackFor = {AssertionError.class, ClassCastException.class})
// 只有一个异常类可以省略大括号
@Transactional(rollbackFor = Exception.class)
要想指定多个类可以用 rollbackForClassName 元素,值是 String 数组。
// 遇到这些异常的类进行回滚,这是多个的情况
@Transactional(rollbackForClassName={"RuntimeException","Exception"})
// 只有一个异常类可以省略大括号
@Transactional(rollbackForClassName="Exception")
遇到哪些异常不进行回滚操作,值需要传递异常对象的 Class 对象。
// 指定多个类
@Transactional(noRollbackFor = {ArithmeticException.class, ClassNotFoundException.class})
// 指定一个类
@Transactional(noRollbackFor = ArithmeticException.class)
同样的,如果同事有多个发生异常不想进行回滚操作,可以指定多个异常同样可以用 noRollbackForClassName 元素,值填 String 数组。
// 遇到下面这些异常不进行回滚。
@Transactional(noRollbackForClassName={"ArithmeticException","ClassNotLoadedException"})
// 单个同样可以省略大括号
@Transactional(noRollbackForClassName="ClassNotLoadedException")
### 3.2 声明式事务 XML 使用方式
前面通过注解的方式启用事务,这次手动通过 AOP 方式启用事务。
1.引用 AOP 框架依赖
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.1.10</version>
2.配置数据源
记得添加 context 命名空间才能使用此标签。
<context:property-placeholder location="jdbc.properties"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="url" value="${jdbc.url}"/>
<!--数据源还有很多配置项可以参考
https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE#1-%E9%80%9A%E7%94%A8%E9%85%8D%E7%BD%AE-->
3.启用事务管理器
<property name="dataSource" ref="druidDataSource"/>
4.启用事务通知
记得添加 tx 命名空间才能使用此标签。
<tx:advice id="txAdvice" transaction-manager="dataSourceTransactionManageer">
<tx:attributes>
<!-- name 属性是必填属性,用于配置事务在哪些方法上使用,不过name 值可以用星号通配符 -->
<!-- 其他事务属性选填,就是配置事务传播行为、回滚策略、隔离级别...... -->
<tx:method name="queryUserInfoByIdAndUpdateRestoreInfo"/>
</tx:attributes>
</tx:advice>
5.配置切面
记得添加 aop 命名空间才能使用此标签。
<aop:config>
<!-- 通过切入点表达式配置哪些方法可以使用事务 -->
<aop:pointcut id ="txPointcut" expression="execution(* com.raingray.service.impl..*(..))"/>
<!-- 添加切面通知。advice-ref 指定事务通知的 id,pointcut-ref 指定切入点表达式的 id -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
## 4 整合其他组件
### 4.1 Log4j2🔨
Log4j2 日志
根据官网提示引入最新版本依赖。
> ```xml
> <properties>
> <log4j2.version>2.23.1</log4j2.version>
> </properties>
>
> <dependencies>
> <dependency>
> <groupId>org.apache.logging.log4j</groupId>
> <artifactId>log4j-api</artifactId>
> <version>${log4j2.version}</version>
> </dependency>
> <dependency>
> <groupId>org.apache.logging.log4j</groupId>
> <artifactId>log4j-core</artifactId>
> <version>${log4j2.version}</version>
> </dependency>
> </dependencies>
> ```
>
> https://logging-log4j.staged.apache.org/log4j/2.x/maven-artifacts.html
XML 配置 Log4j2
> ```xml
> <?xml version="1.0" encoding="UTF-8"?>
> <Configuration status="debug" strict="true" name="XMLConfigTest"
> packages="org.apache.logging.log4j.test">
> <Properties>
> <Property name="filename">target/test.log</Property>
> </Properties>
> <Filter type="ThresholdFilter" level="trace"/>
>
> <Appenders>
> <Appender type="Console" name="STDOUT">
> <Layout type="PatternLayout" pattern="%m MDC%X%n"/>
> <Filters>
> <Filter type="MarkerFilter" marker="FLOW" onMatch="DENY" onMismatch="NEUTRAL"/>
> <Filter type="MarkerFilter" marker="EXCEPTION" onMatch="DENY" onMismatch="ACCEPT"/>
> </Filters>
> </Appender>
> <Appender type="Console" name="FLOW">
> <Layout type="PatternLayout" pattern="%C{1}.%M %m %ex%n"/><!-- class and line number -->
> <Filters>
> <Filter type="MarkerFilter" marker="FLOW" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
> <Filter type="MarkerFilter" marker="EXCEPTION" onMatch="ACCEPT" onMismatch="DENY"/>
> </Filters>
> </Appender>
> <Appender type="File" name="File" fileName="${filename}">
> <Layout type="PatternLayout">
> <Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
> </Layout>
> </Appender>
> </Appenders>
>
> <Loggers>
> <Logger name="org.apache.logging.log4j.test1" level="debug" additivity="false">
> <Filter type="ThreadContextMapFilter">
> <KeyValuePair key="test" value="123"/>
> </Filter>
> <AppenderRef ref="STDOUT"/>
> </Logger>
>
> <Logger name="org.apache.logging.log4j.test2" level="debug" additivity="false">
> <AppenderRef ref="File"/>
> </Logger>
>
> <Root level="trace">
> <AppenderRef ref="STDOUT"/>
> </Root>
> </Loggers>
>
> </Configuration>
> ```
>
> https://logging-log4j.staged.apache.org/log4j/2.x/manual/configuration.html#XML
### 4.2 MyBatis
通过 Spring 管理 Mybatis 对象,尝试用注解的方式配置声明式事务打通整个流程。
#### 4.2.1 引入依赖
引入 spring-context,使用 Spring 的 IoC 和 AOP 功能。
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
引入 Mybatis 依赖。mysql-connector-java 用来连接 MySQL 数据库的驱动,druid 数据源,spring-jdbc 其实是依赖于 spring-fx 但是 spring-jdbc 它有事务管理器,mybatis 就是 MyBatis 框架,[mybatis-spring](https://mybatis.org/spring/zh_CN/index.html) MyBatis 与 Spring 集成库主要靠它完成集成,junit-jupiter-api 是 JUnit5 用来做单元测试。
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.23</version>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.1.10</version>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.3</version>
<scope>test</scope>
#### 4.2.2 创建三层架构结构
com.raingray.control
com.raingray.service
com.raingray.mapper
#### 4.2.2 创建 POJO 类
创建 pojo 包和 control 同级。
UserPojo.java。后面查询出数据封装成对象用。
package com.raingray.pojo;
public class UserPojo {
private Integer id;
private String name;
private String password;
private String realName;
private String gender;
private String tel;
public UserPojo() {
}
public UserPojo(Integer id, String name, String password, String realName, String gender, String tel) {
this.id = id;
this.name = name;
this.password = password;
this.realName = realName;
this.gender = gender;
this.tel = tel;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRealName() {
return realName;
}
public void setRealName(String realName) {
this.realName = realName;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getTel() {
return tel;
}
public void setTel(String tel) {
this.tel = tel;
}
@Override
public String toString() {
return "UserPojo{" +
"id=" + id +
", name='" + name + '\'' +
", password='" + password + '\'' +
", realName='" + realName + '\'' +
", gender='" + gender + '\'' +
", tel='" + tel + '\'' +
'}';
}
}
#### 4.2.3 编写 Mapper 接口和 Mapper XML 配置文件
在 mapper 包里边创建接口 UserMapper.java,用来定义 User 表有哪些方法。
package com.raingray.mapper;
import com.raingray.pojo.UserPojo;
import java.util.ArrayList;
public interface UserMapper {
public ArrayList<UserPojo> queryAll();
public Integer updateUser(UserPojo user);
public UserPojo queryUserById(Integer id);
}
UserMapper.xml。编写接口对应方法的 SQL。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<select id="queryAll" resultType="com.raingray.pojo.UserPojo">
SELECT id, name, password, t_realname as realName, gender, tel FROM t_user
</select>
<select id="queryUserById" parameterType="Integer" resultType="com.raingray.pojo.UserPojo">
SELECT id, name, password, t_realname as realName, gender, tel FROM t_user WHERE id = #{id}
</select>
<update id="updateUser" parameterType="com.raingray.pojo.UserPojo">
UPDATE t_user
SET name = #{name},
password = #{password},
t_realname = #{realName},
gender = #{gender},
tel = #{tel}
WHERE
id = #{id};
</update>
#### 4.2.4 编写 Service 接口和实现类
在 service 包中定义服务接口 UserCenter.java。
package com.raingray.service;
import com.raingray.pojo.UserPojo;
public interface UserCenter {
public int userInfoUpdate(UserPojo user);
public UserPojo queryUserInfo(int id);
public int queryUserInfoByIdAndUpdateRestoreInfo(int id);
}
顺带创建 service.impl 包,在里面创建 UserCenterImpl.java 实现 UserCenter 接口。这个实现类有三个方法,userInfoUpdate 根据用户对象更新数据,queryUserInfo 根据用户 ID 查询用户所有信息,queryUserInfoByIdAndUpdateRestoreInfo 根据 ID 查询用户信息后恢复用户默认昵称,顺带把性别滞空,这个方法由于执行了多条 DML 语句可以启用事务控制避免错误。
package com.raingray.service.impl;
import com.raingray.mapper.UserMapper;
import com.raingray.pojo.UserPojo;
import com.raingray.service.UserCenter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserCenterImpl implements UserCenter {
// MyBatis 动态代理实现接口的 Bean ID 也是 userMapper,但是因此可以根据类型匹配就没必要多指定个名称。
// @Autowired
// @Qualifier(value = "userMapper")
@Autowired
private UserMapper user;
@Override
public int userInfoUpdate(UserPojo userPojo) {
return user.updateUser(userPojo);
}
@Override
public UserPojo queryUserInfo(int id) {
return user.queryUserById(id);
}
private int setNoneToGender(UserPojo userPojo) {
userPojo.setGender("");
return userInfoUpdate(userPojo);
}
/**
* 把指定 ID 的帐户昵称恢复为 defaultName,把 Gender 恢复为 Null
* @param id
* @return
*/
@Transactional
public int queryUserInfoByIdAndUpdateRestoreInfo(int id) {
int result = 0;
UserPojo userPojo = queryUserInfo(id);
userPojo.setName("defaultName");
result = userInfoUpdate(userPojo);
var a = 1/0; // 制造异常测试事务回滚
result += setNoneToGender(userPojo);
return result;
}
}
怎么执行的 SQL 肯定是个问题,原来在 Mybatis 中是用 getMapper 获取动态代理创建的接口实现类,这里是通过 Autowired 根据 UserMapper 的类型注入给 user 成员变量注入 UserMapper 接口的实现类对象。具体来说它是到 IoC 容器里找这个同名的 userMapper 实现类对象,那这个对象是怎么创建出来的呢?下面就来看看怎么注册 Mapper 接口。
#### 4.2.5 创建配置文件
在 Resource 类的跟目录下创建 mybatis-core.xml。这里面只配值了日志输出,后面注册 Mapper 接口和 Mapper XML 都交给 Spring 配置文件来注册 Bean 进行管理。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
创建 jdbc.properties,后面数据源会加载对应值来连接数据库。
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.username=root
jdbc.password=
jdbc.url=jdbc:mysql://localhost:3306/mybatis
创建 Spring 配置文件 SpringConfiguration.xml。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- JDBC 配置文件-->
<context:property-placeholder location="jdbc.properties"/>
<!-- 配置数据源。引用的配置文件内容 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="url" value="${jdbc.url}"/>
<!--数据源还有很多配置项可以参考
https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE#1-%E9%80%9A%E7%94%A8%E9%85%8D%E7%BD%AE-->
</bean>
<!-- 1.创建 DataSourceTransactionManager 事务管理器对象,引用 Druid 数据源 -->
<bean id="dataSourceTransactionManageer" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"/>
</bean>
<!-- 2.开启注解事务管理功能 -->
<tx:annotation-driven transaction-manager="dataSourceTransactionManageer"/>
<!-- 3.创建 MyBatis SqlSessionFactory 对象 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 指定数据源 -->
<property name="dataSource" ref="druidDataSource" />
<!-- 加载配置文件,里面可能有 Cache、Log 等配置 -->
<property name="configLocation" value="mybatis-core.xml"/>
<!-- 扫描 Mapper XML 配置文件,如果默认和 Mapper 接口放在一起的话这个配置可以省略不配也能读取到,
放在其他目录一定要要主动配置否则找到不到对应 SQL,会报
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.raingray.mapper.UserMapper.queryUserById
错误 -->
</bean>
<!-- 扫描 Mapper 接口,mybatis-spring 的 MapperScannerConfigurer 将自动把动态代理实现的 Mapper 接口类注册到 IoC 容器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.raingray.mapper"/>
</bean>
<!-- 4.扫描包注册 Bean -->
<context:component-scan base-package="com.raingray.service.impl"/>
首先是用 context:property-placeholder 加载了 jdbc.properties 中的数据配置信息,接着创建了 Druid 数据源。
有了数据源就创建 MyBatis 需要的数据源管理器 DataSourceTransactionManager 对象,顺带开启注解事务配置。
注解配置完毕后,开始配置 Mybatis,SqlSessionFactoryBean 是有由 mybatis-spring 的 org.mybatis.spring.SqlSessionFactoryBean 创建出来,里面 dataSource 属性设置数据源,configLocation 是加载 MyBatis 主配置文件,如果你 Mapper XML 位置不在 mapper 包下和 Mapper 接口放在一起,那么需要自定义位置 MyBatis 才能找到。
sqlSession 则是 org.mybatis.spring.mapper.MapperScannerConfigurer 自动帮忙创建,里面 basePackage 是指定 Mapper 接口的包名,写好后会自动用动态代理把接口实现类生成。
最后是扫描包里哪些带了注解标签,自动创建程对象放到 IoC 里管理。
#### 4.2.6 测试运行
创建 TestUserCenterImpl.java,这里分别测带事务和不带事务两种情况,先看有事务是如何自动回滚。
import com.raingray.pojo.UserPojo;
import com.raingray.service.UserCenter;
import com.raingray.service.impl.UserCenterImpl;
import org.apache.ibatis.javassist.ClassPath;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestUserCenterImpl {
@Test
void userInfoUpdateTest() {
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("SpringConfiguration.xml");
// 如果目标用了方法上开启了事务,默认事务的 AOP 使用 JDK 代理,只能代理接口。
UserCenter usrCenterImpl = classPathXmlApplicationContext.getBean("userCenterImpl", UserCenter.class);
usrCenterImpl.queryUserInfoByIdAndUpdateRestoreInfo(58);
}
}
测试方法里直接从 IoC 调 Service 层的 userCenterImpl 对象就好,里面 queryUserInfoByIdAndUpdateRestoreInfo 自动通过 Autowired 自动注入 UseerMapper 接口实现类对象,无需担心找不到 Mapper 接口动态代理对象。
我们指定 id 为 58 的用户进行信息恢复,但是这个方法开启了事务,里面又有 1/0 的异常,所以会自动回滚修改的修改默认名称,而滞空 Gender 为 None 也不会运行。
/**
- 把指定 ID 的帐户昵称恢复为 defaultName,把 Gender 恢复为 Null
- @param id
@return
*/
@Transactional
public int queryUserInfoByIdAndUpdateRestoreInfo(int id) {
int result = 0;
UserPojo userPojo = queryUserInfo(id);
userPojo.setName("defaultName");
result = userInfoUpdate(userPojo);
var a = 1/0; // 制造异常测试事务回滚
result += setNoneToGender(userPojo);
return result;
}运行成功跑异常。
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
8月 16, 2024 4:51:56 下午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} inited
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@779dfe55]
JDBC Connection [com.alibaba.druid.pool.DruidStatementConnection@58faa93b] will be managed by Spring
==> Preparing: SELECT id, name, password, t_realname as realName, gender, tel FROM t_user WHERE id = ?
==> Parameters: 58(Integer)
<== Columns: id, name, password, realName, gender, tel
<== Row: 58, zhangsan, 123456, 张三, M, 13412341234
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@779dfe55]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@779dfe55] from current transaction
==> Preparing: UPDATE t_user SET name = ?, password = ?, t_realname = ?, gender = ?, tel = ? WHERE id = ?;
==> Parameters: defaultName(String), 123456(String), 张三(String), M(String), 13412341234(String), 58(Integer)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@779dfe55]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@779dfe55]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@779dfe55]
java.lang.ArithmeticException: / by zero
at com.raingray.service.impl.UserCenterImpl.queryUserInfoByIdAndUpdateRestoreInfo(UserCenterImpl.java:46)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:392)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
at jdk.proxy2/jdk.proxy2.$Proxy21.queryUserInfoByIdAndUpdateRestoreInfo(Unknown Source)
at TestUserCenterImpl.userInfoUpdateTest(TestUserCenterImpl.java:20)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
从数据库结果来看的确回滚。
mysql> SELECT t.* FROM mybatis.t_user t WHERE id = 58; | |||||
---|---|---|---|---|---|
id | name | password | t_realname | gender | tel |
58 | zhangsan | 123456 | 张三 | M | 13412341234 |
1 row in set (0.00 sec)
如果把 @Transactional 从 queryUserInfoByIdAndUpdateRestoreInfo 方法上去除,那么不存在事务的情况下出现异常,理应是默认名称恢复,Gender 无变化。
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2ca47471] was not registered for synchronization because synchronization is not active
8月 16, 2024 4:58:08 下午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} inited
JDBC Connection [com.alibaba.druid.pool.DruidStatementConnection@73d6d0c] will not be managed by Spring
==> Preparing: SELECT id, name, password, t_realname as realName, gender, tel FROM t_user WHERE id = ?
==> Parameters: 58(Integer)
<== Columns: id, name, password, realName, gender, tel
<== Row: 58, zhangsan, 123456, 张三, M, 13412341234
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2ca47471]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a9800f8] was not registered for synchronization because synchronization is not active
JDBC Connection [com.alibaba.druid.pool.DruidStatementConnection@73d6d0c] will not be managed by Spring
==> Preparing: UPDATE t_user SET name = ?, password = ?, t_realname = ?, gender = ?, tel = ? WHERE id = ?;
==> Parameters: defaultName(String), 123456(String), 张三(String), M(String), 13412341234(String), 58(Integer)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a9800f8]
java.lang.ArithmeticException: / by zero
at com.raingray.service.impl.UserCenterImpl.queryUserInfoByIdAndUpdateRestoreInfo(UserCenterImpl.java:46)
at TestUserCenterImpl.userInfoUpdateTest(TestUserCenterImpl.java:20)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
再查数据库,确实名称被修改没有回滚。
mysql> SELECT t.* FROM mybatis.t_user t WHERE id = 58; | |||||
---|---|---|---|---|---|
id | name | password | t_realname | gender | tel |
58 | defaultName | 123456 | 张三 | M | 13412341234 |
1 row in set (0.00 sec)
### 4.3 JUnit5
Spring 提供了 spring-test 作为单元测试的支持,它支持 JUnit 框架。
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.1.10</version>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.3</version>
<scope>test</scope>
@ExtendWith 是JUnit5 的注解,@ContextConfiguration 它是 Spring 的注解,它两配合在一起使用后可以帮我们自动 ClassPathXmlApplicationContext 加载配置文件,后面我们通过 Autowired 根据类型注入,省略 getBean 方法直接即可调用对象中的方法。
import com.raingray.service.UserCenter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:SpringConfiguration.xml")
public class TestUserCenterImplWithJUnit5 {
@Autowired
private UserCenter userCenterImpl;
@Test
void userInfoUpdateTest() {
/*ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("SpringConfiguration.xml");
// // 如果目标用了方法上开启了事务,默认事务的 AOP 使用 JDK 代理,只能代理接口。
UserCenter usrCenterImpl = classPathXmlApplicationContext.getBean("userCenterImpl", UserCenter.class);*/
userCenterImpl.queryUserInfoByIdAndUpdateRestoreInfo(58);
}
}
@BeforeEach,在指定方法上使用 @BeforeEach 注解,后续所有写 @Test 注解的方法在运行前都会先执行使用了 @Before 注解的方法。
@AfterEach,在指定方法上使用 @AfterEach 注解,后续所有写 @Test 注解的方法在运行后结束执行添加了 @After 注解的方法。
最近更新:
发布时间:
首席好厉害,好棒 好棒首席