Spring总结来说就是一个轻量级的控制反转(IOC)和面向切面编程(AOP)的框架
Spring基础 容器:IoC IoC 是 Inversion of Control 的简写,译为“控制反转”,它不是一门技术,而是一种设计思想,是一个重要的面向对象编程法则,能够指导我们如何设计出松耦合、更优良的程序。
Spring 通过 IoC 容器来管理所有 Java 对象的实例化和初始化,控制对象与对象之间的依赖关系。我们将由 IoC 容器管理的 Java 对象称为 Spring Bean,它与使用关键字 new 创建的 Java 对象没有任何区别。
IoC 容器是 Spring 框架中最重要的核心组件之一,它贯穿了 Spring 从诞生到成长的整个过程。
IoC容器 控制反转(IoC)
控制反转是一种思想。
控制反转是为了降低程序耦合度,提高程序扩展力。
控制反转,反转的是什么?
将对象的创建权利交出去,交给第三方容器负责。
将对象和对象之间关系的维护权交出去,交给第三方容器负责。
控制反转这种思想如何实现呢?
DI(Dependency Injection):依赖注入
依赖注入 DI(Dependency Injection):依赖注入,依赖注入实现了控制反转的思想。
依赖注入:
指Spring创建对象的过程中,将对象依赖属性通过配置进行注入
依赖注入常见的实现方式包括两种:
所以结论是:IOC 就是一种控制反转的思想, 而 DI 是对IoC的一种具体实现。
Bean管理说的是:Bean对象的创建,以及Bean对象中属性的赋值(或者叫做Bean对象之间关系的维护)。
IoC容器在Spring的实现 Spring 的 IoC 容器就是 IoC思想的一个落地的产品实现。IoC容器中管理的组件也叫做 bean。在创建 bean 之前,首先需要创建IoC 容器。Spring 提供了IoC 容器的两种实现方式:
①BeanFactory
这是 IoC 容器的基本实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用。
②ApplicationContext
BeanFactory 的子接口,提供了更多高级特性。面向 Spring 的使用者,几乎所有场合都使用 ApplicationContext 而不是底层的 BeanFactory。
③ApplicationContext的主要实现类
类型名
简介
ClassPathXmlApplicationContext
通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象
FileSystemXmlApplicationContext
通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象
ConfigurableApplicationContext
ApplicationContext 的子接口,包含一些扩展方法 refresh() 和 close() ,让 ApplicationContext 具有启动、关闭和刷新上下文的能力。
WebApplicationContext
专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。
基于XML管理Bean 搭建子模块spring6-ioc-xml ①搭建模块
搭建方式如:spring-first
②引入配置文件
引入spring-first模块配置文件:beans.xml、log4j2.xml
③添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 6.0.3</version > </dependency > <dependency > <groupId > org.junit.jupiter</groupId > <artifactId > junit-jupiter-api</artifactId > <version > 5.3.1</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-core</artifactId > <version > 2.19.0</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-slf4j2-impl</artifactId > <version > 2.19.0</version > </dependency > </dependencies >
④引入java类
引入spring-first模块java及test目录下实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.hang.spring6.bean;public class HelloWorld { public HelloWorld () { System.out.println("无参数构造方法执行" ); } public void sayHello () { System.out.println("helloworld" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.hang.spring6.bean;import org.junit.jupiter.api.Test;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class HelloWorldTest { private Logger logger = LoggerFactory.getLogger(HelloWorldTest.class); @Test public void testHelloWorld () { } }
实验一:获取bean ①方式一:根据id获取 由于 id 属性指定了 bean 的唯一标识,所以根据 bean 标签的 id 属性可以精确获取到一个组件对象。上个实验中我们使用的就是这种方式。
②方式二:根据类型获取 1 2 3 4 5 6 @Test public void testHelloWorld1 () { ApplicationContext ac = new ClassPathXmlApplicationContext ("beans.xml" ); HelloWorld bean = ac.getBean(HelloWorld.class); bean.sayHello(); }
③方式三:根据id和类型 1 2 3 4 5 6 @Test public void testHelloWorld2 () { ApplicationContext ac = new ClassPathXmlApplicationContext ("beans.xml" ); HelloWorld bean = ac.getBean("helloworld" , HelloWorld.class); bean.sayHello(); }
④注意的地方 当根据类型获取bean时,要求IOC容器中指定类型的bean有且只能有一个
当IOC容器中一共配置了两个:
1 2 <bean id ="helloworldOne" class ="com.hang.spring6.bean.HelloWorld" > </bean > <bean id ="helloworldTwo" class ="com.hang.spring6.bean.HelloWorld" > </bean >
根据类型获取时会抛出异常:
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘com.hang.spring6.bean.HelloWorld’ available: expected single matching bean but found 2: helloworldOne,helloworldTwo
⑤扩展知识 如果组件类实现了接口,根据接口类型可以获取 bean 吗?
可以,前提是bean唯一
如果一个接口有多个实现类,这些实现类都配置了 bean,根据接口类型可以获取 bean 吗?
不行,因为bean不唯一
结论
根据类型来获取bean时,在满足bean唯一性的前提下,其实只是看:『对象 instanceof 指定的类型』的返回结果,只要返回的是true就可以认定为和类型匹配,能够获取到。
java中,instanceof运算符用于判断前面的对象是否是后面的类,或其子类、实现类的实例。如果是返回true,否则返回false。也就是说:用instanceof关键字做判断时, instanceof 操作符的左右操作必须有继承或实现关系
实验二:依赖注入之setter注入 ①创建学生类Student
1 2 3 4 5 6 7 8 9 10 11 package com.hang.spring6.bean;@Data @NoConstructor @AllConstructor public class Student { private Integer id; private String name; private Integer age; private String sex; }
②配置bean时为属性赋值
spring-di.xml
1 2 3 4 5 6 7 8 9 <bean id ="studentOne" class ="com.hang.spring6.bean.Student" > <property name ="id" value ="1001" > </property > <property name ="name" value ="张三" > </property > <property name ="age" value ="23" > </property > <property name ="sex" value ="男" > </property > </bean >
③测试
1 2 3 4 5 6 @Test public void testDIBySet () { ApplicationContext ac = new ClassPathXmlApplicationContext ("spring-di.xml" ); Student studentOne = ac.getBean("studentOne" , Student.class); System.out.println(studentOne); }
实验三:依赖注入之构造器注入 ①在Student类中添加有参构造
②配置bean
spring-di.xml
1 2 3 4 5 6 <bean id ="studentTwo" class ="com.hang.spring6.bean.Student" > <constructor-arg value ="1002" > </constructor-arg > <constructor-arg value ="李四" > </constructor-arg > <constructor-arg value ="33" > </constructor-arg > <constructor-arg value ="女" > </constructor-arg > </bean >
注意:
constructor-arg标签还有两个属性可以进一步描述构造器参数:
index属性:指定参数所在位置的索引(从0开始)
name属性:指定参数名
③测试
1 2 3 4 5 6 @Test public void testDIByConstructor () { ApplicationContext ac = new ClassPathXmlApplicationContext ("spring-di.xml" ); Student studentOne = ac.getBean("studentTwo" , Student.class); System.out.println(studentOne); }
实验四:特殊值处理 ①字面量赋值
什么是字面量?
int a = 10;
声明一个变量a,初始化为10,此时a就不代表字母a了,而是作为一个变量的名字。当我们引用a的时候,我们实际上拿到的值是10。
而如果a是带引号的:’a’,那么它现在不是一个变量,它就是代表a这个字母本身,这就是字面量。所以字面量没有引申含义,就是我们看到的这个数据本身。
1 2 <property name ="name" value ="张三" />
②null值 1 2 3 <property name ="name" > <null /> </property >
注意:
1 <property name ="name" value ="null" > </property >
以上写法,为name所赋的值是字符串null
③xml实体 1 2 3 <property name ="expression" value ="a < b" />
④CDATA节 1 2 3 4 5 6 7 <property name ="expression" > <value > <![CDATA[a < b]]></value > </property >
实验五:为对象类型属性赋值 ①创建班级类Clazz
1 2 3 4 5 6 7 8 9 @Data @NoConstructor @AllConstructor public class Clazz { private Integer clazzId; private String clazzName; }
②修改Student类
在Student类中添加Get、Set方法
方式一:引用外部bean 配置Clazz类型的bean:
1 2 3 4 <bean id ="clazzOne" class ="com.hang.spring6.bean.Clazz" > <property name ="clazzId" value ="1111" > </property > <property name ="clazzName" value ="财源滚滚班" > </property > </bean >
为Student中的clazz属性赋值:
1 2 3 4 5 6 7 8 <bean id ="studentFour" class ="com.hang.spring6.bean.Student" > <property name ="id" value ="1004" > </property > <property name ="name" value ="赵六" > </property > <property name ="age" value ="26" > </property > <property name ="sex" value ="女" > </property > <property name ="clazz" ref ="clazzOne" > </property > </bean >
错误演示:
1 2 3 4 5 6 7 <bean id ="studentFour" class ="com.hang.spring6.bean.Student" > <property name ="id" value ="1004" > </property > <property name ="name" value ="赵六" > </property > <property name ="age" value ="26" > </property > <property name ="sex" value ="女" > </property > <property name ="clazz" value ="clazzOne" > </property > </bean >
如果错把ref属性写成了value属性,会抛出异常: Caused by: java.lang.IllegalStateException: Cannot convert value of type ‘java.lang.String’ to required type ‘com.hang.spring6.bean.Clazz’ for property ‘clazz’: no matching editors or conversion strategy found
意思是不能把String类型转换成我们要的Clazz类型,说明我们使用value属性时,Spring只把这个属性看做一个普通的字符串,不会认为这是一个bean的id,更不会根据它去找到bean来赋值
方式二:内部bean 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <bean id ="studentFour" class ="com.hang.spring6.bean.Student" > <property name ="id" value ="1004" > </property > <property name ="name" value ="赵六" > </property > <property name ="age" value ="26" > </property > <property name ="sex" value ="女" > </property > <property name ="clazz" > <bean id ="clazzInner" class ="com.hang.spring6.bean.Clazz" > <property name ="clazzId" value ="2222" > </property > <property name ="clazzName" value ="远大前程班" > </property > </bean > </property > </bean >
方式三:级联属性赋值 1 2 3 4 5 6 7 8 9 <bean id ="studentFour" class ="com.hang.spring6.bean.Student" > <property name ="id" value ="1004" > </property > <property name ="name" value ="赵六" > </property > <property name ="age" value ="26" > </property > <property name ="sex" value ="女" > </property > <property name ="clazz" ref ="clazzOne" > </property > <property name ="clazz.clazzId" value ="3333" > </property > <property name ="clazz.clazzName" value ="最强王者班" > </property > </bean >
实验六:为数组类型属性赋值 ①修改Student类
在Student类中添加以下代码:
1 2 3 4 5 6 7 8 9 private String[] hobbies;public String[] getHobbies() { return hobbies; } public void setHobbies (String[] hobbies) { this .hobbies = hobbies; }
②配置bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <bean id ="studentFour" class ="com.hang.spring.bean6.Student" > <property name ="id" value ="1004" > </property > <property name ="name" value ="赵六" > </property > <property name ="age" value ="26" > </property > <property name ="sex" value ="女" > </property > <property name ="clazz" ref ="clazzOne" > </property > <property name ="hobbies" > <array > <value > 抽烟</value > <value > 喝酒</value > <value > 烫头</value > </array > </property > </bean >
实验七:为集合类型属性赋值 ①为List集合类型属性赋值 在Clazz类中添加以下代码:
1 2 3 4 5 6 7 8 9 private List<Student> students;public List<Student> getStudents () { return students; } public void setStudents (List<Student> students) { this .students = students; }
配置bean:
1 2 3 4 5 6 7 8 9 10 11 <bean id ="clazzTwo" class ="com.hang.spring6.bean.Clazz" > <property name ="clazzId" value ="4444" > </property > <property name ="clazzName" value ="Javaee0222" > </property > <property name ="students" > <list > <ref bean ="studentOne" > </ref > <ref bean ="studentTwo" > </ref > <ref bean ="studentThree" > </ref > </list > </property > </bean >
若为Set集合类型属性赋值,只需要将其中的list标签改为set标签即可
②为Map集合类型属性赋值 创建教师类Teacher:
1 2 3 4 5 6 7 8 9 10 11 package com.hang.spring6.bean;@Data @NoConstructor @AllConstructor public class Teacher { private Integer teacherId; private String teacherName; }
在Student类中添加以下代码(添加老师集合,一个学生对应多个老师):
1 2 3 4 5 6 7 8 9 private Map<String, Teacher> teacherMap;public Map<String, Teacher> getTeacherMap () { return teacherMap; } public void setTeacherMap (Map<String, Teacher> teacherMap) { this .teacherMap = teacherMap; }
配置bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <bean id ="teacherOne" class ="com.hang.spring6.bean.Teacher" > <property name ="teacherId" value ="10010" > </property > <property name ="teacherName" value ="大宝" > </property > </bean > <bean id ="teacherTwo" class ="com.hang.spring6.bean.Teacher" > <property name ="teacherId" value ="10086" > </property > <property name ="teacherName" value ="二宝" > </property > </bean > <bean id ="studentFour" class ="com.hang.spring6.bean.Student" > <property name ="id" value ="1004" > </property > <property name ="name" value ="赵六" > </property > <property name ="age" value ="26" > </property > <property name ="sex" value ="女" > </property > <property name ="clazz" ref ="clazzOne" > </property > <property name ="hobbies" > <array > <value > 抽烟</value > <value > 喝酒</value > <value > 烫头</value > </array > </property > <property name ="teacherMap" > <map > <entry > <key > <value > 10010</value > </key > <ref bean ="teacherOne" > </ref > </entry > <entry > <key > <value > 10086</value > </key > <ref bean ="teacherTwo" > </ref > </entry > </map > </property > </bean >
③引用集合类型的bean 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <util:list id ="students" > <ref bean ="studentOne" > </ref > <ref bean ="studentTwo" > </ref > <ref bean ="studentThree" > </ref > </util:list > <util:map id ="teacherMap" > <entry > <key > <value > 10010</value > </key > <ref bean ="teacherOne" > </ref > </entry > <entry > <key > <value > 10086</value > </key > <ref bean ="teacherTwo" > </ref > </entry > </util:map > <bean id ="clazzTwo" class ="com.hang.spring6.bean.Clazz" > <property name ="clazzId" value ="4444" > </property > <property name ="clazzName" value ="Javaee0222" > </property > <property name ="students" ref ="students" > </property > </bean > <bean id ="studentFour" class ="com.hang.spring6.bean.Student" > <property name ="id" value ="1004" > </property > <property name ="name" value ="赵六" > </property > <property name ="age" value ="26" > </property > <property name ="sex" value ="女" > </property > <property name ="clazz" ref ="clazzOne" > </property > <property name ="hobbies" > <array > <value > 抽烟</value > <value > 喝酒</value > <value > 烫头</value > </array > </property > <property name ="teacherMap" ref ="teacherMap" > </property > </bean >
使用util:list、util:map标签必须引入相应的命名空间
1 2 3 4 5 6 7 8 <?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:util ="http://www.springframework.org/schema/util" xsi:schemaLocation ="http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" >
实验八:p命名空间 引入p命名空间
1 2 3 4 5 6 7 8 9 <?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:util ="http://www.springframework.org/schema/util" xmlns:p ="http://www.springframework.org/schema/p" xsi:schemaLocation ="http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" >
引入p命名空间后,可以通过以下方式为bean的各个属性赋值
1 2 <bean id ="studentSix" class ="com.hang.spring6.bean.Student" p:id ="1006" p:name ="小明" p:clazz-ref ="clazzOne" p:teacherMap-ref ="teacherMap" > </bean >
实验九:引入外部属性文件 ①加入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.30</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.2.15</version > </dependency >
②创建外部属性文件
1 2 3 4 jdbc.user =root jdbc.password =hang jdbc.url =jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC jdbc.driver =com.mysql.cj.jdbc.Driver
③引入属性文件
引入context 名称空间
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" ></beans >
1 2 <context:property-placeholder location ="classpath:jdbc.properties" />
注意:在使用 context:property-placeholder 元素加载外包配置文件功能前,首先需要在 XML 配置的一级标签 中添加 context 相关的约束。
④配置bean
1 2 3 4 5 6 <bean id ="druidDataSource" class ="com.alibaba.druid.pool.DruidDataSource" > <property name ="url" value ="${jdbc.url}" /> <property name ="driverClassName" value ="${jdbc.driver}" /> <property name ="username" value ="${jdbc.user}" /> <property name ="password" value ="${jdbc.password}" /> </bean >
⑤测试
1 2 3 4 5 6 7 @Test public void testDataSource () throws SQLException { ApplicationContext ac = new ClassPathXmlApplicationContext ("spring-datasource.xml" ); DataSource dataSource = ac.getBean(DataSource.class); Connection connection = dataSource.getConnection(); System.out.println(connection); }
实验十:bean的作用域 ①概念
在Spring中可以通过配置bean标签的scope属性来指定bean的作用域范围,各取值含义参加下表:
取值
含义
创建对象的时机
singleton(默认)
在IOC容器中,这个bean的对象始终为单实例
IOC容器初始化时
prototype
这个bean在IOC容器中有多个实例
获取bean时
如果是在WebApplicationContext环境下还会有另外几个作用域(但不常用):
取值
含义
request
在一个请求范围内有效
session
在一个会话范围内有效
②创建类User
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package com.hang.spring6.bean;public class User { private Integer id; private String username; private String password; private Integer age; public User () { } public User (Integer id, String username, String password, Integer age) { this .id = id; this .username = username; this .password = password; this .age = age; } public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public String getUsername () { return username; } public void setUsername (String username) { this .username = username; } public String getPassword () { return password; } public void setPassword (String password) { this .password = password; } public Integer getAge () { return age; } public void setAge (Integer age) { this .age = age; } @Override public String toString () { return "User{" + "id=" + id + ", username='" + username + '/' ' + ", password=' " + password + '/'' + " , age=" + age + '}'; } }
③配置bean
1 2 3 <bean class ="com.hang.spring6.bean.User" scope ="prototype" > </bean >
④测试
1 2 3 4 5 6 7 @Test public void testBeanScope () { ApplicationContext ac = new ClassPathXmlApplicationContext ("spring-scope.xml" ); User user1 = ac.getBean(User.class); User user2 = ac.getBean(User.class); System.out.println(user1==user2); }
实验十一:bean生命周期 ①具体的生命周期过程
②修改类User
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 public class User { private Integer id; private String username; private String password; private Integer age; public User () { System.out.println("生命周期:1、创建对象" ); } public User (Integer id, String username, String password, Integer age) { this .id = id; this .username = username; this .password = password; this .age = age; } public Integer getId () { return id; } public void setId (Integer id) { System.out.println("生命周期:2、依赖注入" ); this .id = id; } public String getUsername () { return username; } public void setUsername (String username) { this .username = username; } public String getPassword () { return password; } public void setPassword (String password) { this .password = password; } public Integer getAge () { return age; } public void setAge (Integer age) { this .age = age; } public void initMethod () { System.out.println("生命周期:3、初始化" ); } public void destroyMethod () { System.out.println("生命周期:5、销毁" ); } @Override public String toString () { return "User{" + "id=" + id + ", username='" + username + '/' ' + ", password=' " + password + '/'' + " , age=" + age + '}'; } }
注意其中的initMethod()和destroyMethod(),可以通过配置bean指定为初始化和销毁的方法
③配置bean
1 2 3 4 5 6 7 8 <bean class ="com.hang.spring6.bean.User" scope ="prototype" init-method ="initMethod" destroy-method ="destroyMethod" > <property name ="id" value ="1001" > </property > <property name ="username" value ="admin" > </property > <property name ="password" value ="123456" > </property > <property name ="age" value ="23" > </property > </bean >
④测试
1 2 3 4 5 6 7 @Test public void testLife () { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext ("spring-lifecycle.xml" ); User bean = ac.getBean(User.class); System.out.println("生命周期:4、通过IOC容器获取bean并使用" ); ac.close(); }
⑤bean的后置处理器
bean的后置处理器会在生命周期的初始化前后添加额外的操作,需要实现BeanPostProcessor接口,且配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行
创建bean的后置处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.hang.spring6.process; import org.springframework.beans.BeansException;import org.springframework.beans.factory.config.BeanPostProcessor;public class MyBeanProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization (Object bean, String beanName) throws BeansException { System.out.println("☆☆☆" + beanName + " = " + bean); return bean; } @Override public Object postProcessAfterInitialization (Object bean, String beanName) throws BeansException { System.out.println("★★★" + beanName + " = " + bean); return bean; } }
在IOC容器中配置后置处理器:
1 2 <bean id ="myBeanProcessor" class ="com.hang.spring6.process.MyBeanProcessor" />
实验十二:FactoryBean ①简介
FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的bean不同,配置一个FactoryBean类型的bean,在获取bean的时候得到的并不是class属性中配置的这个类的对象,而是getObject()方法的返回值。通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。
将来我们整合Mybatis时,Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 package org.springframework.beans.factory;import org.springframework.lang.Nullable;public interface FactoryBean <T> { String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType" ; @Nullable T getObject () throws Exception; @Nullable Class<?> getObjectType(); default boolean isSingleton () { return true ; } }
②创建类UserFactoryBean
1 2 3 4 5 6 7 8 9 10 11 12 package com.hang.spring6.bean;public class UserFactoryBean implements FactoryBean <User> { @Override public User getObject () throws Exception { return new User (); } @Override public Class<?> getObjectType() { return User.class; } }
③配置bean
1 <bean id ="user" class ="com.hang.spring6.bean.UserFactoryBean" > </bean >
④测试
1 2 3 4 5 6 7 @Test public void testUserFactoryBean () { ApplicationContext ac = new ClassPathXmlApplicationContext ("spring-factorybean.xml" ); User user = (User) ac.getBean("user" ); System.out.println(user); }
实验十三:基于xml自动装配
自动装配:
根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或接口类型属性赋值
①场景模拟
创建类UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.hang.spring6.autowire.controllerpublic class UserController { private UserService userService; public void setUserService (UserService userService) { this .userService = userService; } public void saveUser () { userService.saveUser(); } }
创建接口UserService
1 2 3 4 5 6 package com.hang.spring6.autowire.servicepublic interface UserService { void saveUser () ; }
创建类UserServiceImpl实现接口UserService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.hang.spring6.autowire.service.implpublic class UserServiceImpl implements UserService { private UserDao userDao; public void setUserDao (UserDao userDao) { this .userDao = userDao; } @Override public void saveUser () { userDao.saveUser(); } }
创建接口UserDao
1 2 3 4 5 6 package com.hang.spring6.autowire.daopublic interface UserDao { void saveUser () ; }
创建类UserDaoImpl实现接口UserDao
1 2 3 4 5 6 7 8 9 package com.hang.spring6.autowire.dao.implpublic class UserDaoImpl implements UserDao { @Override public void saveUser () { System.out.println("保存成功" ); } }
②配置bean
使用bean标签的autowire属性设置自动装配效果
自动装配方式:byType
byType:根据类型匹配IOC容器中的某个兼容类型的bean,为属性自动赋值
若在IOC中,没有任何一个兼容类型的bean能够为属性赋值,则该属性不装配,即值为默认值null
若在IOC中,有多个兼容类型的bean能够为属性赋值,则抛出异常NoUniqueBeanDefinitionException
1 2 3 4 5 <bean id ="userController" class ="com.hang.spring6.autowire.controller.UserController" autowire ="byType" > </bean > <bean id ="userService" class ="com.hang.spring6.autowire.service.impl.UserServiceImpl" autowire ="byType" > </bean > <bean id ="userDao" class ="com.hang.spring6.autowire.dao.impl.UserDaoImpl" > </bean >
自动装配方式:byName
byName:将自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值
1 2 3 4 5 6 7 <bean id ="userController" class ="com.hang.spring6.autowire.controller.UserController" autowire ="byName" > </bean > <bean id ="userService" class ="com.hang.spring6.autowire.service.impl.UserServiceImpl" autowire ="byName" > </bean > <bean id ="userServiceImpl" class ="com.hang.spring6.autowire.service.impl.UserServiceImpl" autowire ="byName" > </bean > <bean id ="userDao" class ="com.hang.spring6.autowire.dao.impl.UserDaoImpl" > </bean > <bean id ="userDaoImpl" class ="com.hang.spring6.autowire.dao.impl.UserDaoImpl" > </bean >
③测试
1 2 3 4 5 6 @Test public void testAutoWireByXML () { ApplicationContext ac = new ClassPathXmlApplicationContext ("autowire-xml.xml" ); UserController userController = ac.getBean(UserController.class); userController.saveUser(); }
基于注解管理Bean(☆) 从 Java 5 开始,Java 增加了对注解(Annotation)的支持,它是代码中的一种特殊标记,可以在编译、类加载和运行时被读取,执行相应的处理。开发人员可以通过注解在不改变原有代码和逻辑的情况下,在源代码中嵌入补充信息。
Spring 从 2.5 版本开始提供了对注解技术的全面支持,我们可以使用注解来实现自动装配,简化 Spring 的 XML 配置。
Spring 通过注解实现自动装配的步骤如下:
引入依赖
开启组件扫描
使用注解定义 Bean
依赖注入
搭建子模块spring6-ioc-annotation ①搭建模块
搭建方式如:spring6-ioc-xml
②引入配置文件
引入spring-ioc-xml模块日志log4j2.xml
③添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 6.0.3</version > </dependency > <dependency > <groupId > org.junit.jupiter</groupId > <artifactId > junit-jupiter-api</artifactId > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-core</artifactId > <version > 2.19.0</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-slf4j2-impl</artifactId > <version > 2.19.0</version > </dependency > </dependencies >
开启组件扫描 Spring 默认不使用注解装配 Bean,因此我们需要在 Spring 的 XML 配置中,通过 context:component-scan 元素开启 Spring Beans的自动扫描功能。开启此功能后,Spring 会自动从扫描指定的包(base-package 属性设置)及其子包下的所有类,如果类上使用了 @Component 注解,就将该类装配到容器中。
1 2 3 4 5 6 7 8 9 10 11 <?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-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" > <context:component-scan base-package ="com.hang.spring6" > </context:component-scan > </beans >
注意:在使用 context:component-scan 元素开启自动扫描功能前,首先需要在 XML 配置的一级标签 中添加 context 相关的约束。
情况一:最基本的扫描方式
1 2 <context:component-scan base-package ="com.hang.spring6" > </context:component-scan >
情况二:指定要排除的组件
1 2 3 4 5 6 7 8 9 10 <context:component-scan base-package ="com.hang.spring6" > <context:exclude-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> </context:component-scan >
情况三:仅扫描指定组件
1 2 3 4 5 6 7 8 9 10 11 12 <context:component-scan base-package ="com.hang" use-default-filters ="false" > <context:include-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> </context:component-scan >
使用注解定义 Bean Spring 提供了以下多个注解,这些注解可以直接标注在 Java 类上,将它们定义成 Spring Bean。
注解
说明
@Component
该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。
@Repository
该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Service
该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Controller
该注解通常作用在控制层(如SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
实验一:@Autowired注入 单独使用@Autowired注解,默认根据类型装配 。【默认是byType】
查看源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.springframework.beans.factory.annotation;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Autowired { boolean required () default true ; }
源码中有两处需要注意:
①场景一:属性注入 创建UserDao接口
1 2 3 4 5 6 package com.hang.spring6.dao;public interface UserDao { public void print () ; }
创建UserDaoImpl实现
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.hang.spring6.dao.impl;import com.hang.spring6.dao.UserDao;import org.springframework.stereotype.Repository;@Repository public class UserDaoImpl implements UserDao { @Override public void print () { System.out.println("Dao层执行结束" ); } }
创建UserService接口
1 2 3 4 5 6 package com.hang.spring6.service;public interface UserService { public void out () ; }
创建UserServiceImpl实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.hang.spring6.service.impl;import com.hang.spring6.dao.UserDao;import com.hang.spring6.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override public void out () { userDao.print(); System.out.println("Service层执行结束" ); } }
创建UserController类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.hang.spring6.controller;import com.hang.spring6.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;@Controller public class UserController { @Autowired private UserService userService; public void out () { userService.out(); System.out.println("Controller层执行结束。" ); } }
测试一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.hang.spring6.bean;import com.hang.spring6.controller.UserController;import org.junit.jupiter.api.Test;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class UserTest { private Logger logger = LoggerFactory.getLogger(UserTest.class); @Test public void testAnnotation () { ApplicationContext context = new ClassPathXmlApplicationContext ("Beans.xml" ); UserController userController = context.getBean("userController" , UserController.class); userController.out(); logger.info("执行成功" ); } }
以上构造方法和setter方法都没有提供,经过测试,仍然可以注入成功。
②场景二:set注入 修改UserServiceImpl类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.hang.spring6.service.impl;import com.hang.spring6.dao.UserDao;import com.hang.spring6.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Service public class UserServiceImpl implements UserService { private UserDao userDao; @Autowired public void setUserDao (UserDao userDao) { this .userDao = userDao; } @Override public void out () { userDao.print(); System.out.println("Service层执行结束" ); } }
修改UserController类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.hang.spring6.controller;import com.hang.spring6.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;@Controller public class UserController { private UserService userService; @Autowired public void setUserService (UserService userService) { this .userService = userService; } public void out () { userService.out(); System.out.println("Controller层执行结束。" ); } }
测试:成功调用
③场景三:构造方法注入 修改UserServiceImpl类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.hang.spring6.service.impl;import com.hang.spring6.dao.UserDao;import com.hang.spring6.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Service public class UserServiceImpl implements UserService { private UserDao userDao; @Autowired public UserServiceImpl (UserDao userDao) { this .userDao = userDao; } @Override public void out () { userDao.print(); System.out.println("Service层执行结束" ); } }
修改UserController类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.hang.spring6.controller;import com.hang.spring6.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;@Controller public class UserController { private UserService userService; @Autowired public UserController (UserService userService) { this .userService = userService; } public void out () { userService.out(); System.out.println("Controller层执行结束。" ); } }
测试:成功调用
④场景四:形参上注入 修改UserServiceImpl类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.hang.spring6.service.impl;import com.hang.spring6.dao.UserDao;import com.hang.spring6.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Service public class UserServiceImpl implements UserService { private UserDao userDao; public UserServiceImpl (@Autowired UserDao userDao) { this .userDao = userDao; } @Override public void out () { userDao.print(); System.out.println("Service层执行结束" ); } }
修改UserController类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.hang.spring6.controller;import com.hang.spring6.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;@Controller public class UserController { private UserService userService; public UserController (@Autowired UserService userService) { this .userService = userService; } public void out () { userService.out(); System.out.println("Controller层执行结束。" ); } }
测试:成功调用
⑤场景五:只有一个构造函数,无注解 修改UserServiceImpl类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.hang.spring6.service.impl;import com.hang.spring6.dao.UserDao;import com.hang.spring6.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.stereotype.Service;@Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; public UserServiceImpl (UserDao userDao) { this .userDao = userDao; } @Override public void out () { userDao.print(); System.out.println("Service层执行结束" ); } }
测试通过
当有参数的构造方法只有一个时,@Autowired注解可以省略。
说明:有多个构造方法时呢?大家可以测试(再添加一个无参构造函数),测试报错
⑥场景六:@Autowired注解和@Qualifier注解联合 添加dao层实现
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.hang.spring6.dao.impl;import com.hang.spring6.dao.UserDao;import org.springframework.stereotype.Repository;@Repository public class UserDaoRedisImpl implements UserDao { @Override public void print () { System.out.println("Redis Dao层执行结束" ); } }
测试:测试异常
错误信息中说:不能装配,UserDao这个Bean的数量等于2
怎么解决这个问题呢?当然要byName,根据名称进行装配了。
修改UserServiceImpl类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.hang.spring6.service.impl;import com.hang.spring6.dao.UserDao;import com.hang.spring6.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Service public class UserServiceImpl implements UserService { @Autowired @Qualifier("userDaoImpl") private UserDao userDao; @Override public void out () { userDao.print(); System.out.println("Service层执行结束" ); } }
总结
@Autowired注解可以出现在:属性上、构造方法上、构造方法的参数上、setter方法上。
当带参数的构造方法只有一个,@Autowired注解可以省略。()
@Autowired注解默认根据类型注入。如果要根据名称注入的话,需要配合@Qualifier注解一起使用。
实验二:@Resource注入 @Resource注解也可以完成属性注入。那它和@Autowired注解有什么区别?
@Resource注解是JDK扩展包中的,也就是说属于JDK的一部分。所以该注解是标准注解,更加具有通用性。(JSR-250标准中制定的注解类型。JSR是Java规范提案。)
@Autowired注解是Spring框架自己的。
@Resource注解默认根据名称装配byName,未指定name时,使用属性名作为name。通过name找不到的话会自动启动通过类型byType装配。
@Autowired注解默认根据类型装配byType,如果想根据名称装配,需要配合@Qualifier注解一起用。
@Resource注解用在属性上、setter方法上。
@Autowired注解用在属性上、setter方法上、构造方法上、构造方法参数上。
@Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入以下依赖:【如果是JDK8的话不需要额外引入依赖。高于JDK11或低于JDK8需要引入以下依赖。 】
1 2 3 4 5 <dependency > <groupId > jakarta.annotation</groupId > <artifactId > jakarta.annotation-api</artifactId > <version > 2.1.1</version > </dependency >
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package jakarta.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Repeatable;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(Resources.class) public @interface Resource { String name () default "" ; String lookup () default "" ; Class<?> type() default Object.class; Resource.AuthenticationType authenticationType () default Resource.AuthenticationType.CONTAINER; boolean shareable () default true ; String mappedName () default "" ; String description () default "" ; public static enum AuthenticationType { CONTAINER, APPLICATION; private AuthenticationType () { } } }
①场景一:根据name注入 修改UserDaoImpl类
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.hang.spring6.dao.impl;import com.hang.spring6.dao.UserDao;import org.springframework.stereotype.Repository;@Repository("myUserDao") public class UserDaoImpl implements UserDao { @Override public void print () { System.out.println("Dao层执行结束" ); } }
修改UserServiceImpl类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.hang.spring6.service.impl;import com.hang.spring6.dao.UserDao;import com.hang.spring6.service.UserService;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.stereotype.Service;@Service public class UserServiceImpl implements UserService { @Resource(name = "myUserDao") private UserDao myUserDao; @Override public void out () { myUserDao.print(); System.out.println("Service层执行结束" ); } }
测试通过
②场景二:name未知注入 修改UserDaoImpl类
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.hang.spring6.dao.impl;import com.hang.spring6.dao.UserDao;import org.springframework.stereotype.Repository;@Repository("myUserDao") public class UserDaoImpl implements UserDao { @Override public void print () { System.out.println("Dao层执行结束" ); } }
修改UserServiceImpl类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.hang.spring6.service.impl;import com.hang.spring6.dao.UserDao;import com.hang.spring6.service.UserService;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.stereotype.Service;@Service public class UserServiceImpl implements UserService { @Resource private UserDao myUserDao; @Override public void out () { myUserDao.print(); System.out.println("Service层执行结束" ); } }
测试通过
当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名。
③场景三 其他情况 修改UserServiceImpl类,userDao1属性名不存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.hang.spring6.service.impl;import com.hang.spring6.dao.UserDao;import com.hang.spring6.service.UserService;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.stereotype.Service;@Service public class UserServiceImpl implements UserService { @Resource private UserDao userDao1; @Override public void out () { userDao1.print(); System.out.println("Service层执行结束" ); } }
测试异常
根据异常信息得知:显然当通过name找不到的时候,自然会启动byType进行注入,以上的错误是因为UserDao接口下有两个实现类导致的。所以根据类型注入就会报错。
@Resource的set注入可以自行测试
总结:
@Resource注解:默认byName注入,没有指定name时把属性名当做name,根据name找不到时,才会byType注入。byType注入时,某种类型的Bean只能有一个
3.3.6、Spring全注解开发 全注解开发就是不再使用spring配置文件了,写一个配置类来代替配置文件。
1 2 3 4 5 6 7 8 9 10 package com.hang.spring6.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan("com.hang.spring6") public class Spring6Config {}
测试类
1 2 3 4 5 6 7 @Test public void testAllAnnotation () { ApplicationContext context = new AnnotationConfigApplicationContext (Spring6Config.class); UserController userController = context.getBean("userController" , UserController.class); userController.out(); logger.info("执行成功" ); }
原理-手写IoC 我们都知道,Spring框架的IOC是基于Java反射机制实现的,下面我们先回顾一下java反射。
回顾Java反射 Java
反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java
语言的反射机制。简单来说,反射机制指的是程序在运行时能够获取自身的信息。
要想解剖一个类,必须先要获取到该类的Class对象 。而剖析一个类或用反射解决具体的问题就是使用相关API(1)java.lang.Class(2)java.lang.reflect ,所以,Class对象是反射的根源 。
自定义类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package com.hang.reflect;public class Car { private String name; private int age; private String color; public Car () { } public Car (String name, int age, String color) { this .name = name; this .age = age; this .color = color; } private void run () { System.out.println("私有方法-run....." ); } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } public String getColor () { return color; } public void setColor (String color) { this .color = color; } @Override public String toString () { return "Car{" + "name='" + name + '/' ' + ", age=" + age + ", color=' " + color + '/'' + '}'; } }
编写测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 package com.hang.reflect;import org.junit.jupiter.api.Test;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;public class TestCar { @Test public void test01 () throws Exception { Class clazz1 = Car.class; Class clazz2 = new Car ().getClass(); Class clazz3 = Class.forName("com.hang.reflect.Car" ); Car car = (Car)clazz3.getConstructor().newInstance(); System.out.println(car); } @Test public void test02 () throws Exception { Class clazz = Car.class; Constructor[] constructors = clazz.getDeclaredConstructors(); for (Constructor c:constructors) { System.out.println("方法名称:" +c.getName()+" 参数个数:" +c.getParameterCount()); } Constructor c2 = clazz.getDeclaredConstructor(String.class, int .class, String.class); c2.setAccessible(true ); Car car2 = (Car)c2.newInstance("捷达" , 15 , "白色" ); System.out.println(car2); } @Test public void test03 () throws Exception { Class clazz = Car.class; Car car = (Car)clazz.getDeclaredConstructor().newInstance(); Field[] fields = clazz.getDeclaredFields(); for (Field field:fields) { if (field.getName().equals("name" )) { field.setAccessible(true ); field.set(car,"五菱宏光" ); System.out.println(car); } System.out.println(field.getName()); } } @Test public void test04 () throws Exception { Car car = new Car ("奔驰" ,10 ,"黑色" ); Class clazz = car.getClass(); Method[] methods = clazz.getMethods(); for (Method m1:methods) { if (m1.getName().equals("toString" )) { String invoke = (String)m1.invoke(car); } } Method[] methodsAll = clazz.getDeclaredMethods(); for (Method m:methodsAll) { if (m.getName().equals("run" )) { m.setAccessible(true ); m.invoke(car); } } } }
实现Spring的IoC 我们知道,IoC(控制反转)和DI(依赖注入)是Spring里面核心的东西,那么,我们如何自己手写出这样的代码呢?下面我们就一步一步写出Spring框架最核心的部分。
①搭建子模块
搭建模块:guigu-spring,搭建方式如其他spring子模块
②准备测试需要的bean
添加依赖
1 2 3 4 5 6 7 8 <dependencies > <dependency > <groupId > org.junit.jupiter</groupId > <artifactId > junit-jupiter-api</artifactId > <version > 5.3.1</version > </dependency > </dependencies >
创建UserDao接口
1 2 3 4 5 6 package com.hang.spring6.test.dao;public interface UserDao { public void print () ; }
创建UserDaoImpl实现
1 2 3 4 5 6 7 8 9 10 11 12 package com.hang.spring6.test.dao.impl;import com.hang.spring.dao.UserDao;public class UserDaoImpl implements UserDao { @Override public void print () { System.out.println("Dao层执行结束" ); } }
创建UserService接口
1 2 3 4 5 6 package com.hang.spring6.test.service;public interface UserService { public void out () ; }
创建UserServiceImpl实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.hang.spring.test.service.impl;import com.hang.spring.core.annotation.Bean;import com.hang.spring.service.UserService;@Bean public class UserServiceImpl implements UserService { @Override public void out () { System.out.println("Service层执行结束" ); } }
③定义注解
我们通过注解的形式加载bean与实现依赖注入
bean注解
1 2 3 4 5 6 7 8 9 10 11 package com.hang.spring.core.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Bean {}
依赖注入注解
1 2 3 4 5 6 7 8 9 10 11 package com.hang.spring.core.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface Di {}
说明:上面两个注解可以随意取名
④定义bean容器接口
1 2 3 4 5 6 package com.hang.spring.core;public interface ApplicationContext { Object getBean (Class clazz) ; }
⑤编写注解bean容器接口实现
AnnotationApplicationContext基于注解扫描bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.hang.spring.core;import java.util.HashMap;public class AnnotationApplicationContext implements ApplicationContext { private HashMap<Class, Object> beanFactory = new HashMap <>(); @Override public Object getBean (Class clazz) { return beanFactory.get(clazz); } public AnnotationApplicationContext (String basePackage) { } }
⑥编写扫描bean逻辑
我们通过构造方法传入包的base路径,扫描被@Bean注解的java对象,完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package com.hang.spring.core;import com.hang.spring.core.annotation.Bean;import java.io.File;import java.util.HashMap;public class AnnotationApplicationContext implements ApplicationContext { private HashMap<Class, Object> beanFactory = new HashMap <>(); private static String rootPath; @Override public Object getBean (Class clazz) { return beanFactory.get(clazz); } public AnnotationApplicationContext (String basePackage) { try { String packageDirName = basePackage.replaceAll("//." , "////" ); Enumeration<URL> dirs =Thread.currentThread().getContextClassLoader().getResources(packageDirName); while (dirs.hasMoreElements()) { URL url = dirs.nextElement(); String filePath = URLDecoder.decode(url.getFile(),"utf-8" ); rootPath = filePath.substring(0 , filePath.length()-packageDirName.length()); loadBean(new File (filePath)); } } catch (Exception e) { throw new RuntimeException (e); } } private void loadBean (File fileParent) { if (fileParent.isDirectory()) { File[] childrenFiles = fileParent.listFiles(); if (childrenFiles == null || childrenFiles.length == 0 ){ return ; } for (File child : childrenFiles) { if (child.isDirectory()) { loadBean(child); } else { String pathWithClass = child.getAbsolutePath().substring(rootPath.length() - 1 ); if (pathWithClass.contains(".class" )) { String fullName = pathWithClass.replaceAll("////" , "." ).replace(".class" , "" ); try { Class<?> aClass = Class.forName(fullName); if (!aClass.isInterface()){ Bean annotation = aClass.getAnnotation(Bean.class); if (annotation != null ){ Object instance = aClass.newInstance(); if (aClass.getInterfaces().length > 0 ) { System.out.println("正在加载【" + aClass.getInterfaces()[0 ] +"】,实例对象是:" + instance.getClass().getName()); beanFactory.put(aClass.getInterfaces()[0 ], instance); }else { System.out.println("正在加载【" + aClass.getName() +"】,实例对象是:" + instance.getClass().getName()); beanFactory.put(aClass, instance); } } } } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { e.printStackTrace(); } } } } } } }
⑦java类标识Bean注解
1 2 @Bean public class UserServiceImpl implements UserService
1 2 @Bean public class UserDaoImpl implements UserDao
⑧测试Bean加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.hang.spring;import com.hang.spring.core.AnnotationApplicationContext;import com.hang.spring.core.ApplicationContext;import com.hang.spring.test.service.UserService;import org.junit.jupiter.api.Test;public class SpringIocTest { @Test public void testIoc () { ApplicationContext applicationContext = new AnnotationApplicationContext ("com.hang.spring.test" ); UserService userService = (UserService)applicationContext.getBean(UserService.class); userService.out(); System.out.println("run success" ); } }
控制台打印测试
⑨依赖注入
只要userDao.print();调用成功,说明就注入成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.hang.spring.test.service.impl;import com.hang.spring.core.annotation.Bean;import com.hang.spring.core.annotation.Di;import com.hang.spring.dao.UserDao;import com.hang.spring.service.UserService;@Bean public class UserServiceImpl implements UserService { @Di private UserDao userDao; @Override public void out () { userDao.print(); System.out.println("Service层执行结束" ); } }
执行第八步:报错了,说明当前userDao是个空对象
⑩依赖注入实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 package com.hang.spring.core;import com.hang.spring.core.annotation.Bean;import com.hang.spring.core.annotation.Di;import java.io.File;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class AnnotationApplicationContext implements ApplicationContext { private HashMap<Class, Object> beanFactory = new HashMap <>(); private static String rootPath; @Override public Object getBean (Class clazz) { return beanFactory.get(clazz); } public AnnotationApplicationContext (String basePackage) { try { String packageDirName = basePackage.replaceAll("//." , "////" ); Enumeration<URL> dirs =Thread.currentThread().getContextClassLoader().getResources(packageDirName); while (dirs.hasMoreElements()) { URL url = dirs.nextElement(); String filePath = URLDecoder.decode(url.getFile(),"utf-8" ); rootPath = filePath.substring(0 , filePath.length()-packageDirName.length()); loadBean(new File (filePath)); } } catch (Exception e) { throw new RuntimeException (e); } loadDi(); } private void loadBean (File fileParent) { if (fileParent.isDirectory()) { File[] childrenFiles = fileParent.listFiles(); if (childrenFiles == null || childrenFiles.length == 0 ){ return ; } for (File child : childrenFiles) { if (child.isDirectory()) { loadBean(child); } else { String pathWithClass = child.getAbsolutePath().substring(rootPath.length() - 1 ); if (pathWithClass.contains(".class" )) { String fullName = pathWithClass.replaceAll("////" , "." ).replace(".class" , "" ); try { Class<?> aClass = Class.forName(fullName); if (!aClass.isInterface()){ Bean annotation = aClass.getAnnotation(Bean.class); if (annotation != null ){ Object instance = aClass.newInstance(); if (aClass.getInterfaces().length > 0 ) { System.out.println("正在加载【" + aClass.getInterfaces()[0 ] +"】,实例对象是:" + instance.getClass().getName()); beanFactory.put(aClass.getInterfaces()[0 ], instance); }else { System.out.println("正在加载【" + aClass.getName() +"】,实例对象是:" + instance.getClass().getName()); beanFactory.put(aClass, instance); } } } } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { e.printStackTrace(); } } } } } } private void loadDi () { for (Map.Entry<Class,Object> entry : beanFactory.entrySet()){ Object obj = entry.getValue(); Class<?> aClass = obj.getClass(); Field[] declaredFields = aClass.getDeclaredFields(); for (Field field : declaredFields){ Di annotation = field.getAnnotation(Di.class); if ( annotation != null ){ field.setAccessible(true ); try { System.out.println("正在给【" +obj.getClass().getName()+"】属性【" + field.getName() + "】注入值【" + beanFactory.get(field.getType()).getClass().getName() +"】" ); field.set(obj,beanFactory.get(field.getType())); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } } }
执行第八步:执行成功,依赖注入成功
面向切面:AOP 场景模拟 搭建子模块:spring6-aop
声明接口 声明计算器接口Calculator,包含加减乘除的抽象方法
1 2 3 4 5 6 7 8 9 10 11 public interface Calculator { int add (int i, int j) ; int sub (int i, int j) ; int mul (int i, int j) ; int div (int i, int j) ; }
创建实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class CalculatorImpl implements Calculator { @Override public int add (int i, int j) { int result = i + j; System.out.println("方法内部 result = " + result); return result; } @Override public int sub (int i, int j) { int result = i - j; System.out.println("方法内部 result = " + result); return result; } @Override public int mul (int i, int j) { int result = i * j; System.out.println("方法内部 result = " + result); return result; } @Override public int div (int i, int j) { int result = i / j; System.out.println("方法内部 result = " + result); return result; } }
创建带日志功能的实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class CalculatorLogImpl implements Calculator { @Override public int add (int i, int j) { System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j); int result = i + j; System.out.println("方法内部 result = " + result); System.out.println("[日志] add 方法结束了,结果是:" + result); return result; } @Override public int sub (int i, int j) { System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j); int result = i - j; System.out.println("方法内部 result = " + result); System.out.println("[日志] sub 方法结束了,结果是:" + result); return result; } @Override public int mul (int i, int j) { System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j); int result = i * j; System.out.println("方法内部 result = " + result); System.out.println("[日志] mul 方法结束了,结果是:" + result); return result; } @Override public int div (int i, int j) { System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j); int result = i / j; System.out.println("方法内部 result = " + result); System.out.println("[日志] div 方法结束了,结果是:" + result); return result; } }
提出问题 ①现有代码缺陷
针对带日志功能的实现类,我们发现有如下缺陷:
对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
附加功能分散在各个业务功能方法中,不利于统一维护
②解决思路
解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
③困难
解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。
代理模式 概念 ①介绍
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接 调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦 。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
②生活中的代理
广告商找大明星拍广告需要经过经纪人
合作伙伴找大老板谈合作要约见面时间需要经过秘书
房产中介是买卖双方的代理
③相关术语
代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
目标:被代理“套用”了非核心逻辑代码的类、对象、方法。
静态代理 创建静态代理类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class CalculatorStaticProxy implements Calculator { private Calculator target; public CalculatorStaticProxy (Calculator target) { this .target = target; } @Override public int add (int i, int j) { System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j); int addResult = target.add(i, j); System.out.println("[日志] add 方法结束了,结果是:" + addResult); return addResult; } }
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。
动态代理 生产代理对象的工厂类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class ProxyFactory { private Object target; public ProxyFactory (Object target) { this .target = target; } public Object getProxy () { ClassLoader classLoader = target.getClass().getClassLoader(); Class<?>[] interfaces = target.getClass().getInterfaces(); InvocationHandler invocationHandler = new InvocationHandler () { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { Object result = null ; try { System.out.println("[动态代理][日志] " +method.getName()+",参数:" + Arrays.toString(args)); result = method.invoke(target, args); System.out.println("[动态代理][日志] " +method.getName()+",结果:" + result); } catch (Exception e) { e.printStackTrace(); System.out.println("[动态代理][日志] " +method.getName()+",异常:" +e.getMessage()); } finally { System.out.println("[动态代理][日志] " +method.getName()+",方法执行完毕" ); } return result; } }; return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler); } }
测试 1 2 3 4 5 6 7 @Test public void testDynamicProxy () { ProxyFactory factory = new ProxyFactory (new CalculatorLogImpl ()); Calculator proxy = (Calculator) factory.getProxy(); proxy.div(1 ,0 ); }
AOP概念及相关术语 概述 AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
相关术语 ①横切关注点 分散在每个各个模块中解决同一样的问题,如用户验证、日志管理、事务处理、数据缓存都属于横切关注点。
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
②通知(增强) 增强,通俗说,就是你想要增强的功能,比如 安全,事务,日志等。
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
前置通知:在被代理的目标方法前 执行
返回通知:在被代理的目标方法成功结束 后执行(寿终正寝 )
异常通知:在被代理的目标方法异常结束 后执行(死于非命 )
后置通知:在被代理的目标方法最终结束 后执行(盖棺定论 )
环绕通知:使用try…catch…finally结构围绕整个 被代理的目标方法,包括上面四种通知对应的所有位置
③切面 封装通知方法的类。
④目标 被代理的目标对象。
⑤代理 向目标对象应用通知之后创建的代理对象。
⑥连接点 这也是一个纯逻辑概念,不是语法定义的。
把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。通俗说,就是spring允许你使用通知的地方
⑦切入点 定位连接点的方式。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
Spring 的 AOP 技术可以通过切入点定位到特定的连接点。通俗说,要实际去增强的方法
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
作用
基于注解的AOP 技术说明
动态代理分为JDK动态代理和cglib动态代理
当目标类有接口的情况使用JDK动态代理和cglib动态代理,没有接口时只能使用cglib动态代理
JDK动态代理动态生成的代理类会在com.sun.proxy包下,类名为$proxy1,和目标类实现相同的接口
cglib动态代理动态生成的代理类会和目标在在相同的包下,会继承目标类
动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口 (兄弟两个拜把子模式)。
cglib:通过继承被代理的目标类 (认干爹模式)实现代理,所以不需要目标类实现接口。
AspectJ:是AOP思想的一种实现。本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件 ,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
准备工作 ①添加依赖
在IOC所需依赖基础上再加入下面依赖即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 6.0.2</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-aop</artifactId > <version > 6.0.2</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-aspects</artifactId > <version > 6.0.2</version > </dependency > <dependency > <groupId > org.junit.jupiter</groupId > <artifactId > junit-jupiter-api</artifactId > <version > 5.3.1</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-core</artifactId > <version > 2.19.0</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-slf4j2-impl</artifactId > <version > 2.19.0</version > </dependency > </dependencies >
②准备被代理的目标资源
接口:
1 2 3 4 5 6 7 8 9 10 11 public interface Calculator { int add (int i, int j) ; int sub (int i, int j) ; int mul (int i, int j) ; int div (int i, int j) ; }
实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Component public class CalculatorImpl implements Calculator { @Override public int add (int i, int j) { int result = i + j; System.out.println("方法内部 result = " + result); return result; } @Override public int sub (int i, int j) { int result = i - j; System.out.println("方法内部 result = " + result); return result; } @Override public int mul (int i, int j) { int result = i * j; System.out.println("方法内部 result = " + result); return result; } @Override public int div (int i, int j) { int result = i / j; System.out.println("方法内部 result = " + result); return result; } }
创建切面类并配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @Aspect @Component public class LogAspect { @Before("execution(public int com.hang.aop.annotation.CalculatorImpl.*(..))") public void beforeMethod (JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:" +methodName+",参数:" +args); } @After("execution(* com.hang.aop.annotation.CalculatorImpl.*(..))") public void afterMethod (JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->后置通知,方法名:" +methodName); } @AfterReturning(value = "execution(* com.hang.aop.annotation.CalculatorImpl.*(..))", returning = "result") public void afterReturningMethod (JoinPoint joinPoint, Object result) { String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->返回通知,方法名:" +methodName+",结果:" +result); } @AfterThrowing(value = "execution(* com.hang.aop.annotation.CalculatorImpl.*(..))", throwing = "ex") public void afterThrowingMethod (JoinPoint joinPoint, Throwable ex) { String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->异常通知,方法名:" +methodName+",异常:" +ex); } @Around("execution(* com.hang.aop.annotation.CalculatorImpl.*(..))") public Object aroundMethod (ProceedingJoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); Object result = null ; try { System.out.println("环绕通知-->目标对象方法执行之前" ); result = joinPoint.proceed(); System.out.println("环绕通知-->目标对象方法返回值之后" ); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("环绕通知-->目标对象方法出现异常时" ); } finally { System.out.println("环绕通知-->目标对象方法执行完毕" ); } return result; } }
在Spring的配置文件中配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?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 http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd" > <context:component-scan base-package ="com.hang.aop.annotation" > </context:component-scan > <aop:aspectj-autoproxy /> </beans >
执行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class CalculatorTest { private Logger logger = LoggerFactory.getLogger(CalculatorTest.class); @Test public void testAdd () { ApplicationContext ac = new ClassPathXmlApplicationContext ("beans.xml" ); Calculator calculator = ac.getBean( Calculator.class); int add = calculator.add(1 , 1 ); logger.info("执行成功:" +add); } }
执行结果:
各种通知
前置通知:使用@Before注解标识,在被代理的目标方法前 执行
返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束 后执行(寿终正寝 )
异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束 后执行(死于非命 )
后置通知:使用@After注解标识,在被代理的目标方法最终结束 后执行(盖棺定论 )
环绕通知:使用@Around注解标识,使用try…catch…finally结构围绕整个 被代理的目标方法,包括上面四种通知对应的所有位置
切入点表达式语法 ①作用
②语法细节
用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
例如:*.Hello匹配com.Hello,不匹配com.hang.Hello
在包名的部分,使用“*..”表示包名任意、包的层次深度任意
在类名的部分,类名部分整体用*号代替,表示类名任意
在类名的部分,可以使用*号代替类名的一部分
例如:*Service匹配所有名称以Service结尾的类或接口
在方法名部分,可以使用*号表示方法名任意
在方法名部分,可以使用*号代替方法名的一部分
例如:*Operation匹配所有方法名以Operation结尾的方法
在方法参数列表部分,使用(..)表示参数列表任意
在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
例如:execution(public int ..Service. (.., int)) 正确 例如:execution( int *..Service. (.., int)) 错误
重用切入点表达式 ①声明
1 2 @Pointcut("execution(* com.hang.aop.annotation.*.*(..))") public void pointCut () {}
②在同一个切面中使用
1 2 3 4 5 6 @Before("pointCut()") public void beforeMethod (JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:" +methodName+",参数:" +args); }
③在不同切面中使用
1 2 3 4 5 6 @Before("com.hang.aop.CommonPointCut.pointCut()") public void beforeMethod (JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:" +methodName+",参数:" +args); }
获取通知的相关信息 ①获取连接点信息
获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参
1 2 3 4 5 6 7 8 @Before("execution(public int com.hang.aop.annotation.CalculatorImpl.*(..))") public void beforeMethod (JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger-->前置通知,方法名:" +methodName+",参数:" +args); }
②获取目标方法的返回值
@AfterReturning中的属性returning,用来将通知方法的某个形参,接收目标方法的返回值
1 2 3 4 5 @AfterReturning(value = "execution(* com.hang.aop.annotation.CalculatorImpl.*(..))", returning = "result") public void afterReturningMethod (JoinPoint joinPoint, Object result) { String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->返回通知,方法名:" +methodName+",结果:" +result); }
③获取目标方法的异常
@AfterThrowing中的属性throwing,用来将通知方法的某个形参,接收目标方法的异常
1 2 3 4 5 @AfterThrowing(value = "execution(* com.hang.aop.annotation.CalculatorImpl.*(..))", throwing = "ex") public void afterThrowingMethod (JoinPoint joinPoint, Throwable ex) { String methodName = joinPoint.getSignature().getName(); System.out.println("Logger-->异常通知,方法名:" +methodName+",异常:" +ex); }
环绕通知 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Around("execution(* com.hang.aop.annotation.CalculatorImpl.*(..))") public Object aroundMethod (ProceedingJoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); Object result = null ; try { System.out.println("环绕通知-->目标对象方法执行之前" ); result = joinPoint.proceed(); System.out.println("环绕通知-->目标对象方法返回值之后" ); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("环绕通知-->目标对象方法出现异常时" ); } finally { System.out.println("环绕通知-->目标对象方法执行完毕" ); } return result; }
切面的优先级 相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套 顺序。
使用@Order注解可以控制切面的优先级:
@Order(较小的数):优先级高
@Order(较大的数):优先级低
基于XML的AOP 准备工作 参考基于注解的AOP环境
实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <context:component-scan base-package ="com.hang.aop.xml" > </context:component-scan > <aop:config > <aop:aspect ref ="loggerAspect" > <aop:pointcut id ="pointCut" expression ="execution(* com.hang.aop.xml.CalculatorImpl.*(..))" /> <aop:before method ="beforeMethod" pointcut-ref ="pointCut" > </aop:before > <aop:after method ="afterMethod" pointcut-ref ="pointCut" > </aop:after > <aop:after-returning method ="afterReturningMethod" returning ="result" pointcut-ref ="pointCut" > </aop:after-returning > <aop:after-throwing method ="afterThrowingMethod" throwing ="ex" pointcut-ref ="pointCut" > </aop:after-throwing > <aop:around method ="aroundMethod" pointcut-ref ="pointCut" > </aop:around > </aop:aspect > </aop:config >
单元测试:JUnit 在之前的测试方法中,几乎都能看到以下的两行代码:
1 2 ApplicationContext context = new ClassPathXmlApplicationContext ("xxx.xml" );Xxxx xxx = context.getBean(Xxxx.class);
这两行代码的作用是创建Spring容器,最终获取到对象,但是每次测试都需要重复编写。针对上述问题,我们需要的是程序能自动帮我们创建容器。我们都知道JUnit无法知晓我们是否使用了 Spring 框架,更不用说帮我们创建 Spring 容器了。Spring提供了一个运行器,可以读取配置文件(或注解)来创建容器。我们只需要告诉它配置文件位置就可以了。这样一来,我们通过Spring整合JUnit可以使程序创建spring容器了
整合JUnit5 搭建子模块 搭建spring-junit模块
引入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 6.0.2</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-test</artifactId > <version > 6.0.2</version > </dependency > <dependency > <groupId > org.junit.jupiter</groupId > <artifactId > junit-jupiter-api</artifactId > <version > 5.9.0</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-core</artifactId > <version > 2.19.0</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-slf4j2-impl</artifactId > <version > 2.19.0</version > </dependency > </dependencies >
添加配置文件 beans.xml
1 2 3 4 5 6 7 8 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" > <context:component-scan base-package ="com.hang.spring6.bean" /> </beans >
copy日志文件:log4j2.xml
添加java类 1 2 3 4 5 6 7 8 9 10 11 package com.hang.spring6.bean;import org.springframework.stereotype.Component;@Component public class User { public User () { System.out.println("run user" ); } }
测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import com.hang.spring6.bean.User;import org.junit.jupiter.api.Test;import org.junit.jupiter.api.extension.ExtendWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit.jupiter.SpringExtension;import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;@SpringJUnitConfig(locations = "classpath:beans.xml") public class SpringJUnit5Test { @Autowired private User user; @Test public void testUser () { System.out.println(user); } }
整合JUnit4 JUnit4在公司也会经常用到,在此也学习一下
添加依赖 1 2 3 4 5 6 <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > </dependency >
测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.hang.spring6.bean.User;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:beans.xml") public class SpringJUnit4Test { @Autowired private User user; @Test public void testUser () { System.out.println(user); } }
事务 JdbcTemplate 简介
Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作
准备工作 ①搭建子模块
搭建子模块:spring-jdbc-tx
②加入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 6.0.2</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.30</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.2.15</version > </dependency > </dependencies >
③创建jdbc.properties
1 2 3 4 jdbc.user =root jdbc.password =root jdbc.url =jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false jdbc.driver =com.mysql.cj.jdbc.Driver
④配置Spring的配置文件
beans.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" > <context:property-placeholder location ="classpath:jdbc.properties" /> <bean id ="druidDataSource" class ="com.alibaba.druid.pool.DruidDataSource" > <property name ="url" value ="${jdbc.url}" /> <property name ="driverClassName" value ="${jdbc.driver}" /> <property name ="username" value ="${jdbc.user}" /> <property name ="password" value ="${jdbc.password}" /> </bean > <bean id ="jdbcTemplate" class ="org.springframework.jdbc.core.JdbcTemplate" > <property name ="dataSource" ref ="druidDataSource" /> </bean > </beans >
⑤准备数据库与测试表
1 2 3 4 5 6 7 8 9 10 11 CREATE DATABASE `spring`; use `spring`; CREATE TABLE `spring` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `name` varchar(20 ) DEFAULT NULL COMMENT '姓名' , `age` int (11 ) DEFAULT NULL COMMENT '年龄' , `sex` varchar(2 ) DEFAULT NULL COMMENT '性别' , PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
实现CURD ①装配 JdbcTemplate 创建测试类,整合JUnit,注入JdbcTemplate
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.hang.spring6;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;@SpringJUnitConfig(locations = "classpath:beans.xml") public class JDBCTemplateTest { @Autowired private JdbcTemplate jdbcTemplate; }
②测试增删改功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void testUpdate () { String sql = "insert into t_emp values(null,?,?,?)" ; int result = jdbcTemplate.update(sql, "张三" , 23 , "男" ); }
③查询数据返回对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Emp { private Integer id; private String name; private Integer age; private String sex; @Override public String toString () { return "Emp{" + "id=" + id + ", name='" + name + '/' ' + ", age=" + age + ", sex=' " + sex + '/'' + '}'; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void testSelectObject () { String sql = "select * from t_emp where id=?" ; Emp emp = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper <>(Emp.class),1 ); System.out.println(emp); }
④查询数据返回list集合 1 2 3 4 5 6 7 @Test public void testSelectList () { String sql = "select * from t_emp" ; List<Emp> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper <>(Emp.class)); System.out.println(list); }
⑤查询返回单个的值 1 2 3 4 5 6 7 @Test public void selectCount () { String sql = "select count(id) from t_emp" ; Integer count = jdbcTemplate.queryForObject(sql, Integer.class); System.out.println(count); }
声明式事务概念 声明式事务基本概念 ①什么是事务 数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
②事务的特性 A:原子性(Atomicity)
一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
C:一致性(Consistency)
事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。
如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。
如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。
I:隔离性(Isolation)
指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
D:持久性(Durability)
指的是只要事务成功结束,它对数据库所做的更新就必须保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。
编程式事务 事务功能的相关操作全部通过自己编写代码来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Connection conn = ...; try { conn.setAutoCommit(false ); conn.commit(); }catch (Exception e){ conn.rollBack(); }finally { conn.close(); }
编程式的实现方式存在缺陷:
细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
7.2.3、声明式事务 既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
好处1:提高开发效率
好处2:消除了冗余的代码
好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化
所以,我们可以总结下面两个概念:
编程式 :自己写代码 实现功能
声明式 :通过配置 让框架 实现功能
基于注解的声明式事务 准备工作 ①添加配置
在beans.xml添加配置
1 2 <context:component-scan base-package ="com.hang.spring6" > </context:component-scan >
②创建表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CREATE TABLE `t_book` ( `book_id` int (11 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `book_name` varchar (20 ) DEFAULT NULL COMMENT '图书名称' , `price` int (11 ) DEFAULT NULL COMMENT '价格' , `stock` int (10 ) unsigned DEFAULT NULL COMMENT '库存(无符号)' , PRIMARY KEY (`book_id`) ) ENGINE= InnoDB AUTO_INCREMENT= 3 DEFAULT CHARSET= utf8; insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1 ,'斗破苍穹' ,80 ,100 ),(2 ,'斗罗大陆' ,50 ,100 );CREATE TABLE `t_user` ( `user_id` int (11 ) NOT NULL AUTO_INCREMENT COMMENT '主键' , `username` varchar (20 ) DEFAULT NULL COMMENT '用户名' , `balance` int (10 ) unsigned DEFAULT NULL COMMENT '余额(无符号)' , PRIMARY KEY (`user_id`) ) ENGINE= InnoDB AUTO_INCREMENT= 2 DEFAULT CHARSET= utf8; insert into `t_user`(`user_id`,`username`,`balance`) values (1 ,'admin' ,50 );
③创建组件
创建BookController:
1 2 3 4 5 6 7 8 9 10 11 12 package com.hang.spring6.controller;@Controller public class BookController { @Autowired private BookService bookService; public void buyBook (Integer bookId, Integer userId) { bookService.buyBook(bookId, userId); } }
创建接口BookService:
1 2 3 4 package com.hang.spring6.service;public interface BookService { void buyBook (Integer bookId, Integer userId) ; }
创建实现类BookServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.hang.spring6.service.impl;@Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; @Override public void buyBook (Integer bookId, Integer userId) { Integer price = bookDao.getPriceByBookId(bookId); bookDao.updateStock(bookId); bookDao.updateBalance(userId, price); } }
创建接口BookDao:
1 2 3 4 5 6 7 8 package com.hang.spring6.dao;public interface BookDao { Integer getPriceByBookId (Integer bookId) ; void updateStock (Integer bookId) ; void updateBalance (Integer userId, Integer price) ; }
创建实现类BookDaoImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.hang.spring6.dao.impl;@Repository public class BookDaoImpl implements BookDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public Integer getPriceByBookId (Integer bookId) { String sql = "select price from t_book where book_id = ?" ; return jdbcTemplate.queryForObject(sql, Integer.class, bookId); } @Override public void updateStock (Integer bookId) { String sql = "update t_book set stock = stock - 1 where book_id = ?" ; jdbcTemplate.update(sql, bookId); } @Override public void updateBalance (Integer userId, Integer price) { String sql = "update t_user set balance = balance - ? where user_id = ?" ; jdbcTemplate.update(sql, price, userId); } }
测试无事务情况 ①创建测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;@SpringJUnitConfig(locations = "classpath:beans.xml") public class TxByAnnotationTest { @Autowired private BookController bookController; @Test public void testBuyBook () { bookController.buyBook(1 , 1 ); } }
②模拟场景
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
假设用户id为1的用户,购买id为1的图书
用户余额为50,而图书价格为80
购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段
此时执行sql语句会抛出SQLException
③观察结果
因为没有添加事务,图书的库存更新了,但是用户的余额没有更新
显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败
加入事务 ①添加事务配置 在spring配置文件中引入tx命名空间
1 2 3 4 5 6 7 8 9 10 11 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xmlns:tx ="http://www.springframework.org/schema/tx" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd" >
在Spring的配置文件中添加配置:
1 2 3 4 5 6 7 8 9 10 <bean id ="transactionManager" class ="org.springframework.jdbc.datasource.DataSourceTransactionManager" > <property name ="dataSource" ref ="druidDataSource" > </property > </bean > <tx:annotation-driven transaction-manager ="transactionManager" />
②添加事务注解 因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理
在BookServiceImpl的buybook()添加注解@Transactional
③观察结果 由于使用了Spring的声明式事务,更新库存和更新余额都没有执行
@Transactional注解标识的位置 @Transactional标识在方法上,则只会影响该方法
@Transactional标识的类上,则会影响类中所有的方法
事务属性:只读 ①介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
②使用方式
1 2 3 4 5 6 7 8 9 10 @Transactional(readOnly = true) public void buyBook (Integer bookId, Integer userId) { Integer price = bookDao.getPriceByBookId(bookId); bookDao.updateStock(bookId); bookDao.updateBalance(userId, price); }
③注意
对增删改操作设置只读会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
事务属性:超时 ①介绍
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。
概括来说就是一句话:超时回滚,释放资源。
②使用方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Transactional(timeout = 3) public void buyBook (Integer bookId, Integer userId) { try { TimeUnit.SECONDS.sleep(5 ); } catch (InterruptedException e) { e.printStackTrace(); } Integer price = bookDao.getPriceByBookId(bookId); bookDao.updateStock(bookId); bookDao.updateBalance(userId, price); }
③观察结果
执行过程中抛出异常:
org.springframework.transaction.TransactionTimedOutException : Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022
事务属性:回滚策略 ①介绍
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
可以通过@Transactional中相关属性设置回滚策略
rollbackFor属性:需要设置一个Class类型的对象
rollbackForClassName属性:需要设置一个字符串类型的全类名
noRollbackFor属性:需要设置一个Class类型的对象
rollbackFor属性:需要设置一个字符串类型的全类名
②使用方式
1 2 3 4 5 6 7 8 9 10 11 @Transactional(noRollbackFor = ArithmeticException.class) public void buyBook (Integer bookId, Integer userId) { Integer price = bookDao.getPriceByBookId(bookId); bookDao.updateStock(bookId); bookDao.updateBalance(userId, price); System.out.println(1 /0 ); }
③观察结果
虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行
事务属性:隔离级别 ①介绍
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。
读已提交:READ COMMITTED、
要求Transaction01只能读取Transaction02已提交的修改。
可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。
串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力见下表:
隔离级别
脏读
不可重复读
幻读
READ UNCOMMITTED
有
有
有
READ COMMITTED
无
有
有
REPEATABLE READ
无
无
有
SERIALIZABLE
无
无
无
各种数据库产品对事务隔离级别的支持程度:
隔离级别
Oracle
MySQL
READ UNCOMMITTED
×
√
READ COMMITTED
√(默认)
√
REPEATABLE READ
×
√(默认)
SERIALIZABLE
√
√
②使用方式
1 2 3 4 5 @Transactional(isolation = Isolation.DEFAULT) @Transactional(isolation = Isolation.READ_UNCOMMITTED) @Transactional(isolation = Isolation.READ_COMMITTED) @Transactional(isolation = Isolation.REPEATABLE_READ) @Transactional(isolation = Isolation.SERIALIZABLE)
事务属性:传播行为 ①介绍
什么是事务的传播行为?
在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。
一共有七种传播行为:
REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】
MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】
REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】
NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】
NEVER:以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】
NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】
②测试
创建接口CheckoutService:
1 2 3 4 5 package com.hang.spring6.service;public interface CheckoutService { void checkout (Integer[] bookIds, Integer userId) ; }
创建实现类CheckoutServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.hang.spring6.service.impl;@Service public class CheckoutServiceImpl implements CheckoutService { @Autowired private BookService bookService; @Override @Transactional public void checkout (Integer[] bookIds, Integer userId) { for (Integer bookId : bookIds) { bookService.buyBook(bookId, userId); } } }
在BookController中添加方法:
1 2 3 4 5 6 @Autowired private CheckoutService checkoutService;public void checkout (Integer[] bookIds, Integer userId) { checkoutService.checkout(bookIds, userId); }
在数据库中将用户的余额修改为100元
③观察结果
可以通过@Transactional中的propagation属性设置事务传播行为
修改BookServiceImpl中buyBook()上,注解@Transactional的propagation属性
@Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。经过观察,购买图书的方法buyBook()在checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了
@Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本。
全注解配置事务 ①添加配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.hang.spring6.config;import com.alibaba.druid.pool.DruidDataSource;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.sql.DataSource;@Configuration @ComponentScan("com.hang.spring6") @EnableTransactionManagement public class SpringConfig { @Bean public DataSource getDataSource () { DruidDataSource dataSource = new DruidDataSource (); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver" ); dataSource.setUrl("jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false" ); dataSource.setUsername("root" ); dataSource.setPassword("root" ); return dataSource; } @Bean(name = "jdbcTemplate") public JdbcTemplate getJdbcTemplate (DataSource dataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate (); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } @Bean public DataSourceTransactionManager getDataSourceTransactionManager (DataSource dataSource) { DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager (); dataSourceTransactionManager.setDataSource(dataSource); return dataSourceTransactionManager; } }
②测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import com.hang.spring6.config.SpringConfig;import com.hang.spring6.controller.BookController;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;public class TxByAllAnnotationTest { @Test public void testTxAllAnnotation () { ApplicationContext applicationContext = new AnnotationConfigApplicationContext (SpringConfig.class); BookController accountService = applicationContext.getBean("bookController" , BookController.class); accountService.buyBook(1 , 1 ); } }
基于XML的声明式事务 场景模拟 参考基于注解的声明式事务
修改Spring配置文件 将Spring配置文件中去掉tx:annotation-driven 标签,并添加配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <aop:config > <aop:advisor advice-ref ="txAdvice" pointcut ="execution(* com.hang.spring.tx.xml.service.impl.*.*(..))" > </aop:advisor > </aop:config > <tx:advice id ="txAdvice" transaction-manager ="transactionManager" > <tx:attributes > <tx:method name ="get*" read-only ="true" /> <tx:method name ="query*" read-only ="true" /> <tx:method name ="find*" read-only ="true" /> <tx:method name ="save*" read-only ="false" rollback-for ="java.lang.Exception" propagation ="REQUIRES_NEW" /> <tx:method name ="update*" read-only ="false" rollback-for ="java.lang.Exception" propagation ="REQUIRES_NEW" /> <tx:method name ="delete*" read-only ="false" rollback-for ="java.lang.Exception" propagation ="REQUIRES_NEW" /> </tx:attributes > </tx:advice >
注意:基于xml实现的声明式事务,必须引入aspectJ的依赖
1 2 3 4 5 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-aspects</artifactId > <version > 6.0.2</version > </dependency >