Spring6
0x00 环境配置
环境:
- IDEA >= 2022.1.4
- JDK 17
- Maven 3.8.6
- Spring 6.0.0
- JUnit 4.13.2
- Log4j2
-
新建模块 spring001 -> 高级设置 -> 组 ID
-
在 /spring001/src/main/java 分别新建软件包
- com.spring.dao:持久层
- com.spring.service:业务层,调用持久层
- com.spring.web:表示层,调用业务层
- com.spring.client:测试
-
层和层之间使用接口交互
-
dao
-
新建接口 UserDao.java
public interface UserDao { void deleteById(); }
-
新建实现包 impl
-
新建实现类 UserDaoImplForMySQL.java
public class UserDaoImplForMySQL implements UserDao { @Override public void deleteById() { System.out.println("Deleting..."); } }
-
-
-
service
-
新建接口 UserService.java
public interface UserService { void deleteUser(); }
-
新建实现包 impl
-
新建实现类 UserServiceImpl.java
public class UserServiceImpl implements UserService { private UserDao userDao = new UserDaoImplForMySQL(); @Override public void deleteUser() { userDao.deleteById(); } }
-
-
-
web
-
新建类 UserAction.java
public class UserAction { private UserService userService = new UserServiceImpl(); public void deleteRequest() { userService.deleteUser(); } }
-
-
client
-
新建类 Test.java
public class Test { public static void main(String[] args) { UserAction userAction = new UserAction(); userAction.deleteRequest(); } }
-
-
-
上述项目在需求升级后需要大面积替换原代码,很不方便且容易出错,故使用 Spring 进行调整
0x01 框架主要思想
-
OCP
- OCP 是软件七大开发原则之开闭原则
- 对扩展开发,对修改关闭
- OCP 是七大开发原则中最核心、最基本的原则,核心为:扩展功能时未修改原代码
- OCP 是软件七大开发原则之开闭原则
-
DIP
- DIP 是软件七大开发原则之依赖倒置原则
- 下层依赖上层时符合该原则
- DIP 核心为面向接口编程
- DIP 是软件七大开发原则之依赖倒置原则
-
IoC
- IoC 是控制反转,是一种编程思想,是新型设计模式
- 在程序中不使用硬编码的方式来新建对象和维护对象关系
- IoC 用于解决当前程序设计既违反了 OCP 又违反 DIP 的问题
- IoC 是控制反转,是一种编程思想,是新型设计模式
-
Spring 框架
- Spring 是实现 IoC 思想的容器
- IoC 的实现方法很多,其中比较重要的是依赖注入(DI)
- DI 常见的方式
- set 注入
- 构造方法注入
0x02 框架概述
(1)八大模块
- Spring AOP:面向切面编程
- Spring Web:支持集成常见的 Web 框架
- Spring Web MVC:Spring 自有的 MVC 框架
- Spring Webflux:Spring 自有的 响应式 Web 框架
- Spring DAO:单独的支持 JDBC 操作的 API
- Spring ORM:集成常见的 ORM 框架,如 MyBatis 等
- Spring Context:国际化消息、事件传播、验证支持、企业服务、Velocity 和 FreeMarket2 集成
- Spring Core:控制反转
(2)特点
- 轻量、非侵入式
- 控制反转
- 面向切面
- 容器
- 框架
0x03 入门程序
(1)Spring 下载与引用
-
graph LR A(Projects<br />Spring Framework)-->Github -->B(Access to Binaries<br />Spring Framework Artifacts) -->C(Spring Repositories<br />https://repo.spring.io) -->Artifacts -->D(plugins-release/<br />org/springframework/<br />spring/6.0.0-RC2) -->E(General/URL to file) -->spring-x.x.x-dist.zip
字符 说明 第一个数字 主版本,有可能进行大的架构调整,各大版本之间并不一定兼容 第二个数字 次版本,在主版本架构不变的前提下,增加了一些新的特性或变化 第三个数字 增量版本,bug修复,细节的完善 M 里程碑版本,测试版本,发布版本的前兆 RC 候选发布版本,稳定版本,并不一定会发布 RELEASE/NULL 发布版本,稳定版本,在项目中真正可用的版本 -
Maven 引用 Spring
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.2.2RC2</version> </dependency> </dependencies>
(2)创建项目
-
修改 pom.xml 文件内容
<!-- 修改打包方式--> <packaging>jar</packaging> <!-- 配置多个仓库--> <repositories> <repository> <id>repository.spring.milestone</id> <name>Spring Milestone Repository</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories> <!-- 依赖项--> <dependencies> <!-- Spring Context 依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.0.4</version> </dependency> <!-- junit 单元测试依赖--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies>
-
添加 Spring 配置
-
在 src/main/resources 目录下新建 spring.xml 文件(名称可变)来配置 Spring
-
在该文件中配置 bean,从而使 Spring 可以帮助我们管理对象(实现 IoC 的作用)
-
bean 有两个重要属性:
id
和class
id
:bean 的唯一标识class
:必须填写类的全路径,如com.xxx.xxx.ClassName
- 带包名的类名,双击需要引用的类名,右键选择“复制引用”
-
如:
<bean id="userBean" class="com.spring6.bean.User" />
-
也可以引用 Java 自带的无参类,如:
<bean id="date" class="java.util.Date" />
-
(3)编写程序
-
获取对象
-
在 src/text/java 目录下新建软件包
-
在该软件包中新建 Java 类,并写入以下代码:
public class FirstSpringTest { @Test public void testFirstSpringCode() { ApplicationContext appContext = new ClassPathXmlApplicationContext("spring.xml"); Object userBean = appContext.getBean("userBean"); } }
-
第一步,获取 Spring 容器对象——
ApplicationContext
ApplicationContext
本质是接口,其中有一个实现类为ClassPathXmlApplicationContext
,专门从类路径当中加载 Spring 配置文件的一个 Spring 上下文对象BeanFactory
是ApplicationContext
接口的超级父接口,是 IoC 容器的顶级接口,反映了 Spring 的 IoC 容器底层实际上使用了工厂模式- XML 解析、工厂模式、反射机制
-
第二步,根据 bean 的 id 从 Spring 容器中获取这个对象,通过
getBean()
方法实现
-
-
-
默认情况下,Spring 会通过反射机制,调用类的无参构造方法来实例化对象,其构造方法如下:
Class example = Class.forName("com.example"); Object obj = example.newInstance();
-
获取多个 Spring 容器对象
ApplicationContext appContext = new ClassPathXmlApplicationContext("spring_1.xml", "spring_2.xml", "spring_3.xml");
-
如果需要访问子类特有属性和方法时
-
方法一:强转
import java.util.Date; public class FirstSpringTest { @Test public void testFirstSpringCode() { // <bean id="date" class="java.util.Date" /> ApplicationContext appContext = new ClassPathXmlApplicationContext("spring.xml"); Date date = (Date)appContext.getBean("date"); } }
-
方法二:
import java.util.Date; public class FirstSpringTest { @Test public void testFirstSpringCode() { // <bean id="date" class="java.util.Date" /> ApplicationContext appContext = new ClassPathXmlApplicationContext("spring.xml"); Date date = appContext.getBean("date", Date.class); } }
-
(4)启用 Log4j2 日志框架
-
环境配置
-
引入依赖
// pom.xml <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>
-
在 src/main/resources 目录下新建文件 log4j2.xml,并写入以下内容
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <loggers> <!-- level 指定日志级别,以下是从低到高的排序--> <!-- ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF--> <root level="DEBUG"> <appender-ref ref="spring6log" /> </root> </loggers> <appenders> <!-- 输出日志信息到控制台--> <console name="spring6log" target="SYSTEM_OUT"> <!-- 控制日志输出的格式--> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n" /> </console> </appenders> </configuration>
-
-
使用 log4j2 记录日志信息
-
在 test 中的类 FirstSpringTest.java 里写入以下代码:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FirstSpringTest { @Test public void testFirstSpringCode() { Logger logger = LoggerFactory.getLogger(FirstSpringTest.class); logger.info("Info Message"); logger.debug("Debug Message"); logger.trace("Trace Message"); } }
- 第一步,创建日志记录器对象
- 第二步,记录日志并根据不同的级别来输出日志
-
0x04 IoC 的实现
(1)IoC 控制反转
- 控制反转是一种思想
- 控制反转的目的
- 降低程序耦合度
- 达到 OCP 原则
- 达到 DIP 原则
- 反转内容
- 将对象的创建权力交出去,由第三方容器负责
- 将对象和对象之间的关系维护权交出去,由第三方容器负责
- 实现方法
- 依赖注入 DI
(2)依赖注入
-
Spring 通过依赖注入完成对 Bean 对象的创建和其中属性赋值的管理
-
依赖注入解释
- 依赖:对象和对象之间的关联关系
- 注入:一种数据传递行为,使对象之间产生关系
-
依赖注入常见形式
-
set 注入
- 基于 set 方法实现的,底层会通过反射机制调用属性对应的 set 方法而后给属性赋值,此方法要求属性必须对外提供 set 方法
- 构造 set 方法时,方法名必须以 set 开始
public void setUserDao(UserDao userDao) { this.userDao = userDao; }
-
Spring 调用对应 set 方法前,需要在 spring.xml 中配置相应 Bean 的属性 property
- property 标签中包含的 name 属性以首字母小写的方式填写 set 方法名中 set 后的全部内容
- property 标签中包含的 type 属性填写形参的 Bean 的 id
<bean id="userDaoBean" class="com.spring6.dao.UserDao" /> <bean id="userServiceBean" class="com.spring6.service.UserService"> <property name="userDao" ref="userDaoBean" /> </bean>
-
构造注入
- 通过构造方法为属性赋值
public UserService(UserDao userDao) { this.userDao = userDao; }
-
在 spring.xml 中配置相应 Bean 的属性 constructor-arg
- constructor-arg 标签中包含的 index 属性指定构造方法的第 index+1 个参数
- constructor-arg 标签中包含的 ref 属性指定对应的 Bean 的 id
<bean id="userDaoBean" class="com.spring6.dao.UserDao" /> <bean id="userServiceBean" class="com.spring6.service.UserService"> <constructor-arg index="0" ref="userDaoBean" /> </bean>
-
或类似于 set 注入方法配置 spring.xml
<bean id="userDaoBean" class="com.spring6.dao.UserDao" /> <bean id="userServiceBean" class="com.spring6.service.UserService"> <constructor-arg name="userDao" ref="userDaoBean" /> </bean>
-
或根据类型注入
<bean id="userDaoBean" class="com.spring6.dao.UserDao" /> <bean id="userServiceBean" class="com.spring6.service.UserService"> <constructor-arg ref="userDaoBean" /> </bean>
-
(3)set 注入详解
public void setUserDao(UserDao userDao) { this.userDao = userDao; }
a. 注入外部 Bean
- Bean 定义到外面,在 property 标签中使用 ref 属性进行注入
<bean id="userDaoBean" class="com.spring6.dao.UserDao" />
<bean id="userServiceBean" class="com.spring6.service.UserService">
<property name="userDao" ref="userDaoBean" />
</bean>
b. 注入内部 Bean
- 在 Bean 标签中嵌套着 Bean 标签
<bean id="userServiceBean" class="com.spring6.service.UserService">
<property name="userDao">
<bean class="com.spring6.dao.UserDao" />
</property>
</bean>
c. 注入简单类型
-
可以通过 set 注入的方式给属性赋值
-
在 src/main/java 新建 com.spring6.bean.User 类
package com.spring6.bean; public class User { private String username; private String password; private int age; public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "User{" + "username='" + username + '\'' + ", password='" + password + '\'' + ", age=" + age + '}'; } }
-
在 src/main/resources/spring.xml 中配置如下,其中重点在于 property 标签的 value 属性用于传值
<bean id="userBean" class="com.spring6.bean.User"> <property name="username" value="张三" /> <property name="password" value="123" /> <property name="age" value="20" /> </bean>
-
测试该注入代码如下
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); User user = applicationContext.getBean("userBean", User.class); System.out.println(user);
-
-
查看简单类型
- 在 IDEA 中双击 Shift 键,搜索 BeanUtils
- 在 BeanUtils.class 中按 Ctrl + F12 搜索 isSimpleValueType 即可查看 Spring 中的全部简单类型
d. 级联属性赋值
-
User.java
private String name; // 级联注入添加 private Group group; public void setName(String name) { this.name = name; } // 级联注入添加 // 对应 spring.xml 中的 group.name public Group getGroup() { return group; } // 级联注入添加 public void setGroup(Group group) { this.group = group; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", group=" + group + '}'; }
-
Group.java
private String name; public void setName(String name) { this.name = name; } @Override public String toString() { return "Group{" + "name='" + name + '\'' + '}'; }
-
Spring.xml
<bean id="userBean" class="com.spring6.bean.User"> <property name="name" value="张三" /> <property name="group" ref="groupBean" /> <property name="group.name" value="第一组" /> </bean> <bean id="groupBean" class="com.spring6.bean.Group" />
-
Test.java
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); User user = applicationContext.getBean("userBean", User.class); System.out.println(user);
-
级联注入重点
- 需要 get 方法
- 配置中先指定 ref 后再赋值 value
e. 注入数组
-
User.java
private String[] hobbies; public void setHobbies(String[] hobbies) { this.hobbies = hobbies; } @Override public String toString() { return "User{" + "hobbies=" + Arrays.toString(hobbies) + '}'; }
-
spring.xml
<bean id="userBean" class="com.spring6.bean.User"> <property name="hobbies"> <array> <value>吃饭</value> <value>睡觉</value> <value>打豆豆</value> </array> </property> </bean>
f. 注入 List 集合
有序可重复
-
User.java
private List<String> hobbies; public void setHobbies(List<String> hobbies) { this.hobbies = hobbies; } @Override public String toString() { return "User{" + "hobbies=" + hobbies + '}'; }
-
spring.xml
<bean id="userBean" class="com.spring6.bean.User"> <property name="hobbies"> <list> <value>吃饭</value> <value>睡觉</value> <value>打豆豆</value> </list> </property> </bean>
g. 注入 Set 集合
无序不可重复
-
User.java
private Set<String> hobbies; public void setHobbies(Set<String> hobbies) { this.hobbies = hobbies; } @Override public String toString() { return "User{" + "hobbies=" + hobbies + '}'; }
-
spring.xml
<bean id="userBean" class="com.spring6.bean.User"> <property name="hobbies"> <set> <value>吃饭</value> <value>吃饭</value> <value>睡觉</value> <value>睡觉</value> <value>打豆豆</value> </set> </property> </bean>
- 有重复元素不会报错,但是仅输出一次
h. 注入 Map 集合
-
User.java
private Map<Integer, String> phones; public void setPhones(Map<Integer, String> phones) { this.phones = phones; } @Override public String toString() { return "User{" + "phones=" + phones + '}'; }
-
spring.xml
<bean id="userBean" class="com.spring6.bean.User"> <property name="phones"> <map> <entry key="1" value="110" /> <entry key="2" value="119" /> <entry key="3" value="120" /> </map> </property> </bean>
- 对应非简单类型可以使用
key-ref
和value-ref
- 对应非简单类型可以使用
i. 注入 Properties
-
User.java
private Properties properties; public void setProperties(Properties properties) { this.properties = properties; } @Override public String toString() { return "User{" + "properties=" + properties + '}'; }
-
spring.xml
<bean id="userBean" class="com.spring6.bean.User"> <property name="properties"> <props> <prop key="name">张三</prop> <prop key="sex">男</prop> </props> </property> </bean>
- prop 的 key 和 value 只能是 String 型
j. 注入 null 和空字符串
未设置时默认为 null
-
User.java
private String string1; private String string2; public void setString1(String string1) { this.string1 = string1; } public void setString2(String string2) { this.string2 = string2; } @Override public String toString() { return "User{" + "string1='" + string1 + '\'' + ", string2='" + string2 + '\'' + '}'; }
-
spring.xml
<bean id="userBean" class="com.spring6.bean.User"> <property name="string1"> <null /> </property> <property name="string2" value=""/> </bean>
k. 注入的值含有特殊字符
-
XML 中有 5 个特殊字符:
<
、>
、'
、"
、&
,这些字符会被当做 XML 语法的一部分进行解析 -
解决方法:
-
使用转义字符代替
特殊字符 转义字符 >
>
<
<
'
'
"
"
&
&
-
在 spring.xml 中配置值
<bean id="userBean" class="com.spring6.bean.User"> <property name="string1" value="2 < 3" /> </bean>
-
-
将含有特殊字符的字符串放到
<![CDATA[]]>
中-
在 spring.xml 中配置值
<bean id="userBean" class="com.spring6.bean.User"> <property name="string2"> <value><![CDATA[2 < 3]]></value> </property> </bean>
-
-
(4)p 命名空间注入
-
目的:简化 set 注入配置
-
前提条件
- 在 XML 头部信息中添加 p 命名空间的配置信息:
xmlns:p="http://www.springframework.org/schema/p"
- 需要对应的属性提供 setter 方法
- 在 XML 头部信息中添加 p 命名空间的配置信息:
-
User.java
private String string; private Date date; public void setString(String string) { this.string = string; } public void setDate(Date date) { this.date = date; } @Override public String toString() { return "User{" + "string='" + string + '\'' + ", date=" + date + '}'; }
-
spring.xml
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="userBean" class="com.spring6.bean.User" p:string="字符串" p:date-ref="dateBean" /> <bean id="dateBean" class="java.util.Date" /> </beans>
(5)c 命名空间注入
-
目的:简化构造方法注入配置
-
前提条件
- 在 XML 头部信息中添加 p 命名空间的配置信息:
xmlns:c="http://www.springframework.org/schema/c"
- 需要提供构造方法
- 在 XML 头部信息中添加 p 命名空间的配置信息:
-
User.java
private String string; private int number; public User(String string, int number) { this.string = string; this.number = number; } @Override public String toString() { return "User{" + "string='" + string + '\'' + ", number=" + number + '}'; }
-
spring.xml
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="userBean" class="com.spring6.bean.User" c:_0="字符串" c:number="123" /> </beans>
(6)util 命名空间
-
目的:配置复用
-
前提条件:在 XML 头部信息中添加配置信息
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"
-
User.java(User2.java)
private Properties properties; public void setProperties(Properties properties) { this.properties = properties; } @Override public String toString() { return "User{" + "properties=" + properties + '}'; }
-
spring.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util" 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 http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <util:properties id="prop"> <prop key="key1">value1</prop> <prop key="key2">value2</prop> <prop key="key3">value3</prop> </util:properties> <bean id="userBean" class="com.spring6.bean.User"> <property name="properties" ref="prop" /> </bean> <bean id="userBean2" class="com.spring6.bean.User2"> <property name="properties" ref="prop" /> </bean> </beans>
-
Test.java
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); User user = applicationContext.getBean("userBean", User.class); User2 user2 = applicationContext.getBean("userBean2", User2.class); System.out.println(user); System.out.println(user2);
(7)基于 XML 的自动装配
-
Spring 可以完成自动化的注入,又称自动装配,基于 set 方法
-
方式:根据名称或类型装配
-
根据名称
-
spring.xml
<bean id="userService" class="com.spring6.service.UserService" autowire="byName" /> <bean id="userDao" class="com.spring6.dao.UserDao" />
- 其中,在第二行的 bean 里,属性 id 必须为 set 方法名中除
set
外其他内容首字母小写
- 其中,在第二行的 bean 里,属性 id 必须为 set 方法名中除
-
-
根据类型
-
spring.xml
<bean id="userDao" class="com.spring6.dao.UserDao"></bean> <bean id="userDao2" class="com.spring6.dao.UserDao2"></bean> <bean id="userService" class="com.spring6.service.UserService" autowire="byType" />
-
(8)Spring 引入外部属性配置
-
设置外部属性配置。在 src/main/resources 下新建 setting.properties
username=root password=1234
-
引入外部 properties 文件。在 spring.xml 中进行下述操作
-
引入 context 命名空间
-
指定属性配置文件的路径
-
使用
${key}
方法从外部属性配置文件中取值-
key 值首先加载当前操作系统默认的值,导致
username
对应的 value 为当前系统管理员名称,为避免上述问题并使 key 名称易于理解,可使用添加前缀的操作user.username=root user.password=1234
-
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" 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 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="setting.properties" /> <bean id="userBean" class="com.spring6.bean.User"> <property name="username" value="${user.username}" /> <property name="password" value="${user.password}" /> </bean> </beans>
-
0x05 Bean 的作用域
(1)singleton
Singleton:单例
- 默认情况下,Bean 是单例的,其在 Spring 上下文初始化的时候实例化,每一次调用
getBean()
方法时,都返回那个单例对象 - 在配置 Bean 时可以通过添加 scope 属性值来设置 Bean 是单例或多例
(2)prototype
Prototype:原型 / 多例
- 当 Bean 的 scope 属性设置为
Prototype
时,Spring 上下文初始化时不会初始化这些 Bean,每一次调用getBean()
方法时,才实例化该对象
(3)其他 Scope
在 pom.xml 中添加依赖 SpringMVC,此时该 Spring 项目为 Web 项目
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>6.0.4</version> </dependency>
- request
- 仅限于在 Web 应用中使用
- 一次请求中只有一个 Bean
- session
- 仅限于在 Web 应用中使用
- 一次会话中只有一个 Bean
- global session
- portlet 应用中专用的
- 如果在 Servlet 的 Web 应用中使用时,其效果与 session 相同
- application
- 仅限于在 Web 应用中使用
- 一个应用对应一个 Bean
- websocket
- 仅限于在 Web 应用中使用
- 一个 websocket 生命周期对应一个 Bean
- 自定义 Scope
(4)自定义 Scope
以自定义线程级 scope 为例:一个线程对应一个 Bean
-
自定义 Scope,实现 Scope 接口
- Spring 内置了线程范围的类:
org.springframework.context.support.SimpleThreadScope
- Spring 内置了线程范围的类:
-
将自定义的 Scope 注册到 Spring 容器中
<!-- filename: spring-scope.xml --> <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="myThread"> <bean class="org.springframework.context.support.SimpleThreadScope" /> </entry> </map> </property> </bean>
-
使用 Scope
<!-- filename: spring-scope.xml --> <bean id="userBean" class="com.spring6.bean.User" scope="myThread" />
-
测试
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); User user = applicationContext.getBean("userBean", User.class); System.out.println(user); new Thread(() -> { User user2 = applicationContext.getBean("userBean", User.class); System.out.println(user2); }).start();
0x06 GoF 之工厂模式
-
设计模式定义:一种可以被重复利用的解决方案
-
GoF:Gang of Four,四人组,出自《设计模式》
-
《设计模式》描述了 23 种设计模式,可分成三大类
-
创建型:解决对象创建问题
单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式
-
结构型:一些类或对象组合在一起的经典结构
代理模式、装饰模式、适配器模式、组合模式、享元模式、外观模式、桥接模式
-
行为型:解决类或对象之间交互问题
策略模式、模板方法模式、责任链模式、观察者模式、迭代子模式、命令模式、备忘录模式、状态模式、访问者模式、中介模式、解释器模式
-
-
(1)工厂模式的三种形态
- 简单工厂模式(Simple Factory)
- 又称静态工厂方法模式,不属于 23 种设计模式之一,是工厂方法模式的一种特殊实现
- 工厂方法模式(Factory Method)
- 抽象工厂模式(Abstract Factory)
(2)简单工厂模式
-
抽象产品角色
// filename: Weapon.java public abstract class Weapon { public abstract void attack(); }
-
具体产品角色
// filename: Tank.java public class Tank extends Weapon { @Override public void attack() { System.out.println("Tank attack"); } }
-
工厂类角色
// filename: WeaponFactory.java public class WeaponFactory { public static Weapon get(String weaponType) { if ("Tank".equals(weaponType)) { return new Tank(); } else { throw new RuntimeException("Weapon type not supported"); } } }
-
测试
// filename: Test.java public class Test { public static void main(String[] args) { Weapon tank = WeaponFactory.get("Tank"); tank.attack(); } }
-
简单工厂模式优点
- 客户端程序不需要关心对象的创建细节,需要时索要即可,初步实现了生产和消费的责任分离
-
简单工厂模式缺点
- 工厂类如果出现问题会导致整个系统瘫痪
- 不符合 OCP 开闭原则,在系统扩展时需要修改工厂类
-
Spring 中的 BeanFactory 就使用了简单工厂模式
(3)工厂方法模式
-
抽象产品角色
// filename: Weapon.java public abstract class Weapon { public abstract void attack(); }
-
具体产品角色
// filename: Tank.java public class Tank extends Weapon { @Override public void attack() { System.out.println("Tank attack"); } }
-
抽象工厂角色
// filename: WeaponFactory.java public abstract class WeaponFactory { public abstract Weapon get(); }
-
具体工厂角色
// filename: TankFactory.java public class TankFactory extends WeaponFactory { @Override public Weapon get() { return new Tank(); } }
-
测试
// filename: Test.java public class Test { public static void main(String[] args) { WeaponFactory weaponFactory = new TankFactory(); Weapon tank = weaponFactory.get(); tank.attack(); } }
-
工厂方法模式优点
- 创建对象时仅需要知到名称即可
- 扩展性高
- 屏蔽产品的具体实现
-
工厂方法模式缺点
- 每增加一个产品就需要对应增加一个工厂,使得系统中类的个数成倍上升,增加了系统复杂度
(4)抽象工厂模式
-
对比
工厂方法模式 抽象工厂模式 针对目标 一个产品系列 多个产品系列 实现效果 一个产品系列一个工厂类 多个产品系列一个工厂类 -
特点:抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式,抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体情况下,创建多个产品族中的产品对象。多个抽象产品类中,每个抽象产品类可以派生出多个具体产品类;一个抽象工厂类可以创建出多个具体产品类的实例
-
抽象工厂模式优点:
- 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象
-
抽象工厂模式缺点
- 产品族扩展十分困难
0x07 Bean 的实例化方式
(1)通过构造方法实例化
- 默认情况下会调用 Bean 的无参数构造方法
(2)通过简单工厂模式实例化
-
创建产品类 Star.java
public class Star { public Star() { System.out.println("Star 无参构造方法"); } }
-
创建工厂类 StarFactory.java
- 静态方法
public class StarFactory { public static Star get() { return new Star(); } }
-
在配置文件中实例化 Bean
<bean id="starFactory" class="com.spring.bean.StarFactory" factory-method="get"/>
-
在测试文件中测试
@Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); Star star = applicationContext.getBean("starFactory", Star.class); System.out.println(star); }
(3)通过 factory-bean 实例化
-
具体产品类:Tank.java
public class Tank { public Tank() { System.out.println("Tank 无参构造"); } }
-
具体工厂类:TankFactory.java
- 实例方法
public class TankFactory { public Tank get() { return new Tank(); } }
-
在配置文件中实例化 Bean
<bean id="tankFactory" class="com.spring.bean.TankFactory" /> <bean id="tank" factory-bean="tankFactory" factory-method="get" />
-
在测试文件中测试
@Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); Tank tank = applicationContext.getBean("tank", Tank.class); System.out.println(tank); }
(4)通过 FactoryBean 接口实例化
-
在 Spring 中,当编写了类直接实现 FactoryBean 接口后,factory-bean 会自动指向实现 FactoryBean 接口的类,factory-method 会自动指向
getObject()
方法 -
创建产品类:Person.java
public class Person { public Person() { System.out.println("person"); } }
-
创建工厂类:PersonFactory.java
import org.springframework.beans.factory.FactoryBean; public class PersonFactory implements FactoryBean<Person> { @Override public Person getObject() throws Exception { return new Person(); } @Override public Class<?> getObjectType() { return null; } @Override public boolean isSingleton() { return true; } }
-
在 spring.xml 配置文件中配置
<bean id="personFactory" class="com.spring.bean.PersonFactory" />
-
测试
@Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); Person person = applicationContext.getBean("personFactory", Person.class); System.out.println(person); }
(5)BeanFactory 与 FactoryBean 的区别
- BeanFactory 是 Spring IoC 容器的顶级对象,负责创建 Bean 对象
- FactoryBean 是一个 Bean,能够辅助 Spring 实例化其他 Bean 对象的一个 Bean
- 在 Spring 中,Bean 可分为普通 Bean 和 工厂 Bean
(6)注入自定义 Date
-
Student.java
private Date birth; public void setBirth(Date birth) { this.birth = birth; } @Override public String toString() { return "Student{" + "birth=" + birth + '}'; }
-
StudentFactory.java
public class StudentFactory implements FactoryBean<Date> { private String strDate; public StudentFactory(String strDate) { this.strDate = strDate; } @Override public Date getObject() throws Exception { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Date date = sdf.parse(strDate); return date; } @Override public Class<?> getObjectType() { return null; } }
-
spring.xml
<bean id="studentFactory" class="com.spring.bean.StudentFactory"> <constructor-arg index="0" value="2000-01-01" /> </bean> <bean id="student" class="com.spring.bean.Student"> <property name="birth" ref="studentFactory" /> </bean>
-
Test.java
@Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); Student student = applicationContext.getBean("student", Student.class); System.out.println(student); }
0x08 Bean 的生命周期
生命周期的本质在于程序在某个时间点调用了某个类的某个方法
Bean 生命周期的管理可以参考 Spring 源码中 AbstractAutowireCapableBeanFactory 类的
doCreateBean()
方法
(1)五步
- 实例化 Bean
- Bean 属性赋值
- 初始化 Bean
- 使用 Bean
- 销毁 Bean
-
User.java
public class User { private String name; public void destroyBean() { System.out.println("Step 5"); } public void initBean() { System.out.println("Step 3"); } public void setName(String name) { System.out.println("Step 2"); this.name = name; } public User() { System.out.println("Step 1"); } }
-
spring.xml
<bean id="user" class="com.spring.bean.User" init-method="initBean" destroy-method="destroyBean"> <property name="name" value="字符串" /> </bean>
-
Test.java
@Test public void test() { ApplicationContext applicationContext = new sPathXmlApplicationContext("spring.xml"); User user = applicationContext.getBean("user", User.class); System.out.println("Step 4 " + user); // 手动关闭 Spring 容器后 Spring 才会销毁 Bean ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext; context.close(); }
-
测试结果
Step 1 Step 2 Step 3 Step 4 com.spring.bean.User@1165b38 Step 5
(2)七步
- 实例化 Bean
- Bean 属性赋值
- 🆕执行“Bean 后处理器”的 before 方法
- 初始化 Bean
- 🆕执行“Bean 后处理器”的 after 方法
- 使用 Bean
- 销毁 Bean
-
编写一个类实现 BeanPostProcessor 类,重写 before 和 after 方法
-
日志 Bean 后处理器:LogBeanPostProcessor.java
public class LogBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { // bean:创建的 Beam 对象 // beanName:Bean 的名字 System.out.println("Step 3 before 方法"); return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName); } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("Step 5 after 方法"); return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName); } }
-
-
在 spring.xml 中配置“Bean 后处理器”
-
该 Bean 后处理器将作用于整个配置文件中所有的 Bean
<bean class="com.spring.bean.LogBeanPostProcessor" />
-
-
测试结果
Step 1 Step 2 Step 3 before 方法 Step 4 Step 5 after 方法 Step 6 com.spring.bean.User@6a78afa0 Step 7
(3)十步
-
实例化 Bean
-
Bean 属性赋值
-
🆕检查 Bean 是否实现了 Aware 的相关接口,并设置相关依赖
Aware 相关接口及说明
- BeanNameAware:将 Bean 的名字传递给 Bean
- BeanClassLoaderAware:将加载该 Bean 的类加载器传递给 Bean
- BeanFactoryAware:将 Bean 工厂对象传递给 Bean
-
执行“Bean 后处理器”的 before 方法
-
🆕检查 Bean 是否实现了 InitializingBean 接口,并调用接口方法
-
初始化 Bean
-
执行“Bean 后处理器”的 after 方法
-
使用 Bean
-
🆕检查 Bean 是否实现了 DisposableBean 接口,并调用接口方法
-
销毁 Bean
-
调用接口
public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean { // ... @Override public void setBeanClassLoader(ClassLoader classLoader) { System.out.println("Step 3 classloader: " + classLoader); } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { System.out.println("Step 3 beanFactory: " + beanFactory); } @Override public void setBeanName(String name) { System.out.println("Step 3 beanName: " + name); } @Override public void afterPropertiesSet() throws Exception { System.out.println("Step 5"); } @Override public void destroy() throws Exception { System.out.println("Step 9"); } }
-
测试结果
Step 1 Step 2 Step 3 beanName: user Step 3 classloader: jdk.internal.loader.ClassLoaders$AppClassLoader@78308db1 Step 3 beanFactory: org.springframework.beans.factory.support.DefaultListableBeanFactory@229d10bd: defining beans [com.spring.bean.LogBeanPostProcessor#0,user]; root of factory hierarchy Step 4 before 方法 Step 5 Step 6 Step 7 after 方法 Step 8 com.spring.bean.User@815b41f Step 9 Step 10
(4)多例 Bean 的生命周期
- Spring 根据 Bean 的作用域来选择管理方式
- 对于单例情况下,Spring 能够精确管理 Bean 的创建、初始化和销毁
- 对于多例情况下,Spring 仅能负责创建,不能跟踪其生命周期
(5)使用 Spring 管理手动创建的对象
-
Student.java
public class Student { }
-
Test.java
@Test public void test() { // 创建对象 Student student = new Student(); System.out.println(student); // 将 student 对象纳入 Spring 容器进行管理 DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); factory.registerSingleton("studentBean", student); // 从 Spring 容器中获取 Object studentBean = factory.getBean("studentBean"); System.out.println(studentBean); }
0x09 Bean 的循环依赖
A 对象中有属性 B,B 对象中有属性 A,此时出现循环依赖
(1)单例和 set 注入时产生的循环依赖
任意一个 Bean 实例化后就会被立即曝光
-
Husband.java
private String name; private Wife wife; public void setName(String name) { this.name = name; } public void setWife(Wife wife) { this.wife = wife; } public String getName() { return name; } @Override public String toString() { return "Husband{" + "name='" + name + '\'' + ", wife=" + wife.getName() + '}'; }
-
Wife.java
private String name; private Husband husband; public void setName(String name) { this.name = name; } public void setHusband(Husband husband) { this.husband = husband; } public String getName() { return name; } @Override public String toString() { return "Wife{" + "name='" + name + '\'' + ", husband=" + husband.getName() + '}'; }
-
spring.xml
<bean id="husband" class="com.spring.bean.Husband" scope="singleton"> <property name="name" value="Husband" /> <property name="wife" ref="wife" /> </bean> <bean id="wife" class="com.spring.bean.Wife" scope="singleton"> <property name="name" value="Wife" /> <property name="husband" ref="husband" /> </bean>
-
Test.java
@Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); Husband husband = applicationContext.getBean("husband", Husband.class); Wife wife = applicationContext.getBean("wife", Wife.class); System.out.println(husband); System.out.println(wife); }
(2)多例和 set 注入时产生的循环依赖
- 多例时任意一个 Bean 实例化后 不 会被立即曝光
- 一个多例,一个单例时不会报错
(3)构造方法时产生的循环依赖
- 无法解决
0x0A 回顾反射机制
(1)分析方法四要素
- 确定需要调用的对象
- 确定需要调用的方法
- 确定需要传递的参数
- 获取方法执行的结果
(2)获取 Method
(3)反射调用 Method
-
SomeService.java
public void function() { System.out.println("function 1"); } public void function(String s) { System.out.println("function 2"); } public void function(String s, int i) { System.out.println("function 3"); }
-
Test.java
// 获取类 Class<?> clazz = Class.forName("com.spring.bean.SomeService"); // 获取方法 Method functionMethods = clazz.getDeclaredMethod("function", String.class, int.class); // 调用方法 Constructor<?> constructor = clazz.getDeclaredConstructor(); Object obj = constructor.newInstance(); Object value = functionMethods.invoke(obj, "字符串", 123); System.out.println(value);
0x0B IoC 注解式开发
(1)回顾注解
-
注解的存在主要是为了简化 XML 的配置,Spring6 倡导全注解开发
-
自定义注解:Component.java
package com.java.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(value = {ElementType.TYPE}) @Retention(value = RetentionPolicy.RUNTIME) public @interface Component { String value(); }
- 以上自定义了一个注解:Component
- 该注解上方修饰的注解包括:Target、Retention,称为元注解
- Target:设置 Component 可以出现的位置,表示该注解只能用在类和接口上
- Retention:设置 Component 保持性策略,表示该注解可以被反射机制读取
String value()
:属性类型为 String,属性名为 value
-
使用自定义注解:User.java
// @Component(value = "user") @Component("user") public class User { }
- 当且仅当属性名仅有 value 时,使用注解可以写成
@Component("user")
- 当且仅当属性名仅有 value 时,使用注解可以写成
-
通过反射机制读取注解:Test.java
package com.java.annotation; public class Test { public static void main(String[] args) throws Exception { // 获取类 Class<?> aClass = Class.forName("com.java.annotation.User"); // 判断类是否有注解 if(aClass.isAnnotationPresent(Component.class)) { // 获取类的注解 Component annotation = aClass.getAnnotation(Component.class); // 访问注解属性 System.out.println(annotation.value()); } } }
-
组件扫描原理:ComponentScan.java
- 路径不可以有空格
package com.mypackage.client; import com.mypackage.annotation.Component; import java.io.*; import java.net.URL; import java.util.*; public class ComponentScan { public static void main(String[] args) { Map<String, Object> beanMap = new HashMap<>(); String packageName = "com.mypackage.bean"; String packagePath = packageName.replaceAll("\\.", "/"); URL url = ClassLoader.getSystemClassLoader().getResource(packagePath); File file = new File(url.getPath()); File[] files = file.listFiles(); Arrays.stream(files).forEach(f -> { String className = packageName + "." + f.getName().split("\\.")[0]; try { Class<?> clazz = Class.forName(className); if (clazz.isAnnotationPresent(Component.class)) { Component component = clazz.getAnnotation(Component.class); String beanId = component.value(); Object bean = clazz.newInstance(); beanMap.put(beanId, bean); } } catch (Exception e) { e.printStackTrace(); } }); System.out.println(beanMap); } }
(2)声明 Bean 的注解
-
负责声明 Bean 的常见注解
-
@Component
@Target(value = {ElementType.TYPE}) @Retention(value = RetentionPolicy.RUNTIME) @Documented @Indexed public @interface Component { String value() default ""; }
-
@Controller
:表示层@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Controller { @AliasFor( annotation = Component.class ) String value() default ""; }
-
@Service
:业务层@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Service { @AliasFor( annotation = Component.class ) String value() default ""; }
-
@Repository
:持久层@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Repository { @AliasFor( annotation = Component.class ) String value() default ""; }
-
(3)Spring 注解的使用
-
加入 aop 依赖
当在 Maven 中加入 spring-context 依赖后,会自动关联 aop 依赖
-
在配置文件中添加 context 命名空间
<beans xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
-
在配置文件中指定扫描的包
<context:component-scan base-package="com.spring.bean" />
如果需要扫描多个包,则可以使用以下方法:
-
<context:component-scan base-package="com.spring.bean, com.spring.bean2" />
-
<context:component-scan base-package="com.spring" />
-
-
在 Bean 类上使用注解
package com.spring.bean; import org.springframework.stereotype.Component; @Component(value = "userBean") public class User { }
(4)选择性实例化 Bean
举例:对使用 Controller 注解的 Bean 实例化
-
方法一
<context:component-scan base-package="com.spring.bean" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" /> </context:component-scan>
- 设置
use-default-filters="false"
- false 表示指定的包中所有声明 Bean 的注解失效
- 在
context:include-filter
中指定允许开启 Bean 实例化的注解
- 设置
-
方法二
<context:component-scan base-package="com.spring.bean" use-default-filters="true"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Component" /> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service" /> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository" /> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Indexed" /> </context:component-scan>
- 设置
use-default-filters="true"
- true 表示指定的包中所有声明 Bean 的注解有效
- 在
context:exclude-filter
中指定不允许开启 Bean 实例化的注解
- 设置
(5)负责注入的注解
a. @Value
-
当属性的类型是简单类型时使用
-
User.java
@Value(value = "张三") private String name; @Value(value = "20") private int age; @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; }
b. @Autowired & @Qualifier
-
@Autowired
可以注入非简单类型- 该注解存在
required
属性- 默认
true
,此时被注入的 Bean 必须存在 - 修改为
false
时 Bean 是否存在不影响
- 默认
- 该注解作用为根据类型 byType 进行自动装配
- 该注解存在
-
UserDao.java
public interface UserDao { void insert(); }
-
UserService.java
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { @Autowired protected UserDao userDao; public void generate() { userDao.insert(); } }
-
UserImpl.java
import org.springframework.stereotype.Repository; @Repository public class UserImpl implements UserDao { @Override public void insert() { System.out.println("UserImpl"); } }
-
@Autowired
和@Qualifier
联合可以根据名字进行装配
c. @Resource
-
作用:完成非简单类型注入
-
特性:
- 作为 JDK 一部分,更具备通用性
- 默认根据名称装配
byName
,未指定时使用属性名;当 name 找不到时会自动启用byType
装配 - 用在属性上或 setter 方法上
-
注解属于 JDK 扩展包,不在 JDK 中,需要额外引入,pom.xml
<dependency> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>2.1.1</version> </dependency>
-
使用
-
UserDao.java
package com.spring.bean; public interface UserDao { void insert(); }
-
UserImpl.java
package com.spring.bean; import org.springframework.stereotype.Repository; @Repository("userImpl") public class UserImpl implements UserDao { @Override public void insert() { System.out.println("UserImpl"); } }
-
UserService.java
package com.spring.bean; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; @Service public class UserService { @Resource(name = "userImpl") private UserDao userDao; public void insertUser() { userDao.insert(); } }
-
spring.xml
<context:component-scan base-package="com.spring.bean" />
-
Test.java
@Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); UserService userService = applicationContext.getBean("userService", UserService.class); userService.insertUser(); }
-
(6)全注解开发
-
使用全注解开发方式时需要写配置类来替代 Spring 配置文件(spring.xml)
-
使用
-
在 src/main/java/com.spring.bean 目录下新建配置类文件 SpringConfig.java
package com.spring.bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan({"com.spring", "com.spring.bean"}) public class SpringConfig { }
-
Test.java
package com.spring.test; import com.spring.bean.SpringConfig; import com.spring.bean.UserService; import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class SpringTest { @Test public void test() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); UserService userService = context.getBean("userService", UserService.class); userService.insertUser(); } }
-
0x0C JDBCTemplate
JDBCTemplate 是 Spring 提供的一个 JDBC 模板类,是对 JDBC 的封装,简化 JDBC 代码
(1)环境配置
-
新建数据库
CREATE SCHEMA spring DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ;
-
新建表
CREATE TABLE spring.user ( id int AUTO_INCREMENT NOT NULL, name varchar(225), age int, PRIMARY KEY(id) );
-
插入数据
INSERT INTO spring.user (name, age) VALUES ('张三', 30); INSERT INTO spring.user (name, age) VALUES ('李四', 40);
-
新建 src/main/java/com.spring.bean/User.java
package com.spring.bean; public class User { private Integer id; private String name; private Integer age; public User() {} public User(Integer id, String name, Integer age) { this.id = id; this.name = name; 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 Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
-
新建 src/main/java/com.spring.bean/MyDataSource.java
package com.spring.bean; import javax.sql.DataSource; import java.io.PrintWriter; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.logging.Logger; public class MyDataSource implements DataSource { private String driver; private String url; private String username; private String password; public void setDriver(String driver) { this.driver = driver; } public void setUrl(String url) { this.url = url; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } @Override public Connection getConnection() throws SQLException { try { // 注册驱动 Class.forName(driver); // 获取连接 Connection connection = DriverManager.getConnection(url, username, password); return connection; } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; } @Override public Connection getConnection(String username, String password) throws SQLException { return null; } @Override public PrintWriter getLogWriter() throws SQLException { return null; } @Override public void setLogWriter(PrintWriter out) throws SQLException {} @Override public void setLoginTimeout(int seconds) throws SQLException {} @Override public int getLoginTimeout() throws SQLException { return 0; } @Override public Logger getParentLogger() throws SQLFeatureNotSupportedException { return null; } @Override public <T> T unwrap(Class<T> iface) throws SQLException { return null; } @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return false; } }
-
在 pom.xml 添加依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>6.0.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.31</version> </dependency>
-
在 src/main/resource/spring.xml 中配置 Bean
<bean id="datasource" class="com.spring.bean.MyDataSource"> <property name="driver" value="com.mysql.cj.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/spring" /> <property name="username" value="root" /> <property name="password" value="root" /> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="datasource" /> </bean>
-
测试
@Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class); System.out.println(jdbcTemplate); }
(2)增 / 删 / 改
在测试文件中进行增加操作
a. 增加
-
单值
@Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class); // 插入操作 String sql = "INSERT INTO user (name, age) VALUES (?, ?)"; int count = jdbcTemplate.update(sql, "王五", 50); System.out.println(count); }
-
多值
@Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class); String sql = "INSERT INTO user (name, age) VALUES (?, ?)"; Object[] objs1 = {"a", 1}; Object[] objs2 = {"b", 2}; Object[] objs3 = {"c", 3}; List<Object[]> list = new ArrayList<>(); list.add(objs1); list.add(objs2); list.add(objs3); int[] count = jdbcTemplate.batchUpdate(sql, list); System.out.println(Arrays.toString(count)); }
b. 修改
-
单值
String sql = "UPDATE user SET name = ?, age = ? WHERE id = ?"; int count = jdbcTemplate.update(sql, "zhangsan", "300", 1); System.out.println(count);
-
多值
String sql = "UPDATE user SET name = ?, age = ? WHERE id = ?"; Object[] objs1 = {"d", 4, 4}; Object[] objs2 = {"e", 5, 5}; Object[] objs3 = {"f", 6, 6}; List<Object[]> list = new ArrayList<>(); list.add(objs1); list.add(objs2); list.add(objs3); int[] count = jdbcTemplate.batchUpdate(sql, list); System.out.println(Arrays.toString(count));
c. 删除
-
单值
String sql = "DELETE FROM user WHERE id = ?"; int count = jdbcTemplate.update(sql, 1); System.out.println(count);
-
多值
String sql = "DELETE FROM user WHERE id = ?"; Object[] objs1 = {4}; Object[] objs2 = {5}; Object[] objs3 = {6}; List<Object[]> list = new ArrayList<>(); list.add(objs1); list.add(objs2); list.add(objs3); int[] count = jdbcTemplate.batchUpdate(sql, list); System.out.println(Arrays.toString(count));
(3)查询
-
查询一个对象
String sql = "SELECT id, name, age FROM user WHERE id = ?"; User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 2); System.out.println(user);
-
查询多个对象
String sql = "SELECT id, name, age FROM user"; List<User> users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class)); System.out.println(users);
-
查值
String sql = "SELECT COUNT(1) FROM user"; Integer count = jdbcTemplate.queryForObject(sql, int.class); System.out.println(count);
(4)回调函数
-
通过使用回调函数可以添加更多操作细节
-
举例:查询 id 为 2 的数据
String sql = "SELECT id, name, age FROM user where id = ?"; User user = jdbcTemplate.execute(sql, new PreparedStatementCallback<User>() { @Override public User doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException { User user = null; ps.setInt(1, 2); ResultSet rs = ps.executeQuery(); if(rs.next()) { user = new User(); user.setId(rs.getInt("id")); user.setName(rs.getString("name")); user.setAge(rs.getInt("age")); } return user; } }); System.out.println(user);
(5)整合德鲁伊连接池
-
在 pom.xml 中添加德鲁伊连接池依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency>
-
在 spring.xml 中配置
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/spring" /> <property name="username" value="root" /> <property name="password" value="admin123" /> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="druidDataSource" /> </bean>
0x0D 面向切面编程 AOP
(1)概述
- AOP(Aspect Oriented Programming):面向切面编程 / 面向方面编程
- AOP 是对 OOP 的补充延申,其底层使用动态代理实现,能够捕捉系统中常用功能并转化为组件
- Spring 的 AOP 使用的动态代理是 JDK 动态代理 + CGLIB 动态代理技术
- 作用:将与核心业务无关的代码独立抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中
- 优点
- 代码复用性强
- 代码易维护
- 使开发者更关注业务逻辑
(2)相关术语
- 连接点 Joinpoint
- 在程序的整个执行流程中,可以织入切面的位置。方法执行前后、异常抛出之后等位置
- 切点 Pointcut
- 在程序执行流程中。真正织入切面的方法。一个切点对应多个连接点
- 通知 Advice
- 又称增强,是需要具体织入的代码
- 通知包括:前置通知、后置通知、环绕通知、异常通知、最终通知
- 切面 Aspect
- 切点加通知
- 织入 Weaving
- 把通知应用到目标对象的过程
- 代理对象 Proxy
- 一个目标对象被织入通知后产生的新对象
- 目标对象 Target
- 被织入的对象
(3)切点表达式
-
切点表达式用于定义通知对哪些方法进行切入
-
语法格式:
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形参列表) [异常])
- 访问控制权限修饰符
- 选填,默认 4 个权限全包括
- 返回值类型
- 必填,
*
表示返回值类型任意
- 必填,
- 全限定类名
- 选填,
..
表示当前包下的所有类,省略则表示所有类
- 选填,
- 方法名
- 必填,
*
表示所有方法,set*
表示所有 set 方法
- 必填,
- 形参列表
- 必填,省略表示没有参数,
..
表示参数类型和个数随意的方法,*
只有一个参数的方法,*, String
表示第一个参数类型随意,第二个参数为字符串类型
- 必填,省略表示没有参数,
- 异常
- 选填,省略时表示任意异常类型
- 访问控制权限修饰符
-
举例
-
service 包下的所有类中以 delete 开头的所有方法
execution(public * com.spring.service).*.delete*(..))
-
service 包下的所有类的所有方法
execution(* com.spring.service..*(..))
-
所有类的所有方法
execution(* *(..))
-
(4)Spring AOP
- Spring 对 AOP 的实现方式
- 基于注解方式,结合 Spring + AspectJ
- 基于 XML 方式,结合 Spring + AspectJ
- 基于 XML 方式,结合 Spring
- AspectJ 是独立于 Spring 框架之外的框架
- Spring + AspectJ 最为常用,以下内容以 Spring + AspectJ 方式为主
a. 环境配置
-
在 pom.xml 中添加依赖项
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.0.4</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>6.0.4</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>6.0.4</version> </dependency>
-
在 spring.xml 添加 context 命名空间和 aop 命名空间
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns: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"> </beans>
b. 基于注解方式
I. 实现步骤
-
定义目标类及目标方法
新建 com.spring.service 包并新建 UserService.java 文件
package com.spring.service; public class UserService { // 目标类 public void login() { // 目标方法 System.out.println("Login"); } }
-
定义切面类
在 com.spring.service 包新建 MyAspect.java 文件
package com.spring.service; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class MyAspect { // 切面 // 切面 = 通知 + 切点 // 前置通知 @Before("execution(* com.spring.service.UserService.*(..))") public void loginAspect() { System.out.println("Login Aspect"); } }
-
将两个类纳入 Bean 管理
// filename: UserService.java @Service("userService") public class UserService {...}
// filename: MyAspect.java @Component("myAspect") public class MyAspect {...}
-
在 spring.xml 中添加组件扫描并开启自动代理
<context:component-scan base-package="com.spring.service" /> <aop:aspectj-autoproxy proxy-target-class="true" />
proxy-target-class
:true-强制使用 CGLIB 动态管理,false-使用 JDK 动态管理(默认)
-
测试
@Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); UserService userService = applicationContext.getBean("userService", UserService.class); userService.login(); }
II. 通知
-
通知类型
- 前置通知:
@Before
目标方法执行之前的通知 - 后置通知:
@AfterReturning
目标方法执行之后的通知 - 环绕通知:
@Around
目标方法之前添加通知,同时目标方法执行之后添加通知 - 异常通知:
@AfterThrowing
发生异常之后执行的通知 - 最终通知:
@After
放在finally
语句块中执行的通知
- 前置通知:
-
通知顺序
@Aspect @Component("myAspect") public class MyAspect { // 前置通知 @Before("execution(* com.spring.service.UserService.*(..))") public void beforeAdvice() { System.out.println("前置通知"); } // 后置通知 @AfterReturning("execution(* com.spring.service.UserService.*(..))") public void afterReturningAdvice() { System.out.println("后置通知"); } // 环绕通知 @Around("execution(* com.spring.service.UserService.*(..))") public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("环绕(前)"); joinPoint.proceed(); // 执行目标 System.out.println("环绕(后)"); } // 异常通知 @AfterThrowing("execution(* com.spring.service.UserService.*(..))") public void afterThrowingAdvice() { System.out.println("异常通知"); } // 最终通知 @After("execution(* com.spring.service.UserService.*(..))") public void afterAdvice() { System.out.println("最终通知"); } }
graph LR 环绕通知-前--> 前置通知--> Service--> A{异常}--否--> 后置通知--> B[最终通知]--> 环绕通知-后 A--是--> 异常通知--> 最终通知
III. 切面顺序
-
可以使用
@Order
注解标识切面类,该注解需要value
值,值越小,优先级越高 -
SecurityAspect.java
package com.spring.service; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Aspect @Component @Order(1) public class SecurityAspect { // 安全切面 // 前置通知 @Before("execution(* com.spring.service.UserService.*(..))") public void beforeAdvice() { System.out.println("安全切面的前置通知"); } }
IV. 通用切点
-
定义并使用该通用切点
public class MyAspect { // 定义通用切点表达式 @Pointcut("execution(* com.spring.service.UserService.*(..))") public void CommonPointcut() {} // 前置通知 @Before("CommonPointcut()") public void beforeAdvice() { System.out.println("前置通知"); } // 后置通知 @AfterReturning("CommonPointcut()")... // 环绕通知 @Around("CommonPointcut()")... // 异常通知 @AfterThrowing("CommonPointcut()")... // 最终通知 @After("CommonPointcut()")... }
-
在其他切点使用该通用切点
@Before("com.spring.service.MyAspect.CommonPointcut()") public void beforeAdvice() { System.out.println("安全切面的前置通知"); }
V. 连接点
-
在 Spring 容器调用方法时会自动传递
joinPoint
连接点参数 -
获得目标方法签名,从而获取目标方法的详细信息
@Before("CommonPointcut()") public void beforeAdvice(JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); System.out.println(signature); }
VI. 全注解开发
-
编写配置类,在其中使用注解代替 Spring 配置文件,Spring Config.java
package com.spring.service; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan("com.spring.service") @EnableAspectJAutoProxy(proxyTargetClass = true) public class SpringConfig { }
-
修改测试文件
@Test public void test() { ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class); UserService userService = applicationContext.getBean("userService", UserService.class); userService.generate(); }
c. 基于 XML 方式
-
编写目标类
package com.spring.service; public class UserService { public void generate() { System.out.println("UserService"); } }
-
编写切面类和通知
package com.spring.service; import org.aspectj.lang.ProceedingJoinPoint; public class MyAspect { public void advice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long begin = System.currentTimeMillis(); proceedingJoinPoint.proceed(); long end = System.currentTimeMillis(); System.out.println((end - begin) + " ms"); } }
-
编写 Spring 配置文件
<bean id="userService" class="com.spring.service.UserService"></bean> <bean id="myAspect" class="com.spring.service.MyAspect"></bean> <aop:config> <aop:pointcut id="mypointcut" expression="execution(* com.spring.service..*(..))" /> <aop:aspect ref="myAspect"> <aop:around method="advice" pointcut-ref="mypointcut" /> </aop:aspect> </aop:config>
-
测试
import com.spring.service.UserService; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class SpringTest { @Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); UserService userService = applicationContext.getBean("userService", UserService.class); userService.generate(); } }
-End-