spring6合集——spring概述以及OCP、DIP、IOC原则
spring6合集——Spring6核心知识点总结
- 启示录
- 一、SOLID原则
- 1. 单一职责原则(SRP)
- 2. 开闭原则(OCP)
- 3. 里氏替换原则(LSP)
- 4. 接口隔离原则(ISP)
- 5. 依赖倒置原则(DIP)
- 二、控制反转IOC(Inversion of Control)
- 1. 为什么会出现控制反转
- 2. 依赖注入(DI)
- 2.1 set注入
- 2.2 构造方法注入
- 3. set注入专题
- 3.1 外部注入Bean
- 3.2 内部注入Bean
- 3.3 注入简单类型的属性
- 3.5 注入数组
- 3.6 set注入List集合
- 3.8 set注入Map集合
- 3.9 set注入Properties
- 3.10 set注入null和空串
- 3.11 set注入特殊字符串
- 4. p命名空间注入
- 5. c命名空间注入
- 6. util命名空间
- 7. 自动装配
- 7.1 根据名称自动装配
- 7.2 根据类型自动装配
- 8. Spring引入外部属性配置文件
- 三、Bean的作用域
- 1. 单例模式(singleton)
- 2. 原型作用域(property)
- 3. 其它作用域
- 四、GoF之工厂模式
- 1. 工厂模式的三种形态
- 2. 简单工厂模式
- 3. 工厂方法模式
- 5. 抽象工厂模式
- 6. 三种工厂模式对比(重点:exclamation:)
- **对比总结**
- 五、Bean的实例化方式
- 1. 构造方法实例化
- 2、、通过简单工厂模式实例化
- 3. 通过factory-bean实例化
- 4. 通过FactoryBean接口实例化
- 5. BeanFactory和FactoryBean的区别
- 6. 为什么要学习Bean的不同创建方式
- 六. Bean的生命周期
- 1. Bean生命周期之5步
- 2. Bean生命周期之7步
- 3. Bean生命周期之10步
- 七、Bean的循环依赖
- 1. 什么是循环依赖
- 2. singleton下的set注入
- 3. property下的set注入
- 4. singleton+构造注入
- 5. Spring解决循环依赖的原理和实现
- 6. 总结
- 八、注解开发
- 1. 核心组件注解
- 2. 依赖注入相关注解
- 九、GOF之代理模式
- 1. 静态代理
- 2. 动态代理(Proxy类)
- 2.1 JDK动态代理
- 2.2 CgLib动态代理
- 十、Aop切面编程
- 1. Aop介绍
- 2. Aop的七大术语
- 2.1 连接点( Joinpoint)
- 2.2 切点(Pointcut)
- 2.3 通知(Advice)
- 2.4 切面(Aspect)
- 2.5 织入(Weaving)
- 2.6 代理对象(Proxy)
- 2.7 目标对象(Target)
- 3. 切点表达式
- 4. 使用Spring的Aop编程
- 4.1 准备工作
- 4.2 核心步骤
- 4.3 通知类型
- 4.4 切面的执行顺序
- 4.5 简化切点表达式
- 十一、事务
- 1. 事务概述
- 2. Spring对事务的支持(@Transactional)
- 3. 事务的属性
- 3.1 属性的分类
- 3.2 事务传播行为
- Spring事务传播行为选择指南
- 使用说明:
- 3.3 事务的隔离级别
- 事务隔离级别详解
- 隔离级别选择建议:
- 在Spring中使用事务的隔离性
- 3.4 事务超时
- 3.5 只读事务
- 3.6 哪些异常回滚事务
- 3.7 哪些异常不会滚事务
- 下期预告
启示录
同志们大家好,以及亲爱的作者你好,今天2025年6月19日开启spring6的新篇章,由于之前已经系统学习过spring6,但是碍于没有做笔记,只是跟着写了代码,对于很多的知识点又忘记啦。因此今天开始全面记录和学习spring6,加深记忆, gogogo出发喽!🎉🎉🎉
本文主要参考资料为动力节点老杜的spring6
课程,B站可以直接搜到课程,非常的nice,同时也有笔记,我的思路是跟着笔记挑出重点进行总结和复习
,出发点可能略有不同,以下是笔记的地址。
https://www.yuque.com/dujubin/ltckqu/kipzgd?singleDoc#p1WjS
一、SOLID原则
SOLID是面向对象设计
(Object-Oriented Design, OOD)的五大基本原则的缩写,这些原则旨在提高软件的✨可维护性、✨可扩展性、✨可复用性和✨灵活性。
有点子抽象,一直再说OCP、IOC,是不是还不知道这是五大设计原则中的成员啊!
缩写 | 全称 | 中文含义 |
---|---|---|
S | Single Responsibility Principle | 单一职责原则 |
O | Open/Closed Principle | 开闭原则 |
L | Liskov Substitution Principle | 里氏替换原则 |
I | Interface Segregation Principle | 接口隔离原则 |
D | Dependency Inversion Principle | 依赖倒置原则 |
1. 单一职责原则(SRP)
一个类只能有一个引起它变化的原因,或者说一个类只负责一项职责,只干一类工作
含义 🔍
- 一个类或方法只干一类事情
- 每个模块或组件只负责一类事情
示例 ✅
public class UserController {public User getUserInfo();public List<User> getUserInfo();}
public class DeptController {public Dept getDeptInfo();public List<Dept> getDeptInfo();}
错误示例 ❌
public class UserController {public User getUserInfo();public Dept getDeptInfo();}
优点 💡
- 提高代码可读性
- 降低耦合度
- 易于维护和测试
上面的例子很清晰,针对于类来说,SRP原则比较容易实行,在正式的编码中对于不同的业务都有不同的Java类
进行实现,当然也有为了方便,在一个类中存在多个类型的业务功能。我只能说,尽量尽量避免,一个优秀的程序员不能这样干。
但是在方法中,这个例子就不太好举了,因为可能我们的业务需求就需要整合多项数据并分析得出结论
,那SRP原则不就冲突了吗?我们根据它的设计初衷进行分析,就是为了降低代码耦合度啊和可读性这些,其实在方法中,我们谨记,如果有重复的代码,提出来成为一个方法
,减少耦合度,同时对于有规律的代码块或者说目标明确的代码块
,同时又比较臃肿,这类代码我们大多数情况下是为了得出一个结论或者一批数据
,过程和其它代码没有关系,那这个方法就可以提出来,虽然它仅你调用,这样可读性就会很高,后面定位问题也非常好定位。
2. 开闭原则(OCP)
对扩展开放,对修改关闭
含义 🔍
- 类、函数、模块应通过扩展来支持或增加新行为,而不是通过修改代码。
✅ 示例:
interface Shape {double area();
}class Rectangle implements Shape {public double area() { return width * height; }
}class Circle implements Shape {public double area() { return Math.PI * radius * radius; }
}
新增图形只需添加新的子类,无需修改原有逻辑
💡 优点:
- 提高系统可扩展性
- 避免因修改导致的副作用
对于OCP原则来说核心就是:别改,你就新增。
其实OCP原则才是我们在开发中最容易忽略的点,你一旦动了别人的代码,或者说你自己曾经写过的代码,这就意味了需要全面测试
,不要说你有绝对的自信,你永远不会知道线上会因为什么报错,这都是经验之谈。所以在后面的代码中,能尽量将新的方法嵌入到之前的代码
或者说直接新增加一个方法
(前提是一个全新的功能,避免耦合度过高),如果情况所迫,其实我们还是得在原有基础上进行修改,对于你不知道或者没有完全把握的代码,不要去动,在下面写就行了。
3. 里氏替换原则(LSP)
子类型必须能够替换其基类型。
🔍 含义:
- 所有引用基类的地方必须能够透明的使用其子类的对象。
- 子类不能违反父类的行为契约。
📌 关键约束
-
子类方法参数类型应比父类更宽松
-
子类返回值类型应比父类更严格
-
子类不应抛出父类未声明的异常
💡 优点:
- 提高代码的健壮性和可重用性
- 支持多态和接口编程
错误示例❌
class Rectangle {protected int width, height;void setWidth(int w) { width = w; }void setHeight(int h) { height = h; }
}class Square extends Rectangle {// 破坏父类行为约束void setWidth(int w) { width = height = w; }
}
正确示例✅
interface Shape {int getArea();
}class Rectangle implements Shape { /*...*/ }
class Square implements Shape { /*...*/ }
对于LSP原则我的理解是,在正常的程序开发中,我们尽量使用接口继承的方式,定义一个通用的接口,让业务组件都实现这个接口,从而达到LSP原则。但是这个原则我们其实用到的并不多,大多数体现在Java的源码中,或者第三方框架的源码中,就比如Map,HashMap,LinkedHashMap
这种,父子类有严格的要求,HashMap的功能更加复杂,返回值更加详细,但又没有突破父级的限制,这就是典型的LSP原则。
4. 接口隔离原则(ISP)
客户端不应该被迫依赖于他们不使用的接口
🔍 含义:
- 大而全的接口应该拆分为
更小、更具体
的接口。 - 客户端只需知道它们
实际使用的方法
。 - 减少不必要的依赖和耦合
💡 优点:
- 提高代码的可读性
对于ISP原则在最新的springCloud
项目中非常的典型,我们使用的都是Controller、Service、ServiceImpl、Mapper、Dao
等不同的业务包,一般客户端使用的都是Service中的接口,不需要关注内部实现,同时Service下面如果调用数据库的话,也只需要调用Mapper中的接口即可,不需要知道其内部实现
,这样的拆分非常的清晰,在大公司中是分Java开发和数据库开发人员的,可能Java开发不需要知道数据库内部怎么实现,只需要调用对应的接口即可。
同时也需要注意,如果你的一个接口需要多个实现类继承,但是其中只有某些接口是共有的,这时候双方可能会有多余的方法继承,这时候可以使用抽象类,也就是public abstract interface
,可以选择性继承。
错误示例❌
// 违反ISP
interface Worker {void work();void eat();void sleep();
}class Robot implements Worker {void work() { /*...*/ }void eat() { /* 机器人不需要吃饭 */ }void sleep() { /* 机器人不需要睡觉 */ }
}// 遵循ISP
interface Workable {void work();
}interface Eatable {void eat();
}interface Sleepable {void sleep();
}class Human implements Workable, Eatable, Sleepable {// 实现所有方法
}class Robot implements Workable {// 只实现需要的方法
}
5. 依赖倒置原则(DIP)
高层模块不应该依赖于底层模块,两者都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象
上面听起来花里胡哨的,其实核心就一句话:面向接口编程
。
🔍 含义:
-
通过抽象(接口或抽象类)进行解耦
-
减少类之间的直接依赖
-
提高代码的可测试性和灵活性
就是这张图,这就是DIP原则告诉我们应该做的。
嘿,到这里五个原则就讲完了,那这五个原则其实就是为了给IOC原则做铺垫,注意:IOC是spring中的核心原则,上面SOLID是软件设计需要遵循的五大原则,而spring做到了,实现的原理就是IOC控制反转
。
二、控制反转IOC(Inversion of Control)
1. 为什么会出现控制反转
Spring的核心就是IOC控制反转,那么为什么要出现这个控制反转,直接通过一个例子来说明。
public class UserService {private userDao;public UserService () {// 一旦替换数据库,这里就要变化this.userDao= new UserDaoForMysql();} ... 具体的业务实现}
上面的例子是一个简单的数据库查询,但是如果有一天甲方要求mysql数据库不安全,我要替换为其它数据库,如果按照上面那种旧版写法,那一个一个改去吧,很累的,因为耦合度太高了。因为我们没有办法直接new接口,只能new具体的实现类
,而为了解耦合,Spring就出现了IOC控制反转,将userDao的创建和关系维护交给容器(Spring)去做,这样代码的改动就很小了,而且在编码中也非常的简洁。
2. 依赖注入(DI)
注意,上面说的IOC是Spring中非常重要的一种思想,而实现这种思想的手段是DI依赖注入💡。
依赖:指的是对象A和B之间的关系,也就是UserService和UserServiceImpl之间的关系。
注入:注入是一种手段,通过这种手段,可以让对象A和B对象产生关系
一般我们说的注入手段其实就是怎么给对象赋值
,我们要使用userService就是new,那么注入实际上就是把new的这个过程交出去了,有两种常见的注入方式。
- set注入:执行对象的set方法给属性赋值。
- 构造方法注入:使用有参的构造方法给对象赋值。
2.1 set注入
📌 以下的例子来自老杜的笔记。
package com.powernode.spring6.dao;/*** @author 动力节点* @version 1.0* @className UserDao* @since 1.0**/
public class UserDao {public void insert(){System.out.println("正在保存用户数据。");}
}
package com.powernode.spring6.service;import com.powernode.spring6.dao.UserDao;/*** @author 动力节点* @version 1.0* @className UserService* @since 1.0**/
public class UserService {private UserDao userDao;// 使用set方式注入,必须提供set方法。// 反射机制要调用这个方法给属性赋值的。public void setUserDao(UserDao userDao) {this.userDao = userDao;}public void save(){userDao.insert();}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/><bean id="userServiceBean" class="com.powernode.spring6.service.UserService"><property name="userDao" ref="userDaoBean"/></bean></beans>
📌 实现原理:
- 通过property标签获取到属性名
- 通过属性名推断出set方法名
- 通过反射机制调用set方法给属性赋值
❗️ 需要注意的是:
- 对象的set方法一定要存在。
- ref属性是要注入的bean对象的ID,而且不能重复
💡 核心
- 通过反射机制调用bean的set方法,让两个对象之间产生关系
2.2 构造方法注入
package com.powernode.spring6.dao;/*** @author 动力节点* @version 1.0* @className OrderDao* @since 1.0**/
public class OrderDao {public void deleteById(){System.out.println("正在删除订单。。。");}
}
package com.powernode.spring6.service;import com.powernode.spring6.dao.OrderDao;/*** @author 动力节点* @version 1.0* @className OrderService* @since 1.0**/
public class OrderService {private OrderDao orderDao;// 通过反射机制调用构造方法给属性赋值public OrderService(OrderDao orderDao) {this.orderDao = orderDao;}public void delete(){orderDao.deleteById();}
}
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService"><!--index="0"表示构造方法的第一个参数,将orderDaoBean对象传递给构造方法的第一个参数。--><constructor-arg index="0" ref="orderDaoBean"/>
</bean>
不使用参数的下标,使用参数的名字也可以
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/><bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService"><!--这里使用了构造方法上参数的名字--><constructor-arg name="orderDao" ref="orderDaoBean"/><constructor-arg name="userDao" ref="userDaoBean"/>
</bean><bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
不指定下标,不指定名字,也可以
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService"><!--没有指定下标,也没有指定参数名字--><constructor-arg ref="orderDaoBean"/><constructor-arg ref="userDaoBean"/>
</bean><bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
当然,不指定顺序,也可以
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/><bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService"><!--顺序已经和构造方法的参数顺序不同了--><constructor-arg ref="userDaoBean"/><constructor-arg ref="orderDaoBean"/>
</bean><bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
3. set注入专题
3.1 外部注入Bean
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/><bean id="userServiceBean" class="com.powernode.spring6.service.UserService"><property name="userDao" ref="userDaoBean"/></bean></beans>
📌 特点
- bean定义到外面,在property标签中使用ref属性进行注入,是最常用的方式
3.2 内部注入Bean
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="userServiceBean" class="com.powernode.spring6.service.UserService"><property name="userDao"><bean class="com.powernode.spring6.dao.UserDao"/></property></bean></beans>
3.3 注入简单类型的属性
package com.powernode.spring6.beans;/*** @author 动力节点* @version 1.0* @className User* @since 1.0**/
public class User {private int age;public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "User{" +"age=" + age +'}';}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="userBean" class="com.powernode.spring6.beans.User"><!--如果像这种int类型的属性,我们称为简单类型,这种简单类型在注入的时候要使用value属性,不能使用ref--><!--<property name="age" value="20"/>--><property name="age"><value>20</value></property></bean>
</beans>
❗️ ❗️ ❗️
有人说这种注入是为了什么,有什么作用,其实这就是@Value注解的前身
。我们经常将一些固定的配置放在yml或者properties文件中,如果在程序中想要使用,一般都是使用@Value注解,而上面的代码就是在@Value注解没有出现之前的替代方案。
@Component
public class DatabaseConfig {@Value("${db.url}")private String url;@Value("${db.username}")private String username;@Value("${db.password}")private String password;@Value("${db.pool.size:10}") // 默认值10private int poolSize;// getters...
}
db.url=jdbc:mysql://localhost:3306/mydb
db.username=admin
db.password=secret
db.pool.size=20
set注入简单数据类型如下
● 基本数据类型
● 基本数据类型对应的包装类
● String或其他的CharSequence子类
● Number子类
● Date子类
● Enum子类
● URI
● URL
● Temporal子类
● Locale
● Class
● 还包括以上简单值类型对应的数组类型。
3.5 注入数组
📌当注入数组类型是简单类型时
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="person" class="com.powernode.spring6.beans.Person"><property name="favariteFoods"><array><value>鸡排</value><value>汉堡</value><value>鹅肝</value></array></property></bean>
</beans>
📌 当数组中是非简单类型(对象)时使用ref注入
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="goods1" class="com.powernode.spring6.beans.Goods"><property name="name" value="西瓜"/></bean><bean id="goods2" class="com.powernode.spring6.beans.Goods"><property name="name" value="苹果"/></bean><bean id="order" class="com.powernode.spring6.beans.Order"><property name="goods"><array><!--这里使用ref标签即可--><ref bean="goods1"/><ref bean="goods2"/></array></property></bean></beans>
💡 要点
- 如果数组中是简单类型,使用
value
标签。 - 如果数组中是非简单类型,使用
ref
标签。
3.6 set注入List集合
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="peopleBean" class="com.powernode.spring6.beans.People"><property name="phones"><set><!--非简单类型可以使用ref,简单类型使用value--><value>110</value><value>110</value><value>120</value><value>120</value><value>119</value><value>119</value></set></property></bean>
</beans>
💡 要点:
- 使用
set
标签 - set集合中元素是简单类型的使用
value标签,反之使用ref标签
。
3.8 set注入Map集合
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="peopleBean" class="com.powernode.spring6.beans.People"><property name="addrs"><map><!--如果key不是简单类型,使用 key-ref 属性--><!--如果value不是简单类型,使用 value-ref 属性--><entry key="1" value="北京大兴区"/><entry key="2" value="上海浦东区"/><entry key="3" value="深圳宝安区"/></map></property></bean>
</beans>
💡 要点
- 使用
map标签
- 如果key是简单类型,使用
key
属性,反之使用key-ref
属性。 - 如果value是简单类型,使用
value
属性,反之使用value-ref
属性。
3.9 set注入Properties
📌 Properties继承java.util.Hashtable,所以Properties也是一个Map集合
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="peopleBean" class="com.powernode.spring6.beans.People"><property name="properties"><props><prop key="driver">com.mysql.cj.jdbc.Driver</prop><prop key="url">jdbc:mysql://localhost:3306/spring</prop><prop key="username">root</prop><prop key="password">123456</prop></props></property></bean>
</beans>
💡 要点
- 使用标签嵌套标签完成。
3.10 set注入null和空串
📌 注入空串的两种方式
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="vipBean" class="com.powernode.spring6.beans.Vip"><!--空串的第一种方式--><!--<property name="email" value=""/>--><!--空串的第二种方式--><property name="email"><value/></property></bean></beans>
📌 注入null的两种方式
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="vipBean" class="com.powernode.spring6.beans.Vip" /></beans><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="vipBean" class="com.powernode.spring6.beans.Vip"><property name="email"><null/></property></bean></beans>
💡 要点
- 注入空字符串可以使用
<value/>和value=''
- 注入null可以使用
<null/>标签或者不给属性赋值
3.11 set注入特殊字符串
📌 xml中的五个特殊字符串 <、>、'、"、&
💡第一种解决方案,使用特殊符号转义
特殊字符 | 转义字符 |
---|---|
> | > |
< | < |
’ | ' |
" | " |
& | & |
💡 第二种解决方案,将含有特殊符号的字符串放到:<![CDATA[]]>
当中。因为放在CDATA区中的数据不会被XML文件解析器解析
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="mathBean" class="com.powernode.spring6.beans.Math"><property name="result"><!--只能使用value标签--><value><![CDATA[2 < 3]]></value></property></bean></beans>
4. p命名空间注入
📌 目的:简化配置
📌 前提条件
- 添加p命名空间的配置信息:xmlns:p=“http://www.springframework.org/schema/p”
- p命名空间是基于setter方法注入的,需要对应的setter方法
<?xml version="1.0" encoding="UTF-8"?>
<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="customerBean" class="com.powernode.spring6.beans.Customer" p:name="zhangsan" p:age="20"/></beans>
5. c命名空间注入
📌 目的:简化配置
,不过这里简化的是构造方法的注入。
📌 前提条件
- 添加p命名空间的配置信息:xmlns:c=“http://www.springframework.org/schema/c”
- 需要提供构造方法
<?xml version="1.0" encoding="UTF-8"?>
<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="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:year="1970" c:month="1" c:day="1"/>--><bean id="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:_0="2008" c:_1="8" c:_2="8"/></beans>
6. util命名空间
📌 目的:配置复用,也就是定义的对象可以使用在多个Bean中。
📌 前提条件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:util="http://www.springframework.org/schema/util"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"><util:properties id="prop"><prop key="driver">com.mysql.cj.jdbc.Driver</prop><prop key="url">jdbc:mysql://localhost:3306/spring</prop><prop key="username">root</prop><prop key="password">123456</prop></util:properties><bean id="dataSource1" class="com.powernode.spring6.beans.MyDataSource1"><property name="properties" ref="prop"/></bean><bean id="dataSource2" class="com.powernode.spring6.beans.MyDataSource2"><property name="properties" ref="prop"/></bean>
</beans>
7. 自动装配
7.1 根据名称自动装配
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="userService" class="com.powernode.spring6.service.UserService" autowire="byName"/><bean id="aaa" class="com.powernode.spring6.dao.UserDao"/></beans>
📌 关键点:
- 添加属性:autowire=“byName”
- UserSerivice类中有一个对象aaa,必须拥有aaa对象的set方法,同时名称和配置文件中的Bean的id相同。
7.2 根据类型自动装配
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="accountService" class="com.powernode.spring6.service.AccountService" autowire="byType"/><bean id="x" class="com.powernode.spring6.dao.AccountDao"/><bean id="y" class="com.powernode.spring6.dao.AccountDao"/></beans>
📌 要点:
- 添加属性:autowire=“byType”
- 底层同样基于set方法注入,但是要注意,同一个配置文件中
不能同时拥有两个相同类型的Bean
。
8. Spring引入外部属性配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:property-placeholder location="jdbc.properties"/><bean id="dataSource" class="com.powernode.spring6.beans.MyDataSource"><property name="driver" value="${driver}"/><property name="url" value="${url}"/><property name="username" value="${username}"/><property name="password" value="${password}"/></bean>
</beans>
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spring
username=root
password=root123
📌 要点
- 引入依赖:xmlns:context=“http://www.springframework.org/schema/context”
- 使用context标签引入对应的文件:<context:property-placeholder location=“jdbc.properties”/>
- 使用value属性,用${}符号获取对应的值。
三、Bean的作用域
1. 单例模式(singleton)
默认情况下,Spring的Ioc容器创建的Bean对象是单例的,这个单例的含义是:每个Spring容器中,这个Bean对应一个唯一的实例,也就是内存地址相同。
❓ 为什么默认采用单例?
-
性能考虑:
减少对象创建和销毁的开销
-
资源共享:适合无状态的Bean共享使用
-
设计合理性:大多数情况下,服务类对象不需要多个实例
2. 原型作用域(property)
原型作用域与单例模式相反,每次获取Bean实例的时候都会创建新的对象,代码中实现如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="sb" class="com.powernode.spring6.beans.SpringBean" scope="prototype" /></beans>
❗️ ❗️ ❗️ 注意,如果要使用原型作用域,必须了解以下几点:
- Spring会在每次调用时创建新的对象,但是
不会主动去销毁!
- 原型作用域适用的情况是
需要线程安全的场景
,或者说对象中的属性是隔离的
,不同方法不能互相影响。
// 假设有以下Bean类
public class SpringBean {// 这个属性是隔离的,不同方法不能互相影响private int counter;public void increment() {counter++;}public int getCount() {return counter;}
}
❓ 怎么手动销毁原型作用域
- 实现 DisposableBean 接口
public class SpringBean implements DisposableBean {private int counter;public void increment() {counter++;}public int getCount() {return counter;}@Overridepublic void destroy() throws Exception {System.out.println("SpringBean 实例正在被销毁,执行清理工作...");// 在这里释放资源,如关闭文件、数据库连接等}
}
- xml中配置destroy-method
<bean id="sb" class="com.powernode.spring6.beans.SpringBean" scope="prototype" destroy-method="customDestroy"/>
💡 真实使用场景
public class PrototypeBeanTest {public static void main(String[] args) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");// 获取 prototype beanSpringBean bean1 = context.getBean("sb", SpringBean.class);SpringBean bean2 = context.getBean("sb", SpringBean.class);// 使用bean...// 手动触发销毁(Spring不会自动调用)// 需要根据实际实现选择调用方式if (bean1 instanceof DisposableBean) {try {((DisposableBean) bean1).destroy();} catch (Exception e) {e.printStackTrace();}}// 或者如果使用destroy-method方式// 可以通过反射调用指定方法try {Method destroyMethod = bean2.getClass().getMethod("customDestroy");destroyMethod.invoke(bean2);} catch (Exception e) {e.printStackTrace();}context.close();}
}
3. 其它作用域
● singleton:默认的,单例。
● prototype:原型。每调用一次getBean()方法则获取一个新的Bean对象。或每次注入的时候都是新对象。
● request:一个请求对应一个Bean。仅限于在WEB应用中使用。
● session:一个会话对应一个Bean。仅限于在WEB应用中使用。
● global session:portlet应用中专用的。如果在Servlet的WEB应用中使用global session的话,和session一个效果。(portlet和servlet都是规范。servlet运行在servlet容器中,例如Tomcat。portlet运行在portlet容器中。)
● application:一个应用对应一个Bean。仅限于在WEB应用中使用。
● websocket:一个websocket生命周期对应一个Bean。仅限于在WEB应用中使用。
● 自定义scope:很少使用。
四、GoF之工厂模式
📌 设计模式:一种可以被重复利用的解决方案
。
📌 GoF(Gang of Four),中文名——四人组。
《Design Patterns: Elements of Reusable Object-Oriented Software》(即《设计模式》一书),1995年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著。这几位作者常被称为"四人组(Gang of Four)"。
📌 该书中描述了23种设计模式。我们平常所说的设计模式就是指这23种设计模式。不过除了GoF23种设计模式之外,还有其它的设计模式,比如:JavaEE的设计模式(DAO模式、MVC模式等)。
📌 GoF23种设计模式可分为三大类:
- 创建型(5个):解决对象创建问题。
■单例模式
■工厂方法模式
■抽象工厂模式
■ 建造者模式
■原型模式
- 结构型(7个):一些类或对象组合在一起的经典结构。
■ 代理模式
■ 装饰模式
■ 适配器模式
■ 组合模式
■ 享元模式
■ 外观模式
■ 桥接模式 - 行为型(11个):解决类或对象之间的交互问题。
■ 策略模式
■ 模板方法模式
■ 责任链模式
■ 观察者模式
■ 迭代子模式
■ 命令模式
■ 备忘录模式
■ 状态模式
■ 访问者模式
■ 中介者模式
■ 解释器模式
1. 工厂模式的三种形态
- 简单工厂模式:不属于23中设计模式,又叫做
静态工厂方法模式
,实际就是工厂模式的特殊实现(简化版)。 - 工厂方法模式:23种设计模式之一。
- 抽象工厂模式:23种设计模式之一。
2. 简单工厂模式
📌 三个主要角色
- 抽象产品:某一类产品的统一父类,比如美食。
- 具体产品:某一类产品的具体代表,比如火鸡面。
- 工厂类:根据类型可以生产处具体的美食。
📌 抽象产品角色
package com.powernode.factory;/*** 武器(抽象产品角色)* @author 动力节点* @version 1.0* @className Weapon* @since 1.0**/
public abstract class Weapon {/*** 所有的武器都有攻击行为*/public abstract void attack();
}
📌 具体产品角色
package com.powernode.factory;/*** 坦克(具体产品角色)* @author 动力节点* @version 1.0* @className Tank* @since 1.0**/
public class Tank extends Weapon{@Overridepublic void attack() {System.out.println("坦克开炮!");}
}
📌 工厂类角色
package com.powernode.factory;/*** 工厂类角色* @author 动力节点* @version 1.0* @className WeaponFactory* @since 1.0**/
public class WeaponFactory {/*** 根据不同的武器类型生产武器* @param weaponType 武器类型* @return 武器对象*/public static Weapon get(String weaponType){if (weaponType == null || weaponType.trim().length() == 0) {return null;}Weapon weapon = null;if ("TANK".equals(weaponType)) {weapon = new Tank();} else if ("FIGHTER".equals(weaponType)) {weapon = new Fighter();} else if ("DAGGER".equals(weaponType)) {weapon = new Dagger();} else {throw new RuntimeException("不支持该武器!");}return weapon;}
}
📌 客户端角色
package com.powernode.factory;/*** @author 动力节点* @version 1.0* @className Client* @since 1.0**/
public class Client {public static void main(String[] args) {Weapon weapon1 = WeaponFactory.get("TANK");weapon1.attack();Weapon weapon2 = WeaponFactory.get("FIGHTER");weapon2.attack();Weapon weapon3 = WeaponFactory.get("DAGGER");weapon3.attack();}
}
📌 简单工厂模式的优点:
- 客户端不需要关注细节,直接根据参数类型得到对应的对象,初步实现了
责任的分离
。客户端只负责“消费”,工厂负责“生产”,生产和消费分离
。
📌 简单工厂模式的缺点:
- 工厂类集中了所有产品的生产逻辑,可以称之为上帝类,
一旦出现问题则系统瘫痪
。 不符合OCP开闭原则
,进行系统扩展时需要修改工厂类,而不是新增。
3. 工厂方法模式
📌 角色:
抽象工厂
:申明工厂方法,返回抽象产品类型,就是工厂的父类或者父接口。具体工厂
:重写抽象工厂方法,返回具体的产品实例。- 抽象产品
- 具体产品
📌 抽象产品角色
package com.powernode.factory;/*** 武器类(抽象产品角色)* @author 动力节点* @version 1.0* @className Weapon* @since 1.0**/
public abstract class Weapon {/*** 所有武器都有攻击行为*/public abstract void attack();
}
📌 具体产品角色
package com.powernode.factory;/*** 具体产品角色* @author 动力节点* @version 1.0* @className Gun* @since 1.0**/
public class Gun extends Weapon{@Overridepublic void attack() {System.out.println("开枪射击!");}
}
📌 抽象工厂角色
package com.powernode.factory;/*** 武器工厂接口(抽象工厂角色)* @author 动力节点* @version 1.0* @className WeaponFactory* @since 1.0**/
public interface WeaponFactory {Weapon get();
}
📌 具体工厂角色
package com.powernode.factory;/*** 具体工厂角色* @author 动力节点* @version 1.0* @className GunFactory* @since 1.0**/
public class GunFactory implements WeaponFactory{@Overridepublic Weapon get() {return new Gun();}
}
📌 客户端
package com.powernode.factory;/*** @author 动力节点* @version 1.0* @className Client* @since 1.0**/
public class Client {public static void main(String[] args) {WeaponFactory factory = new GunFactory();Weapon weapon = factory.get();weapon.attack();WeaponFactory factory1 = new FighterFactory();Weapon weapon1 = factory1.get();weapon1.attack();}
}
💡 工厂方法模式就是为了解决简单工厂的弊端:不符合OCP开闭原则的问题,在上面的例子中,如果后期要扩展的话,我们只需要增加对应的产品和抽象工厂以及具体工厂
,没有修改原来的代码,在其基础上进行扩展。
💡 优点:
- 客户端可以直接根据名称创建对象。
- 扩展性高,只需要扩展工厂类即可。
- 屏蔽产品的具体实现,只需要关心产品的接口。
❌ 缺点:
- 类爆炸,需要不断的扩展工厂,增加了系统的复杂度。
5. 抽象工厂模式
📌 角色:
抽象工厂
:申明工厂方法,返回抽象产品类型,就是工厂的父类或者父接口。具体工厂
:重写抽象工厂方法,返回具体的产品实例。- 抽象产品
- 具体产品
武器产品族
package com.powernode.product;/*** 武器产品族* @author 动力节点* @version 1.0* @className Weapon* @since 1.0**/
public abstract class Weapon {public abstract void attack();
}
package com.powernode.product;/*** 武器产品族中的产品等级1* @author 动力节点* @version 1.0* @className Gun* @since 1.0**/
public class Gun extends Weapon{@Overridepublic void attack() {System.out.println("开枪射击!");}
}
package com.powernode.product;/*** 武器产品族中的产品等级2* @author 动力节点* @version 1.0* @className Dagger* @since 1.0**/
public class Dagger extends Weapon{@Overridepublic void attack() {System.out.println("砍丫的!");}
}
水果产品族
package com.powernode.product;/*** 水果产品族* @author 动力节点* @version 1.0* @className Fruit* @since 1.0**/
public abstract class Fruit {/*** 所有果实都有一个成熟周期。*/public abstract void ripeCycle();
}
package com.powernode.product;/*** 水果产品族中的产品等级1* @author 动力节点* @version 1.0* @className Orange* @since 1.0**/
public class Orange extends Fruit{@Overridepublic void ripeCycle() {System.out.println("橘子的成熟周期是10个月");}
}
package com.powernode.product;/*** 水果产品族中的产品等级1* @author 动力节点* @version 1.0* @className Orange* @since 1.0**/
public class Orange extends Fruit{@Overridepublic void ripeCycle() {System.out.println("橘子的成熟周期是10个月");}
}
package com.powernode.product;/*** 水果产品族中的产品等级2* @author 动力节点* @version 1.0* @className Apple* @since 1.0**/
public class Apple extends Fruit{@Overridepublic void ripeCycle() {System.out.println("苹果的成熟周期是8个月");}
}
抽象工厂类
package com.powernode.factory;import com.powernode.product.Fruit;
import com.powernode.product.Weapon;/*** 抽象工厂* @author 动力节点* @version 1.0* @className AbstractFactory* @since 1.0**/
public abstract class AbstractFactory {public abstract Weapon getWeapon(String type);public abstract Fruit getFruit(String type);
}
具体工厂类
package com.powernode.factory;import com.powernode.product.Dagger;
import com.powernode.product.Fruit;
import com.powernode.product.Gun;
import com.powernode.product.Weapon;/*** 武器族工厂* @author 动力节点* @version 1.0* @className WeaponFactory* @since 1.0**/
public class WeaponFactory extends AbstractFactory{public Weapon getWeapon(String type){if (type == null || type.trim().length() == 0) {return null;}if ("Gun".equals(type)) {return new Gun();} else if ("Dagger".equals(type)) {return new Dagger();} else {throw new RuntimeException("无法生产该武器");}}@Overridepublic Fruit getFruit(String type) {return null;}
}
package com.powernode.factory;import com.powernode.product.*;/*** 水果族工厂* @author 动力节点* @version 1.0* @className FruitFactory* @since 1.0**/
public class FruitFactory extends AbstractFactory{@Overridepublic Weapon getWeapon(String type) {return null;}public Fruit getFruit(String type){if (type == null || type.trim().length() == 0) {return null;}if ("Orange".equals(type)) {return new Orange();} else if ("Apple".equals(type)) {return new Apple();} else {throw new RuntimeException("我家果园不产这种水果");}}
}
package com.powernode.client;import com.powernode.factory.AbstractFactory;
import com.powernode.factory.FruitFactory;
import com.powernode.factory.WeaponFactory;
import com.powernode.product.Fruit;
import com.powernode.product.Weapon;/*** @author 动力节点* @version 1.0* @className Client* @since 1.0**/
public class Client {public static void main(String[] args) {// 客户端调用方法时只面向AbstractFactory调用方法。AbstractFactory factory = new WeaponFactory(); // 注意:这里的new WeaponFactory()可以采用 简单工厂模式 进行隐藏。Weapon gun = factory.getWeapon("Gun");Weapon dagger = factory.getWeapon("Dagger");gun.attack();dagger.attack();AbstractFactory factory1 = new FruitFactory(); // 注意:这里的new FruitFactory()可以采用 简单工厂模式 进行隐藏。Fruit orange = factory1.getFruit("Orange");Fruit apple = factory1.getFruit("Apple");orange.ripeCycle();apple.ripeCycle();}
}
✅ 优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象
。
❌ 缺点:产品族扩展非常困难
,要增加一个系列的某一产品,既要在AbstractFactory里加代码,又要在具体的里面加代码。
6. 三种工厂模式对比(重点❗️)
模式 | 核心区别 | 适用场景 |
---|---|---|
简单工厂 | 一个工厂类通过条件判断创建所有产品。 | 产品种类少,逻辑简单。 |
工厂方法 | 一个工厂类只生产一种产品,通过子类扩展。 | 需要灵活扩展单一产品类型。 |
抽象工厂 | 一个工厂类生产一个产品族(多个相关产品)。 | 需要保证一组产品的一致性(如跨平台)。 |
对比总结
- 简单工厂:集中式管理,但违反开闭原则(新增产品需修改工厂类)。
- 工厂方法:单一职责,支持扩展,但每个产品需对应一个工厂类。
- 抽象工厂:解决产品族协同问题,但扩展新产品类型需修改接口。
注意了,如果在反复阅读之后还是不懂上面三种工厂模式的区别,不妨听我用大白话叙述一下,用我们生活中常见的例子进行说明。
- 简单工厂:小商店
- 工厂方法:超市
- 抽象工厂:商超
拿简单工厂来说,因为其只有一个核心的工厂类(上帝类),所以并不太适合复杂的场景,因为其并不符合OCP开闭原则,一旦业务复杂,扩展将会很复杂,但是如果我们的业务比较简单,事实上可以使用简单工厂,这和小商店是一样的,里面有很多种类的东西,但其实类型并不是很复杂,一个售货员就可以管理和出售
,如果规模大,可以多雇几个,但实际上内部的实现逻辑是比较简单的。
接上,如果小商店扩展扩展发现,管理逐渐费劲,而且效率不够高的时候,他就需要进行拆分,进化为超市,也就是工厂方法模式
,对于不同种类的商品进行分类,并且招聘对应的售货员(工厂类),如果后期需要扩展,那就增加对应商品的分类,同时招收售货员。这样一来管理就变得方便了,在一定承受范围内可以继续扩展。
继续,在不断发展之下,这个老板又想继续扩展,而且想要增加不同类型和系列的产品比如娱乐、休息和衣服等模块,所以他成立了一个商超,也就是抽象工厂模式
。到这一步普通的管理就显得微不足道了,如果不断的扩展是很麻烦的,所以他成立了一个管理部门(抽象工厂),由这个管理部门去吸纳不同模块的管理人才,然后不同模块的管理者在根据职责实现对应产品的创建、宣传、售卖等等,但是如果要继续扩展则需要从管理层开始,不断的一层一层维护,直到最后的具体商品
,同时也需要改变原有的管理层架构。这样做的好处就是不同的但又类型统一的模块都在一起
,用户可以根据需求直接面向抽象工厂,由抽象工厂对应到具体的工厂,最后得到用户的产品。
在这种设计模式下,商超的魅力就在于解耦
,用户不需要直面具体的产品,直接面向销售或者大厅管理者,由他们带领用户去到指定的模块,再由对应模块的负责人对接,然后拿到具体的产品。解耦解耦,无非就是将一个需求实现的过程拆分,每一步都有清晰的职责,虽然过程复杂,但是实现简单有效
。这样的痛点就是怎样维护这个过程,而这个过程对于用户来说是隐藏的。
再到我们的互联网公司,越大的公司,每一个员工的职责越详细,板块越清晰
,但需要一些特殊的人才来将其管理和汇总,我们可以理解为中枢。
其实到头来不过一句话,没有什么是加一层解决不了的
,仔细想想这一步一步的过程不就是多加了一层吗?
五、Bean的实例化方式
1. 构造方法实例化
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="userBean" class="com.powernode.spring6.bean.User"/></beans>
2、、通过简单工厂模式实例化
package com.powernode.spring6.bean;/*** @author 动力节点* @version 1.0* @className Vip* @since 1.0**/
public class Vip {
}
package com.powernode.spring6.bean;/*** @author 动力节点* @version 1.0* @className VipFactory* @since 1.0**/
public class VipFactory {public static Vip get(){return new Vip();}
}
<bean id="vipBean" class="com.powernode.spring6.bean.VipFactory" factory-method="get"/>
@Test
public void testSimpleFactory(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Vip vip = applicationContext.getBean("vipBean", Vip.class);System.out.println(vip);
}
3. 通过factory-bean实例化
package com.powernode.spring6.bean;/*** @author 动力节点* @version 1.0* @className Order* @since 1.0**/
public class Order {
}
package com.powernode.spring6.bean;/*** @author 动力节点* @version 1.0* @className OrderFactory* @since 1.0**/
public class OrderFactory {public Order get(){return new Order();}
}
<bean id="orderFactory" class="com.powernode.spring6.bean.OrderFactory"/>
<bean id="orderBean" factory-bean="orderFactory" factory-method="get"/>
@Test
public void testSelfFactoryBean(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Order orderBean = applicationContext.getBean("orderBean", Order.class);System.out.println(orderBean);
}
实质就是通过工厂方法模式创建。
4. 通过FactoryBean接口实例化
package com.powernode.spring6.bean;/*** @author 动力节点* @version 1.0* @className Person* @since 1.0**/
public class Person {
}
package com.powernode.spring6.bean;import org.springframework.beans.factory.FactoryBean;/*** @author 动力节点* @version 1.0* @className PersonFactoryBean* @since 1.0**/
public class PersonFactoryBean implements FactoryBean<Person> {@Overridepublic Person getObject() throws Exception {return new Person();}@Overridepublic Class<?> getObjectType() {return null;}@Overridepublic boolean isSingleton() {// true表示单例// false表示原型return true;}
}
<bean id="personBean" class="com.powernode.spring6.bean.PersonFactoryBean"/>
@Test
public void testFactoryBean(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Person personBean = applicationContext.getBean("personBean", Person.class);System.out.println(personBean);Person personBean2 = applicationContext.getBean("personBean", Person.class);System.out.println(personBean2);
}
FactoryBean在Spring中是一个接口。被称为“工厂Bean”。“工厂Bean”是一种特殊的Bean。所有的“工厂Bean”都是用来协助Spring框架来创建其他Bean对象的。
5. BeanFactory和FactoryBean的区别
BeanFactory是一个工厂
,可以用来创建Bean对象。FactoryBean是一种Bean
,他是Spring中用来辅助创建其它Bean对象的一种特殊Bean。
6. 为什么要学习Bean的不同创建方式
📌 其主要目的是让我们更好的理解Spring的底层哲学,就是最重要的Ioc控制反转思想的实现,主要体现在:
- 解耦
- 扩展性
📌 为什么我们在看这一章的时候感觉很懵逼,没什么用,因为在正常开发中是用不到的,大多数使用在框架的开发和程序中特殊业务下需要统一提供的功能
,这些都是架构师来做的。比如简单工厂模式,我们可以用在不同数据源的切换上。
<bean id="dataSource" class="com.example.DataSourceFactory" factory-method="createDataSource"/>
public class DataSourceFactory {public static DataSource createDataSource() {return new HikariDataSource(); // 根据配置返回不同实现}
}
📌 或者说在mybatis和shiro中也是常用的,不能每一次数据库请求都需要创建一次连接吧,用户每次登陆的统一信息管理只存一份就行了吧,也好管理,类似与这种,有兴趣的伙伴可以看源码,看看他们的底层是怎么实现的。
六. Bean的生命周期
💡 Spring其实就是一个管理Bean对象的工厂
,它负责对象的创建和销毁,Bean的生命周期实际上就是对象从创建到销毁的整个过程,比如:
- 什么时候创建Bean?
- 创建Bean前后会调用什么方法?
- Bean对象什么时候销毁?
- Bean对象销毁前后会调用什么方法?
💡 所以其本质可以总结为:我们需要知道在哪一步调用了什么方法,以便我们可以添加自己的逻辑!
1. Bean生命周期之5步
- 实例化Bean
- Bean属性赋值
- 初始化Bean
- 使用Bean
- 销毁Bean
package com.powernode.spring6.bean;/*** @author 动力节点* @version 1.0* @className User* @since 1.0**/
public class User {private String name;public User() {System.out.println("1.实例化Bean");}public void setName(String name) {this.name = name;System.out.println("2.Bean属性赋值");}public void initBean(){System.out.println("3.初始化Bean");}public void destroyBean(){System.out.println("5.销毁Bean");}}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--init-method属性指定初始化方法。destroy-method属性指定销毁方法。--><bean id="userBean" class="com.powernode.spring6.bean.User" init-method="initBean" destroy-method="destroyBean"><property name="name" value="zhangsan"/></bean></beans>
package com.powernode.spring6.test;import com.powernode.spring6.bean.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;/*** @author 动力节点* @version 1.0* @className BeanLifecycleTest* @since 1.0**/
public class BeanLifecycleTest {@Testpublic void testLifecycle(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");User userBean = applicationContext.getBean("userBean", User.class);System.out.println("4.使用Bean");// 只有正常关闭spring容器才会执行销毁方法ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;context.close();}
}
❗️ 需要注意的是:
- 只有正常关闭Spring容器的时候,bean的销毁方法才会被调用
- ClassPathXmlApplicationContext类才有close()方法
- 配置文件中的
init-method
指定初始化方法。destroy-method
指定销毁方法
2. Bean生命周期之7步
💡 这里相比于上面多了两步,分别是Bean初始化前和初始化后
package com.powernode.spring6.bean;import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;/*** @author 动力节点* @version 1.0* @className LogBeanPostProcessor* @since 1.0**/
public class LogBeanPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {System.out.println("Bean后处理器的before方法执行,即将开始初始化");return bean;}@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {System.out.println("Bean后处理器的after方法执行,已完成初始化");return bean;}
}
💡 配置Bean后处理器
<!--配置Bean后处理器。这个后处理器将作用于当前配置文件中所有的bean。-->
<bean class="com.powernode.spring6.bean.LogBeanPostProcessor"/>
3. Bean生命周期之10步
Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware
- 当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean。
- 当Bean实现了BeanClassLoaderAware,Spring会将加载该Bean的类加载器传递给Bean。
- 当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给Bean。
package com.powernode.spring6.bean;import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;/*** @author 动力节点* @version 1.0* @className User* @since 1.0**/
public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {private String name;public User() {System.out.println("1.实例化Bean");}public void setName(String name) {this.name = name;System.out.println("2.Bean属性赋值");}public void initBean(){System.out.println("6.初始化Bean");}public void destroyBean(){System.out.println("10.销毁Bean");}@Overridepublic void setBeanClassLoader(ClassLoader classLoader) {System.out.println("3.类加载器:" + classLoader);}@Overridepublic void setBeanFactory(BeanFactory beanFactory) throws BeansException {System.out.println("3.Bean工厂:" + beanFactory);}@Overridepublic void setBeanName(String name) {System.out.println("3.bean名字:" + name);}@Overridepublic void destroy() throws Exception {System.out.println("9.DisposableBean destroy");}@Overridepublic void afterPropertiesSet() throws Exception {System.out.println("5.afterPropertiesSet执行");}
}
通过测试可以看出来:
- InitializingBean的方法早于init-method的执行。
- DisposableBean的方法早于destroy-method的执行。
七、Bean的循环依赖
1. 什么是循环依赖
A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我
准备两个对象,演示在spring中什么情况下会出现循环依赖问题
package com.powernode.spring6.bean;/*** @author 动力节点* @version 1.0* @className Husband* @since 1.0**/
public class Husband {private String name;private Wife wife;public void setName(String name) {this.name = name;}public String getName() {return name;}public void setWife(Wife wife) {this.wife = wife;}// toString()方法重写时需要注意:不能直接输出wife,输出wife.getName()。要不然会出现递归导致的栈内存溢出错误。@Overridepublic String toString() {return "Husband{" +"name='" + name + '\'' +", wife=" + wife.getName() +'}';}
}
package com.powernode.spring6.bean;/*** @author 动力节点* @version 1.0* @className Wife* @since 1.0**/
public class Wife {private String name;private Husband husband;public void setName(String name) {this.name = name;}public String getName() {return name;}public void setHusband(Husband husband) {this.husband = husband;}// toString()方法重写时需要注意:不能直接输出husband,输出husband.getName()。要不然会出现递归导致的栈内存溢出错误。@Overridepublic String toString() {return "Wife{" +"name='" + name + '\'' +", husband=" + husband.getName() +'}';}
}
2. singleton下的set注入
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton"><property name="name" value="张三"/><property name="wife" ref="wifeBean"/></bean><bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="singleton"><property name="name" value="小花"/><property name="husband" ref="husbandBean"/></bean>
</beans>
package com.powernode.spring6.test;import com.powernode.spring6.bean.Husband;
import com.powernode.spring6.bean.Wife;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;/*** @author 动力节点* @version 1.0* @className CircularDependencyTest* @since 1.0**/
public class CircularDependencyTest {@Testpublic void testSingletonAndSet(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);System.out.println(husbandBean);System.out.println(wifeBean);}
}
💡 在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。
3. property下的set注入
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="prototype"><property name="name" value="张三"/><property name="wife" ref="wifeBean"/></bean><bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="prototype"><property name="name" value="小花"/><property name="husband" ref="husbandBean"/></bean>
</beans>
执行测试程序:发生了异常,异常信息如下:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException
: Error creating bean with name ‘husbandBean’: Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:265)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
… 44 more
翻译为:创建名为“husbandBean”的bean时出错:请求的bean当前正在创建中:是否存在无法解析的循环引用?
📌 经过测试,只有都为property的情况下才会出现,只要其中有一个是singleton就可以避免这个问题
,主要原因如下:
4. singleton+构造注入
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="hBean" class="com.powernode.spring6.bean2.Husband" scope="singleton"><constructor-arg name="name" value="张三"/><constructor-arg name="wife" ref="wBean"/></bean><bean id="wBean" class="com.powernode.spring6.bean2.Wife" scope="singleton"><constructor-arg name="name" value="小花"/><constructor-arg name="husband" ref="hBean"/></bean>
</beans>
📌 现象和原因
依旧会产生和上面一样的循环依赖问题,主要原因是因为通过构造方法注入导致的。因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开
,必须在一起完成导致的。
5. Spring解决循环依赖的原理和实现
📌 前提条件
set + singleton模式
📌 根本原因
根本的原因在于:这种方式可以做到将“实例化Bean”和“给Bean属性赋值”这两个动作分开去完成。
- 实例化Bean的时候:调用无参数构造方法来完成。此时可以
先不给属性赋值
,可以提前将该Bean对象“曝光”给外界
。 - 给Bean属性赋值的时候:调用setter方法来完成。
- 两个步骤是完全可以分离开去完成的,并且这
两步不要求在同一个时间点上完成
。 - 也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为
缓存
),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调用setter方法给属性赋值
。这样就解决了循环依赖的问题。
📌 代码实现
- Cache of singleton objects: bean name to bean instance. 单例对象的缓存:key存储bean名称,value存储Bean对象【一级缓存】
- Cache of early singleton objects: bean name to bean instance. 早期单例对象的缓存:key存储bean名称,value存储早期的Bean对象【二级缓存】
- Cache of singleton factories: bean name to ObjectFactory. 单例工厂缓存:key存储bean名称,value存储该Bean对应的ObjectFactory对象【三级缓存】
这三个缓存其实本质上是三个Map集合。
💡 提前曝光的核心:addSingletonFactory
💡 获取曝光对象的步骤
💡 从源码中可以看到,spring会先从一级缓存
中获取Bean,如果获取不到,则从二级缓存
中获取Bean,如果二级缓存还是获取不到,则从三级缓存
中获取之前曝光的ObjectFactory对象,通过ObjectFactory对象获取Bean实例,这样就解决了循环依赖的问题。
6. 总结
💡 Spring只能解决setter方法注入的单例bean之间的循环依赖
。ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存
当中。在解析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。
八、注解开发
上面都是基于配置文件进行Bean的装配的,当然我们后面都是基于注解开发的,这大大减少了我们的工作量,不过还是得需要了解其中的底层工作原理,下面介绍一下我们常用的一些Spring注解。
1. 核心组件注解
- @Component
-
作用:通用的组件注解,标识一个类为Spring组件
-
使用场景:当不确定一个类属于哪一层时使用
- @Controller
-
作用:标识一个类为Spring MVC控制器
-
特点:是@Component的特殊化,主要用于处理HTTP请求
- @Service
-
作用:标识一个类为业务服务层组件
-
特点:是@Component的特殊化,用于业务逻辑层
- @Repository
-
作用:标识一个类为数据访问层组件
-
特点:是@Component的特殊化,具有将数据库操作抛出的原生异常转换为Spring的持久化异常的功能
四个注解的关系:@Controller、@Service、@Repository都是@Component的特殊化,从功能上可以互相替换,但使用特定注解能更好地表达类的用途,并且某些特定注解会有额外的功能(如@Repository的异常转换)。
2. 依赖注入相关注解
- @Autowired
-
作用:自动装配依赖对象
-
特点:
1.1 默认按类型匹配
1.2 可以用在构造器、方法、字段和参数上
1.3 是Spring提供的注解
- @Resource
-
作用:自动装配依赖对象
-
特点:
2.1 默认按名称匹配,名称可通过name属性指定
2.2 是JSR-250标准注解,不属于Spring
2.3 可以用在字段和方法上
- @Qualifier
-
作用:当有多个相同类型的bean时,用于指定具体的bean
-
常与@Autowired一起使用
- @Value
-
作用:注入属性值,支持SpEL表达式
-
使用场景:
4.1 注入简单值
4.2 注入配置文件中的属性
4.3 使用SpEL表达式
注解 | 来源 | 主要用途 | 特点 |
---|---|---|---|
@Component | Spring | 通用组件声明 | 最基础的组件注解 |
@Controller | Spring | MVC控制器 | 处理HTTP请求,@Component的特殊化 |
@Service | Spring | 业务服务层 | 业务逻辑处理,@Component的特殊化 |
@Repository | Spring | 数据访问层 | 异常自动转换,@Component的特殊化 |
@Autowired | Spring | 依赖注入 | 默认按类型匹配,支持构造器/方法/字段/参数 |
@Resource | JSR-250 | 依赖注入 | 默认按名称匹配,支持name属性指定 |
@Qualifier | Spring | 限定注入 | 配合@Autowired解决多个同类型bean的歧义 |
@Value | Spring | 属性注入 | 支持直接值注入、配置文件属性注入(${})和SpEL表达式注入(#{})) |
九、GOF之代理模式
代理模式是GOF中23种设计模式之一,核心就是两个字“代理”,简单点来说就是中间件(在生活中就是中介和销售的例子)
,代理模式属于结构型设计模式
,主要特点如下:
- 保护目标对象,不会直接接触到目标对象,使用代理类。
- 简化代码,增加灵活性。
- 代理类拥有和目标类一样的功能。
代理模式的角色:
- 代理类(代理主题)
- 目标类(真实主题)
- 共用接口(代理类和目标类共同实现的接口)
💡 可以联想一下Shiro,可以判断当前的登陆状态,也可以获取到当前登录的用户信息。
1. 静态代理
package com.powernode.mall.service.impl;import com.powernode.mall.service.OrderService;/*** @author 动力节点* @version 1.0* @className OrderServiceImpl* @since 1.0**/
public class OrderServiceImpl implements OrderService {@Overridepublic void generate() {try {Thread.sleep(1234);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单已生成");}@Overridepublic void detail() {try {Thread.sleep(2541);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单信息如下:******");}@Overridepublic void modify() {try {Thread.sleep(1010);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单已修改");}
}
现在的场景是有一个类中有三个方法,需要统计这三个方法的用时,正常有三种解决办法。
📌 第一种:直接在源码上添加统计时间的代码,弊端是违反OCP原则,优势是实现简单。
package com.powernode.mall.service.impl;import com.powernode.mall.service.OrderService;/*** @author 动力节点* @version 1.0* @className OrderServiceImpl* @since 1.0**/
public class OrderServiceImpl implements OrderService {@Overridepublic void generate() {long begin = System.currentTimeMillis();try {Thread.sleep(1234);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单已生成");long end = System.currentTimeMillis();System.out.println("耗费时长"+(end - begin)+"毫秒");}@Overridepublic void detail() {long begin = System.currentTimeMillis();try {Thread.sleep(2541);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单信息如下:******");long end = System.currentTimeMillis();System.out.println("耗费时长"+(end - begin)+"毫秒");}@Overridepublic void modify() {long begin = System.currentTimeMillis();try {Thread.sleep(1010);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单已修改");long end = System.currentTimeMillis();System.out.println("耗费时长"+(end - begin)+"毫秒");}
}
📌 第二种,编写一个子类继承上面的目标类,在子类中统计对应的方法耗时,优势是符合OCP原则,缺点是存在类爆炸和重复代码的问题,增加了代码的耦合度。
package com.powernode.mall.service.impl;/*** @author 动力节点* @version 1.0* @className OrderServiceImplSub* @since 1.0**/
public class OrderServiceImplSub extends OrderServiceImpl{@Overridepublic void generate() {long begin = System.currentTimeMillis();super.generate();long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");}@Overridepublic void detail() {long begin = System.currentTimeMillis();super.detail();long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");}@Overridepublic void modify() {long begin = System.currentTimeMillis();super.modify();long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");}
}
📌 第三种:使用静态代理
package com.powernode.mall.service;/*** @author 动力节点* @version 1.0* @className OrderServiceProxy* @since 1.0**/
public class OrderServiceProxy implements OrderService{ // 代理对象// 目标对象private OrderService orderService;// 通过构造方法将目标对象传递给代理对象public OrderServiceProxy(OrderService orderService) {this.orderService = orderService;}@Overridepublic void generate() {long begin = System.currentTimeMillis();// 执行目标对象的目标方法orderService.generate();long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");}@Overridepublic void detail() {long begin = System.currentTimeMillis();// 执行目标对象的目标方法orderService.detail();long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");}@Overridepublic void modify() {long begin = System.currentTimeMillis();// 执行目标对象的目标方法orderService.modify();long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");}
}
❗️ 注意,这里还是创建了一个新的类,但是我们是实现了对应的Service接口,相当于最开始说的代理类和目标类实现同一个接口,代理类拥有和目标类相同的功能,但是在代理类中我们可以增加个性化功能,不影响目标类。
❗️ 弊端:无法解决类爆炸的问题。
2. 动态代理(Proxy类)
动态代理主要是为了解决静态代理的类爆炸
问题,原理就是在内存中动态的生成对应的字节码文件
,用完之后销毁,代码中只需要一份逻辑代码。
2.1 JDK动态代理
只能代理接口
。
📌 创建一个统计时间的类
package com.powernode.mall.service;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;/*** @author 动力节点* @version 1.0* @className TimerInvocationHandler* @since 1.0**/
public class TimerInvocationHandler implements InvocationHandler {// 目标对象private Object target;// 通过构造方法来传目标对象public TimerInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 目标执行之前增强。long begin = System.currentTimeMillis();// 调用目标对象的目标方法Object retValue = method.invoke(target, args);// 目标执行之后增强。long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");// 一定要记得返回哦。return retValue;}
}
package com.powernode.mall;import com.powernode.mall.service.OrderService;
import com.powernode.mall.service.TimerInvocationHandler;
import com.powernode.mall.service.impl.OrderServiceImpl;import java.lang.reflect.Proxy;/*** @author 动力节点* @version 1.0* @className Client* @since 1.0**/
public class Client {public static void main(String[] args) {// 创建目标对象OrderService target = new OrderServiceImpl();// 创建代理对象OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),new TimerInvocationHandler(target));// 调用代理对象的代理方法orderServiceProxy.detail();orderServiceProxy.modify();orderServiceProxy.generate();}
}
InvocationHandler接口中有一个方法invoke,这个invoke方法上有三个参数:
- 第一个参数:
Object proxy。代理对象
。设计这个参数只是为了后期的方便,如果想在invoke方法中使用代理对象的话,尽管通过这个参数来使用。 - 第二个参数:
Method method。目标方法
。 - 第三个参数:
Object[] args。目标方法调用时要传的参数
。
可以看到JDK的动态代理完美的解决了类爆炸的问题,而且大幅度的简化了代码,只需要改动调用的地方即可,符合OCP原则(没有改动业务代码)。
2.2 CgLib动态代理
既可以代理接口,也可以代理类
。
📌 创建代理类
package com.powernode.mall.service;import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;/*** @author 动力节点* @version 1.0* @className TimerMethodInterceptor* @since 1.0**/
public class TimerMethodInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {// 前增强long begin = System.currentTimeMillis();// 调用目标Object retValue = methodProxy.invokeSuper(target, objects);// 后增强long end = System.currentTimeMillis();System.out.println("耗时" + (end - begin) + "毫秒");// 一定要返回return retValue;}
}
📌 客户端使用
package com.powernode.mall;import com.powernode.mall.service.TimerMethodInterceptor;
import com.powernode.mall.service.UserService;
import net.sf.cglib.proxy.Enhancer;/*** @author 动力节点* @version 1.0* @className Client* @since 1.0**/
public class Client {public static void main(String[] args) {// 创建字节码增强器Enhancer enhancer = new Enhancer();// 告诉cglib要继承哪个类enhancer.setSuperclass(UserService.class);// 设置回调接口enhancer.setCallback(new TimerMethodInterceptor());// 生成源码,编译class,加载到JVM,并创建代理对象UserService userServiceProxy = (UserService)enhancer.create();userServiceProxy.login();userServiceProxy.logout();}
}
CgLib的动态代理实现起来更加的舒服和简洁,个人比较推荐CgLib,其中有字节码增强器,效率也会更快一些。
对于高版本的jdk需要增加两个参数。
● --add-opens java.base/java.lang=ALL-UNNAMED
● --add-opens java.base/java.lang=ALL-UNNAMED
十、Aop切面编程
📌 Aop是一种编程技术
;
📌 Aop底层是通过JDK和CjLib动态
代理实现的,接口使用JDK,类使用CjLib,也可以强制使用CjLib
;
📌 Aop技术通常使用在日志和全局的某些通用功能处理上,比如上面的时间分析
,又或者是行为日志
等
1. Aop介绍
Aop大部分的使用场景是交叉业务
,所谓交叉业务指的是系统中的一些通用服务例如:安全、日志、事务管理等
。
📌 为什么会出现交叉业务?
- 类似日志和安全这种问题在代码中大多数都是重复代码,提出来单独处理可以减少重复代码,并且方便管理和扩展。
- 交叉业务出现可以让程序员更加专注业务代码,增加开发效率。
📌 为什么要学习Aop?
- 这是一种重要的编程思想,能增加我们在开发中的灵活性。
- 重点:普通程序猿转向高级程序猴的重要一步,操作系统的架构。
📌 总结:将与核心业务无关的代码抽出来形成一个单独的组件,然后以横向交叉的方式应用到业务中的过程被称作Aop编程。
📌 Aop的优点
- 代码复用性高。
- 灵活性强,好扩展
- 让开发者更加关注业务代码
2. Aop的七大术语
2.1 连接点( Joinpoint)
📌 在程序的整个执行流程中,可以织入切面的位置
。例如方法的执行前后,异常抛出之后的位置等。
2.2 切点(Pointcut)
📌 在程序执行过程织入切面的方法
。(一个切点对应多个连接点)
2.3 通知(Advice)
📌 通知又叫做增强,就是具体织入的代码,可分为以下几种:
- 前置通知
- 后置通知
- 环绕通知
- 异常通知
- 最终通知
2.4 切面(Aspect)
📌 切点+通知组成切面。
2.5 织入(Weaving)
📌 把通知应用到目标对象上的过程。
2.6 代理对象(Proxy)
📌 一个目标对象被织入通知后产生的新对象。
2.7 目标对象(Target)
📌 被织入通知的对象。
3. 切点表达式
切点表达式用来定义通知(Advice)往哪些方法上切入。
切入点表达式语法格式:
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])
访问控制权限修饰符:
● 可选项。
● 没写,就是4个权限都包括。
● 写public就表示只包括公开的方法。
返回值类型:
● 必填项。
● * 表示返回值类型任意。
全限定类名:
● 可选项。
● 两个点“…”代表当前包以及子包下的所有类。
● 省略时表示所有的类。
方法名:
● 必填项。
● 表示所有方法。
● set表示所有的set方法。
形式参数列表:
● 必填项
● () 表示没有参数的方法
● (…) 参数类型和个数随意的方法
● () 只有一个参数的方法
● (, String) 第一个参数类型随意,第二个参数是String的。
异常:
● 可选项。
● 省略时表示任意异常类型。
service包下所有的类中以delete开始的所有方法execution(public * com.powernode.mall.service.*.delete*(..))
mall包下所有的类的所有的方法execution(* com.powernode.mall..*(..))
所有类的所有方法execution(* *(..))
4. 使用Spring的Aop编程
- 第一种方式:
Spring框架结合AspectJ框架实现的AOP,基于注解方式
。(常用) - 第二种方式:
Spring框架结合AspectJ框架实现的AOP,基于XML方式
。 - 第三种方式:Spring框架自己实现的AOP,基于XML配置方式。
❓ 什么是AspectJ?(Eclipse组织的一个支持AOP的框架。AspectJ框架是独立于Spring框架之外的一个框架,Spring框架用了AspectJ)
AspectJ项目起源于帕洛阿尔托(Palo Alto)研究中心(缩写为PARC)。该中心由Xerox集团资助,Gregor Kiczales领导,从1997年开始致力于AspectJ的开发,1998年第一次发布给外部用户,2001年发布1.0 release。为了推动AspectJ技术和社团的发展,PARC在2003年3月正式将AspectJ项目移交给了Eclipse组织,因为AspectJ的发展和受关注程度大大超出了PARC的预期,他们已经无力继续维持它的发展。
4.1 准备工作
使用Spring+AspectJ的AOP需要引入的依赖如下
<!--spring context依赖-->
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.0.0-M2</version>
</dependency>
<!--spring aop依赖-->
<dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>6.0.0-M2</version>
</dependency>
<!--spring aspects依赖-->
<dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>6.0.0-M2</version>
</dependency>
<?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.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"></beans>
4.2 核心步骤
📌 第一步:目标对象必须被Spring管理,也就是添加@Component注解
。
package com.powernode.spring6.service;// 目标类
@Component
public class OrderService {// 目标方法public void generate(){System.out.println("订单已生成!");}
}
📌 第二步:开启组件扫描
和自动代理
<?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.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><!--开启组件扫描--><context:component-scan base-package="com.powernode.spring6.service"/><!--开启自动代理--><aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
<aop:aspectj-autoproxy proxy-target-class=“true”/> 开启自动代理之后,凡事带有@Aspect注解的bean都会生成代理对象。
proxy-target-class=“true” 表示采用cglib动态代理。
proxy-target-class=“false” 表示采用jdk动态代理。默认值是false。即使写成false,当没有接口的时候,也会自动选择cglib生成代理类。
📌 第三步:添加切面类和通知,并配置切点表达式
package com.powernode.spring6.service;import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;// 切面类
@Aspect
@Component
public class MyAspect {// 切点表达式@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")// 这就是需要增强的代码(通知)public void advice(){System.out.println("我是一个通知");}
}
到这里一个简单的例子就完成了,Aop编程在有些公司中非常的普遍,如果要更好的使用Aop编程,对于我们程序猴的要求也是比较高的,比如说严格遵循SOLID五大原则
,尤其是ISP接口隔离
和SRP单一职责原则
,这样可以更好的织入通知,嵌入交叉业务。更有甚者对于方法名也有严格的要求
,现在知道为什么了吧,都是为了更好的实现Aop。
4.3 通知类型
- 前置通知:@Before 目标方法执行之前的通知
- 后置通知:@AfterReturning 目标方法执行之后的通知
- 环绕通知:@Around 目标方法之前添加通知,同时目标方法执行之后添加通知。
- 异常通知:@AfterThrowing 发生异常之后执行的通知
- 最终通知:@After 放在finally语句块中的通知
💡 这个顺序就代表了在程序中通知的执行顺序!
package com.powernode.spring6.service;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;// 切面类
@Component
@Aspect
public class MyAspect {@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("环绕通知开始");// 执行目标方法。proceedingJoinPoint.proceed();System.out.println("环绕通知结束");}@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")public void beforeAdvice(){System.out.println("前置通知");}@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")public void afterReturningAdvice(){System.out.println("后置通知");}@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")public void afterThrowingAdvice(){System.out.println("异常通知");}@After("execution(* com.powernode.spring6.service.OrderService.*(..))")public void afterAdvice(){System.out.println("最终通知");}}
❗️ 注意:
- 最终通知在发生异常后也是会执行的。
- 发生异常后,环绕通知的结束部分不会执行,后置通知也不会执行。
4.4 切面的执行顺序
🚀 我们知道,业务流程当中不一定只有一个切面,可能有的切面控制事务,有的记录日志,有的进行安全控制,如果多个切面的话,顺序如何控制:可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高
。
package com.powernode.spring6.service;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;@Aspect
@Component
@Order(1) //设置优先级
public class YourAspect {@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("YourAspect环绕通知开始");// 执行目标方法。proceedingJoinPoint.proceed();System.out.println("YourAspect环绕通知结束");}@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")public void beforeAdvice(){System.out.println("YourAspect前置通知");}@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")public void afterReturningAdvice(){System.out.println("YourAspect后置通知");}@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")public void afterThrowingAdvice(){System.out.println("YourAspect异常通知");}@After("execution(* com.powernode.spring6.service.OrderService.*(..))")public void afterAdvice(){System.out.println("YourAspect最终通知");}
}
4.5 简化切点表达式
💡 使用@Pointcut注解定义切点表达式,方法名可以被其它注解作为切点表达式的key。
package com.powernode.spring6.service;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;// 切面类
@Component
@Aspect
@Order(2)
public class MyAspect {@Pointcut("execution(* com.powernode.spring6.service.OrderService.*(..))")public void pointcut(){}@Around("pointcut()")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("环绕通知开始");// 执行目标方法。proceedingJoinPoint.proceed();System.out.println("环绕通知结束");}@Before("pointcut()")public void beforeAdvice(){System.out.println("前置通知");}@AfterReturning("pointcut()")public void afterReturningAdvice(){System.out.println("后置通知");}@AfterThrowing("pointcut()")public void afterThrowingAdvice(){System.out.println("异常通知");}@After("pointcut()")public void afterAdvice(){System.out.println("最终通知");}}
十一、事务
1. 事务概述
📌 在一个业务流程中,多条DML语句要么全部成功要么全部失败,这就叫做事务。
📌事务的处理过程
- 第一步:开启事务
- 第二步:执行业务代码
- 第三步:提交事务(commit)
- 第四步:回滚事务(rollback)(如果出现异常的情况下)
📌事务的四个特性
- 原子性:事务是最小工作单元,不可分割
- 一致性:要么同时成功,要么同时失败
- 持久性:持久性是事务结束的标识
- 隔离性:不同事务之间不能互相影响
2. Spring对事务的支持(@Transactional)
前提:xmlns:tx="http://www.springframework.org/schema/tx"开启事务支持
<tx:annotation-driven transaction-manager="transactionManager"/>
📌 首先开启事务的支持,后面我们直接使用@Transactional
注解即可,添加在类和方法上都可,底层还是使用Aop实现的,原理和我们手动提交是一样的,只不过使用Aop进行了简化。
package com.powernode.bank.service.impl;import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;/*** @author 动力节点* @version 1.0* @className AccountServiceImpl* @since 1.0**/
@Service("accountService")
@Transactional
public class AccountServiceImpl implements AccountService {@Resource(name = "accountDao")private AccountDao accountDao;@Overridepublic void transfer(String fromActno, String toActno, double money) {// 查询账户余额是否充足Account fromAct = accountDao.selectByActno(fromActno);if (fromAct.getBalance() < money) {throw new RuntimeException("账户余额不足");}// 余额充足,开始转账Account toAct = accountDao.selectByActno(toActno);fromAct.setBalance(fromAct.getBalance() - money);toAct.setBalance(toAct.getBalance() + money);int count = accountDao.update(fromAct);// 模拟异常String s = null;s.toString();count += accountDao.update(toAct);if (count != 2) {throw new RuntimeException("转账失败,请联系银行");}}
}
3. 事务的属性
3.1 属性的分类
事务中的重点属性:
- 事务传播行为
- 事务隔离级别
- 事务超时
- 只读事务
- 设置出现哪些异常回滚事务
- 设置出现哪些异常不回滚事务
3.2 事务传播行为
📌 在Service中,方法a和方法b都有事务,那这两个事务是单独的还是会合并为一个呢?这就是事务的传播行为。
@Transactional(propagation = Propagation.REQUIRED)
📌 一共有七种传播行为:
- REQUIRED:支持当前事务,如果不存在就新建一个(默认)
【没有就新建,有就加入】
- SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行
【有就加入,没有就不管了】
- MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常
【有就加入,没有就抛异常】
- REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起
【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】
- NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务
【不支持事务,存在就挂起】
- NEVER:以非事务方式运行,如果有事务存在,抛出异常
【不支持事务,存在就抛异常】
- NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。
【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】
Spring事务传播行为选择指南
传播行为 | 当前有事务 | 当前无事务 | 适用场景 |
---|---|---|---|
REQUIRED | 加入当前事务 | 新建一个事务 | 大多数业务方法(默认行为) |
REQUIRES_NEW | 新建事务并挂起当前事务 | 新建一个事务 | 需要独立事务的操作(如日志记录) |
SUPPORTS | 加入当前事务 | 非事务方式执行 | 查询方法,支持但不要求事务 |
NOT_SUPPORTED | 挂起当前事务,非事务执行 | 非事务方式执行 | 不需要事务支持的操作 |
MANDATORY | 加入当前事务 | 抛出异常 | 必须被事务方法调用的操作 |
NEVER | 抛出异常 | 非事务方式执行 | 禁止在事务中执行的操作 |
NESTED | 创建嵌套事务(保存点) | 新建一个事务 | 需要部分回滚的复杂业务 |
使用说明:
- REQUIRED:最常用,适用于大多数业务场景
- REQUIRES_NEW:用于需要独立提交/回滚的操作
- NESTED:提供部分回滚能力,但并非所有数据库都支持
- SUPPORTS/NOT_SUPPORTED:根据是否需要事务支持选择
- MANDATORY/NEVER:用于强制/禁止事务环境
3.3 事务的隔离级别
📌 三大读问题
- 脏读:读取到没有提交到数据库中的数据。
- 不可重复读:同一个事务中两次读写的数据不一致。
- 幻读:读取的数据是假的。
📌 四个隔离级别
- 读未提交 READ_UNCOMMITTED
- 读提交 READ_COMMITTED
- 可重复度 REPEATABLE_READ
- 序列化 SERIALIZABLE
事务隔离级别详解
隔离级别 | 脏读(Dirty Read) | 不可重复读(Non-repeatable Read) | 幻读(Phantom Read) | 典型实现方式 | 适用场景 |
---|---|---|---|---|---|
读未提交 (Read Uncommitted) | ✅ 可能 | ✅ 可能 | ✅ 可能 | 读不加锁,写加排他锁 | 对一致性要求极低,追求最高性能 |
读已提交 (Read Committed) | ❌ 避免 | ✅ 可能 | ✅ 可能 | 读时加共享锁(立即释放),写加排他锁 | 多数数据库默认级别,平衡一致性与性能 |
可重复读 (Repeatable Read) | ❌ 避免 | ❌ 避免 | ✅ 可能* | 读时加共享锁(事务结束释放),写加排他锁 | 需要事务内多次读取一致的场景 |
串行化 (Serializable) | ❌ 避免 | ❌ 避免 | ❌ 避免 | 范围锁,完全串行化执行 | 最高一致性要求,如金融交易 |
*注:MySQL的InnoDB引擎在"可重复读"级别通过MVCC机制避免了大部分幻读问题
隔离级别选择建议:
- 优先使用数据库默认级别(通常为Read Committed)
- 对数据一致性要求高的场景使用Repeatable Read
- 只有特别敏感的业务(如资金结算)才考虑Serializable
- 几乎不应该使用Read Uncommitted
在Spring中使用事务的隔离性
@Transactional(isolation = Isolation.READ_COMMITTED)
3.4 事务超时
@Transactional(timeout = 10)
📌 这里的超时时间指的是事务中最后一条DML语句之前的执行时间,超时了就回滚。
不计入超时时间
@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {accountDao.insert(act);// 睡眠一会try {Thread.sleep(1000 * 15);} catch (InterruptedException e) {e.printStackTrace();}
}
计入超时时间
@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {// 睡眠一会try {Thread.sleep(1000 * 15);} catch (InterruptedException e) {e.printStackTrace();}accountDao.insert(act);
}
3.5 只读事务
@Transactional(readOnly = true)
📌 作用:启用Spring的优化策略,提高查询语句的效率,其余增删改DML无法执行。
3.6 哪些异常回滚事务
@Transactional(rollbackFor = RuntimeException.class)
3.7 哪些异常不会滚事务
@Transactional(noRollbackFor = NullPointerException.class)
下期预告
老杜的笔记最后是有说明Spring的八大设计模式的,虽然在课程中详细的学习了单例、原型链、简单工厂、工厂模式、抽象工厂、代理模式、静态代理、动态代理等,如果在加上其它的例如装饰器模式
,说实话还是有一点混,所以决定重开一章单独学习23种设计模式。