Spring学习笔记
Spring学习笔记
1.Spring概述
1.Spring是一个轻量级的JavaEE框架
2.它是为了解决企业应用开发的复杂性而创建的
3.Spring有两个核心部分:IoC和Aop
- IoC:控制反转,把创建对象的过程交给Spring来进行管理
- Aop:面向切面,在不修改源码的情况下进行功能增强
4.Spring的特点
-
1)轻量
-
Spring框架使用的jar都比较小,一般在1M以下或者几百KB。Spring核心功能所需要的jar总共在3M左右。
-
Spring框架运行占用的资源少,运行效率高。
-
Spring框架可以独立运行,不依赖其他jar包。
-
-
2)方便解耦
- Spring提供了IoC控制反转,由容器来管理对象和对象之间的依赖关系。
- 原来在程序代码中创建对象,现在用容器来完成对象的创建,不需要修改代码。对象之间解耦和。
-
3)AOP编程的支持
- Spring提供了Aop面向切面的支持,可以在不修改源码的情况下进行功能增强
-
4)方便整合各种框架
- Spring提供了各种优秀框架如Mybatis等的直接支持,同时简化框架的使用。
- Spring就像插座一样,其他框架就像插头,可以很容易地组合到一起,也可以很容易地移除。
-
5)方便程序测试
-
6)方便进行事务操作
-
7)降低API开发难度
5.本文使用的Spring版本是5.2.6
6.Spring体系结构
2.入门案例
创建一个普通java类,在这个类中编写一个方法。
演示在不手动new这个类的对象的情况下,使用Spring得到这个类的对象并调用其方法。
2.1获取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>
2.2创建一个普通maven项目
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
2.3创建一个普通java类
在/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方法");
}
}
2.4创建Spring配置文件,配置创建对象
1.在/main/resources目录下创建一个Spring配置文件
2.在配置文件的beans标签内,添加bean标签
在bean标签里添加id和class两个属性
- 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>
2.5编写测试代码
在/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方法
3.IoC控制反转
3.1什么是IoC
控制反转(Inversion of Control,简称IoC):是面向对象编程中的一种设计原则。是指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。
IoC的实现:当前最常见的方式叫做依赖注入(Dependency Injection,简称DI),程序代码不做定位查询,这些工作由容器自行完成。
依赖注入是指程序运行过程中,若需要调用另一个对象协助时,无需在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。
Spring的依赖注入对调用者和被调用者几乎没有任何要求,完全支持对象之间依赖关系的管理。
Spring容器是一个对象工厂,负责创建、管理所有的java对象,这些java对象被称为Bean。Spring容器管理着Bean之间的依赖关系。
使用IoC可以降低对象之间的耦合度。
通俗地讲:
- IoC就是把对象的创建和对象之间的调用过程,都交给Spring来管理。
- 使用IoC的目的:就是为了降低耦合度。
上面的入门案例就是使用IoC来实现的。
3.2IoC底层原理
IoC底层主要使用了xml解析、工厂模式、反射三种技术。
下面画图演示:
现在有两个类,UserService和User。
我想在UserService的execute()方法里调用User的doSome()方法。实现方式如下:
第一种方式:在execute()方法里new对象
在UserService类的execute()方法里new一个User对象,然后用这个对象调用doSome()方法
这种方式最为简单,但使得UserService类和User类紧紧关联在了一起,耦合度很高。
什么是耦合?
- 当一个事物改变了,另一个事物也要随之改变。
- 两个事物之间依赖关系的强弱就是耦合度。
- 理论上耦合度不会消失,我们只能尽可能地降低。
如果耦合度过高,比如上面的关联方式,就会出现如下情况:
- 当User类的路径变了,UserService类也要跟着变;
- 当User类的doSome方法变了,UserService类也要跟着变。
- 假如说现在有1000个UserService类,当User类改变时,这1000个UserService类都要跟着变。牵一发而动全身。
我们开发时追求的是低耦合高内聚,高耦合不利于程序拓展。
我们可以使用一些技术来降低耦合度。
第二种方式:使用工厂模式
工厂模式就是创建一个第三方类,实现目标类对象的创建,然后将对象交给调用它的地方。
工厂模式的目的就是为了降低耦合度。
工厂模式虽然降低了UserService类和User类之间的耦合度,但是仍没有降到最低。
为了进一步降低耦合度,我们需要使用到IoC,控制反转。
第三种方式:IoC
IoC就是在工厂模式的基础上,将创建对象的过程进一步解耦:
- 首先,创建一个xml配置文件,在该文件里的class属性里写明目标类的全限定名,也就是目标类编译后的class文件所在位置。
- 然后在工厂类里通过xml解析读取配置文件里的class属性值,得到User类的全限定名。
- 最后通过反射创建该类的对象。
IoC将对象间的耦合度大大降低了,假如说现在User类的路径变了,我们只需要在配置文件里修改路径即可,不需要更改源代码。
3.3容器接口及其实现类
3.3.1ApplicationContext接口(容器)
ApplicationContext用于加载Spring的配置文件,在程序中充当"容器"的角色。
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
User user = (User)ac.getBean("user");
3.3.2ApplicationContext接口的实现类
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");
3.3.3Application容器中对象的装配时机
ApplicationContext容器,会在容器对象初始化时,将其中所有的对象一次性装配好。以后代码中如果要用这些对象,只需要从内存中直接获取即可。执行效率高,但占用内存。
也就是说,在加载配置文件的时候就会创建配置文件里所有的对象。
//1.加载配置文件
String config = "beans.xml";//beans.xml所在路径
//获取容器:此时容器中所有对象都创建好了
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
3.4依赖注入(DI)
为了实现在Spring的配置文件中,给java对象的属性赋值,需要使用DI,依赖注入。
DI是基于创建对象来实现的。
DI有两种实现:
- 基于xml的DI实现:在Spring的配置文件中,使用标签和属性来完成属性赋值
- 基于注解的DI实现:使用Spring中的注解,完成属性赋值
3.5基于XML的DI
3.5.1DI的语法分类
1)set注入(设值注入):Spring调用类的set方法,在set方法中可以实现属性的赋值
2)构造注入:Spring调用类的有参构造方法创建对象,在构造方法中完成赋值
3.5.2简单类型的set注入
简单类型的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方法
- 如果没有set方法,程序会报错
- 如果set方法里没有赋值操作,得到的对象就没有属性值
<bean id="student" class="com.tsccg.set.Student">
<property name="name" value="张三"/><!-- setName("张三") -->
<property name="age" value="20"/><!-- setAge(20) -->
</bean>
3.5.3引用类型的set注入
引用类型的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='南阳'}}
3.5.4构造注入
构造注入:Spring调用目标类的有参构造方法,在创建对象的同时,在构造方法中给属性赋值。
构造注入需要使用<constructor-arg >标签
<constructor-arg >标签属性:
- name:表示构造方法的形参名
- index:表示构造方法的参数位置,从左到右,以0开始
- value:给简单类型赋值
- ref:给引用类型赋值
一般使用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:表示构造方法的参数位置,从左到右,以0开始
- value:给简单类型赋值
- ref:给引用类型赋值
分别使用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='北京'}}
3.5.5引用类型的自动注入
引用类型的自动注入: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能够赋值给引用类型。
同源就是一类的关系:
- java类中引用类型的数据类型和bean的class的值是一样的。
- java类中引用类型的数据类型和bean的class的值是父子类关系的。
- java类中引用类型的数据类型和bean的class的值是接口与实现类关系的。
语法:
<bean id="xxx" class="ooo" autowire="byType">
给简单类型赋值(必须使用set注入)
</bean>
3.5.5.1演示自动注入ByName
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='南阳'}}
3.5.5.2演示自动注入ByType
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不能同时出现在同一个配置文件里。
3.5.6多个配置文件
1.多个配置文件的优势
- 多个配置文件比一个配置文件效率高:
- 假如项目中有几百个类。
- 如果全部写入一个配置文件里,那么这个文件就会很大,执行效率很慢。
- 如果将所有类划分到多个配置文件里,那么单个配置文件就会很小,执行单个配置文件的效率就会很高。
- 多个配置文件可以避免多人开发带来的冲突:
- 假如项目中有多个模块,每个模块都由不同的人负责开发。
- 如果将所有模块的类都放到一个配置文件里,那么可能造成代码的冲突。
- 如果一个模块一个配置文件,即能避免冲突,又能方便操作。
2.多个配置文件的分配方式
- 按功能模块:一个模块一个配置文件
- 按类的功能:
- 数据库相关的配置放到一个配置文件
- 事务相关的功能放到一个配置文件
- service功能相关的放到一个配置文件等
3.多配置文件用法
以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目录下,必须新建一个文件夹,在该文件夹里存放配置文件。
3.6基于注解的DI
通过注解完成java对象的创建和属性赋值。
3.6.1注解的使用步骤
1.添加spirng-context依赖:在加入spring-context依赖后,maven会将spring-aop的依赖一并添加,使用注解完成DI必须使用spring-aop
2.创建类,在类中加入spring的注解(多个不同功能的注解)
- @Component
- @Respotoy
- @Service
- @Controller
- @Value
- @Autowired
- @Resource
3.创建spring配置文件,在spring的配置文件中,加入一个组件扫描器的标签,指明注解在你的项目中的位置。
4.使用注解创建对象,创建容器ApplicationContext
3.6.2定义Bean的注解
@Component:创建对象,相当于<bean >的功能
- value属性:该注解的value属性用于指定该bean的id值。 vlaue的值是唯一的,创建的对象在整个Spring中独一个。
- 使用位置:类的上面。
- 使用格式:
- @Component(value="自定义对象名"):相当于<bean id="自定义对象名 class="类的全限定名"/>
- @Component("自定义对象名"):省略value,最常用。
- @Component:不指定对象名,由spring提供默认名。(类名首字母小写)
和@Component功能一致,可以创建对象的注解还有三个:
- @Repository:用于持久层类
- 放在dao的实现类上面,表示创建dao对象
- 额外功能:dao对象是用于数据访问的,具有访问数据库的功能
- @Service:用于业务层类
- 放在service的实现类上面,表示创建service对象
- 额外功能:service对象是做业务处理的,具有处理事务等功能
- @Controller:用于控制器类
- 放在控制器(处理器)类的上面,表示创建控制器对象
- 额外功能:控制器对象能够接收用户提交的参数,显示请求的处理结果
以上三个注解的使用语法和@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标签后,配置文件的变化:
- 加入一个新的约束文件spring-context.xsd
- 给这个新的约束文件起个命名空间的名称
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}
由结果来看,使用注解创建对象时,调用的是类的无参构造方法。
3.6.3扫描多个包的三种方式
<!--指定多个包的三种方式-->
<!-- 第一种方式 -->
<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"/>
3.6.5@Value:简单类型属性注入
@Value:给简单类型的属性赋值
- 属性:value。是String类型的,表示简单类型的属性值,可省略不写。
- 使用位置:
- 1)在属性定义的上面,不需要写set方法,推荐使用
- 2)在set方法上面
实际使用:
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}
3.6.6@Autowired:引用类型属性注入(byType)
@Autowired:spring框架提供的注解,实现给引用类型的属性赋值。
- spring中通过注解给引用类型赋值,使用的是自动注入原理,支持byName和byType。
- @Autowired默认使用的是byType自动注入。
- 属性:required,是一个boolean类型的,默认为true
- true:表示当引用类型属性赋值失败时,程序报错,并终止执行
- false:表示当引用类型属性赋值失败时,程序仍正常执行,引用类型属性值为null
- 一般推荐使用true,便于暴露问题
使用位置:
- 在引用类型属性定义的上面,不需要写set方法,推荐使用
- 在set方法上面
实例演示:
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='北京'}}
3.6.7@Autowired与@Qualifier:引用类型属性注入(byName)
如果要使用byName方式给引用类型赋值,那么就需要使用到@Qualifier注解。
- 在属性上面加入@Autowired
- 在属性上面再加入@Qualifier(value="bean的id值"):表示使用指定名称的bean对象完成赋值
- @Qualifier的 value 属性用于指定要匹配的bean的id值
如:
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;
3.6.8JDK注解@Resource自动注入
@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;
3.6.9注解与XML的对比
注解的优点:
- 方便
- 直观
- 高效:代码量少,没有配置文件的书写那么复杂
注解的缺点:注解是写到java代码里的,耦合度高,修改后需要重新编译代码。
xml的优点:
- 配置信息与代码是分离的,耦合度低
- 在xml中修改数据,无需重新编译代码,只需要重启服务器即可加载新的配置
xml的缺点:编写麻烦,效率低,开发大型项目时过于复杂。
总结:
- 当不需要经常修改配置信息时,使用注解
- 当需要经常修改配置信息时,使用xml
- 在实际开发中,能用注解就用注解。
4.AOP面向切面编程
4.1动态代理
动态代理就是为了在不修改目标类的基础上,实现调用目标方法和功能增强。
实现方式:
1.JDK动态代理:使用jdk中的Proxy,Method,InvocationHandler创建代理对象。
jdk动态代理要求目标类必须实现接口。
2.CGLIB动态代理:使用第三方的工具库,创建代理对象。原理是继承,通过继承目标类,创建其子类对象,这个子类对象就是代理对象,在代理对象中实现调用目标方法以及功能增强。
CGLIB要求目标类必须是可继承的,不能由final修饰,方法也不能是final的。
4.2JDK动态代理例子
现在有一个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
4.3动态代理的作用
- 在目标类源代码不改动的情况下,调用目标方法并增强功能
- 减少代码的重复
- 让开发者更专注于业务逻辑代码
- 解耦合,让业务功能代码和日志、事务等非业务功能代码分离
4.4AOP概述
4.4.1AOP基于动态代理
AOP就是面向切面编程,是基于动态代理的,可以使用jdk、cglib两种代理方式。
AOP就是动态代理的规范化,把动态代理的实现步骤、实现方式都定义好了,让开发人员用一种统一的方式去使用动态代理。
4.4.2什么是面向切面编程?
AOP(Aspect Orient Programming),面向切面编程
- Aspect:切面,给你的目标类增加的功能。就比如说上面例子中的日志和事务都是切面。
- Orient:面向,对着
- Programming:编程
好比OOP面向对象编程,面向对象编程就是在分析项目功能时,先考虑可以由哪些类来实现目标功能。
AOP面向切面编程就是分析项目功能时,先找出切面,然后合理地安排切面执行的时间、位置。(时间:在目标方法前还是后;位置:在哪个类、哪个方法上)
4.4.3AOP常见术语
- Aspect:切面,表示增强的功能。
- 是非业务功能代码
- 常见的切面功能有日志、事务、统计信息、参数检查和权限验证。
- JoinPoint:连接点,可以添加切面的具体方法。
- 通常业务接口中的方法都是连接点
- Pointcut:切入点,指声明的一个或多个连接点的集合。
- 通常切入点指定一组方法。
- 被final标记的方法是不能作为连接点与切入点的,因为final意味着是最终的,是不能被修改的,不能被增强的。
- Target:目标对象,给哪个类的对象增强功能,哪个类的对象就是目标对象
- 目标对象通常就是包含主业物逻辑的类的对象,其类叫目标类
- Advice:通知,表示切面功能执行的时间,也就是在目标方法之前执行,还是目标方法之后执行
4.4.4执行一个切面有三个要素
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表示时间,就是说切面执行在目标方法之前,还是目标方法之后
4.5AOP的实现框架:AspectJ
AOP是一种规范,是动态的一种规范化,是一个标准。
AOP的技术实现框架有:Spring、AspectJ
1.Spring:在内部实现了AOP规范,能处理AOP的工作
- 但是AspectJ也实现了AOP的功能,且实现方式更为简捷,使用更加方便。
- 所以,Spring又将AspectJ的对于AOP的实现也引入到了自己的框架中。
- 在Spring中使用AOP开发时,一般使用AspectJ的实现方式。
- Spring主要在事务处理时使用AOP。
2.AspectJ:一个开源的,专门做AOP的框架
- Spring框架中集成了AspectJ框架,通过Spring就能使用AspectJ的功能。
- AspectJ框架实现AOP有两种方式:
- 使用xml的配置文件:配置全局事务
- 使用注解:我们在项目中一般都使用注解来实现AOP功能。
4.6AspectJ框架的使用
4.6.1AspectJ的通知类型
切面的执行时间在AOP规范中也叫做Advice(通知,增强),在AspectJ框架有5个通知类型,都有相对应的注解:
- @Before:前置通知
- @AfterReturning:后置通知
- @Around:环绕通知
- @AfterThrowing:异常通知
- @After:最终通知
当然,也可以使用xml配置文件中的标签来表示。
4.6.2AspectJ的切入点表达式
AspectJ定义了专门的表达式用于指定切入点。
表达式原型:
execution(modifiers-pattern? ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
说明:(带有?的表示是可选部分)
- modifiers-pattern?:访问权限类型,可选
- ret-type-pattern:返回值类型
- declaring-type-pattern?:类的全限定名,可选
- name-pattern(param-pattern) :方法名(参数类型和参数个数),如
doSome(String,Integer)
- throws-pattern?:抛出异常类型,可选
以上表达式共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 子包下所有类(接口)中所有方法为切入点
4.6.3使用AspectJ框架实现AOP的步骤
使用AOP的目的是:在不修改原有代码的情况下,给已经存在的一些类和方法增加额外的功能。
完整步骤:
- 新建maven项目
- 加入Spring和AspectJ等依赖
- 创建目标类:先定义一个接口,然后创建它的实现类
- 创建切面类:就是一个普通java类
- 在java类的上面添加@Aspect注解,表明这是一个切面类
- 在切面类中定义方法,方法就是切面,就是要增强的功能代码
- 在方法上面添加通知注解,如@Before,指定切面执行时间
- 在通知注解的value属性里编写切入点表达式execution(),指定切面执行位置
- 创建Spring的配置文件:声明Bean对象,把对象交给容器统一管理
- 可通过注解或xml配置文件
<bean>
来声明对象 - 声明目标对象和切面类对象
- 声明AspectJ框架中的自动代理生成器,用来完成代理对象的自动创建功能
- 可通过注解或xml配置文件
- 编写测试代码,从Spring容器中获取目标对象(实际上已经是代理对象了),通过代理对象执行方法,发现已经实现了功能增强
实例演示:
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业务方法
4.6.4切入点表达式的多种写法
//不使用通配符
@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());
}
4.6.5通知方法中的参数:JoinPoint
通知方法:被通知注解所修饰的方法就是通知方法。
JoinPoint:连接点,指的是要加入切面功能的业务方法。
在通知方法中可以包含一个JoinPoint类型的参数,该类型的对象本身就是切入点表达式,通过该参数,可以在执行切面方法时,获取业务方法的信息,例如业务方法名称、业务方法的实参。
- 如果在切面功能中需要用到业务方法的信息,就需要加入JointPoint参数。
- 这个JointPoint参数的值由AspectJ框架赋予,使用时必须在参数列表第一位。
如:
切面类:
@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业务方法
4.6.6前置通知@Before
@Before修饰的前置通知方法在目标方法执行前执行。
4.6.7后置通知@AfterReturning
被@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.使用后置通知定义方法
方法定义要求:
- 必须为公共方法:public
- 没有返回值:void
- 方法名自定义
- 方法可以有参数,推荐使用Object类型,参数名必须与后置通知的returning属性的值一致
@AfterReturning:
- 属性:
- value:切入点表达式
- returning:自定义的变量名,表示目标方法返回值,必须和通知方法的形参名一致
- 位置:在方法定义上面
- 特点:
- 在目标方法执行后执行
- 能获取到目标方法的返回值,可以根据这个返回值做不同的处理功能
@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}
由结果来看,已成功修改返回结果。
4.6.8环绕通知@Around
环绕通知可以在目标方法执行前执行,也可以在目标方法执行后执行。
被注解为环绕通知的方法要有返回值,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.定义切面
环绕通知的方法定义格式:
- 必须为公共类:public
- 必须有一个返回值,推荐使用Object
- 方法名称自定义
- 方法可以包含一个ProceedingJoinPoint类型的参数
@Around:环绕通知注解
- 属性:value,切入点表达式
- 位置:在方法的定义上
- 特点:
- 它是功能最强的通知
- 在目标方法的前和后都能增强功能
- 可以控制目标方法是否被调用执行
- 可以修改原来的目标方法的执行结果
@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
4.6.9异常通知@AfterThrowing
异常通知是在目标方法抛出异常后执行。该注解的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.定义异常通知方法
异常通知方法的定义格式
- public
- 没有返回值
- 方法名自定义
- 方法可以没有参数,如果有则是JoinPoint,Exception
@AfterThrowing:异常通知注解
- 属性:
- value:切入点表达式
- throwing:自定义的变量,表示目标方法抛出的异常对象。变量名必须和方法的参数名一致
- 特点:
- 在目标方法抛出异常时执行
- 可以做异常的监控程序,监控目标方法执行时是不是有异常。如果有异常,可以发送邮件、短信进行通知。
//异常通知方法
@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)
}
4.6.10最终通知@After
最终通知就是无论目标方法是否抛出异常,都会执行切面。
演示最终通知:
1.接口中添加方法
public interface SomeService {
//最终通知方法
void doThird();
}
2.实现方法
public class SomeServiceImpl implements SomeService {
@Override
public void doThird() {
System.out.println("执行doThird业务方法");
}
}
3.定义切面
最终通知方法的定义格式
- public
- 没有返回值
- 方法名称自定义
- 方法可以有JoinPoint类型的参数
@After:最终通知注解
- 属性:value,切入点表达式
- 位置:方法定义上面
- 特点:
- 无论有无异常发生,总是会被执行
- 在目标方法之后执行
@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);
}
重新执行测试方法:
4.6.11定义切入点@Pointcut
当较多的通知方法使用的切入点表达式是相同的时,编写、维护都比较麻烦。为此,AspectJ提供了@Pointcut注解,用于定义execution切入点表达式。@Pointcut不是通知注解。
@Pointcut:定义和管理切入点,如果项目中有多个切入点表达式是重复的,可以使用@Pointcut
- 属性:value,切入点表达式
- 位置:在自定义的方法上面
- 特点:当使用@Pointcut注解在一个方法的上面时,这个方法的名称就是@Pointcut中所编写的切入点表达式的别名。在其他通知注解中,就可以直接使用这个方法的名称来代替切入点表达式了。
- @Pointcut注解的方法没有实际的作用,无需在方法体内编写代码,一般用private修饰。
用法:
- 定义:将@Pointcut注解在一个私有的空方法之上,然后编写切入点表达式
- 使用:在使用其他通知注解时,直接在注解的value属性里写空方法的名称代替切入点表达式
@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();
}
测试结果:
4.6.12当目标类没有接口时,使用cglib动态代理
定义一个普通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动态代理。
4.6.13有接口的情况下,也可以是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业务方法
5.Spring整合MyBatis
5.1Spring整合MyBatis的思路
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);
- 读取mybatis主配置文件,通过SqlSessionFactoryBuilder对象调用build()方法创建SqlSessionFactory对象
- 使用SqlSessionFactory对象,调用openSession()方法,获取SqlSession对象
- 使用SqlSession对象,调用getMapper()方法,获取dao对象
要获取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创建以下对象:
- 独立的连接池类对象,使用阿里的druid连接池
- SqlSessionFactory对象
- dao对象
我们学习Spring整合MyBatis主要就是学习以上三个对象的创建语法。
5.2Spring整合MyBatis的基本步骤说明
- 新建maven项目
- 添加所需依赖
- Spring依赖
- MyBatis依赖
- MySQL驱动依赖
- Spring的事务依赖
- MyBatis和Spring的集成依赖:MyBatis官方提供的,用来在Spring项目中创建MyBatis的SqlSessionFactory对象和dao对象
- 创建实体类
- 创建dao接口和mapper文件
- 创建MyBatis主配置文件
- 创建service接口和实现类,属性是dao,调用dao实现访问数据库
- 创建Spring的配置文件:在Spring配置文件中声明mybatis对象,把mybatis对象的管理交给Spring
- 独立的连接池类对象:数据源,用于代替MyBatis自带的连接池
<dataSource type="POOLED">
- SqlSessionFactory对象
- Dao对象
- 自定义的service对象
- 独立的连接池类对象:数据源,用于代替MyBatis自带的连接池
- 创建测试类,获取service对象,通过service调用dao完成数据库的访问
5.3Spring整合MyBatis的步骤
5.3.1建表
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`;
5.3.2新建maven子模块
新建一个maven子模块spring-06-mybatis
5.3.3添加依赖
在父模块的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>
5.3.4创建数据库实体类
在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 +
'}';
}
}
5.3.5创建dao接口和mapper文件
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>
5.3.6创建mybatis主配置文件
在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>
5.3.7创建service接口和实现类
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);
}
}
5.3.8创建Spring配置文件
创建Spring的配置文件:在Spring配置文件中声明mybatis对象,把mybatis对象的管理交给Spring。
在main/resources目录下新建spring配置文件【applicationContext.xml】。
在spring配置文件中声明各种mybatis对象:
1.声明数据源DataSource
我们这里使用阿里的druid连接池来代替mybatis自带的POOLED
druid官方地址:https://github.com/alibaba/druid
druid官方中文帮助文档:https://github.com/alibaba/druid/wiki/常见问题
DruidDataSource基本配置:
- id="myDataSource":自定义名称,作为bean对象中的唯一标识
- class="com.alibaba.druid.pool.DruidDataSource":druid数据源类的全限定名称
- init-method="init":初始方法,属性值是初始方法的名字。当Spring容器创建时,会自动调用init方法
- destroy-method="close":销毁方法,属性值是销毁方法的名字。当Spring容器关闭时,会自动调用close方法来做资源的销毁
<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>
注意:
- 在上面的配置中,通常需要配置driverClassName、url、username、password、maxActive这五项属性
- 虽然druid官方文档上说了能根据输入的url自动识别驱动名称,不用配置driverClassName。但亲测没用,该配还是得配...
使用属性配置文件保存连接数据库信息
在实际开发中,我们会将连接数据库的信息放到一个独立的属性配置文件里
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>
2.声明SqlSessionFactory类对象
在这里声明的是mybatis提供的SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory的对象。
创建SqlSessionFactory类对象需要读取mybatis主配置文件的信息,而mybatis主配置文件主要由数据源DataSource和指定mapper文件所在位置的路径组成的。
- 为此,我们需要将上面声明的DruidDataSource类型对象的bean的id赋给SqlSessionFactoryBean类的dataSource属性
- 同时把mybatis主配置文件的路径赋给其configLocation属性
<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>
3.创建dao对象
创建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>
4.声明service对象
<bean id="studentService" class="com.tsccg.service.impl01.StudentServiceImpl">
<!--这里引用的是步骤3中自动创建的dao代理对象-->
<property name="studentDao" ref="studentDao"/>
</bean>
5.完整配置文件
<?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>
5.3.9创建测试类
编写测试代码,获取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 ? "删除成功":"删除失败");
}
}
测试结果:
测试查询方法:
测试插入方法:
测试删除方法:
6.Spring事务
6.1事务相关问题
6.1.1什么是事务
事务是一组sql语句的集合,其中,sql语句可能是insert、update、delete、select。
我们希望在一个事务中,所有的sql语句能同时执行成功或者同时执行失败。让这些sql语句是一致的,是作为一个整体来执行的。
6.1.2在什么时候需要用到事务
当访问数据库时,涉及到多个表,或者是多条不同的sql语句如insert、update和delete,我就需要保证这些sql语句全部执行成功才能实现我的功能,或者全部执行失败来保证原本数据库中的数据不受影响。
在java代码中编写程序,需要控制事务,那么事务应该在程序的哪里使用呢?
我们应该在service类的业务方法中使用事务,因为业务方法往往会调用多个dao方法,执行多条sql语句。
6.1.3使用不同数据库访问技术处理事务的方式
1.JDBC访问数据库,处理事务:
conn.commit();
conn.rollback();
2.MyBatis访问数据库,处理事务:
sqlSession.commit();
sqlSession.rollback();
3.hibernate访问数据库,处理事务:
session.commit();
session.rollback();
6.1.4上一步中,事务的处理方式有什么不足之处,该如何解决?
不足之处:
- 不同的数据库访问技术,处理事务的对象、方法不同
- 使用时需要掌握多种数据库访问技术:
- 使用事务的原理
- 使用事务的处理逻辑,即什么时候提交事务,什么时候回滚事务
- 处理事务的对象、方法
解决方法:
Spring提供了一种处理事务的统一模型,能够使用统一步骤,统一方式来完成不同数据库访问技术的事务处理。
6.2在Spring中处理事务
6.2.1在Spring中处理事务的方式
我们在Spring中,是使用Spring事务处理模型来处理事务的。
Spring事务处理模型其实就是Spring内部写好的一系列代码,使用步骤是固定的,我们只需要把处理事务需要用到的信息提供给Spring,Spring就能处理事务的提交、回滚了。几乎不用我们自己编写事务相关的代码。
Spring的事务管理,主要用到两个事务相关的接口
- 事务管理器接口(重点)
- 事务定义接口
6.2.2事务管理器接口(重点)
Spring事务处理模型中,通过事务管理器来完成事务的提交、回滚,以及事务的状态信息。
事务管理器是PlatformTransactionManager接口实现类对象,接口定义提交、回滚方法:
PlatformTransactionManager接口有两个常用的实现类:对应不同的数据库访问技术
- DataSourceTransactionManager:使用JDBC或MyBatis访问数据库时使用
- HibernateTransactionManager:使用Hibername访问数据库时使用
我们需要告诉Spring使用哪个事务管理器:
可以在Spring配置文件中,声明数据库访问技术对应的事务管理器接口实现类,使用<bean>
声明即可,如:
<!--使用MyBatis数据库访问技术-->
<bean id="xxx" class="...DataSourceTransactionManager">
</bean>
6.2.3事务定义接口
我们还需要告诉Spring,业务方法需要什么样的事务,说明事务的类型。
而事务定义接口TransactionDefinition就定义了事务描述相关的三类常量:事务隔离级别、事务传播方式和事务默认超时时限,以及对它们的操作。
1)定义事务隔离级别常量:有4个值
这些常量都是以ISOLATION_
开头,如:ISOLATION_READ_COMMITTED
- READ_UNCOMMITTED:读未提交
- 能够读取另一个事务中尚未提交的数据
- 存在脏读、不可重复读、幻读等所有并发问题
- READ_COMMITTED:读已提交
- 能够读取另一个事务中已经提交的数据
- 解决脏读问题
- 存在不可重复读与幻读问题
- REPEATABLE_READ:可重复读
- 一个事务A开启后,只要不关闭,不管过多久,每一次在事务A中读取到的数据都是刚开启事务A时的数据。即使期间另一个事务B修改了事务A正在使用的数据,事务A读取到的数据也不会变。
- 解决脏读、不可重复读问题
- 存在幻读问题
- SERIALIZABLE:串行化/序列化
- 同一时间只能执行一个事务
- 最高隔离级别,效率最低
- 解决了所有并发问题
- DEFAULT:采用DB默认的事务隔离级别
- MySql:REPEATABLE_READ,可重复读
- Oracle:READ_COMMITTED,读已提交
2)定义事务传播行为:指的是当一个事务中的方法被另一个事务中的方法调用时,这个被调用的方法处理事务关系的方式
如:在A事务中的方法doSome()调用B事务中的方法doOther()时,doOther()是按事务A运行,还是为自己开启一个新事务B运行,这就是由doOther的事务传播行为所决定的。
事务传播行为是加在方法上的。
Spring定义了7个事务传播行为常量:只有前三个是常用的,其余了解即可
- PROPAGATION_REQUIRED
- PROPAGATION_SUPPORTS
- PROPAGATION_REQUIRES_NEW
- PROPAGATION_MANDATORY
- PROPAGATION_NESTED
- PROPAGATION_NEVER
- PROPAGATION_NOT_SUPPORTED
a)PROPAGATION_REQUIRED
表示指定的当前方法必须在事务中执行。如果当前存在事务,则使当前方法加入到当前事务中;如果当前没有事务,则使当前方法新建一个事务。
比如:现在有doSome()和doOther()两个方法:
- doSome()方法可能会在事务A中运行,也可能不在任何事务中运行
- doOther()方法单独运行时会开启事务B
现需要在doSome()方法中调用doOther()方法。
在doOther方法上添加PROPAGATION_REQUIRED传播行为。
- 如果在doSome()方法中调用doOther()方法时,doSome()方法是在事务A中运行的,那么doOther()也会在事务A中运行;【传播】
- 如果doSome()方法没有在任何事务中运行,那么doSome()方法就会开启一个新的事务B。
打个生活中的例子,就好比蹭基友热点,如果他开启了热点(有事务),我就连他的热点上网(传播);如果他没开热点(没有事务),我就开自己的流量上网(自己开启事务)。
b)PROPAGATION_SUPPORTS
表示指定的当前方法支持当前事务,但如果当前没有事务,就以非事务形式执行。
还是doSome()和doOther方法:用PROPAGATION_SUPPORTS指定doOther方法
- 如果在doSome()方法中调用doOther()方法时,doSome()方法在事务A中运行,那么doOther()方法也会在事务A中运行
- 如果doSome()方法没在任何事务中运行,那么doOther()方法同样在无事务下运行
You jump! I jump!
c)PROPAGATION_REQUIRES_NEW
表示指定的方法总是新开启一个事务,如果当前存在事务,则将当前事务挂起,直到新开启的事务执行完毕。
3)定义默认事务超时时限:
常量TIMEOUT_DEFAULT定义了事务底层默认的超时时限,表示一个事务方法最长的执行时间。
如果方法执行时超过了指定时限,那么事务就进行回滚。单位是秒,整数值,默认是-1。
注意:影响事务超时时限的因素比较多,且超时的时间计算点较复杂。所以,该值一般用默认的就行了,不用设置。
6.2.4Spring中事务提交、回滚的时机
- 当业务方法执行成功,没有抛出异常时,Spring会在方法执行后自动提交事务。(调用事务管理器的commit()方法)
- 当业务方法执行时,抛出运行时异常或者ERROR,Spring就会执行事务回滚。(调用事务管理器的rollback()方法)
- 运行时异常:程序运行时抛出的异常,RuntimeException及其子类,如:NullPointException、NumberFormatException
- 当业务方法执行时,抛出受检异常,Spring就会提交事务
- 受检异常:写代码时不许处理的异常,如:IOException、SQLException
6.2.5总结Spring的事务
1)管理事务的是事务管理器
2)Spring的事务管理是一个统一模型,使用步骤固定:
- 指定要使用哪一个事务管理器接口实现类,使用
<bean id="xxx" class="...DataSourceTransactionManager"></bean>
- 指定哪些类、哪些方法需要加入事务
- 指定需要加入的事务类型
- 隔离级别
- 传播行为
- 超时时限
6.3实例演示Spring事务管理
实例项目:模拟用户购买商品
本项目中要实现模拟用户下订单,购买商品的功能。
分为两个步骤:
- 向订单表中添加销售记录
- 从商品表中减少相应商品库存量
6.3.1环境搭建
1.建表
本次项目需要创建两个数据库表: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`;
2.新建maven项目,添加所需依赖
新建子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>
3.创建实体类
在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方法
}
4.创建dao接口和mapper文件
在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>
5.创建MyBatis主配置文件
在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>
6.创建自定义异常类
在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);
}
}
7.创建service接口及其实现类
在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;
}
}
8.创建Spring的配置文件
在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
- 独立的连接池类对象
- SqlSessionFactory对象
- Dao对象
- 自定义的service对象
<?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>
9.编写测试代码
测试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的事务注解,适合中小项目使用
- 使用AspectJ的AOP配置,适合大型项目使用
6.3.2使用Spring事务注解管理事务
Spring框架使用自身的aop来给业务方法增加事务功能,使用@Transactional
注解。适合中小项目。
1.@Transactional
注解的属性
- propagation:用于设置事务的传播行为。该属性类型为Propagation枚举,默认值为:
Propagation.REQUIRED
- isolation:用于设置事务的隔离级别。该属性类型为Isolation枚举,默认值为:
Isolation.DEFAULT
- readOnly:用于设置该方法对数据库的操作是否是只读的。该属性类型为boolean,默认值为:false
- timeout:用于设置本次操作与数据库连接的超时时限。单位为秒,类型为int,默认值为-1,即没有时限限制
- rollbackFor:指定需要回滚的异常类
- 即抛出指定的异常类型的异常时进行回滚操作
- 类型为Class[],需要指定异常类的class属性值,默认为空数组。当然,如果只有一个异常类,可以不使用数组。
- 处理逻辑:Spring框架会先检查方法中抛出的异常是不是在rollbackFor指定的异常列表中
- 如果抛出的异常在指定的异常列表中,不管是什么类型的异常,都会回滚
- 如果抛出的异常不在指定的异常列表中,Spring会判断该异常是不是运行时异常,如果是则回滚,如果不是则提交事务。
- rollbackForClassName:指定需要回滚的异常类类名
- 和rollbackFor一样的功能,只不过是当抛出与该异常类的名称相同的异常时进行回滚操作。
- 类型为String[],默认为空数组
- 当然,如果只有一个异常类,可以不使用数组。
- noRollbackFor:指定不需要回滚的异常类。与rollbackFor功能相反,类型为Class[]。
- noRollbackForClassName:指定不需要回滚的异常类类名。与rollbackForClassName功能相反,类型为String[]。
注意:
- 若@Transactional注解在方法上,该方法必须是public的。Spring会忽略掉所有非public方法上的@Transactional注解,如果注解方法是非public的,那么虽然Spring不会报错,但是Spring不会将指定事务织入该方法中。
- 若@Transaction注解在类上,则表示该类上所有的方法均将在执行时织入事务。
2.使用事务注解方案的步骤
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 ...
...
由测试结果可见,当业务方法开始执行后:
- 先执行insert向销售表中插入了一条销售记录
saleDao.insertSale(sale);
- 在向商品表中验证指定商品时,没有发现编号为1005的商品,抛出空指针异常
- 检验到异常抛出后,Spring调用事务管理器执行回滚操作,将insert操作rollback,销售表中无记录添加
3.佐证事务起了作用
思路:
-
销售表的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)
4.@Transaction注解的属性可省略不写
当只有@Transaction注解,没有设定其属性值时:
- 默认的传播行为是REQUIRED
- 默认的隔离级别是DEFAULT
- 默认抛出运行时异常时,进行回滚操作
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);//过量商品
}
由测试结果可见,当抛出我们自定义的运行时异常时,即使没有提前指定,也会执行回滚。
6.3.3使用AspectJ的AOP配置管理事务
这种方案是用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)购买过量商品,抛出异常
7.在web项目中使用Spring
7.1在web项目中使用Spring的步骤
在web项目中使用Spring框架,首先要解决在Servlet中获取到Spring容器的问题。只要在Servlet中能获取到Spring容器,就能从容器中获取到service对象,从而调用dao,实现访问数据库。
下面将上面Spring整合MyBatis的例子修改为web项目
1.新建一个web项目
新建一个web类型的maven子模块:spring-09-web,使用模板:maven-archetype-webapp
然后完善maven结构目录
main|--java
|--resources
|--webapp
2.添加依赖
复制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>
3.复制Spring整合mybatis项目的代码
直接将Spring整合mybatis项目的java和resources文件夹复制到当前项目中覆盖
4.定义index页面
在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>
5.创建servlet
在创建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);
}
}
6.创建响应页面
在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>
7.发布网站
指定网站别名为MyWeb
8.开启服务器,通过浏览器发送请求
7.2在web项目中使用Spring容器的问题
我们通过浏览器再发送一次请求:
可以发现,发送两次请求,创建了两个Spring容器:
Spring容器一次性就可以创建我们所需的所有对象,无需每次都创建,现在的这种情况是不被允许的。
解决方法:使用监听器,在监听器中创建一个Spring容器对象,然后将Spring容器对象放入全局作用域对象中。这样,我们就不需要在servlet中创建Spring容器了,需要时,直接从全局作用域对象中拿就可以了。
7.3使用Spring的监听器
监听器有两个作用:
- 创建Spring容器对象:执行
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
- 将容器对象放入全局作用域对象中:
ServletContext.setAttribute(key,ac)
监听器可以自己创建,也可以使用Spring框架中提供的ContextLoaderListener。
7.3.1ContextLoaderListener源码分析
若要实现在ServletContext初始化时创建Spring容器,就需要使用监听器接口ServletContextListener对ServletContext进行监听。
Spring为该监听器接口定义了一个实现类:ContextLoaderListener,完成了两个很重要的工作:
- 创建监听器对象
- 将容器对象放入到ServletContext的空间中
使用时在web.xml中注册该监听器:
<!--注册监听器ContextLoaderListener-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
打开ContextLoaderListener的源码,可以看到有四个方法:
- ContextLoaderListener():无参构造
- ContextLoaderListener(WebApplicationContext):有参构造
- contextInitialized(ServletContextEvent):初始化方法【重要】
- contextDestroyed(ServletContextEvent):销毁方法
其中,初始化方法contextInitialized()比较重要,就是在其内部实现创建容器对象并放入全局作用域
追踪initWebApplicationContext()方法,可以看到其具体实现步骤
在将Spring容器对象放入全局作用域时,key值为一个常量:WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
追踪此常量,可以发现,就是Spring容器对象的名字加上.ROOT
的字符串
7.3.2使用Spring监听器步骤
1.添加Spring监听器依赖
在pom.xml文件中加入如下依赖,才可以使用Spring监听器对象
<!--Spring监听器-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
2.注册监听器
在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>
3.获取Spring容器对象
在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;
}
4.测试能否起作用
开启服务器,连续发送两次请求
查看后台,发现两次使用的Spring容器对象是同一个,测试成功。