1.Spring是一个轻量级的JavaEE框架
2.它是为了解决企业应用开发的复杂性而创建的
3.Spring有两个核心部分:IoC和Aop
4.Spring的特点
1)轻量
Spring框架使用的jar都比较小,一般在1M以下或者几百KB。Spring核心功能所需要的jar总共在3M左右。
Spring框架运行占用的资源少,运行效率高。
Spring框架可以独立运行,不依赖其他jar包。
2)方便解耦
3)AOP编程的支持
4)方便整合各种框架
5)方便程序测试
6)方便进行事务操作
7)降低API开发难度
5.本文使用的Spring版本是5.2.6
6.Spring体系结构
创建一个普通java类,在这个类中编写一个方法。
演示在不手动new这个类的对象的情况下,使用Spring得到这个类的对象并调用其方法。
方式1:从官网下载jar包
地址:
http://repo.spring.io/release/org/springframework/spring/
由于是外网,可能进的比较慢。
方式2:添加依赖
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.6.RELEASE</version> </dependency>
1.创建父工程
创建好后删除src目录。
2.在pom.xml中添加spring依赖以及其他基本设置
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.tsccg</groupId> <artifactId>spring-project</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <!--单元测试依赖--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!--spring依赖--> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.6.RELEASE</version> </dependency> </dependencies> </project>
3.创建子module
在/main/java目录下新建一个普通java类User,编写一个方法doSome
package com.tsccg.service; /** * @Author: TSCCG * @Date: 2021/09/13 18:19 */ public class User { public void doSome(){ System.out.println("执行doSome方法"); } }
1.在/main/resources目录下创建一个Spring配置文件
2.在配置文件的beans标签内,添加bean标签
在bean标签里添加id和class两个属性
<?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"> <!--配置User对象的创建--> <bean id="user" class="com.tsccg.service.User"/> </beans>
在/test/java目录下新建一个测试类TestUser,编写测试方法
public class TestUser { /** * 使用spring获取User对象,调用其方法 */ @Test public void testDoSome() { //1.加载配置文件 String config = "beans.xml";//beans.xml所在路径 ApplicationContext ac = new ClassPathXmlApplicationContext(config); //2.获取配置创建的对象,强转为User类型 //User user = ac.getBean("user",User.class); User user = (User)ac.getBean("user");//这里需要输入配置文件里bean的id值 System.out.println(user); //调用对象的方法 user.doSome(); } }
测试结果:
com.tsccg.service.User@32d2fa64 执行doSome方法
控制反转(Inversion of Control,简称IoC):是面向对象编程中的一种设计原则。是指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。
IoC的实现:当前最常见的方式叫做依赖注入(Dependency Injection,简称DI),程序代码不做定位查询,这些工作由容器自行完成。
依赖注入是指程序运行过程中,若需要调用另一个对象协助时,无需在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。
Spring的依赖注入对调用者和被调用者几乎没有任何要求,完全支持对象之间依赖关系的管理。
Spring容器是一个对象工厂,负责创建、管理所有的java对象,这些java对象被称为Bean。Spring容器管理着Bean之间的依赖关系。
使用IoC可以降低对象之间的耦合度。
通俗地讲:
上面的入门案例就是使用IoC来实现的。
IoC底层主要使用了xml解析、工厂模式、反射三种技术。
下面画图演示:
现在有两个类,UserService和User。
我想在UserService的execute()方法里调用User的doSome()方法。实现方式如下:
第一种方式:在execute()方法里new对象
在UserService类的execute()方法里new一个User对象,然后用这个对象调用doSome()方法
这种方式最为简单,但使得UserService类和User类紧紧关联在了一起,耦合度很高。
什么是耦合?
如果耦合度过高,比如上面的关联方式,就会出现如下情况:
我们开发时追求的是低耦合高内聚,高耦合不利于程序拓展。
我们可以使用一些技术来降低耦合度。
第二种方式:使用工厂模式
工厂模式就是创建一个第三方类,实现目标类对象的创建,然后将对象交给调用它的地方。
工厂模式的目的就是为了降低耦合度。
工厂模式虽然降低了UserService类和User类之间的耦合度,但是仍没有降到最低。
为了进一步降低耦合度,我们需要使用到IoC,控制反转。
第三种方式:IoC
IoC就是在工厂模式的基础上,将创建对象的过程进一步解耦:
IoC将对象间的耦合度大大降低了,假如说现在User类的路径变了,我们只需要在配置文件里修改路径即可,不需要更改源代码。
ApplicationContext用于加载Spring的配置文件,在程序中充当"容器"的角色。
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); User user = (User)ac.getBean("user");
ApplicationContext接口有两个实现类
这两个实现类的区别是:
1)当使用FileSystemXmlApplicationContext类时,需要传入xml配置文件编译后的绝对路径
ApplicationContext ac = new FileSystemXmlApplicationContext("D:\\code\\常用框架\\04-Spring\\spring-project\\spring-01\\target\\classes\\beans.xml"); User user = (User)ac.getBean("user");
2)当使用ClassPathXmlApplicationContext类时,需要传入xml配置文件编译后的相对路径
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); User user = (User)ac.getBean("user");
ApplicationContext容器,会在容器对象初始化时,将其中所有的对象一次性装配好。以后代码中如果要用这些对象,只需要从内存中直接获取即可。执行效率高,但占用内存。
也就是说,在加载配置文件的时候就会创建配置文件里所有的对象。
//1.加载配置文件 String config = "beans.xml";//beans.xml所在路径 //获取容器:此时容器中所有对象都创建好了 ApplicationContext ac = new ClassPathXmlApplicationContext(config);
为了实现在Spring的配置文件中,给java对象的属性赋值,需要使用DI,依赖注入。
DI是基于创建对象来实现的。
DI有两种实现:
1)set注入(设值注入):Spring调用类的set方法,在set方法中可以实现属性的赋值
2)构造注入:Spring调用类的有参构造方法创建对象,在构造方法中完成赋值
简单类型的set注入格式:
简单类型:Spring中规定java的基本数据类型和String都是简单类型
<!-- 一个Bean标签代表一个对象 --> <bean id="自定义标识" class="目标类全限定名"> <!-- 一个property只能给一个属性赋值 --> <property name="简单类型形参名1" value="此属性值"></property> <property name="简单类型形参名2" value="此属性值"></property> </bean>
1.创建一个普通java类Student,设置两个属性,name和age,编写这两个属性的set方法以及重写toString方法。
package com.tsccg.set; /** * @Author: TSCCG * @Date: 2021/09/14 22:51 */ public class Student { //声明简单类型 private String name; private Integer age; public void setName(String name) { this.name = name; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
2.在/main/resources/set目录下创建xml配置文件beans.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"> <!-- 声明Student对象 --> <bean id="student" class="com.tsccg.set.Student"> <property name="name" value="张三"/> <property name="age" value="20"/> </bean> </beans>
3.编写测试代码
在/test/java目录下新建测试类TestStudent
import com.tsccg.set.Student; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @Author: TSCCG * @Date: 2021/09/14 22:54 */ public class TestStudent { /** * 测试set注入 */ @Test public void testSet() { String config = "set/beans.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); Student student = (Student)ac.getBean("student"); System.out.println(student); } }
测试结果:
Student{name='张三', age=20}
需要注意的是:set注入执行的是目标类的set方法
<bean id="student" class="com.tsccg.set.Student"> <property name="name" value="张三"/><!-- setName("张三") --> <property name="age" value="20"/><!-- setAge(20) --> </bean>
引用类型的set注入格式:
假如现在需要在目标类中声明另一个类的引用
<!-- 一个Bean标签代表一个对象 --> <!--创建目标类对象--> <bean id="目标对象自定义标识" class="目标类全限定名"> <property name="简单类型形参名" value="属性值"></property> <property name="引用类型形参名" ref="另一个类的bean标签的id值"></property> </bean> <!--创建引用类的对象--> <bean id="自定义标识" class="引用类型的全限定名"> <property name="简单类型形参名" value="属性值"></property> </bean>
1.创建一个普通java类School,有两个属性name和address
package com.tsccg.set2; /** * @Author: TSCCG * @Date: 2021/09/15 17:45 */ public class School { private String name; private String address; public void setName(String name) { this.name = name; } public void setAddress(String address) { this.address = address; } @Override public String toString() { return "School{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; } }
2.创建一个普通java对象StudentPlus,在其中声明一个School类型引用,并为其编写set方法
package com.tsccg.set2; /** * @Author: TSCCG * @Date: 2021/09/14 22:51 */ public class StudentPlus { //声明简单类型 private String name; private Integer age; //声明一个引用类型 private School school; public void setName(String name) { this.name = name; } public void setAge(Integer age) { this.age = age; } //给引用类型编写一个set方法 public void setSchool(School school) { this.school = school; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", school=" + school + '}'; } }
3.创建Beans.xml,配置StudentPlus对象的创建和属性的注入
<?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"> <!--创建StudentPlus对象--> <bean id="myStudent" class="com.tsccg.set2.StudentPlus"> <property name="name" value="李四"/><!-- setName("张三") --> <property name="age" value="26"/><!-- setAge(20) --> <!-- 为School类型的引用赋值 --> <property name="school" ref="mySchool"/><!-- setSchool(mySchool) --> </bean> <!-- 创建School对象 --> <bean id="mySchool" class="com.tsccg.set2.School"> <property name="name" value="南阳理工"/><!--setName("南阳理工")--> <property name="address" value="南阳"/><!--setAddress("南阳")--> </bean> </beans>
4.编写测试代码
/** * 测试引用类型set注入 */ @Test public void testSet2() { ApplicationContext ac = new ClassPathXmlApplicationContext("set2/beans.xml"); StudentPlus myStudent = (StudentPlus) ac.getBean("myStudent"); System.out.println(myStudent); }
测试结果:
Student{name='李四', age=26, school=School{name='南阳理工', address='南阳'}}
构造注入:Spring调用目标类的有参构造方法,在创建对象的同时,在构造方法中给属性赋值。
构造注入需要使用<constructor-arg >标签
<constructor-arg >标签属性:
一般使用name属性,因为name属性可读性更高。
格式:
<!--使用name属性--> <bean id="studentPro1" class="com.tsccg.construct.StudentPro"> <constructor-arg name="简单类型形参名" value="属性值"/> <constructor-arg name="引用类型形参名" ref="bean的id值"/> </bean> <!--使用index属性--> <bean id="studentPro2" class="com.tsccg.construct.StudentPro"> <constructor-arg index="1" ref="bean的id值"/> <constructor-arg index="0" value="属性值"/> </bean> <!--当不使用name或index时,默认按index,赋值顺序必须按照构造方法中形参列表的顺序--> <bean id="studentPro3" class="com.tsccg.construct.StudentPro"> <constructor-arg value="属性值"/> <constructor-arg ref="bean的id值"/> </bean>
演示:
1.创建一个普通java类StudentPro和SchoolPro,分别编写无参构造方法和有参构造方法,并重写toString方法。
package com.tsccg.construct; /** * @Author: TSCCG * @Date: 2021/09/14 22:51 * StudentPro类 */ public class StudentPro { private String name; private Integer age; private SchoolPro schoolPro; /** * 无参构造 */ public StudentPro() { System.out.println("StudentPro----执行无参构造方法----"); } /** * 有参构造 */ public StudentPro(String myName, Integer myAge, SchoolPro mySchoolPro) { System.out.println("StudentPro----执行有参构造方法----"); this.name = myName; this.age = myAge; this.schoolPro = mySchoolPro; } @Override public String toString() { return "StudentPro{" + "name='" + name + '\'' + ", age=" + age + ", schoolPro=" + schoolPro + '}'; } } /** * SchoolPro类 */ class SchoolPro { private String name; private String address; /** * 无参构造方法 */ public SchoolPro() { System.out.println("SchoolPro----执行无参构造方法----"); } /** * 有参构造方法 */ public SchoolPro(String myName, String myAddress) { System.out.println("SchoolPro----执行有参构造方法----"); this.name = myName; this.address = myAddress; } @Override public String toString() { return "School{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; } }
2.创建bean.xml,配置创建对象
<constructor-arg >标签属性:
分别使用name属性和index属性进行构造注入
<?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"> <!--创建StudentPlus对象--> <!-- 使用name --> <bean id="studentPro1" class="com.tsccg.construct.StudentPro"> <constructor-arg name="myName" value="王五"/> <constructor-arg name="myAge" value="28"/> <constructor-arg name="mySchoolPro" ref="schoolPro"/> </bean> <!-- 使用index --> <bean id="studentPro2" class="com.tsccg.construct.StudentPro"> <constructor-arg index="0" value="王五五"/> <constructor-arg index="2" ref="schoolPro"/> <constructor-arg index="1" value="30"/> </bean> <!-- 省略index 如果不使用name和index,默认按index,必须按照形参顺序赋值 --> <bean id="studentPro3" class="com.tsccg.construct.StudentPro"> <constructor-arg value="王六六"/> <constructor-arg value="35"/> <constructor-arg ref="schoolPro"/> </bean> <!-- 创建School对象 --> <bean id="schoolPro" class="com.tsccg.construct.SchoolPro"> <constructor-arg name="myName" value="清华大学"/> <constructor-arg name="myAddress" value="北京"/> </bean> </beans>
3.编写测试代码
/** * 测试构造注入 */ @Test public void testCon1() { String config = "con1/beans.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); //使用name属性 StudentPro studentPro1 = (StudentPro)ac.getBean("studentPro1"); System.out.println("使用name:" + studentPro1); //使用index属性 StudentPro studentPro2 = (StudentPro)ac.getBean("studentPro2"); System.out.println("使用index:" + studentPro2); //不使用name和index属性 StudentPro studentPro3 = (StudentPro)ac.getBean("studentPro3"); System.out.println("不使用name和index:" + studentPro3); }
测试结果:
SchoolPro----执行有参构造方法---- StudentPro----执行有参构造方法---- StudentPro----执行有参构造方法---- StudentPro----执行有参构造方法----//使用ApplicationContext,在加载配置文件时会创建所有对象 使用name:StudentPro{name='王五', age=28, schoolPro=School{name='清华大学', address='北京'}} 使用index:StudentPro{name='王五五', age=30, schoolPro=School{name='清华大学', address='北京'}} 不使用name和index:StudentPro{name='王六六', age=35, schoolPro=School{name='清华大学', address='北京'}}
引用类型的自动注入:Spring框架根据某些规则可以自动给引用类型赋值。不需要手动给引用类型赋值。
使用的规则常用的是byName、byType
1.byName(按名称注入):java类中引用类型的属性名和配置文件中<bean>
标签的id值一样,且数据类型是一致的,这样的bean,spring能够赋值给引用类型。
语法:
<bean id="xxx" class="ooo" autowire="byName"> 给简单类型属性赋值(必须使用set注入) </bean>
2.byType(按类型)注入:java类中引用类型的数据类型和配置文件<bean >标签的class属性是同源关系的,这样的bean,spring能够赋值给引用类型。
同源就是一类的关系:
语法:
<bean id="xxx" class="ooo" autowire="byType"> 给简单类型赋值(必须使用set注入) </bean>
1.创建一个Student类和一个School类,在Student类里声明一个School类型对象school,编写set方法
package com.tsccg.autowair1; /** * @Author: TSCCG * @Date: 2021/09/16 13:49 */ public class Student { private String name; private Integer age; School school; public Student() { System.out.println("创建Student对象"); } public void setName(String name) { this.name = name; } public void setAge(Integer age) { this.age = age; } public void setSchool(School school) { System.out.println("school:" + school); this.school = school; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", school=" + school + '}'; } } class School { private String name; private String address; public School() { System.out.println("创建School对象"); } public void setName(String name) { this.name = name; } public void setAddress(String address) { this.address = address; } @Override public String toString() { return "School{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; } }
2.创建beans.xml,使用byName方式完成school的自动注入
使用byName后,就不需要在Student的bean标签中引入School的bean。
当程序开始执行后,Spring会遍历所有的bean标签的id属性,找到和Student类里声明的School类型引用名相同的值,将其注入到Student对象的School类型引用里。
<?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"> <!--创建Student对象--> <bean id="myStudent" class="com.tsccg.autowair1.Student" autowire="byName"><!--自动注入byName--> <property name="name" value="赵六"/> <property name="age" value="19"/> <!-- <property name="school" ref="school"/>--><!--不需要在这里引入School的bean--> </bean> <!-- 创建School对象 --> <bean id="school" class="com.tsccg.autowair1.School"> <property name="name" value="小清华"/> <property name="address" value="南阳"/> </bean> </beans>
3.编写测试代码
/** * 测试自动注入byName */ @Test public void testAuto1() { String config = "auto1/beans.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); Student student = (Student)ac.getBean("myStudent"); System.out.println(student); }
测试结果:
创建Student对象 创建School对象 school:School{name='小清华', address='南阳'} Student{name='赵六', age=19, school=School{name='小清华', address='南阳'}}
1.还是使用上面的Student和School类,然后创建一个普通java类PrimarySchool,继承School类
package com.tsccg.autowair2; /** * @Author: TSCCG * @Date: 2021/09/16 13:49 */ public class Student { private String name; private Integer age; School school; public Student() { System.out.println("创建Student对象"); } public void setName(String name) { this.name = name; } public void setAge(Integer age) { this.age = age; } public void setSchool(School school) { System.out.println("school:" + school); this.school = school; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", school=" + school + '}'; } } /** * 定义School类 */ class School { private String name; private String address; public School() { System.out.println("创建School对象"); } public void setName(String name) { this.name = name; } public void setAddress(String address) { this.address = address; } @Override public String toString() { return "School{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; } } /** * 定义PrimarySchool,继承School */ class PrimarySchool extends School { private String name; private String address; public PrimarySchool() { System.out.println("创建PrimarySchool对象"); } @Override public void setName(String name) { this.name = name; } @Override public void setAddress(String address) { this.address = address; } @Override public String toString() { return "PrimarySchool{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; } }
2.创建beans.xml,使用byType完成自动注入引用类型属性
<?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"> <!--创建Student对象--> <bean id="myStudent" class="com.tsccg.autowair2.Student" autowire="byType"><!--自动注入byType--> <property name="name" value="赵六"/> <property name="age" value="22"/> </bean> <!-- 创建School对象 --> <bean id="school" class="com.tsccg.autowair2.School"> <property name="name" value="家里蹲大学"/> <property name="address" value="地球"/> </bean> </beans>
3.编写测试代码
/** * 测试自动注入byType */ @Test public void testAutoByType() { String config = "auto2/beans.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); Student student = (Student)ac.getBean("myStudent"); System.out.println(student); }
测试结果:
创建Student对象 创建School对象 school:School{name='家里蹲大学', address='地球'} Student{name='赵六', age=22, school=School{name='家里蹲大学', address='地球'}}
4.在beans.xml配置文件里,将School替换为PrimarySchool,继续测试
<?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"> <!--创建Student对象--> <bean id="myStudent" class="com.tsccg.autowair2.Student" autowire="byType"><!--自动注入byType--> <property name="name" value="赵六"/> <property name="age" value="22"/> </bean> <!-- 创建School对象 --> <!-- <bean id="school" class="com.tsccg.autowair2.School">--> <!-- <property name="name" value="家里蹲大学"/>--> <!-- <property name="address" value="地球"/>--> <!-- </bean>--> <!-- 创建PrimarySchool对象 --> <bean id="primarySchool" class="com.tsccg.autowair2.PrimarySchool"> <property name="name" value="家里蹲小学"/> <property name="address" value="地球"/> </bean> </beans>
测试结果:
创建Student对象 创建School对象 创建PrimarySchool对象 school:PrimarySchool{name='家里蹲小学', address='地球'} Student{name='赵六', age=22, school=PrimarySchool{name='家里蹲小学', address='地球'}}
PrimarySchool是School的子类,属于同源关系。当在Student类里声明一个School类型属性时,假如Spring在配置文件没找到School类型的bean标签,就会找School的同源类完成注入。
注意:在使用byType时,xml配置文件里声明的bean只能有一个符合条件的,不然会报错。也就是说,同源类的bean不能同时出现在同一个配置文件里。
以Student类和School类为例,分别为其创建一个配置文件,演示如何使用。
包含关系的配置文件
1)在main/resources目录下创建一个文件夹xmls,在其中新建三个配置文件:spring-student.xml、spring-school.xml、spring-total.xml。
spring-student.xml:只声明Student对象,在其bean标签中,使用byType完成School类型引用的自动注入
<?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"> <!--创建Student对象--> <bean id="myStudent" class="com.tsccg.xmls.Student" autowire="byType"> <property name="name" value="铁柱"/> <property name="age" value="24"/> </bean> </beans>
spring-school.xml:只声明School对象
<?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"> <!-- 创建School对象 --> <bean id="school" class="com.tsccg.xmls.School"> <property name="name" value="家里站大学"/> <property name="address" value="河南"/> </bean> </beans>
spring-total.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"> <!-- resource:其他配置文件的路径 classpath:告诉spring到项目编译后的位置去找配置文件 --> <import resource="classpath:xmls/spring-student.xml"/> <import resource="classpath:xmls/spring-school.xml"/> </beans>
2)测试
/** * 测试多配置文件使用 */ @Test public void testAutoByType() { //加载的是主配置文件 String config = "xmls/spring-total.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); Student student = (Student)ac.getBean("myStudent"); System.out.println(student); }
测试结果:
school:School{name='家里站大学', address='河南'} Student{name='铁柱', age=24, school=School{name='家里站大学', address='河南'}}
3)在包含关系的配置文件中,可以使用通配符(*:表示任意字符)引入一个目录下包含指定字符的所有配置文件
在主配置文件中进行如下修改:
<?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="classpath:xmls/spring-student.xml"/>--> <!-- <import resource="classpath:xmls/spring-school.xml"/>--> <import resource="classpath:xmls/spring-*.xml"/><!--加载xmls目录下的所有以spring-开头的xml文件--> </beans>
当然,在主配置文件中使用通配符后,主配置文件名不能包含指定字符,不然会将主配置文件一并加载,造成死循环。将主配置文件名改为:total.xml
重新进行测试,测试结果:
school:School{name='家里站大学', address='河南'} Student{name='铁柱', age=24, school=School{name='家里站大学', address='河南'}}
注意:通配符加载配置文件时,配置文件上面至少要有一级目录,不然无法加载。也就是说在resources目录下,必须新建一个文件夹,在该文件夹里存放配置文件。
通过注解完成java对象的创建和属性赋值。
1.添加spirng-context依赖:在加入spring-context依赖后,maven会将spring-aop的依赖一并添加,使用注解完成DI必须使用spring-aop
2.创建类,在类中加入spring的注解(多个不同功能的注解)
3.创建spring配置文件,在spring的配置文件中,加入一个组件扫描器的标签,指明注解在你的项目中的位置。
4.使用注解创建对象,创建容器ApplicationContext
@Component:创建对象,相当于<bean >的功能
和@Component功能一致,可以创建对象的注解还有三个:
以上三个注解的使用语法和@Component一样。
注意:虽然这三个注解和@Component都可以创建对象,但是这三个注解都有额外的功能,不能一概而论。
@Repository,@Service,@Controller的作用是给项目中的对象分层。
演示:
1.新建一个子模块spring-03-DI-anno,由于父模块的pom中已经添加了spring-context依赖,所以不用再加了。
2.在main/java下创建com.tsccg.anno01.Student,在类名定义处添加@Component注解
package com.tsccg.anno01; import org.springframework.stereotype.Component; /** * @Author: TSCCG * @Date: 2021/09/17 15:04 */ @Component("myStudent")//使用@Component注解 public class Student { private String name; private Integer age; public Student() { System.out.println("调用无参构造"); } public Student(String name, Integer age) { System.out.println("调用有参构造"); this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
3.创建Spring配置文件,在配置文件里添加组件扫描器的标签,指明注解在你的项目中的位置
在main/resources目录下新建applicationContext.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" 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"> <!-- 声明组件扫描器(component-scan),组件就是java对象 base-package:指定注解在项目中所处包名 component-scan工作方式:Spring会扫描遍历base-package指定的包及其子包中的所有类,找到类中的注解,按照注解的功能创建对象,或者给属性赋值。 --> <context:component-scan base-package="com.tsccg.anno01"/> </beans>
加入了component-scan标签后,配置文件的变化:
4.使用注解创建对象
编写测试代码:
/** * 测试Component */ @Test public void test01() { String config = "applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); Student myStudent = ac.getBean("myStudent", Student.class); System.out.println(myStudent); }
测试结果:
调用无参构造 Student{name='null', age=null}
由结果来看,使用注解创建对象时,调用的是类的无参构造方法。
<!--指定多个包的三种方式--> <!-- 第一种方式 --> <context:component-scan base-package="com.tsccg.anno01"/> <context:component-scan base-package="com.tsccg.anno02"/> <!-- 第二种方式 --> <context:component-scan base-package="com.tsccg.anno01;com.tsccg.anno02"/> <!-- 第三种方式 --> <context:component-scan base-package="com.tsccg"/>
@Value:给简单类型的属性赋值
实际使用:
package com.tsccg.anno01; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** * @Author: TSCCG * @Date: 2021/09/17 15:04 */ @Component("myStudent")//创建对象 public class Student { @Value("张三")//给name属性赋值 private String name; @Value("25")//给age属性赋值 private Integer age; public Student() { System.out.println("调用无参构造"); } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
测试结果:
调用无参构造 Student{name='张三', age=25}
@Autowired:spring框架提供的注解,实现给引用类型的属性赋值。
使用位置:
实例演示:
1.创建一个School类,使用注解创建其对象以及实现简单类型属性赋值
@Component("mySchool") public class School { @Value("北京大学") private String name; @Value("北京") private String address; public School() { System.out.println("调用School的无参构造"); } @Override public String toString() { return "School{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; } }
2.创建一个Student类,在Student类里声明一个School类型属性,使用注解创建其对象以及实现引用类型属性赋值
package com.tsccg.anno02; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** * @Author: TSCCG * @Date: 2021/09/17 15:04 */ @Component("myStudent")//创建对象 public class Student { @Value("张三")//给name属性赋值 private String name; @Value("25")//给age属性赋值 private Integer age; @Autowired//给引用类型属性赋值,默认byType private School school;//声明引用类型属性 public Student() { System.out.println("调用Student的无参构造"); } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", school=" + school + '}'; } }
3.创建spring配置文件applicationContext2.xml,告诉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" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.tsccg.anno02"/> </beans>
4.编写测试代码
/** * 测试用注解给引用类型赋值 */ @Test public void test01() { String config = "applicationContext2.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); Student myStudent = ac.getBean("myStudent", Student.class); System.out.println(myStudent); }
测试结果:
调用School的无参构造 调用Student的无参构造 Student{name='张三', age=25, school=School{name='北京大学', address='北京'}}
如果要使用byName方式给引用类型赋值,那么就需要使用到@Qualifier注解。
如:
Student类:
@Component("myStudent")//创建对象 public class Student { @Value("张三") private String name; @Value("25") private Integer age; //使用自动注入给引用类型属性赋值 @Autowired @Qualifier("mySchool")//设置为byName private School school;
School类:
@Component("mySchool")//mySchool:创建的对象名 public class School { @Value("北京大学") private String name; @Value("北京") private String address;
@Resource是来自JDK中的注解,Spring框架提供了对这个注解的功能支持。
可以使用它给引用类型属性赋值,使用的同样是自动注入原理,同样支持byName和byType,默认是byName。
在无法通过byName方式找到bean时,会使用byType来找。
@Component("myStudent")//创建对象 public class Student { @Value("李四") private String name; @Value("30") private Integer age; //给引用类型属性赋值 @Resource//默认byName,当使用byName方式找不到bean时,会转而使用byType找 //@Resource(name="mySchool")//设置为只使用byName,找不到就报错 private School school;
注解的优点:
注解的缺点:注解是写到java代码里的,耦合度高,修改后需要重新编译代码。
xml的优点:
xml的缺点:编写麻烦,效率低,开发大型项目时过于复杂。
总结:
动态代理就是为了在不修改目标类的基础上,实现调用目标方法和功能增强。
实现方式:
1.JDK动态代理:使用jdk中的Proxy,Method,InvocationHandler创建代理对象。
jdk动态代理要求目标类必须实现接口。
2.CGLIB动态代理:使用第三方的工具库,创建代理对象。原理是继承,通过继承目标类,创建其子类对象,这个子类对象就是代理对象,在代理对象中实现调用目标方法以及功能增强。
CGLIB要求目标类必须是可继承的,不能由final修饰,方法也不能是final的。
现在有一个service接口,该接口有两个业务方法。(接口方法也称为主业务逻辑)
接口:
public interface SomeService { void doSome(); void doOther(); }
编写它的一个实现类,在该实现类中实现接口中的业务方法
实现类:
public class SomeServiceImpl implements SomeService { @Override public void doSome() { System.out.println("执行业务方法doSome"); } @Override public void doOther() { System.out.println("执行业务方法doOther"); } }
现在需要在两个业务方法前添加执行日期,在两个业务方法执行后添加事务提交处理。
执行日期和事务提交处理属于非业务方法(非业务方法也称为交叉业务逻辑)
我们可以将这些非业务方法放到一个工具类里实现,然后再在实现类里调用。
工具类:
public class SomeServiceUtil { public static void doLog() { System.out.println("非业务功能-->方法开始执行时间为:" + new Date()); } public static void doTrans() { System.out.println("非业务功能-->事务提交"); } }
实现类:
public class SomeServiceImpl implements SomeService { @Override public void doSome() { SomeServiceUtil.doLog();//调用工具类,显示执行日期 System.out.println("执行业务方法doSome"); SomeServiceUtil.doTrans();//提交事务 } @Override public void doOther() { SomeServiceUtil.doLog();//显示执行日期 System.out.println("执行业务方法doOther"); SomeServiceUtil.doTrans();//提交事务 } }
测试代码:
@Test public void testSomeService() { SomeService someService = new SomeServiceImpl(); someService.doSome(); System.out.println("================================"); someService.doOther(); }
测试结果:
非业务功能-->方法开始执行时间为:Sat Sep 18 20:51:17 CST 2021 执行业务方法doSome 非业务功能-->业务提交 ================================ 非业务功能-->方法开始执行时间为:Sat Sep 18 20:51:17 CST 2021 执行业务方法doOther 非业务功能-->业务提交
我们分析以上程序,还是存在弊端:业务功能代码和非业务功能代码深度耦合在一起
当非业务代码较多时,在业务代码中会出现大量的非业务功能代码调用语句,大大影响了业务功能代码的可读性,降低了代码的可维护性,同时也增加了开发难度。
所以,可以采用动态代理方式,在不修改目标业务类源码的前提下,扩展和增强其功能。
service接口实现类:
public class SomeServiceImpl implements SomeService { @Override public void doSome() { System.out.println("执行业务方法doSome"); } @Override public void doOther() { System.out.println("执行业务方法doOther"); } }
编写InvocationHandler接口实现类
public class MyInvocationHandler implements InvocationHandler { //定义目标类 private final Object target; public MyInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; //得到当前调用的方法名 String methodName = method.getName(); //判断当前调用的方法是否是doSome方法,如果是,则实现日志、事务的增强;如果不是,则只执行目标方法 if ("doSome".equals(methodName)) { SomeServiceUtil.doLog();//在方法执行前输出日志 result = method.invoke(target,args);//执行目标方法,执行target对象的方法 SomeServiceUtil.doTrans();//在方法执行后,执行提交事务 } else { result = method.invoke(target,args);//执行目标方法 } return result; } }
测试代码:
@Test public void testSomeService() { //创建目标对象 SomeService target = new SomeServiceImpl(); InvocationHandler handler = new MyInvocationHandler(target); //创建代理对象 SomeService proxy = (SomeService)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),handler); //通过代理对象执行业务方法,实现日志、事务的增强 proxy.doSome(); System.out.println("================================"); proxy.doOther(); }
测试结果:
非业务功能-->方法开始执行时间为:Sat Sep 18 20:55:36 CST 2021 执行业务方法doSome 非业务功能-->事务提交 ================================ 执行业务方法doOther
AOP就是面向切面编程,是基于动态代理的,可以使用jdk、cglib两种代理方式。
AOP就是动态代理的规范化,把动态代理的实现步骤、实现方式都定义好了,让开发人员用一种统一的方式去使用动态代理。
AOP(Aspect Orient Programming),面向切面编程
好比OOP面向对象编程,面向对象编程就是在分析项目功能时,先考虑可以由哪些类来实现目标功能。
AOP面向切面编程就是分析项目功能时,先找出切面,然后合理地安排切面执行的时间、位置。(时间:在目标方法前还是后;位置:在哪个类、哪个方法上)
1.切面的功能代码:切面要实现的功能
在上面的例子中,工具类中的方法就是切面
public static void doLog() { System.out.println("非业务功能-->方法开始执行时间为:" + new Date()); }
2.切面的执行位置:使用Pointcut表示切面执行的位置
在上面的例子中,doSome方法就是JoinPoint连接点。又因为例子中只有一个连接点,所以doSome方法又是Pointcut切入点。
if ("doSome".equals(methodName)) { SomeServiceUtil.doLog();//执行切面 result = method.invoke(target,args);//doSome方法----》连接点----》切入点 SomeServiceUtil.doTrans();//执行切面 } else { result = method.invoke(target,args); }
3.切面的执行时间:使用Advice表示时间,就是说切面执行在目标方法之前,还是目标方法之后
AOP是一种规范,是动态的一种规范化,是一个标准。
AOP的技术实现框架有:Spring、AspectJ
1.Spring:在内部实现了AOP规范,能处理AOP的工作
2.AspectJ:一个开源的,专门做AOP的框架
切面的执行时间在AOP规范中也叫做Advice(通知,增强),在AspectJ框架有5个通知类型,都有相对应的注解:
当然,也可以使用xml配置文件中的标签来表示。
AspectJ定义了专门的表达式用于指定切入点。
表达式原型:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
说明:(带有?的表示是可选部分)
doSome(String,Integer)
以上表达式共4个部分:
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
切入点表达式要匹配的对象就是目标方法的方法名。所以,execution表达式中明显就是方法的签名。
注意:表达式中“访问权限”和“异常类型”是可省略部分,各个部分间用空格分开。一个表达式中只有方法返回值和方法声明是必须的。
切入点表达式支持通配符:
符号 | 意义 |
---|---|
* | 0到多个任意字符 |
.. | 1.用在方法参数中时,表示任意多个参数 2.用在包名后时,表示当前包及其子包路径 |
+ | 1.用在类名后,表示当前类及其子类 2.用在接口后,表示当前接口及其实现类 |
举例:
1.execution(public * *(..))
*
表示任意方法返回值*
表示任意方法名..
表示任意多个参数指定切入点为:任意公共方法。
2.execution(* set*(..))
指定切入点为:任何一个以“set”开始的方法。
3.execution(* com.tsccg.service.*.*(..))
指定切入点为:定义在service 包里的任意类的任意方法,不包含service包的子包中的类。
4.execution(* com.tsccg.service..*.*(..))
指定切入点为:定义在service 包或者子包里的任意类的任意方法。“..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。
execution(* *..service.*.*(..))
指定所有包下的serivce 子包下所有类(接口)中所有方法为切入点
使用AOP的目的是:在不修改原有代码的情况下,给已经存在的一些类和方法增加额外的功能。
完整步骤:
<bean>
来声明对象实例演示:
1.创建一个maven子模块:spring-05-aop-aspectj
2.在父模块的pom.xml文件中添加AspectJ依赖,然后刷新
<!-- AspectJ --> <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.9.RELEASE</version> </dependency>
3.创建目标接口和目标类
目标接口:
package com.tsccg.service; /** * @Author: TSCCG * @Date: 2021/09/19 21:30 */ public interface SomeService { void doSome(String name,Integer age); }
目标类:
package com.tsccg.service.impl01; import com.tsccg.service.SomeService; /** * @Author: TSCCG * @Date: 2021/09/19 21:30 */ public class SomeServiceImpl implements SomeService { @Override public void doSome(String name,Integer age) { System.out.println("执行doSome业务方法"); } }
4.创建切面类
package com.tsccg.aspectj; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import java.util.Date; /** * @Author: TSCCG * @Date: 2021/09/19 21:33 * 切面类:用来给业务方法增加功能的类,在这个类中有切面的功能代码 */ /* @Aspect:AspectJ框架中的注解 作用:表示当前类是切面类 位置:在类定义的上面作用:表示当前类是切面类 */ @Aspect public class MyAspectJ { /* 定义方法doLog,方法是实现切面功能的代码 方法的定义要求: 1.必须为公共的方法:public 2.方法没有返回值:void 3.方法名自定义 4.方法可以有参也可以无参,如果有参数,参数不是自定义的,有几个参数类型可以使用 */ /* @Before:前置通知注解,指定切面的执行时间 属性:value,是切入点表达式,指定切面的执行位置 位置:在方法上面编写 特点: 1.在目标方法前先执行切面 2.不会影响目标方法的执行 3.不会改变目标方法的执行结果 */ @Before(value="execution(public void com.tsccg.service.impl01.SomeServiceImpl.doSome(String,Integer))") public void doLog() { //切面要执行的增强功能代码 System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date()); } }
5.创建Spring的配置文件:声明Bean对象,把对象交给容器统一管理
在resources目录下创建applicationContext.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: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/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <!--通过xml配置文件的bean标签,把对象的创建和管理交给Spring容器--> <!--声明目标对象--> <bean id="mySomeServiceImpl" class="com.tsccg.service.impl01.SomeServiceImpl"/> <!--声明切面类对象--> <bean id="myAspect" class="com.tsccg.aspectj.MyAspectJ"/> <!--声明自动代理器:使用AspectJ框架内部的功能,创建目标对象的代理对象。 创建代理对象是在内存中实现的,修改目标对象中的结构,创建为代理对象。 故而从Spring中获取目标对象时,获取的实际上是被修改后的代理对象。 aspectj-autoproxy:会把Spring容器中的所有目标对象,一次性都生成代理对象 --> <aop:aspectj-autoproxy/> </beans>
6.编写测试代码,从Spring容器中获取目标对象(实际上已经是代理对象了)
public class MyTest01 { @Test public void testSomeService() { String config="applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); //从Spring容器中获取目标对象(实际上已经是代理对象了) SomeService proxy = (SomeService) ac.getBean("mySomeServiceImpl"); //打印这个对象的类名 System.out.println(proxy.getClass().getName()); //调用代理对象调用目标方法 proxy.doSome("张三",20); } }
测试结果:
com.sun.proxy.$Proxy7//jdk动态代理 前置通知,切面功能:在方法执行之前输出执行时间:Mon Sep 20 22:45:51 CST 2021//增强的功能 执行doSome业务方法
//不使用通配符 @Before(value="execution(public void com.tsccg.service.impl01.SomeServiceImpl.doSome(String,Integer))") public void doLog() { System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date()); } //省略访问权限 @Before(value="execution(void com.tsccg.service.impl01.SomeServiceImpl.doSome(String,Integer))") public void doLog() { System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date()); } //用通配符代替返回值 @Before(value="execution(* com.tsccg.service.impl01.SomeServiceImpl.doSome(String,Integer))") public void doLog() { System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date()); } //用通配符代替包名 @Before(value="execution(* *..SomeServiceImpl.doSome(String,Integer))") public void doLog() { System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date()); } //用通配符代替部分类名 @Before(value="execution(* *..Some*.doSome(String,Integer))") public void doLog() { System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date()); } //用通配符代替部分方法名 @Before(value="execution(* *..SomeServiceImpl.do*(String,Integer))") public void doLog() { System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date()); } //省略类名,用通配符代替方法中参数 @Before(value="execution(* doSome(..))") public void doLog() { System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date()); } //用通配符代替方法名 @Before(value="execution(* *..SomeServiceImpl.*(..))") public void doLog() { System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date()); }
通知方法:被通知注解所修饰的方法就是通知方法。
JoinPoint:连接点,指的是要加入切面功能的业务方法。
在通知方法中可以包含一个JoinPoint类型的参数,该类型的对象本身就是切入点表达式,通过该参数,可以在执行切面方法时,获取业务方法的信息,例如业务方法名称、业务方法的实参。
如:
切面类:
@Aspect public class MyAspectJ { @Before(value="execution(public void com.tsccg.service.impl01.SomeServiceImpl.doSome(String,Integer))") public void doLog(JoinPoint jp) { System.out.println("业务方法的签名(定义):" + jp.getSignature()); System.out.println("业务方法的名称:" + jp.getSignature().getName()); Object[] args = jp.getArgs(); for (Object arg:args) { System.out.println("业务方法的参数:" + arg); } System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date()); } }
测试结果:
com.sun.proxy.$Proxy7 业务方法的签名(定义):void com.tsccg.service.SomeService.doSome(String,Integer) 业务方法的名称:doSome 业务方法的参数:张三 业务方法的参数:20 前置通知,切面功能:在方法执行之前输出执行时间:Tue Sep 21 17:08:17 CST 2021 执行doSome业务方法
@Before修饰的前置通知方法在目标方法执行前执行。
被@AfterReturning修饰的后置通知方法在目标方法执行之后执行。
因为是在目标方法执行之后执行,所以可以获取到目标方法的返回值。该注解的returning属性就是专门用于接收目标方法返回值的变量名的。
同时,被注解为后置通知的方法,除了可以包含JoinPoint参数外,还可以包含用于接收返回值的变量。该变量最好为Object类型。因为目标方法的返回值可能是任何类型。
1.在接口中添加方法
public interface SomeService { void doSome(String name,Integer age); //后置通知方法 String doOther(String name,Integer age); }
2.在实现类中实现方法
public class SomeServiceImpl implements SomeService { @Override public void doSome(String name,Integer age) {...} //返回一个字符串 @Override public String doOther(String name, Integer age) { System.out.println("执行doOther业务方法"); return "abcd"; } }
3.使用后置通知定义方法
方法定义要求:
@AfterReturning:
@AfterReturning(value="execution(String *..SomeServiceImpl.doOther(..))",returning="res") public void myAfterReturning(Object res) {//Object res:是目标方法执行后的返回值,根据返回值做切面的功能处理 System.out.println("后置通知,切面功能:在目标方法执行之后获取返回值:" + res); /* 对返回值进行修改 如果返回值不为null,则将"Hello"字符串赋给res */ if (res != null) { res = "Hello"; } System.out.println("修改后:" + res); }
4.编写测试方法
@Test public void testSomeService() { String config="applicationContext1.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); SomeService proxy = (SomeService) ac.getBean("mySomeServiceImpl"); System.out.println(proxy.getClass().getName()); String str = proxy.doOther("李四", 30); System.out.println("str:" + str); }
测试结果:
com.sun.proxy.$Proxy7 执行doOther业务方法 后置通知,切面功能:在目标方法执行之后获取返回值:abcd 修改后:Hello str:abcd
由结果可见,在执行目标方法后,执行了切面。但切面中对返回结果的修改并未成功,最终结果仍为abcd。
上面的例子中,目标方法返回值为String类型。当目标方法执行后,将"abcd"的地址复制一份给切面方法的res形参。
当给res重新指定一个新字符串时,修改的只是res形参所存储的地址,目标方法的返回值不会改变。
又由于String底层是一个被final修改的字符数组,故字符串本身不可被修改,故当目标方法返回结果为String类型时,不可被修改。
这代表着后置通知无法对返回值进行修改吗?当然不是。
当目标方法的返回值为一个自定义的类型比如Student,我们就可以在形参中通过set方法修改Student的属性值,从而实现修改返回结果。
Student类:
public class Student { private String name; private Integer age; public Student() { } public Student(String name, Integer age) { this.name = name; this.age = age; } public void setName(String name) { this.name = name; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
接口:
public interface SomeService { void doSome(String name,Integer age); String doOther(String name,Integer age); /** * 返回值为Student类型 */ Student doOther2(String name,Integer age); }
实现类:
public class SomeServiceImpl implements SomeService { @Override public void doSome(String name,Integer age) { System.out.println("执行doSome业务方法"); } @Override public String doOther(String name, Integer age) { System.out.println("执行doOther业务方法"); return "abcd"; } /** * 目标方法 */ @Override public Student doOther2(String name, Integer age) { Student student = new Student(name,age); System.out.println("执行doOther2业务方法"); return student; } }
切面方法:
@AfterReturning(value="execution(* *..SomeServiceImpl.doOther2(..))",returning="res") public void myAfterReturning2(Object res) { System.out.println("后置通知,切面功能:在目标方法执行之后获取返回值:" + res); if (res instanceof Student) { //res = new Student("张飞",30);//这种方式只会修改res形参的指向,不会修改返回结果 //修改返回结果中的属性值 Student student = (Student)res; student.setName("张飞"); student.setAge(30); } System.out.println("修改后:" + res); }
测试方法:
@Test public void testSomeService() { String config="applicationContext1.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); SomeService proxy = (SomeService) ac.getBean("mySomeServiceImpl"); System.out.println(proxy.getClass().getName()); String str = proxy.doOther("李四", 30); System.out.println("str:" + str); System.out.println("==================================="); Student student = proxy.doOther2("王五",23); System.out.println("student:" + student); }
测试结果:
com.sun.proxy.$Proxy7 执行doOther业务方法 后置通知,切面功能:在目标方法执行之后获取返回值:abcd 修改后:Hello str:abcd =================================== 执行doOther2业务方法 后置通知,切面功能:在目标方法执行之后获取返回值:Student{name='王五', age=23} 修改后:Student{name='张飞', age=30} student:Student{name='张飞', age=30}
由结果来看,已成功修改返回结果。
环绕通知可以在目标方法执行前执行,也可以在目标方法执行后执行。
被注解为环绕通知的方法要有返回值,Object类型。
方法可以包含一个ProceedingJoinPoint类型的参数,ProceedingJoinPoint接口有一个方法proceed,用于执行目标方法。如果目标方法有返回值,那么该方法的返回值就是目标方法的返回值。最后,环绕通知方法将其返回值返回。
环绕通知方法实际上是拦截了目标方法的执行。
环绕通知一般都是做事务处理,在目标方法之前开启事务,执行目标方法,在目标方法之后提交事务。
1.在接口中添加方法doFirst
返回一个字符串
public interface SomeService { void doSome(String name,Integer age); String doOther(String name,Integer age); Student doOther2(String name,Integer age); //环绕通知方法 String doFirst(String name,Integer age); }
2.实现类中实现doFirst方法
返回"Hello"
public class SomeServiceImpl implements SomeService { @Override public void doSome(String name,Integer age) {...} @Override public String doOther(String name, Integer age) {...} @Override public Student doOther2(String name, Integer age) {...} @Override public String doFirst(String name, Integer age) { System.out.println("执行doFirst业务方法"); return "Hello"; } }
3.定义切面
环绕通知的方法定义格式:
@Around:环绕通知注解
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))") public Object myAround(ProceedingJoinPoint pjp) throws Throwable { //定义目标方法执行结果 Object result = null; System.out.println("环绕通知:在目标方法执行前,输出时间:" + new Date()); result = pjp.proceed();//执行目标方法 System.out.println("环绕通知:在目标方法执行后,提交事务"); return result; }
环绕通知等同于jdk动态代理中的InvocationHandler接口实现类
InvocationHandler实现类:
public class MyInvocationHandler implements InvocationHandler { //定义目标类 private final Object target; //有参构造方法 public MyInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; String methodName = method.getName(); if ("doSome".equals(methodName)) { SomeServiceUtil.doLog();//在目标方法执行前增强功能 result = method.invoke(target,args); SomeServiceUtil.doTrans();//在目标方法执行后增强功能 } else { result = method.invoke(target,args); } return result; } }
环绕通知方法中的ProceedingJoinPoint类型参数相当于InvocationHandler接口实现类里的Method类型参数,作用是执行目标方法。
环绕通知方法的返回值就是执行目标方法的执行结果,可以被修改。
4.编写测试方法
@Test public void testSomeService() { String config="applicationContext1.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); SomeService proxy = (SomeService) ac.getBean("mySomeServiceImpl"); System.out.println(proxy.getClass().getName()); //这里相当于执行String result = proxy.myAround(); String result = proxy.doFirst("Tom",18); System.out.println("result:" + result); }
测试结果:
com.sun.proxy.$Proxy8 环绕通知:在目标方法执行前,输出时间:Fri Sep 24 19:18:53 CST 2021 执行doFirst业务方法 环绕通知:在目标方法执行后,提交事务 result:Hello
我们可以编写一个判断,当在测试方法里调用目标方法传入的name不是"Tom"时,不执行目标方法。
为此,我们需要在环绕通知方法里获取目标方法的实参。
我们可以查看ProceedingJoinPoint接口的源代码,可以发现,其继承了JoinPoint接口
public interface ProceedingJoinPoint extends JoinPoint {...}
JoinPoint类型的参数,可以在执行切面时,获取目标方法的信息,例如目标方法名称、目标方法的实参等。作为JoinPoint的子接口,一样可以获取目标方法的实参。
编写环绕通知方法:
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))") public Object myAround2(ProceedingJoinPoint pjp) throws Throwable { //定义传入的name参数 String name = null; //获取目标方法的所有实参 Object[] args = pjp.getArgs(); if (args != null && args.length > 1) { //获取第一个实参 name = (String)args[0]; } //定义目标方法执行结果 Object result = null; System.out.println("环绕通知:在目标方法执行前,输出时间:" + new Date()); //判断 if ("Tom".equals(name)) { result = pjp.proceed();//执行目标方法 } System.out.println("环绕通知:在目标方法执行后,提交事务"); return result; }
在测试方法中传入Jerroy:
@Test public void testSomeService() { String config="applicationContext1.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); SomeService proxy = (SomeService) ac.getBean("mySomeServiceImpl"); System.out.println(proxy.getClass().getName()); String result = proxy.doFirst("Jerroy",18); System.out.println("result:" + result); }
测试结果:
com.sun.proxy.$Proxy8 环绕通知:在目标方法执行前,输出时间:Fri Sep 24 20:17:03 CST 2021 环绕通知:在目标方法执行后,提交事务 result:null
可见,目标方法并未执行。
在环绕通知方法返回目标类执行结果处,添加:
System.out.println("环绕通知:在目标方法执行后,提交事务"); //控制返回结果 if (result != null) { result = "修改后结果:ABCD"; } return result;
执行测试方法,返回结果:
com.sun.proxy.$Proxy8 环绕通知:在目标方法执行前,输出时间:Fri Sep 24 20:27:51 CST 2021 执行doFirst业务方法 环绕通知:在目标方法执行后,提交事务 result:修改后结果:ABCD
异常通知是在目标方法抛出异常后执行。该注解的throwing属性用于指定所发生的异常类对象。
当然,被注解为异常通知的方法可以包含一个参数Throwable,参数名称为throwing指定的名称,表示发生的异常对象。
1.在接口中添加业务方法
public interface SomeService { void doSome(String name,Integer age); String doOther(String name,Integer age); Student doOther2(String name,Integer age); String doFirst(String name,Integer age); //异常通知方法 void doSecond(); }
2.实现业务方法
public class SomeServiceImpl implements SomeService { @Override public void doSecond() { System.out.println("执行doSecond业务方法"); } }
3.定义异常通知方法
异常通知方法的定义格式
@AfterThrowing:异常通知注解
//异常通知方法 @AfterThrowing(value="execution(* *..SomeServiceImpl.doSecond(..))" ,throwing = "ex") public void myAfterThrowing(Exception ex) { System.out.println("异常通知:在目标方法抛出异常时执行:" + ex.getMessage()); }
4.编写测试方法
@Test public void testSomeService() { String config = "applicationContext1.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); SomeService proxy = (SomeService)ac.getBean("mySomeServiceImpl"); System.out.println(proxy.getClass().getName()); proxy.doSecond(); }
测试结果:
com.sun.proxy.$Proxy9 执行doSecond业务方法
在目标方法里添加一个异常:
@Override public void doSecond() { System.out.println("执行doSecond业务方法" + 10/0); }
重新执行测试:
异常通知就好比在try...catch语句中,被catch所包含的语句。当没有异常时不会执行,当有异常时才执行。
try { SomeServiceImpl.doSecond(..) } catch (Exception ex) { //异常通知 myAfterThrowing(Exception ex) }
最终通知就是无论目标方法是否抛出异常,都会执行切面。
演示最终通知:
1.接口中添加方法
public interface SomeService { //最终通知方法 void doThird(); }
2.实现方法
public class SomeServiceImpl implements SomeService { @Override public void doThird() { System.out.println("执行doThird业务方法"); } }
3.定义切面
最终通知方法的定义格式
@After:最终通知注解
@After("execution(* *..SomeServiceImpl.doThird(..))") public void myAfter() { System.out.println("最终通知:总是会被执行的代码"); //一般做资源清除工作 }
最终通知就好比try...catch...finally语句中,finally所包含的代码
try { SomeServiceImpl.doThird(..) } catch(Exception ex) { } finally { myAfter() }
4.测试方法
@Test public void testSomeService() { String config = "applicationContext1.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); SomeService proxy = ac.getBean("mySomeServiceImpl", SomeService.class); proxy.doThird(); }
测试结果:
执行doThird业务方法 最终通知:总是会被执行的代码
现在在目标方法中添加一个异常:
@Override public void doThird() { System.out.println("执行doThird业务方法" + 10/0); }
重新执行测试方法:
当较多的通知方法使用的切入点表达式是相同的时,编写、维护都比较麻烦。为此,AspectJ提供了@Pointcut注解,用于定义execution切入点表达式。@Pointcut不是通知注解。
@Pointcut:定义和管理切入点,如果项目中有多个切入点表达式是重复的,可以使用@Pointcut
用法:
@Aspect public class MyAspectJ { /** * 定义execution切入点表达式 */ @Pointcut(value="execution(* *..SomeServiceImpl.doThird(..))") private void myEx() { } //使用 @After(value = "myEx()") public void myAfter() { System.out.println("最终通知:总是会被执行的代码"); } @AfterThrowing(value="myEx()",throwing = "ex") public void myAfterThrowing(Exception ex) { System.out.println("异常通知:在目标方法抛出异常时执行:" + ex.getMessage()); } }
测试方法:
@Test public void testSomeService() { String config = "applicationContext1.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); SomeService proxy = ac.getBean("mySomeServiceImpl", SomeService.class); proxy.doThird(); }
测试结果:
定义一个普通java类,不继承任何接口:
package com.tsccg.service.impl02; /** * @Author: TSCCG * @Date: 2021/09/19 21:30 */ public class SomeServiceImpl { public void doSome(String name,Integer age) { System.out.println("执行doSome业务方法"); } }
定义切面:
package com.tsccg.aspectj.before02; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import java.util.Date; /** * @Author: TSCCG * @Date: 2021/09/19 21:33 */ @Aspect public class MyAspectJ { @Before(value="execution(public void com.tsccg.service.impl02.SomeServiceImpl.doSome(String,Integer))") public void doLog() { System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date()); } }
定义Spring配置文件:
applicationContext1.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: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/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="mySomeServiceImpl" class="com.tsccg.service.impl02.SomeServiceImpl"/> <bean id="myAspect" class="com.tsccg.aspectj.before02.MyAspectJ"/> <aop:aspectj-autoproxy/> </beans>
定义测试方法:
@Test public void testSomeService() { String config="applicationContext1.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); SomeServiceImpl proxy = (SomeServiceImpl) ac.getBean("mySomeServiceImpl"); System.out.println(proxy.getClass().getName()); proxy.doSome("张三",20); }
测试结果:
com.tsccg.service.impl02.SomeServiceImpl$$EnhancerBySpringCGLIB$$ea9fa087 前置通知,切面功能:在方法执行之前输出执行时间:Sat Sep 25 15:03:55 CST 2021 执行doSome业务方法
由结果可见,在目标对象没有接口的情况下,使用的是cglib动态代理。
目标方法有接口的情况下,仍然可以使用cglib动态代理
如果在目标方法中有接口的情况下,也使用cglib动态代理,就需要在配置文件里的自动代理生成器中指定proxy-target-class属性值为true。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" ... <bean id="mySomeServiceImpl" class="com.tsccg.ba02.SomeServiceImpl"/> <bean id="myAspect" class="com.tsccg.ba02.MyAspectJ"/> <!-- 指定proxy-target-class属性值为true --> <aop:aspectj-autoproxy proxy-target-class="true"/> </beans>
接口:
package com.tsccg.ba02; public interface SomeService { void doSome(String name,Integer age); }
接口实现类:
public class SomeServiceImpl implements SomeService{ @Override public void doSome(String name, Integer age) { System.out.println("执行doSome业务方法"); } }
定义切面:
@Aspect public class MyAspectJ { @Before(value="execution(public void com.tsccg.ba02.SomeServiceImpl.doSome(String,Integer))") public void doLog() { System.out.println("前置通知,切面功能:在方法执行之前输出执行时间:" + new Date()); } }
配置文件:
applicationContext2.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: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/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="mySomeServiceImpl" class="com.tsccg.ba02.SomeServiceImpl"/> <bean id="myAspect" class="com.tsccg.ba02.MyAspectJ"/> <!-- 指定proxy-target-class属性值为true --> <aop:aspectj-autoproxy proxy-target-class="true"/> </beans>
测试方法:
@Test public void testSomeService() { String config = "applicationContext2.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); SomeService proxy = ac.getBean("mySomeServiceImpl", SomeService.class); System.out.println(proxy.getClass().getName()); proxy.doSome("张三",30); }
测试结果:
com.tsccg.ba02.SomeServiceImpl$$EnhancerBySpringCGLIB$$99435831 前置通知,切面功能:在方法执行之前输出执行时间:Sat Sep 25 15:47:10 CST 2021 执行doSome业务方法
Spring整合MyBatis就是把Spring框架和MyBatis框架集成在一起,像一个框架一样来使用。
使用的技术是IoC。IoC可以创建对象,可以把MyBatis框架中的对象交给Spring统一创建,开发人员从Spring中获取对象。这样的好处就是开发人员不用同时面对两个或多个框架了,只需要面对Spring一个框架。
MyBatis使用基本步骤:
添加MyBatis和数据库驱动依赖
定义dao接口:StudentDao
创建mapper文件,编写sql语句:StudentDao.xml
创建MyBatis的主配置文件:mybatis.xml
创建dao的代理对象:StudentDao dao = sqlSession.getMapper(StudentDao.class);
调用接口中的方法:List<Studetn> students = dao.selectStudents();
获取dao对象的步骤:
InputStream in = Resources.getResourceAsStream("mybatis.xml");//读取mybatis主配置文件 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(in);//创建SqlSessionFactory对象 SqlSession sqlSession = factory.openSession();//创建SqlSesion对象 StudentDao dao = sqlSession.getMapper(StudentDao.class);//获取dao对象 int count = dao.countStudent();//使用dao对象调用接口中的方法 sqlSession.close(); System.out.println("count:" + count);
要获取dao对象,需要SqlSession对象,就需要SqlSessionFactory对象来创建SqlSession对象,从而需要创建SqlSessionFactory对象,而创建SqlSessionFactory对象需要读取mybatis的主配置文件。
主配置文件中主要包括:
数据库信息:
<environment id="mydev"> <!-- transactionManager:mybatis的事务类型 type:JDBC(表示使用JDBC中的Connection对象的commit,rollback做事务处理) --> <transactionManager type="JDBC"/> <!-- dataSource:表示数据源,连接数据库的 type:表示数据源的类型,POOLED表示使用连接池 --> <dataSource type="POOLED"> <!-- driver,url,username,password都是固定的,不能自定义 --> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/db_mybatis?useUnicode=true&serverTimezone=GMT&useSSL=false&characterEncoding=utf-8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment>
mapper文件所在位置:
<mappers> <!-- 一个mapper标签指定一个文件的位置 从类路径(target/classes)开始的路径信息 --> <mapper resource="com/tsccg/dao/StudentDao.xml"/> </mappers>
主配置文件中,连接数据库默认使用的是POOLED连接池,这个连接池是mybatis自带的,性能较弱,支撑不了大型项目的运作。
所以,我们在开发大型项目时,会使用性能更佳的独立的连接池类来替换原有的连接池,把连接池类也交给Spring创建。
综上,我们最终需要让Spring创建以下对象:
我们学习Spring整合MyBatis主要就是学习以上三个对象的创建语法。
<dataSource type="POOLED">
drop database if exists `db_mybatis`; create database `db_mybatis`; use `db_mybatis`; create table `t_student`( `id` int primary key, `name` varchar(255) default null, `email` varchar(255) default null, `age` int default null )engine=InnoDB default charset=utf8; insert into `t_student`(`id`,`name`,`email`,`age`) values (1001,'张三','zhangsan@qq.com',20), (1002,'李四','lisi@qq.com',21), (1003,'王五','wangwu@qq.com',22); commit; select * from `t_student`;
新建一个maven子模块spring-06-mybatis
在父模块的pom.xml中添加所需的各种依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!--指定自身坐标--> <groupId>com.tsccg</groupId> <artifactId>spring-project</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <!--指定子模块--> <modules> <module>spring-01</module> <module>spring-04-dynamicAgent</module> <module>spring-06-mybatis</module> </modules> <!--指定项目jdk版本--> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <!--添加依赖--> <dependencies> <!-- 单元测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- Spring核心:IoC --> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.6.RELEASE</version> </dependency> <!-- AspectJ:AOP --> <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.9.RELEASE</version> </dependency> <!-- 做Spring事务需要用到的 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!-- 做Spring事务需要用到的 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!-- mybatis依赖 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.1</version> </dependency> <!-- mybatis和spring集成的依赖,由mybatis提供 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.1</version> </dependency> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.9</version> </dependency> <!-- 阿里公司的数据库连接池:德鲁伊 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> </dependencies> <!--插件--> <build> <resources> <!-- 目的是把src/main/java目录中的mapper文件包含到输出结果中,输出到target/classes目录中 --> <resource> <directory>src/main/java</directory><!--mapper所在的目录--> <includes><!--包括目录下的.properties,.xml 文件都会扫描到--> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> <!-- 目的是把src/main/resources目录中的xml文件包含到输出结果中,输出到target/classes目录中 --> <resource> <directory>src/main/resources</directory><!--主配置文件所在的目录--> <includes><!--包括目录下的.properties,.xml 文件都会扫描到--> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources> </build> </project>
在main/java目录下新建【com.tsccg.entity.Student】实体类
package com.tsccg.entity; /** * @Author: TSCCG * @Date: 2021/09/25 19:54 * 数据库实体类 */ public class Student { private Integer id; private String name; private String email; private Integer age; public Student() { } public Student(Integer id, String name, String email, Integer age) { this.id = id; this.name = name; this.email = email; this.age = age; } 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 getEmail() { return email; } public void setEmail(String email) { this.email = email; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", email='" + email + '\'' + ", age=" + age + '}'; } }
1.创建dao接口:
在main/java目录下新建【com.tsccg.dao.StudentDao】接口,定义三个抽象方法,分别是增删查
package com.tsccg.dao; import com.tsccg.entity.Student; import java.util.List; /** * @Author: TSCCG * @Date: 2021/09/25 19:59 */ public interface StudentDao { /** * 添加学生信息 * @param student 包含学生信息的实体类对象 * @return 返回处理结果数目 */ int insertStudent(Student student); /** * 查询所有学生信息 * @return 返回包含所有学生信息的Student对象集合 */ List<Student> selectAllStudent(); /** * 删除学生信息 * @param id 学生id * @return 返回处理结果数目 */ int deleteStudent(@Param("id") Integer id); }
2.创建mapper文件:
在StudentDao接口同级目录下,新建一个mapper文件【StudentDao.xml】文件,编写sql代码
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.tsccg.dao.StudentDao"> <!--增--> <insert id="insertStudent"> insert into t_student(id,name,email,age) values(#{id},#{name},#{email},#{age}); </insert> <!--查--> <select id="selectAllStudent" resultType="com.tsccg.entity.Student"> select id,name,email,age from t_student </select> <!--删--> <delete id="deleteStudent" > delete from t_student where id = #{id} </delete> </mapper>
在main/resources目录下新建mybatis主配置文件【mybatis.xml】
因为我们要使用阿里的druid连接池代替mybatis自带的连接池,且交给Spring来管理,所以在这里不需要添加dataSource标签。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- settings:控制mybatis全局行为 --> <settings> <!-- 控制mybatis输出日志 --> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> <!--设置实体类别名--> <typeAliases> <!--name:实体类所在包名,设置之后,实体类的类名就是别名--> <package name="com.tsccg.entity"/> </typeAliases> <!-- 指定mapper文件的位置--> <mappers> <!-- name:包名,这个包名下所有的mapper.xml文件一次都能加载 --> <package name="com.tsccg.dao"/> </mappers> </configuration>
1.定义service接口
在main/java目录下新建【com.tsccg.service.StudentService】接口
package com.tsccg.service; import com.tsccg.entity.Student; import java.util.List; /** * @Author: TSCCG * @Date: 2021/09/25 20:37 */ public interface StudentService { int addStudent(Student student); List<Student> findAllStudent(); int removeStudent(Integer id); }
2.创建service接口实现类
在main/java目录下新建【com.tsccg.service.impl.StudentServiceImpl】,实现StudentService接口
package com.tsccg.service.impl01; import com.tsccg.dao.StudentDao; import com.tsccg.entity.Student; import com.tsccg.service.StudentService; import java.util.List; /** * @Author: TSCCG * @Date: 2021/09/25 20:39 */ public class StudentServiceImpl implements StudentService { //定义一个dao类型属性 private StudentDao studentDao; /** * 定义set方法,使用set注入来给dao属性赋值 */ public void setStudentDao(StudentDao studentDao) { this.studentDao = studentDao; } @Override public int addStudent(Student student) { return studentDao.insertStudent(student); } @Override public List<Student> findAllStudent() { return studentDao.selectAllStudent(); } @Override public int removeStudent(Integer id) { return studentDao.deleteStudent(id); } }
创建Spring的配置文件:在Spring配置文件中声明mybatis对象,把mybatis对象的管理交给Spring。
在main/resources目录下新建spring配置文件【applicationContext.xml】。
在spring配置文件中声明各种mybatis对象:
我们这里使用阿里的druid连接池来代替mybatis自带的POOLED
druid官方地址:https://github.com/alibaba/druid
druid官方中文帮助文档:https://github.com/alibaba/druid/wiki/常见问题
DruidDataSource基本配置:
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!--使用set注入给DruidDataSource提供连接数据库信息--> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/db_mybatis?useSSL=false&characterEncoding=utf8" /> <property name="username" value="root" /> <property name="password" value="123456" /> <!--maxActive:连接池最多容纳的连接对象数目,默认20个--> <property name="maxActive" value="20" /> </bean>
注意:
使用属性配置文件保存连接数据库信息
在实际开发中,我们会将连接数据库的信息放到一个独立的属性配置文件里
1)在main/resources目录下新建一个属性配置文件jdbc.properties
jdbc.mysql.driver=com.mysql.jdbc.Driver jdbc.mysql.url=jdbc:mysql://localhost:3306/db_mybatis?characterEncoding=utf8&useSSL=false jdbc.mysql.username=root jdbc.mysql.password=123456 jdbc.mysql.maxActive=20
2)在spring配置文件中指定属性配置文件的位置,添加如下语句:
<context:property-placeholder location="classpath:jdbc.properties"/>
3)在spring配置文件中读取属性配置文件信息
语法:${key}
<context:property-placeholder location="classpath:jdbc.properties"/> <!-- 声明数据源DataSource --> <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!--使用set注入给DruidDataSource提供连接数据库信息--> <property name="driverClassName" value="${jdbc.mysql.driver}"/> <property name="url" value="${jdbc.mysql.url}" /> <property name="username" value="${jdbc.mysql.username}" /> <property name="password" value="${jdbc.mysql.password}" /> <!--maxActive:连接池最多容纳的连接对象数目,默认20个--> <property name="maxActive" value="${jdbc.mysql.maxActive}" /> </bean>
在这里声明的是mybatis提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory的对象。
创建SqlSessionFactory类对象需要读取mybatis主配置文件的信息,而mybatis主配置文件主要由数据源DataSource和指定mapper文件所在位置的路径组成的。
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--set注入,把数据库连接池赋给了dataSource属性--> <property name="dataSource" ref="myDataSource"/> <!--指定mybatis主配置文件的位置 configLocation属性是Resource类型,专门用于读取配置文件 使用value进行赋值,指定mybatis.xml文件编译后的路径,使用classpath:表示文件的位置 --> <property name="configLocation" value="classpath:mybatis.xml"/> </bean>
创建dao对象,可以使用SqlSession类的getMapper()方法,同时需要传入dao接口的class属性值
StudentDao dao = sqlSession.getMapper(StudentDao.class);//获取dao代理对象
而在Spring配置文件中,可以通过声明MapperScannerConfigurer对象,在内部调用getMapper()方法来生成指定包中每个dao接口的代理对象。
<!--不需要指定id属性--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--指定上面SqlSessionFactory对象的bean的id--> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> <!--指定dao接口所在包名 MapperScannerConfigurer会扫描这个包名下的所有接口, 然后把每个接口都执行一次getMapper()方法,得到每个接口的dao对象, 然后把创建好的dao对象都放入到Spring的容器中,dao对象的名字是各自对应dao接口的首字母小写 --> <property name="basePackage" value="com.tsccg.dao"/> <!--<property name="basePackage" value="com.tsccg.dao,com.tsccg.dao2"/>--> </bean>
<bean id="studentService" class="com.tsccg.service.impl01.StudentServiceImpl"> <!--这里引用的是步骤3中自动创建的dao代理对象--> <property name="studentDao" ref="studentDao"/> </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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 声明数据源DataSource --> <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!--使用set注入给DruidDataSource提供连接数据库信息--> <property name="driverClassName" value="${jdbc.mysql.driver}"/> <property name="url" value="${jdbc.mysql.url}" /> <property name="username" value="${jdbc.mysql.username}" /> <property name="password" value="${jdbc.mysql.password}" /> <!--maxActive:连接池最多容纳的连接对象数目,默认20个--> <property name="maxActive" value="${jdbc.mysql.maxActive}" /> </bean> <!--声明SqlSessionFactory对象--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="myDataSource"/> <property name="configLocation" value="classpath:mybatis.xml"/> </bean> <!--声明dao代理对象--> <!--不需要指定id属性--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--指定SqlSessionFactory对象的id--> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> <!--指定dao接口所在包名 MapperScannerConfigurer会扫描这个包名下的所有接口, 然后把每个接口都执行一次getMapper()方法,得到每个接口的dao对象, 然后把创建好的dao对象都放入到Spring的容器中,dao对象的名字是各自对应dao接口的首字母小写 --> <property name="basePackage" value="com.tsccg.dao"/> </bean> <!--声明service对象--> <bean id="studentService" class="com.tsccg.service.impl01.StudentServiceImpl"> <property name="studentDao" ref="studentDao"/> </bean> </beans>
编写测试代码,获取service对象,通过service调用dao完成数据库的访问
package com.tsccg; import com.tsccg.dao.StudentDao; import com.tsccg.entity.Student; import com.tsccg.service.StudentService; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import java.util.List; /** * @Author: TSCCG * @Date: 2021/09/27 15:37 */ public class Test01 { @Test public void testSelectAll() { String config = "applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); //直接从Spring容器中拿dao代理对象 StudentService service = ac.getBean("studentService", StudentService.class); List<Student> studentList = service.findAllStudent(); for (Student student : studentList) { System.out.println(student); } } @Test public void testAdd() { String config = "applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); StudentService service = ac.getBean("studentService", StudentService.class); Student student = new Student(1004,"赵六","zhaoliu@qq.com",28); //使用service类调用dao对象来访问数据库 int result = service.addStudent(student); //会自动执行commit(); System.out.println(result > 0 ? "插入成功":"插入失败"); } @Test public void testRemove() { String config = "applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); StudentService service = ac.getBean("studentService", StudentService.class); int result = service.removeStudent(1004); System.out.println(result > 0 ? "删除成功":"删除失败"); } }
测试结果:
测试查询方法:
测试插入方法:
测试删除方法:
事务是一组sql语句的集合,其中,sql语句可能是insert、update、delete、select。
我们希望在一个事务中,所有的sql语句能同时执行成功或者同时执行失败。让这些sql语句是一致的,是作为一个整体来执行的。
当访问数据库时,涉及到多个表,或者是多条不同的sql语句如insert、update和delete,我就需要保证这些sql语句全部执行成功才能实现我的功能,或者全部执行失败来保证原本数据库中的数据不受影响。
在java代码中编写程序,需要控制事务,那么事务应该在程序的哪里使用呢?
我们应该在service类的业务方法中使用事务,因为业务方法往往会调用多个dao方法,执行多条sql语句。
1.JDBC访问数据库,处理事务:
conn.commit(); conn.rollback();
2.MyBatis访问数据库,处理事务:
sqlSession.commit(); sqlSession.rollback();
3.hibernate访问数据库,处理事务:
session.commit(); session.rollback();
不足之处:
解决方法:
Spring提供了一种处理事务的统一模型,能够使用统一步骤,统一方式来完成不同数据库访问技术的事务处理。
我们在Spring中,是使用Spring事务处理模型来处理事务的。
Spring事务处理模型其实就是Spring内部写好的一系列代码,使用步骤是固定的,我们只需要把处理事务需要用到的信息提供给Spring,Spring就能处理事务的提交、回滚了。几乎不用我们自己编写事务相关的代码。
Spring的事务管理,主要用到两个事务相关的接口
Spring事务处理模型中,通过事务管理器来完成事务的提交、回滚,以及事务的状态信息。
事务管理器是PlatformTransactionManager接口实现类对象,接口定义提交、回滚方法:
PlatformTransactionManager接口有两个常用的实现类:对应不同的数据库访问技术
我们需要告诉Spring使用哪个事务管理器:
可以在Spring配置文件中,声明数据库访问技术对应的事务管理器接口实现类,使用<bean>
声明即可,如:
<!--使用MyBatis数据库访问技术--> <bean id="xxx" class="...DataSourceTransactionManager"> </bean>
我们还需要告诉Spring,业务方法需要什么样的事务,说明事务的类型。
而事务定义接口TransactionDefinition就定义了事务描述相关的三类常量:事务隔离级别、事务传播方式和事务默认超时时限,以及对它们的操作。
1)定义事务隔离级别常量:有4个值
这些常量都是以ISOLATION_
开头,如:ISOLATION_READ_COMMITTED
2)定义事务传播行为:指的是当一个事务中的方法被另一个事务中的方法调用时,这个被调用的方法处理事务关系的方式
如:在A事务中的方法doSome()调用B事务中的方法doOther()时,doOther()是按事务A运行,还是为自己开启一个新事务B运行,这就是由doOther的事务传播行为所决定的。
事务传播行为是加在方法上的。
Spring定义了7个事务传播行为常量:只有前三个是常用的,其余了解即可
a)PROPAGATION_REQUIRED
表示指定的当前方法必须在事务中执行。如果当前存在事务,则使当前方法加入到当前事务中;如果当前没有事务,则使当前方法新建一个事务。
比如:现在有doSome()和doOther()两个方法:
现需要在doSome()方法中调用doOther()方法。
在doOther方法上添加PROPAGATION_REQUIRED传播行为。
打个生活中的例子,就好比蹭基友热点,如果他开启了热点(有事务),我就连他的热点上网(传播);如果他没开热点(没有事务),我就开自己的流量上网(自己开启事务)。
b)PROPAGATION_SUPPORTS
表示指定的当前方法支持当前事务,但如果当前没有事务,就以非事务形式执行。
还是doSome()和doOther方法:用PROPAGATION_SUPPORTS指定doOther方法
You dump! I dump!
c)PROPAGATION_REQUIRES_NEW
表示指定的方法总是新开启一个事务,如果当前存在事务,则将当前事务挂起,直到新开启的事务执行完毕。
3)定义默认事务超时时限:
常量TIMEOUT_DEFAULT定义了事务底层默认的超时时限,表示一个事务方法最长的执行时间。
如果方法执行时超过了指定时限,那么事务就进行回滚。单位是秒,整数值,默认是-1。
注意:影响事务超时时限的因素比较多,且超时的时间计算点较复杂。所以,该值一般用默认的就行了,不用设置。
1)管理事务的是事务管理器
2)Spring的事务管理是一个统一模型,使用步骤固定:
<bean id="xxx" class="...DataSourceTransactionManager"></bean>
实例项目:模拟用户购买商品
本项目中要实现模拟用户下订单,购买商品的功能。
分为两个步骤:
本次项目需要创建两个数据库表:t_sale(销售表),t_commodity(商品表)
t_sale 销售表
drop table if exists `t_sale`; create table `t_sale`( `id` int primary key auto_increment,#销售记录编号 `cid` int not null,#商品编号 `nums` int#购买商品数量 );
t_commodity 商品表
drop table if exists `t_commodity`; create table `t_commodity`( `id` int primary key,#商品编号 `name` varchar(255),#商品名称 `count` int,#商品货存 `price` float#商品价格 ); insert into `t_commodity`(id,name,count,price) values (1001,'酱烧猪蹄',10,30), (1002,'红烧牛肉',20,20); select * from `t_commodity`;
新建子maven模块spring-07-transaction,所需依赖在父模块中已添加过,如下:
<dependencies> <!-- 单元测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- Spring核心:IoC --> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.6.RELEASE</version> </dependency> <!-- AspectJ:AOP --> <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.9.RELEASE</version> </dependency> <!-- 做Spring事务需要用到的 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!-- 做Spring事务需要用到的 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!-- mybatis依赖 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.1</version> </dependency> <!-- mybatis和spring集成的依赖,由mybatis提供 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.1</version> </dependency> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.9</version> </dependency> <!-- 阿里公司的数据库连接池:德鲁伊 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> </dependencies>
在main/java/com/tsccg/entity目录下,分别创建销售表和商品表对应的实体类
1)Sale:销售表实体类
package com.tsccg.entity; /** * @Author: TSCCG * @Date: 2021/09/29 17:41 * 销售表对应实体类 */ public class Sale { private Integer id;//销售记录编号 private Integer cid;//商品编号 private Integer nums;//商品购买数量 public Sale() { } public Sale(Integer id, Integer cid, Integer nums) { this.id = id; this.cid = cid; this.nums = nums; } ...get、set方法 }
2)Commodity:商品表实体类
package com.tsccg.entity; /** * @Author: TSCCG * @Date: 2021/09/29 17:43 * 商品表对应实体类 */ public class Commodity { private Integer id;//商品编号 private String name;//商品名称 private Integer count;//商品货存 private Float price;//商品价格 public Commodity() { } public Commodity(Integer id, String name, Integer count, Float price) { this.id = id; this.name = name; this.count = count; this.price = price; } ...get、set方法 }
在main/java/com/tsccg/dao目录下,分别创建SaleDao和CommodityDao接口,并分别为其创建mapper文件。
SaleDao接口:
public interface SaleDao { //添加销售记录 public int insertSale(Sale sale); //删除销售记录 public int deleteSale(Integer id); }
SaleDao.xml文件:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.tsccg.dao.SaleDao"> <insert id="insertSale"> insert into t_sale(cid,nums) values(#{cid},#{nums}) </insert> <delete id="deleteSale"> delete from t_sale where id = #{id} </delete> </mapper>
CommodityDao接口:
public interface CommodityDao { /** * 根据商品编号查询指定商品信息 * @param id 商品编号 * @return 指定商品信息 */ Commodity selectCommodity(Integer id); /** * 更新库存 * @param comm 表示本次用户购买的商品信息 * @return 返回处理结果数 */ int updateCount(Commodity comm); }
CommodityDao.xml文件:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.tsccg.dao.CommodityDao"> <select id="selectCommodity" resultType="com.tsccg.entity.Commodity"> select id,name,count,price from t_commodity where id = #{id} </select> <update id="updateCount"> update t_commodity set count = count - #{count} where id = #{id} </update> </mapper>
在main/resources目录下,新建MyBatis主配置文件:mybatis.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 指定mapper文件的位置--> <mappers> <!-- name:包名,这个包名下所有的mapper.xml文件一次都能加载 --> <package name="com.tsccg.dao"/> </mappers> </configuration>
在main/java/com/tsccg/exception目录下新建一个运行时异常类NotEnoughException
自定义异常类的作用是处理商品库存不足时的情况。
package com.tsccg.exception; /** * @Author: TSCCG * @Date: 2021/09/29 19:32 * 自定义运行时异常类 * 当商品库存不足时,抛出该异常 */ public class NotEnoughException extends RuntimeException{ public NotEnoughException() { } public NotEnoughException(String message) { super(message); } }
在main/java/com/tsccg/service目录下新建BuyCommodityService接口
public interface BuyCommodityService { /** * 购买商品服务 * @param cId 商品编号 * @param nums 购买商品数量 */ void buy(Integer cId,Integer nums); }
在同目录下新建impl01包,编写其实现类BuyCommodityServiceImpl
package com.tsccg.service.impl01; import com.tsccg.dao.CommodityDao; import com.tsccg.dao.SaleDao; import com.tsccg.entity.Commodity; import com.tsccg.entity.Sale; import com.tsccg.exception.NotEnoughException; import com.tsccg.service.BuyCommodityService; /** * @Author: TSCCG * @Date: 2021/09/29 19:24 */ public class BuyCommodityServiceImpl implements BuyCommodityService { //定义两个dao属性 private SaleDao saleDao; private CommodityDao commDao; @Override public void buy(Integer cId, Integer nums) { //1.添加销售记录 Sale sale = new Sale();//创建销售表实体类 sale.setCid(cId); sale.setNums(nums); int result1 = saleDao.insertSale(sale); System.out.println(result1 > 0 ? "添加销售记录成功" : "添加销售记录失败"); //2.进行验证 Commodity oldComm = commDao.selectCommodity(cId);//查询指定编号的商品信息 if (oldComm == null) { throw new NullPointerException("编号为" + cId + "的商品不存在"); } else if (oldComm.getCount() < nums) { throw new NotEnoughException("编号为" + cId + "的商品库存不足"); } //3.更新商品库存 Commodity newComm = new Commodity(); newComm.setId(cId); newComm.setCount(nums); int result2 = commDao.updateCount(newComm); System.out.println(result2 > 0 ? "更新商品库存成功" : "更新商品库存失败"); } //通过set注入给saleDao属性赋值 public void setSaleDao(SaleDao saleDao) { this.saleDao = saleDao; } //通过set注入给commDao属性赋值 public void setCommDao(CommodityDao commDao) { this.commDao = commDao; } }
在main/resources目录下新建spring配置文件applicatonContext.xml和jdbc属性配置文件jdbc.properties
jdbc属性配置文件:jdbc.properties
jdbc.mysql.driver=com.mysql.jdbc.Driver jdbc.mysql.url=jdbc:mysql://localhost:3306/db_mybatis?characterEncoding=utf8&useSSL=false jdbc.mysql.username=root jdbc.mysql.password=123456 jdbc.mysql.maxActive=20
spring配置文件:applicationContext.xml
在Spring配置文件中声明mybatis对象,把mybatis对象的管理交给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" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 声明数据源DataSource --> <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!--使用set注入给DruidDataSource提供连接数据库信息--> <property name="driverClassName" value="${jdbc.mysql.driver}"/> <property name="url" value="${jdbc.mysql.url}"/> <property name="username" value="${jdbc.mysql.username}"/> <property name="password" value="${jdbc.mysql.password}"/> <!--maxActive:连接池最多容纳的连接对象数目,默认20个--> <property name="maxActive" value="${jdbc.mysql.maxActive}"/> </bean> <!--声明SqlSessionFactory对象--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="myDataSource"/> <property name="configLocation" value="classpath:mybatis.xml"/> </bean> <!--声明dao代理对象--> <!--不需要指定id属性值--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--指定SqlSessionFactory对象的id--> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <!--指定dao接口所在包名 MapperScannerConfigurer会扫描这个包名下的所有接口, 然后把每个接口都执行一次getMapper()方法,得到每个接口的dao对象, 然后把创建好的dao对象都放入到Spring的容器中,dao对象的名字是各自对应dao接口的首字母小写 --> <property name="basePackage" value="com.tsccg.dao"/> </bean> <!--声明service对象--> <bean id="buyService" class="com.tsccg.service.impl01.BuyCommodityServiceImpl"> <property name="saleDao" ref="saleDao"/> <property name="commDao" ref="commodityDao"/> </bean> </beans>
测试1:购买不存在的商品
@Test public void test01() { String config = "applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class); buyService.buy(1003,5); }
测试2:购买过量的商品
@Test public void test01() { String config = "applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class); buyService.buy(1001,100); }
测试3:购买合适数量的商品
@Test public void test01() { String config = "applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class); buyService.buy(1001,5); }
以上测试中,当发生异常时,商品货存不会更新,但销售记录仍会添加。也就是发生了“做假账”的行为。
为了避免这种情况发生,我们需要使用到事务来保证操作的一致性。同时,我们不希望在源代码上添加额外的非业务功能代码,所以,我们就要使用Spring的事务管理模型来处理事务。
Spring框架中提供了两种事务处理方案:
Spring框架使用自身的aop来给业务方法增加事务功能,使用@Transactional
注解。适合中小项目。
@Transactional
注解的属性Propagation.REQUIRED
Isolation.DEFAULT
注意:
1.在spring配置文件中声明事务管理器对象
<!--声明事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--连接数据库,指定数据源--> <property name="dataSource" ref="myDataSource"/> </bean>
2.在spring配置文件中开启事务注解驱动
告诉Spring框架,使用注解的方式管理事务。
<!--开启事务注解驱动 告诉Spring使用注解管理事务,创建代理对象 annotation-driven:引入tx的那个 transaction-manager:属性值为事务管理器bean的id --> <tx:annotation-driven transaction-manager="transactionManager"/>
开启之后,Spring会在内部使用AOP机制,创建@Transactional所在的类的代理对象,给业务方法加入事务的功能。
怎么加的呢?其实就是使用@Around环绕通知注解,在业务方法之前开启事务,在业务方法执行之后进行提交或者回滚事务。
@Around("切入点表达式") Object myFunction(){ Spirng开启事务 try{ buy(1001,5); 事务管理器.commit(); } catch(Exception e) { 事务管理器.rollback(); } }
3.在业务方法上添加@Transactional注解
public class BuyCommodityServiceImpl implements BuyCommodityService { private SaleDao saleDao; private CommodityDao commDao; //添加注解 @Transactional( //指定传播行为 propagation = Propagation.REQUIRED, //指定隔离级别 isolation = Isolation.DEFAULT, //指定需要回滚的异常 rollbackFor = {NullPointerException.class,NotEnoughException.class} ) @Override public void buy(Integer cId, Integer nums) { System.out.println("====buy方法执行开始===="); //添加销售记录 Sale sale = new Sale(); sale.setCid(cId); sale.setNums(nums); int result1 = saleDao.insertSale(sale); System.out.println(result1 > 0 ? "添加销售记录成功" : "添加销售记录失败"); //进行验证 Commodity oldComm = commDao.selectCommodity(cId); if (oldComm == null) { throw new NullPointerException("编号为" + cId + "的商品不存在"); } else if (oldComm.getCount() < nums) { throw new NotEnoughException("编号为" + cId + "的商品库存不足"); } //更新商品库存 Commodity newComm = new Commodity(); newComm.setId(cId); newComm.setCount(nums); int result2 = commDao.updateCount(newComm); System.out.println(result2 > 0 ? "更新商品库存成功" : "更新商品库存失败"); System.out.println("====buy方法执行结束===="); } public void setSaleDao(SaleDao saleDao) { this.saleDao = saleDao; } public void setCommDao(CommodityDao commDao) { this.commDao = commDao; } }
4.进行测试
1)商品编号和数量都合理,没有异常发生
@Test public void test02() { String config = "applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class); System.out.println("buyService是代理对象:" + buyService.getClass().getName()); buyService.buy(1002,5); }
结果:
buyService是代理对象:com.sun.proxy.$Proxy17 ====buy方法执行开始==== 添加销售记录成功 更新商品库存成功 ====buy方法执行结束====
功能正常
2)商品编号不存在,抛出异常
@Test public void test02() { String config = "applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class); System.out.println("buyService是代理对象:" + buyService.getClass().getName()); buyService.buy(1005,5); }
结果:
buyService是代理对象:com.sun.proxy.$Proxy17 ====buy方法执行开始==== 添加销售记录成功 java.lang.NullPointerException: 编号为1005的商品不存在 at ... at ... ...
由测试结果可见,当业务方法开始执行后:
saleDao.insertSale(sale);
思路:
销售表的id字段是设置为自增的auto_increment
,每个数字只能使用一次。
在发生异常前,向销售表中插入了一条销售记录,占用了一个数字;
发生异常后,如果执行了回滚操作,那么插入操作会被撤销,所占用的数字却是不可再被复用的。
我们再来执行一次正常的购买操作:
@Test public void test02() { String config = "applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class); System.out.println("buyService是代理对象:" + buyService.getClass().getName()); buyService.buy(1001,5); }
销售表中id为2的记录被使用过了,由此可证,事务起了作用
mysql> select * from t_sale; +----+------+------+ | id | cid | nums | +----+------+------+ | 1 | 1002 | 5 | | 3 | 1001 | 5 | +----+------+------+ 2 rows in set (0.00 sec)
当只有@Transaction注解,没有设定其属性值时:
public class BuyCommodityServiceImpl implements BuyCommodityService { private SaleDao saleDao; private CommodityDao commDao; // @Transactional( // //传播行为 // propagation = Propagation.REQUIRED, // //隔离级别 // isolation = Isolation.DEFAULT, // //指定需要回滚的异常 // rollbackFor = {NullPointerException.class,NotEnoughException.class} // ) @Transactional//属性可省略不写,全部按默认 @Override public void buy(Integer cId, Integer nums) { System.out.println("====buy方法执行开始===="); //添加销售记录 Sale sale = new Sale(); sale.setCid(cId); sale.setNums(nums); int result1 = saleDao.insertSale(sale); System.out.println(result1 > 0 ? "添加销售记录成功" : "添加销售记录失败"); ... }
测试:购买过量商品
@Test public void test02() { String config = "applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); BuyCommodityService buyService = ac.getBean("buyService", BuyCommodityService.class); System.out.println("buyService是代理对象:" + buyService.getClass().getName()); buyService.buy(1001,500);//过量商品 }
由测试结果可见,当抛出我们自定义的运行时异常时,即使没有提前指定,也会执行回滚。
这种方案是用AspectJ框架实现的AOP功能,在Spirng配置文件中向指定业务方法织入事务功能。
这种方式能够使得业务方法和事务功能完全分离,耦合度低。
这种方案适合有很多类,很多方法,需要大量配置事务的大型项目。同时,由于每个目标类都需要配置事务代理,当目标类较多时,配置文件会非常臃肿。
实现步骤:
1.添加AspectJ依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.9.RELEASE</version> </dependency>
2.声明事务管理器对象
不管使用哪种方案,都必须声明事务管理器对象
<!--声明事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--连接数据库,指定数据源--> <property name="dataSource" ref="myDataSource"/> </bean>
3.声明事务方法的事务属性(声明事务类型)
<!--声明业务方法的事务类型 tx:advice: tx:表示事务 advice选末尾是tx那个 id:自定义名称,表示当前<tx:advicd></tx:advice>之间的配置内容 transaction-manager:属性值是事务管理器对象bean的id值 --> <tx:advice id="myAdvice" transaction-manager="transactionManager"> <!--配置事务的属性--> <tx:attributes> <!--tx:method:给具体的某个方法指定事务属性,可以有多个 name:完整的业务方法名,不含有包名和类名,可以使用通配符 --> <tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT" rollback-for="java.lang.NullPointerException, com.tsccg.exception.NotEnoughException"/> <!--可以使用通配符指定多个业务方法的事务属性--> <!--指定所有增加方法:add--> <tx:method name="add*" propagation="SUPPORTS"/> <!--指定所有删除方法:remove delete--> <tx:method name="remove*" propagation="REQUIRES_NEW"/> <!--指定所有查询方法:search、find、query--> <tx:method name="search*" propagation="REQUIRES_NEW"/> <!--指定所有更新方法:update、modify--> <tx:method name="update*" propagation="SUPPORTS"/> <!--只有*:指定以上所有业务方法以外的方法--> <tx:method name="*" read-only="true"/> </tx:attributes> </tx:advice>
4.配置AOP
指定将配置好的事务功能,织入给哪些类,哪些方法
<!--AOP配置:通知应用的切入点--> <aop:config> <!--定义切入点表达式 id:自定义切入点表达式的名称,唯一值 expression:切入点表达式,指定哪些类哪些方法要使用事务,aspectJ会创建其代理对象 com.service.AddService com.crm.service.RemoveService com.tsccg.service.impl01.BuyCommodityServiceImpl execution(* *..service..*.*(..)):指定所有service中的所有类的所有方法 --> <aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))"/> <!--配置增强器:关联advice和pointcut,将配置好的事务功能,织入指定的业务方法 advice-ref:上面声明的业务方法的事务类型 pointcut-ref:切入点表达式的id --> <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt"/> </aop:config>
5.测试
1)正常购买一件商品,无异常抛出
2)购买过量商品,抛出异常
在web项目中使用Spring框架,首先要解决在Servlet中获取到Spring容器的问题。只要在Servlet中能获取到Spring容器,就能从容器中获取到service对象,从而调用dao,实现访问数据库。
下面将上面Spring整合MyBatis的例子修改为web项目
新建一个web类型的maven子模块:spring-09-web,使用模板:maven-archetype-webapp
然后完善maven结构目录
main|--java |--resources |--webapp
复制Spring整合mybatis项目中的依赖和基本配置,并新增servlet和jsp依赖
完整pom.xml配置内容如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.tsccg</groupId> <artifactId>spring-09-web</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <!--添加依赖--> <dependencies> <!-- 单元测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- Spring核心:IoC --> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.6.RELEASE</version> </dependency> <!-- AspectJ:AOP --> <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.9.RELEASE</version> </dependency> <!-- 做Spring事务需要用到的 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!-- 做Spring事务需要用到的 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!-- mybatis依赖 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.1</version> </dependency> <!-- mybatis和spring集成的依赖,由mybatis提供 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.1</version> </dependency> <!-- mysql驱动 --> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <!-- 阿里公司的数据库连接池:德鲁伊 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <!--新增servlet依赖 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!--新增jsp依赖 --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.1</version> <scope>provided</scope> </dependency> </dependencies> <!--配置插件--> <build> <resources> <!-- 目的是把src/main/java目录中的mapper文件包含到输出结果中,输出到target/classes目录中 --> <resource> <directory>src/main/java</directory><!--mapper所在的目录--> <includes><!--包括目录下的.properties,.xml 文件都会扫描到--> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> <!-- 目的是把src/main/resources目录中的xml文件包含到输出结果中,输出到target/classes目录中 --> <resource> <directory>src/main/resources</directory><!--主配置文件所在的目录--> <includes><!--包括目录下的.properties,.xml 文件都会扫描到--> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources> </build> </project>
直接将Spring整合mybatis项目的java和resources文件夹复制到当前项目中覆盖
在webapp目录下新建一个index.jsp文件,编写一个表单页面,携带参数,申请访问后台的servlet
index.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>注册</title> <style type="text/css"> * { font-size: 20px; } </style> </head> <body> <center> <h2>注册学生信息</h2> <form action="/MyWeb/register" method="get"> <table> <tr> <td>id:</td> <td><input type="text" name="id" /></td> </tr> <tr> <td>姓名:</td> <td><input type="text" name="name" /></td> </tr> <tr> <td>邮箱:</td> <td><input type="text" name="email" /></td> </tr> <tr> <td>年龄:</td> <td><input type="text" name="age" /></td> </tr> <tr> <td><input type="submit" value="注册" /></td> </tr> </table> </form> </center> </body> </html>
在创建servlet之前,先检查/webapp/WEB-INF目录下的web.xml文件,如果自动创建的内容如下,则需要更换为新的版本。
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> </web-app>
小技巧:在项目结构中,先删除原本的低版本web.xml配置文件,再创建一个高版本的web.xml配置文件。需要注意的是,创建时,需要修改一下名称,不然无法指定版本,创建完成后再修改回来。
新建一个servlet,全限定名为:com.tsccg.controller.RegisterServlet,别名为:register
然后在RegisterServlet类的doGet方法中,向数据库中插入一条记录并返回处理结果。
RegisterServlet:
public class RegisterServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //1.获取请求包中的参数信息 String userId = request.getParameter("id").trim(); String userName = request.getParameter("name").trim(); String userEmail = request.getParameter("email").trim(); String userAge = request.getParameter("age").trim(); //2.创建Spring容器,获取service对象 String config = "applicationContext.xml"; ApplicationContext ac = new ClassPathXmlApplicationContext(config); System.out.println("容器对象的信息:" + ac); StudentService service = ac.getBean("studentService", StudentService.class); //创建一个Student实体类对象,将参数赋给它 Student student = new Student(); student.setId(Integer.parseInt(userId)); student.setName(userName); student.setEmail(userEmail); student.setAge(Integer.parseInt(userAge)); //调用service,通过dao向数据库中插入该数据 int result = service.addStudent(student); //3.将执行结果写入请求作用域对象中 request.setAttribute("result",result > 0 ? "注册成功":"注册失败"); //请求转发,调用result.jsp将响应结果写入响应协议包中 request.getRequestDispatcher("/result.jsp").forward(request,response); } }
在webapp目录下,新建一个result.jsp文件,编写响应页面
result.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> <style type="text/css"> * { color:red; font-size: 20px; } </style> </head> <body> ${requestScope.result} </body> </html>
指定网站别名为MyWeb
我们通过浏览器再发送一次请求:
可以发现,发送两次请求,创建了两个Spring容器:
Spring容器一次性就可以创建我们所需的所有对象,无需每次都创建,现在的这种情况是不被允许的。
解决方法:使用监听器,在监听器中创建一个Spring容器对象,然后将Spring容器对象放入全局作用域对象中。这样,我们就不需要在servlet中创建Spring容器了,需要时,直接从全局作用域对象中拿就可以了。
监听器有两个作用:
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
ServletContext.setAttribute(key,ac)
监听器可以自己创建,也可以使用Spring框架中提供的ContextLoaderListener。
若要实现在ServletContext初始化时创建Spring容器,就需要使用监听器接口ServletContextListener对ServletContext进行监听。
Spring为该监听器接口定义了一个实现类:ContextLoaderListener,完成了两个很重要的工作:
使用时在web.xml中注册该监听器:
<!--注册监听器ContextLoaderListener--> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
打开ContextLoaderListener的源码,可以看到有四个方法:
其中,初始化方法contextInitialized()比较重要,就是在其内部实现创建容器对象并放入全局作用域
追踪initWebApplicationContext()方法,可以看到其具体实现步骤
在将Spring容器对象放入全局作用域时,key值为一个常量:WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
追踪此常量,可以发现,就是Spring容器对象的名字加上.ROOT
的字符串
在pom.xml文件中加入如下依赖,才可以使用Spring监听器对象
<!--Spring监听器--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.2.5.RELEASE</version> </dependency>
在web.xml中注册监听器:
<!--注册监听器ContextLoaderListener--> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
当服务器启动时,会自动创建Spring监听器对象。在创建该对象时,默认会读取/webapp/WEB-INF/applicationContext.xml文件,也就是Spring配置文件。
为什么需要读取Spring配置文件?因为需要在监听器中创建ApplicationContext容器对象,需要加载Spring配置文件。
而我们的Spring配置文件路径为:/main/resources/applicationContext.xml,在运行程序时,监听器按照默认路径找不到配置文件,就会报异常。
我们可以使用<context-param>
标签重新指定Spring配置文件的位置。
<!--自定义Spring配置文件的路径--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param>
web.xml完整配置:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>RegisterServlet</servlet-name> <servlet-class>com.tsccg.controller.RegisterServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>RegisterServlet</servlet-name> <url-pattern>/register</url-pattern> </servlet-mapping> <!--自定义Spring配置文件的路径--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <!--注册监听器ContextLoaderListener--> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> </web-app>
在Servlet中获取Spring容器对象的常用方式有两种:
1)直接从全局作用域中获取
从Spring提供的监听器ContextLoaderListener源码可知,容器对象在全局作用域中的key值为:WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
。所以,可以直接通过ServletContext的getAttribute()方法,根据key值获取容器对象。
WebApplicationContext ac = null; String key = WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE; Object attr = getServletContext().getAttribute(key); if (attr != null) { ac = (WebApplicationContext) attr; }
2)通过Spring提供的工具类获取
Spring提供了一个工具类WebApplicationContextUtils,其中有一个方法专门用来从ServletContext【全局作用域】中获取Spring容器对象:getRequiredWebApplicationContext(ServletContext sc)
调用方式:
//获取当前全局作用域对象 ServletContext sc = getServletContext(); //使用工具类获取Spring容器对象 WebApplicationContext ac = WebApplicationContextUtils.getRequiredWebApplicationContext(sc);
查其源码,看其调用关系,就可以发现,基本和前面获取Spring容器对象的方式一样
getRequiredWebApplicationContext(ServletContext sc)方法源码:
public static WebApplicationContext getRequiredWebApplicationContext(ServletContext sc) throws IllegalStateException { //调用getWebApplicationContext(ServletContext sc)方法,输入当前全局作用域对象,获取Spring容器对象 WebApplicationContext wac = getWebApplicationContext(sc); if (wac == null) { throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?"); } //将Spring容器对象返回 return wac; }
getWebApplicationContext(ServletContext sc)方法源码:
@Nullable public static WebApplicationContext getWebApplicationContext(ServletContext sc) { //调用getWebApplicationContext(ServletContext sc, String attrName)方法,输入全局作用域对象和Spring容器对象在其中的key值 return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); }
getWebApplicationContext(ServletContext sc, String attrName)方法源码:
@Nullable public static WebApplicationContext getWebApplicationContext(ServletContext sc, String attrName) { Assert.notNull(sc, "ServletContext must not be null"); //根据key值,从全局作用域对象中获取Spring容器对象 Object attr = sc.getAttribute(attrName); if (attr == null) { return null; } ... //将Spring容器对象返回 return (WebApplicationContext) attr; }
开启服务器,连续发送两次请求
查看后台,发现两次使用的Spring容器对象是同一个,测试成功。