Spring 基本使用总结 && 手动实现 Spring
前两天在准备期末周, 所以有一段时间没有更新文章, 本篇文章简洁的总结了Spring使用方式
, 无过多废话.
Spring 环境部署方式
官方源码下载: https://github.com/spring-projects/spring-frameworkMaven 仓库:https://mvnrepository.com/artifact/org.springframework/spring-core/5.3.8 # spring 框架所依赖的https://mvnrepository.com/artifact/org.springframework/spring-beans/5.3.8https://mvnrepository.com/artifact/org.springframework/spring-context/5.3.8https://mvnrepository.com/artifact/org.springframework/spring-expression/5.3.8https://mvnrepository.com/artifact/commons-logging/commons-logging/1.1.3 # spring 写日志需要的 jar
可以通过官方源码下载的方式, 也可以通过Maven
进行部署, 在这里准备好pom.xml
, 如下:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency> <!-- 引入 Junit 单元测试 -->
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>compile</scope>
</dependency>
</dependencies>
Spring 框架 API 配置
IOC 各 API
调用对象的 setter 方法进行配置 bean 对象
创建com.heihu577.beans
包, 并创建如下类:
public class Monster {
private Integer id;
private String name;
// getter && setter && 有参构造 && 无参构造 && toString 方法
}
随后在Maven
仓库中的resources
目录中创建beans.xml
, 代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="monster01" class="com.heihu577.beans.Monster"> <!-- 配置好 id 以及 class -->
<property name="id" value="1"/> <!-- property 标签默认调用对象的 setter 方法进行创建 -->
<property name="name">
<value>张三</value> <!-- 当然, 也可以通过 value 标签配置字符串 -->
</property>
</bean>
</beans>
创建测试代码如下:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() {
Monster monster01 = ioc.getBean("monster01", Monster.class);
System.out.println(monster01);
/*
运行结果: Monster{id=1, name='张三'} -> hashCode = 767010715
*/
}
}
运行流程图:
使用 p 进行属性注入
默认也是调用的 setter 方法, 直接给出代码案例:
<bean id="monster03" class="com.heihu577.beans.Monster" p:name="张三" p:id="222"/>
调用对象的 constructor 方法配置 bean 对象
当然, 这里介绍的就是 <constructor-arg>
标签, 只需要如下配置即可:
<!-- 使用 setter 方法传递属性 -->
<bean id="monster01" class="com.heihu577.beans.Monster">
<property name="id" value="1"/>
<property name="name">
<value>张三</value>
</property>
</bean>
<!-- 使用 p 进行属性赋值 -->
<bean id="monster03" class="com.heihu577.beans.Monster" p:name="张三" p:id="222"/>
<!-- 使用构造方法方法传递属性 -->
<bean id="monster02" class="com.heihu577.beans.Monster">
<constructor-arg index="0" value="2"/>
<constructor-arg index="1" value="李四"/>
</bean>
那么我们只需要调用ioc.getBean("monster02")
即可得到第二个对象, 如图:
bean 与 bean 之间的依赖关系
准备如下类关系:
public class Son {
private String name;
// getter && setter && 有参构造 && 无参构造 && toString 方法
}
public class Father {
private Son son;
private String name;
// getter && setter && 有参构造 && 无参构造 && toString 方法
}
准备如下 xml:
<bean id="son01" class="com.heihu577.beans.Son">
<property name="name" value="儿子"/>
</bean>
<bean id="father01" class="com.heihu577.beans.Father">
<property name="name" value="爸爸"/>
<property name="son" ref="son01"/> <!-- 使用 ref 进行依赖关系 -->
</bean>
当然也可以这样:
<bean id="father01" class="com.heihu577.beans.Father">
<property name="name" value="爸爸"/>
<property name="son">
<bean class="com.heihu577.beans.Son">
<property name="name" value="儿子"/>
</bean>
</property>
</bean>
运行结果:
生成 list, map, set, array, props 类型的引用对象
准备如下JavaBean:
public class Obey {
private String name;
}
以及包含多个类型的类:
public class Master {
private String name;
private Map<String, Obey> maps;
private Set<Obey> sets;
private List<Obey> lists;
private Obey[] obeys;
private Properties props;
// 有参构造 && 无参构造 && getter && setter && toString
}
那么所对应的xml配置为:
<bean id="obey01" class="com.heihu577.beans.Obey"/> <!-- 准备一个 JavaBean -->
<bean id="master01" class="com.heihu577.beans.Master">
<property name="name">
<value>字符串用value</value>
</property>
<property name="lists">
<list> <!-- List 类型用 list 标签 -->
<ref bean="obey01"/> <!-- lists.get(0) -->
<bean class="com.heihu577.beans.Obey"/> <!-- lists.get(1) -->
</list>
</property>
<property name="obeys">
<array> <!-- 数组类型用 array 标签 -->
<ref bean="obey01"/> <!-- obeys[0] -->
<bean class="com.heihu577.beans.Obey"/> <!-- obeys[1] -->
</array>
</property>
<property name="sets">
<set> <!-- set 类型 用 set 标签 -->
<ref bean="obey01"/>
<bean class="com.heihu577.beans.Obey"/>
</set>
</property>
<property name="maps">
<map> <!-- Map 类型用 map 标签 -->
<entry> <!-- map 类型包含 n 个 entry -->
<key><value>key01</value></key> <!-- entry 中的 key 指明了是 string 类型, 所以用 value 标签 -->
<ref bean="obey01"/> <!-- map 中的 value 部分 -->
</entry>
</map>
</property>
<property name="props">
<props> <!-- Properties 类型用 props 标签 -->
<prop key="username">root</prop>
<prop key="password">password</prop>
</props>
</property>
</bean>
测试运行结果:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() {
Master bean = ioc.getBean(Master.class);
System.out.println(bean);
/*
Master{name='字符串用value', maps={key01=com.heihu577.beans.Obey@1a3869f4}, sets=[com.heihu577.beans.Obey@1a3869f4, com.heihu577.beans.Obey@a38d7a3], lists=[com.heihu577.beans.Obey@1a3869f4, com.heihu577.beans.Obey@396e2f39], obeys=[com.heihu577.beans.Obey@1a3869f4, com.heihu577.beans.Obey@a74868d], props={password=password, username=root}}
*/
}
}
使用 utils 标签生成特殊对象
上面的list, set, map
等标签都在property
标签内声明的, 我们可以在外面声明, 然后通过ref
进行引入, 例如:
<utils:list id="myList">
<ref bean="obey01"/> <!-- lists.get(0) -->
<bean class="com.heihu577.beans.Obey"/> <!-- lists.get(1) -->
</utils:list>
<utils:set id="mySet">
<ref bean="obey01"/> <!-- lists.get(0) -->
<bean class="com.heihu577.beans.Obey"/> <!-- lists.get(1) -->
</utils:set>
<utils:map id="myMap">
<entry> <!-- map 类型包含 n 个 entry -->
<key><value>key01</value></key> <!-- entry 中的 key 指明了是 string 类型, 所以用 value 标签 -->
<ref bean="obey01"/> <!-- map 中的 value 部分 -->
</entry>
</utils:map>
<utils:properties id="myProperties">
<prop key="username">root</prop>
<prop key="password">password</prop>
</utils:properties>
<bean id="obey01" class="com.heihu577.beans.Obey"/> <!-- 准备一个 JavaBean -->
<bean id="master01" class="com.heihu577.beans.Master">
<property name="name">
<value>字符串用value</value>
</property>
<property name="lists" ref="myList"/>
<property name="obeys" ref="myList"/>
<property name="sets" ref="mySet"/>
<property name="maps" ref="myMap"/>
<property name="props" ref="myProperties"/>
</bean>
工厂类
静态工厂
准备如下工厂类:
public class MyStaticFactory {
private static final Map<String, Monster> monsterMap = new ConcurrentHashMap<>();
static { // 放入所对应的数据
monsterMap.put("monster01", new Monster(1, "monster01"));
monsterMap.put("monster02", new Monster(1, "monster02"));
}
public static Monster getMonster(String key) { // 提供工厂方法
return monsterMap.get(key);
}
}
对应的xml配置如下:
<bean id="monster01" class="com.heihu577.factory.MyStaticFactory" factory-method="getMonster">
<!-- factory-method="静态方法名" -->
<constructor-arg index="0" value="monster01"/> <!-- 此时则作为静态方法的参数, 传入 monster01 的 key -->
</bean>
运行结果:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() {
Monster monster01 = ioc.getBean("monster01", Monster.class);
System.out.println(monster01);
/*
Monster{id=1, name='monster01'} -> hashCode = 511473681
*/
}
}
成员工厂
准备如下工厂类:
public class MyFactory {
private final Map<String, Monster> monsterMap = new ConcurrentHashMap<>();
{
monsterMap.put("monster01", new Monster(1, "monster01"));
monsterMap.put("monster02", new Monster(1, "monster02"));
}
public Monster getMonster(String key) {
return monsterMap.get(key);
}
}
准备如下xml:
<bean id="myFactory" class="com.heihu577.factory.MyFactory"/>
<bean id="monster02" factory-bean="myFactory" factory-method="getMonster">
<constructor-arg index="0" value="monster02"/>
</bean>
运行结果:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() {
Monster monster02 = ioc.getBean("monster02", Monster.class);
System.out.println(monster02);
/*
Monster{id=1, name='monster02'} -> hashCode = 171497379
*/
}
}
FactoryBean 工厂
FactoryBean
是Spring
提供的工厂类的编写模式, 准备如下代码:
public class MyFactoryBean implements FactoryBean<Monster> {
private final Map<String, Monster> monsterMap = new ConcurrentHashMap<>();
private String key;
public MyFactoryBean() {
monsterMap.put("monster01", new Monster(1, "monster01"));
monsterMap.put("monster02", new Monster(1, "monster02"));
}
public void setKey(String key) { // 提供 key 的 setter
this.key = key;
}
@Override
public Monster getObject() throws Exception {
return monsterMap.get(this.key); // 在这里返回具体的对象
}
@Override
public Class<?> getObjectType() {
return Monster.class; // 与指定的泛型一致
}
@Override
public boolean isSingleton() {
return FactoryBean.super.isSingleton();
}
}
对应xml:
<bean id="monster01" class="com.heihu577.factory.MyFactoryBean">
<!-- 当spring读取到MyFactoryBean实现了FactoryBean时,则会走工厂方案,在这里我们只需要使用property配置属性即可 -->
<property name="key" value="monster01"/>
</bean>
运行结果:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() {
Monster monster01 = ioc.getBean("monster01", Monster.class);
System.out.println(monster01);
// Monster{id=1, name='monster01'} -> hashCode = 640363654
}
}
bean 继承属性
<bean id="monster01" class="com.heihu577.beans.Monster">
<property name="id" value="1"/>
<property name="name" value="monster01"/>
</bean>
<bean id="monster02" parent="monster01"/>
然后获取monster02
即可得到属性值与monster01
一致的对象.
bean 对象单例和多例
默认bean对象都是单例, 若需要多例, 我们需指定scope
属性, 如下:
<bean id="monster01" scope="prototype" class="com.heihu577.beans.Monster">
<property name="id" value="1"/>
<property name="name" value="monster01"/>
</bean>
随后运行结果如下:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() {
System.out.println(ioc.getBean("monster01", Monster.class));
System.out.println(ioc.getBean("monster01", Monster.class));
/*
运行结果:
Monster{id=1, name='monster01'} -> hashCode = 1177377518
Monster{id=1, name='monster01'} -> hashCode = 1773206895
可以发现两次的 hashCode 并不一样.
*/
}
}
bean 对象钩子函数
定义如下类:
public class Monster {
private Integer id;
private String name;
public void init() {
System.out.println("Monster init...");
}
public void destroy() {
System.out.println("Monster destroy...");
}
// ... 其他方法
}
对应的xml如下:
<bean id="monster01" class="com.heihu577.beans.Monster" init-method="init" destroy-method="destroy">
<property name="id" value="1"/>
<property name="name" value="张三"/>
</bean>
运行结果:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() throws Exception {
System.out.println(ioc.getBean("monster01", Monster.class));
System.out.println(ioc.getBean("monster01", Monster.class));
((AbstractApplicationContext) ioc).close();
/*
运行结果:
Monster init...
Monster{id=1, name='张三'} -> hashCode = 926370398
Monster{id=1, name='张三'} -> hashCode = 926370398
Monster destroy...
*/
}
}
bean 后置处理器 (应用于所有 bean, 有 AOP 的感觉)
定义后置处理器:
public class MyBeanPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean instanceof Monster) {
System.out.println("Monster -> " + beanName + " postProcessBeforeInitialization");
}
return bean;
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Monster) {
System.out.println("Monster -> " + beanName + " postProcessAfterInitialization");
}
return bean;
}
}
随后定义如下bean:
<bean id="monster01" class="com.heihu577.beans.Monster" init-method="init" destroy-method="destroy">
<property name="id" value="1"/>
<property name="name" value="monster01"/>
</bean>
<bean id="myBeanPostProcessor" class="com.heihu577.beans.MyBeanPostProcessor"/>
运行结果:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() throws Exception {
System.out.println(ioc.getBean("monster01", Monster.class));
((AbstractApplicationContext) ioc).close();
/*
运行结果:
Monster -> monster01 postProcessBeforeInitialization
Monster init...
Monster -> monster01 postProcessAfterInitialization
Monster{id=1, name='monster01'} -> hashCode = 422392391
Monster destroy...
*/
}
}
Spring 读取文件配置
读取文件变量, 配置到Bean中, 首先准备setting.properties
, 如下:
<context:property-placeholder location="classpath:settings.properties"/>
<!--
文件内容:
id=1
name=zhangsan
-->
效果如图:随后我们即可使用如下配置进行读取配置文件并配置bean, 如下:
<context:property-placeholder location="classpath:settings.properties"/>
<bean id="monster01" class="com.heihu577.beans.Monster">
<property name="id" value="${id}"/>
<property name="name" value="${name}"/>
</bean>
运行效果如图:
自动依赖注入 (xml)
准备如下关系:
public class UserController {
private UserService userService;
public void sayHello() {
System.out.println("UserController sayHello");
userService.sayHello();
}
// getter && setter && 有参构造 && 无参构造 && toString 方法
// 一定要提供 setter, 否则无法自动装配
}
public class UserService {
private UserDAO userDAO;
public void sayHello() {
System.out.println("UserService sayHello");
userDAO.sayHello();
}
// getter && setter && 有参构造 && 无参构造 && toString 方法
// 一定要提供 setter, 否则无法自动装配
}
public class UserDAO {
public void sayHello() {
System.out.println("UserDAO sayHello");
}
}
准备如下xml:
<bean id="userDAO" class="com.heihu577.beans.UserDAO"/>
<bean autowire="byName" id="userService" class="com.heihu577.beans.UserService"/>
<bean autowire="byName" id="userController" class="com.heihu577.beans.UserController"/>
运行结果:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() throws Exception {
UserController userController = ioc.getBean("userController", UserController.class);
userController.sayHello();
/*
UserController sayHello
UserService sayHello
UserDAO sayHello
*/
}
}
通过 EL 表达式给 Bean 赋值
准备如下JavaBean:
public class Person {
private Integer id; // 调用 returnId 成员方法得到值
private String name; // 调用 returnString 静态方法得到值
private String hobby; // 使用 EL 进行表达式计算
private Integer age; // 使用 EL 进行表达式计算
public Person(Integer id, String name) {
this.id = id;
this.name = name;
}
public Person() {
}
public Integer returnId() {
return 1;
}
public static String returnString() {
return "张三";
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getHobby() {
return hobby;
}
public void setHobby(String hobby) {
this.hobby = hobby;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + ''' +
", hobby='" + hobby + ''' +
", age=" + age +
'}';
}
}
那么我们对应的Bean配置
如下:
<bean id="person01" class="com.heihu577.beans.Person">
<property name="id" value="#{person01.returnId()}"/>
<property name="name" value="#{T(com.heihu577.beans.Person).returnString()}"/>
<property name="age" value="#{1+17}"/>
<property name="hobby" value="#{'打篮球'}"/>
</bean>
运行结果:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() throws Exception {
Person person01 = ioc.getBean("person01", Person.class);
System.out.println(person01);
/*
Person{id=1, name='张三', hobby='打篮球', age=18}
*/
}
}
通过注解放置Bean
创建com.heihu577.components
包, 在该包下进行定义我们的组件, 如图:其中所对应的代码如下:
// Controller 代码
public class UserController {
private UserService userService;
public void sayHello() {
System.out.println("UserDAO sayHello...");
userService.sayHello();
}
public UserService getUserService() {
return userService;
}
public void setUserService(UserService userService) {
this.userService = userService;
}
@Override
public String toString() {
return "UserController{" +
"userService=" + userService +
'}';
}
}
// Service 代码
public class UserService {
private UserDAO userDAO;
public void sayHello() {
System.out.println("UserService sayHello...");
userDAO.sayHello();
}
public UserDAO getUserDAO() {
return userDAO;
}
public void setUserDAO(UserDAO userDAO) {
this.userDAO = userDAO;
}
@Override
public String toString() {
return "UserService{" +
"userDAO=" + userDAO +
'}';
}
}
// DAO 代码
public class UserDAO {
public void sayHello() {
System.out.println("UserDAO sayHello...");
}
}
那么, 我们可以通过@Service,@Controller,@Repository
进行修饰它们, 并且将其放入到IOC容器
中.当然, 放入完之后我们需要配置
beans.xml
文件, 进行扫描包, 加入如下配置项:
<context:component-scan base-package="com.heihu577.components">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/> <!-- 只扫描 Service 注解 -->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/> <!-- 除了 Service 注解不扫描, 其他都扫描 -->
<!-- 如果想使用这两种模式, 必须在 component-scan 标签中增加该属性 use-default-filters="false" -->
</context:component-scan>
<!--
会扫描包即可, 当然 component-scan 还有其他参数, 但使用的方式并不多.
-->
测试代码:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() throws Exception {
UserController userController = ioc.getBean("userController", UserController.class);
System.out.println(userController);
/*
UserController{userService=null}
*/
}
}
这里放入容器的规则是, 类名首字母小写, 其他不变.
自动装配依赖关系
在上面我们可以看到, 虽然我们成功将Bean
对象放入到IOC容器
中, 但是属性之间的依赖关系并没有解决, 在这里我们有两种自动装配的方式.
@Autowired + @Qualifier 装配
@Autowired
是通过class 类型
进行装配的, 如果IOC容器
中存在该类型, 那么会进行自动装配, 例如:
@Controller
public class UserController {
@Autowired
private UserService userService;
// 其他原有方法...
}
public class UserService {
@Autowired
private UserDAO userDAO;
// 其他原有方法...
}
@Repository
public class UserDAO {
// 其他原有方法...
}
运行案例:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() throws Exception {
UserController userController = ioc.getBean("userController", UserController.class);
System.out.println(userController);
userController.sayHello();
/*
运行结果:
UserController{userService=UserService{userDAO=com.heihu577.components.UserDAO@6646153}}
UserDAO sayHello...
UserService sayHello...
UserDAO sayHello...
*/
}
}
介绍完@Autowired
后我们看一下@Qualifier
, @Qualifier注解
用在@Autowired注解
之后, 用于指定IOC容器
中的key
, 例如当前xml为如下情况:
<context:component-scan base-package="com.heihu577.components"/> <!-- 当前 IOC 容器已经扫描到了一个 UserService -->
<bean id="userService02" class="com.heihu577.components.UserService"/> <!-- 第二个 userService -->
那么我们可以通过@Qualifier("userService02")
进行指定装配key为userService02的bean对象
, 在这里就不掩饰了.
@Resource 装配
@Resource
可以通过类型,bean名称
进行匹配, 功能相当于@Autowired + @Qualifier
. 参数使用: @Resource(type = UserService.class, name = "userService")
, 按照类型匹配, 如果有多个同类型的bean
, 匹配不上按照name
匹配, 如果不指定参数则使用这套机制进行匹配(name 值则默认为成员属性的名称)
.
其中, 自动装配Bean对象的原理模拟, 放在这篇知乎文章中: https://zhuanlan.zhihu.com/p/682619049
泛型依赖注入
有如下Bean
关系, 如图:准备如下代码案例:
执行测试代码:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void testBasicBean() throws Exception {
BookService bookService = ioc.getBean("bookService", BookService.class);
System.out.println(bookService);
bookService.sayHello();
/*
com.heihu577.Service.BookService@5db45159
BookDAO sayHello...
*/
}
}
可以看到, 因为BaseService<T>
中的BaseDAO<T>
是泛型的, 但是因为BookService
指明了BaseService<Book>
, 那么容器就会寻找指明了Book类型的BaseDAO
去使用@Autowired
进行自动装配.
AOP 各 API
动态代理
有这么一个场景:
定义一个
Vehicle (交通工具)
接口, 定义Run
方法.
定义 Car (汽车)
类, 实现交通工具接口, 并且实现Run
方法.定义 Airplane (飞机)
类, 实现交通工具接口, 并且实现Run
方法.
我们需要在Car|Airplane
类调用Run方法
前后进行监控方法的运行前后, 例如:
交通工具开始运行了... Car Run方法正在调用... 交通工具运行完毕...
我们可以使用传统的方法进行解决, 例如:虽然可以成功, 但是形成了代码冗余, 那么我们的解决方式则是动态代理. 其核心想法则是, 在调用该对象的某个方法前后进行监控.
那么接下来我们就看一下动态代理如何实现, 首先定义VehicleProxyProvider
类, 例如:
package com.heihu577.proxy01;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class VehicleProxyProvider {
private Vehicle target_vehicle;
public VehicleProxyProvider(Vehicle target_vehicle) {
this.target_vehicle = target_vehicle;
}
public Vehicle getProxy() {
ClassLoader classLoader = target_vehicle.getClass().getClassLoader();
Class<?>[] interfaces = target_vehicle.getClass().getInterfaces();
/*
* public static Object newProxyInstance(ClassLoader loader, 指定类的 classLoader
Class<?>[] interfaces, 指定类实现的接口数组
InvocationHandler h) 使用匿名类进行实现该方法
* */
Vehicle o = (Vehicle) Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/* proxy -> 代理对象 method -> 方法名称 args -> 方法传递的参数 */
System.out.println("交通工具开始运行了...");
Object result = method.invoke(target_vehicle, args); // 方法的返回结果
System.out.println("交通工具运行完毕...");
return result; // 将方法返回
}
});
return o;
}
}
当然, 原来的Airplane|Car
中删除掉System.out.println("交通工具开始运行了..."); && System.out.println("交通工具运行完毕...");
这两行代码, 下面我们测试一下该代理对象:
@Test
public void testProxy01() throws Exception {
Vehicle vehicle = new Car();
VehicleProxyProvider vehicleProxyProvider = new VehicleProxyProvider(vehicle); // 传递给代理对象
Vehicle proxy = vehicleProxyProvider.getProxy(); // 调用 getProxy Vehicle 类型的代理对象
proxy.run();
/*
交通工具开始运行了... (代理对象调用的)
Car Run方法正在调用... (Vehicle.run方法的实现)
交通工具运行完毕... (代理对象调用的)
*/
}
动态代理 DEBUG 执行过程
当然了, 浓缩成如下代码更容易理解 (没有多余的关系):
@Test
public void testProxy01() throws Exception {
Vehicle vehicle = new Airplane(); // 首先准备一个真正的实例
Vehicle vehicleProxy = (Vehicle) Proxy.newProxyInstance(vehicle.getClass().getClassLoader(),
vehicle.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null; // 运行结果保存
if (method.getName().equals("run")) { // 如果调用的方法名称为 run 方法的话
System.out.println("进入到 Run 方法...");
result = method.invoke(vehicle, args); // 真正的实例.run(args) 这样调用, 返回结果
System.out.println("运行完毕 Run 方法...");
}
return result; // 返回方法运行完毕结果
}
});
vehicleProxy.run(); // 调用 run 方法
}
其中, 将其引入到AOP
中, 可以这样理解:
@Test
public void testProxy01() throws Exception {
Vehicle vehicle = new Airplane();
Vehicle vehicleProxy = (Vehicle) Proxy.newProxyInstance(vehicle.getClass().getClassLoader(),
vehicle.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
System.out.println("前置通知");
try {
Object invoke = method.invoke(vehicle, args);
} catch (Exception e) {
e.printStackTrace();
System.out.println("异常通知");
} finally {
System.out.println("最终通知");
}
System.out.println("后置通知");
return result;
}
});
}
动态代理的缺点则是, 在实际开发中, 我们并不希望使用
System.out.println
放在method.invoke
前, 而是希望一个真正的方法放在method.invoke
前, 这该如何解决?
动态代理问题解决
使用土方法解决
public void before() { // 定义前置通知方法, 当然, 也可以定义参数, 从外部传递进来即可.
System.out.println("前置通知");
}
public void after() {
System.out.println("后置通知");
}
@Test
public void testProxy01() throws Exception {
Vehicle vehicle = new Airplane();
Vehicle vehicleProxy = (Vehicle) Proxy.newProxyInstance(vehicle.getClass().getClassLoader(),
vehicle.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
before(); // 调用前置方法
try {
Object invoke = method.invoke(vehicle, args);
} catch (Exception e) {
e.printStackTrace(); // 抛出异常
System.out.println("异常通知");
} finally {
System.out.println("最终通知");
}
after(); // 调用后置方法
return result;
}
});
}
这样即可进行在前后插入方法. 但是这样使用耦合度较高, 我们对其进行解耦.
创建com.heihu577.proxy02.HeihuAOP
类, 放入前置通知&&后置通知
方法, 如下代码:
public class HeihuAOP {
public static void before() {
System.out.println("前置通知");
}
public static void after() {
System.out.println("后置通知");
}
}
当然, 这样就比较类似于工具类. 然后在上面的案例中, 将before()
换为HeihuAOP.before()
即可.
AOP 引入
理解完动态代理后, 下面就引用一下Spring
框架的切面编程, 具体概念图如下:
使用 AspectJ 框架引入切面编程
在pom.xml
文件中增加如下依赖项:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
<!-- AOP Alliance -->
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<!-- CGLIB -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2</version>
</dependency>
<!-- Spring Aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.8</version>
</dependency>
<!-- Spring AOP -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.8</version>
</dependency>
修改pom.xml
为如下:
<context:component-scan base-package="com.heihu577.aspectj"/>
<aop:aspectj-autoproxy/> <!-- 当加入这行代码后, 被@Aspect注解类所指明的类|接口 ioc.getBean()返回的则是代理对象 (只能使用接口类型进行接收), 如果没有指明则是普通对象, 或者增加该属性 proxy-target-class="true" -->
定义接口并实现:
public interface Vehicle {
public void run();
public String fly(String msg);
}
实现代码:
@Component // 一定要加入扩展, 放入 IOC 容器中
public class Airplane implements Vehicle {
@Override
public void run() {
System.out.println("Airplane Run方法正在调用...");
}
@Override
public String fly(String msg) {
String result = "Airplane fly方法正在调用..." + msg;
System.out.println(result);
return result;
}
}
随后定义切面类, 并加入@Aspect
注解, 如下:
@Aspect
@Component
public class VehicleAspectJ {
// execution(访问修饰符 方法返回类型 接口|类名.方法名(参数类型))
@Before("execution(public String com.heihu577.aspectj.Vehicle.fly(String))") // 不需要携带形式参数
public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature(); // 获取方法签名, joinPoint 可以获取调用的参数等信息
System.out.println("前置通知 - 方法签名 -> " + signature.getName() + " 参数 -> " + Arrays.toString(joinPoint.getArgs()));
}
@After("execution(public String com.heihu577.aspectj.Vehicle.fly(String))")
public void after() { // 这里就不用静态方法了, 使用成员方法
System.out.println("后置通知");
}
}
最终运行结果:
@Test
public void testAspectJ() {
com.heihu577.aspectj.Vehicle airplane = ioc.getBean("airplane", com.heihu577.aspectj.Vehicle.class); // 这里接收一定要用接口类型, 因为返回来的是代理对象.
airplane.fly("哈哈哈");
/*
前置通知 - 方法签名 -> fly 参数 -> [哈哈哈]
Airplane fly方法正在调用...哈哈哈
后置通知
*/
}
其中有五种通知, 如下:
前置通知: @Before 执行方法前 返回通知: @AfterReturning 返回结果后 异常通知: @AfterThrowing try-catch 捕获到异常 后置通知: @After 执行方法后 环绕通知: @Around
模糊配置
在前面我们使用如下代码进行匹配类中的方法:
@Before("execution(public String com.heihu577.aspectj.Vehicle.fly(String))") // 不需要携带形式参数
当然这种方式也支持模糊匹配, 例如:
@After("execution(* *.*(..))") // 所有包的所有类全部执行 (这里的含义是 任意返回类型 任意类.任意方法(任意参数))
切入点表达式
在前面的execution()
则是我们的切入点表达式, 已经应用过了, 在这里记载一下用法.有如下规律:
-
若表达式指明的是 已经实现某个普通类
的某一个方法, 那么将对该普通类
进行切入. -
若表达式指明的是 接口类
, 那么将对实现该接口的所有类
进行切入. -
若表达式指明的是 未实现接口的类
的某一个方法, 那么使用ioc.getBean
会返回一个CGlib
对象, 具体可以参考https://www.cnblogs.com/threeAgePie/p/15832586.html
切入点表达式重用
当我们想对某个方法进行切面, 同时放置前置通知, 后置通知时, 我们需要写两遍相同的execution()
表达式, 例如:当然了, 我们具体的优化思路则是使用
@PointCut()
进行包装, 如图:
JoinPoint
在这里记载一下JoinPoint
的常用方法:
public void beforeMethod(JoinPoint joinPoint){
joinPoint.getSignature().getName(); // 获取目标方法名
joinPoint.getSignature().getDeclaringType().getSimpleName(); // 获取目标方法所属类的简单类名
joinPoint.getSignature().getDeclaringTypeName(); // 获取目标方法所属类的类名
joinPoint.getSignature().getModifiers(); // 获取目标方法声明类型(public、private、protected)
Object[] args = joinPoint.getArgs(); // 获取传入目标方法的参数,返回一个数组
joinPoint.getTarget(); // 获取被代理的对象, 比如说 ioc.getBean(Car.class) 返回的是 Car 的代理对象, 在这里可以得到 Car 对象自己
joinPoint.getThis(); // 获取代理对象自己, 得到代理对象
}
AfterReturning 得到目标方法返回结果
在@AfterReturning
注解中, 可以获取方法的返回结果, 下面是@AfterReturning
方法的定义:
public @interface AfterReturning {
String value() default "";
String pointcut() default "";
String returning() default ""; // 重点关注该注解属性
String argNames() default "";
}
编写如下代码进行测试, JavaBean
如下:
@Component
public class Hack {
public String getString() {
System.out.println("Hack getString...");
return "Hack String..."; // 返回结果
}
}
在切面类中定义如下方法进行切面:
@AfterReturning(value = "execution(String com.heihu577.components.Hack.getString())", returning = "res")
public void hackGetString(JoinPoint joinPoint, Object res) { // 这里的 res 与 returning 指定的 res 相同
System.out.println("方法返回结果: res = " + res); // 在这里可以得到返回值
}
运行结果:
@Test
public void t2() {
Hack hack = ioc.getBean("hack", Hack.class);
hack.getString();
/*
Hack getString...
方法返回结果: res = Hack String...
*/
}
AfterThrowing 捕获异常
@AfterThrowing
注解的定义如下:
public @interface AfterThrowing {
String value() default "";
String pointcut() default "";
String throwing() default ""; // 重点关注该方法
String argNames() default "";
}
随后我们定义如下方法:
@Component
public class Hack {
public Integer testThrowing() {
int i;
try {
i = 1 / 0;
} catch (Exception e) {
throw new RuntimeException("除0错误");
}
return i;
}
}
增加如下切面:
@AfterThrowing(value = "execution(Integer com.heihu577.components.Hack.testThrowing())", throwing = "throwMsg")
public void hackTestThrowing(JoinPoint joinPoint, RuntimeException throwMsg) {
System.out.println(throwMsg.getClass());
System.out.println(throwMsg.toString());
}
最终运行结果:
@Test
public void t3() {
Hack hack = ioc.getBean("hack", Hack.class);
hack.testThrowing();
/*
class java.lang.RuntimeException
java.lang.RuntimeException: 除0错误 // 输出信息
java.lang.RuntimeException: 除0错误 // 报错信息
at com.heihu577.components.Hack.testThrowing(Hack.java:21)
at com.heihu577.components.Hack$$FastClassBySpringCGLIB$$1e6e314.invoke(<generated>)
*/
}
环绕通知 [ 了解 ]
环绕通知则是四种通知的组合体, 定义类以及方法如下:
@Component
public class Hack {
public String sayHi() {
return "Hi";
}
}
定义切面类中的方法对其进行切面:
@Around(value = "execution(String com.heihu577.components.Hack.sayHi())")
public void AroundTest(ProceedingJoinPoint proceedingJoinPoint) {
try {
Object[] args = proceedingJoinPoint.getArgs();
System.out.println("前置通知, 执行参数: " + Arrays.asList(args));
Object result = proceedingJoinPoint.proceed(); // 执行该函数
System.out.println("返回通知, 函数运行结果 => " + result);
} catch (Throwable e) {
System.out.println("异常通知");
throw new RuntimeException(e);
} finally {
System.out.println("最终通知");
}
}
可以看到, 其中环绕通知的逻辑是我们程序员进行指明的, 最大的区别在于, 函数在哪里执行我们可以进行指定. 编写如下测试代码, 进行测试结果:
@Test
public void t4() {
Hack hack = (Hack) ioc.getBean("hack");
String data = hack.sayHi();
/*
前置通知, 执行参数: []
后置通知, 函数运行结果 => Hi
最终通知
*/
}
AOP 优先级问题
如果我们用多个切面类进行切面一个对象的方法
, 那么我们的执行优先级顺序可以使用@Order(n)
进行控制, n的值越小, 优先级越高.
这里 @Order 如果不传递值, 默认是最低级的.
使用如下代码进行测试:
@Aspect
@Component
public class AspectJUtils {
@Pointcut("execution(void com.heihu577.beans.Hack.sayHello())")
public void myPointCut() {}
@Before("myPointCut()")
public void beforeSayHello() {
System.out.println("SayHello1 Before...");
}
@After("myPointCut()")
public void afterSayHello() {
System.out.println("SayHello1 After...");
}
}
// 以及 AspectJUtils2
@Aspect
@Component
public class AspectJUtils2 {
@Pointcut("execution(void com.heihu577.beans.Hack.sayHello())")
public void myPointCut(){}
@Before("myPointCut()")
public void beforeSayHello() {
System.out.println("SayHello2 Before...");
}
@After("myPointCut()")
public void afterSayHello() {
System.out.println("SayHello2 After...");
}
}
被切面的类定义如下:
@Component
public class Hack {
public void sayHello() {
System.out.println("Hello World");
}
}
执行结果:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void before() {
Hack hack = ioc.getBean("hack", Hack.class);
hack.sayHello();
/*
SayHello1 Before...
SayHello2 Before...
Hello World
SayHello2 After...
SayHello1 After...
*/
}
}
下面我们使用@Order
进行控制优先级, 让SayHello2
先进行执行:
@Order(2)
@Aspect
@Component
public class AspectJUtils {
@Pointcut("execution(void com.heihu577.beans.Hack.sayHello())")
public void myPointCut(){}
@Before("myPointCut()")
public void beforeSayHello() {
System.out.println("SayHello1 Before...");
}
@After("myPointCut()")
public void afterSayHello() {
System.out.println("SayHello1 After...");
}
}
// 以及 AspectJUtils2
@Order(1)
@Aspect
@Component
public class AspectJUtils2 {
@Pointcut("execution(void com.heihu577.beans.Hack.sayHello())")
public void myPointCut(){}
@Before("myPointCut()")
public void beforeSayHello() {
System.out.println("SayHello2 Before...");
}
@After("myPointCut()")
public void afterSayHello() {
System.out.println("SayHello2 After...");
}
}
最终运行结果:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void before() {
Hack hack = ioc.getBean("hack", Hack.class);
hack.sayHello();
/*
SayHello2 Before...
SayHello1 Before...
Hello World
SayHello1 After...
SayHello2 After...
*/
}
}
其中前置通知, 返回通知等的调用顺序如下:
AOP 基于 XML 配置
准备如下接口:
package com.heihu577.xml;
public interface UsbInterface {
public String start();
}
实现该接口的JavaBean
:
package com.heihu577.xml;
public class Phone implements UsbInterface {
@Override
public String start() {
System.out.println("Phone Starting...");
return "OK";
}
}
准备切面类:
public class AspectUtil {
public void beforeStart() {
System.out.println("AspectUtil beforeStart...");
}
public void afterStart() {
System.out.println("AspectUtil AfterStart...");
}
public void afterStartReturning(JoinPoint joinPoint, Object res) {
System.out.println("AspectUtil afterStartReturning...");
System.out.println(res);
}
public void afterStartThrowing(JoinPoint joinPoint, RuntimeException runtimeException) {
System.out.println("AspectUtil afterStartThrowing...");
System.out.println(runtimeException.toString());
}
}
xml配置如下:
<bean id="phone" class="com.heihu577.xml.Phone"/> <!-- 配置普通的 bean 对象 -->
<bean id="aspectUtil" class="com.heihu577.xml.AspectUtil"/> <!-- 配置一个自认为的切面类 -->
<aop:config> <!-- 将自认为的切面类配置为真正的切面类 -->
<aop:pointcut id="myPointCut" expression="execution(String com.heihu577.xml.Phone.start())"/> <!-- 配置切面表达式 -->
<aop:aspect ref="aspectUtil"> <!-- 指定切面对象 -->
<aop:before method="beforeStart" pointcut-ref="myPointCut"/> <!-- 配置前置通知 -->
<aop:after method="afterStart" pointcut-ref="myPointCut"/> <!-- 配置后置通知 -->
<aop:after-returning method="afterStartReturning" pointcut-ref="myPointCut" returning="res"/> <!-- 配置最终通知 -->
<aop:after-throwing method="afterStartThrowing" pointcut-ref="myPointCut" throwing="runtimeException"/> <!-- 配置异常通知 -->
<!-- <aop:around method="" 配置环绕通知... -->
</aop:aspect>
</aop:config>
最终运行结果:
public class MyTester {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void before() {
UsbInterface phone = ioc.getBean("phone", UsbInterface.class);
phone.start();
/*
AspectUtil beforeStart...
Phone Starting...
AspectUtil AfterStart...
AspectUtil afterStartReturning...
OK
*/
}
}
手动实现 Spring 底层机制
准备工作
搭建 Maven 项目
使用IDEA
创建Maven
工程后, 修改pom.xml
文件内容如下:
<dependencies>
<dependency> <!-- 加入 Spring 开发的基本包 -->
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.8</version>
</dependency>
<dependency> <!-- 加入 Spring 开发切面编程需要的包 -->
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.8</version>
</dependency>
<dependency> <!-- 引入 Junit 单元测试 -->
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>compile</scope>
</dependency>
</dependencies>
准备类与类之间的关系
准备beans.xml
文件, 用于扫描包用, 例如:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.heihu577.component"/> <!-- 扫描包 -->
</beans>
并且在该包下创建如下类关系, 例如: UserAction:
package com.heihu577.component;
import org.springframework.stereotype.Component;
@Component // 也可以使用 @Controller 注解
public class UserAction {
}
UserDAO:
package com.heihu577.component;
import org.springframework.stereotype.Component;
@Component // 也可以使用 @Repository 注解
public class UserDAO {
public void hi() {
System.out.println("UserDAO hi...");
}
}
UserService:
package com.heihu577.component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component // 也可以使用 @Service 注解
public class UserService {
@Autowired // 也可以使用 @Resource
private UserDAO userDAO;
public void m1() {
userDAO.hi();
}
}
测试:
public class T {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void t1() {
UserAction userAction = ioc.getBean("userAction", UserAction.class);
System.out.println(userAction); // com.heihu577.component.UserAction@358c99f5
UserService userService = ioc.getBean("userService", UserService.class);
System.out.println(userService); // com.heihu577.component.UserService@3ee0fea4
UserDAO userDAO = ioc.getBean("userDAO", UserDAO.class);
System.out.println(userDAO); // com.heihu577.component.UserDAO@48524010
}
}
单例多例问题
在这里我们默认使用@Component, @Service, @Controller, @Repository
注解默认是单例的, 如果我们想配置多例, 那么可以使用@Scope
注解, 例如:
package com.heihu577.component;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component // 也可以使用 @Controller 注解
@Scope("prototype")
public class UserAction {
}
里面有很多想法需要我们实现, 例如: 为什么加入@Autowired
, Spring 容器将自动实现依赖注入. 为什么加入@Scope
, Spring 容器将获取多例.
BeanPostProcessor 问题
在Spring中, 有BeanPostProcessor
接口, 如果实现了这个接口, 相当于在该对象初始化前后进行切面一个方法, 例如:
public class MyBeanPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = " + beanName);
return bean;
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = " + beanName);
return bean;
}
}
并且在xml
文件中进行配置该类:
<context:component-scan base-package="com.heihu577.component"/>
<bean id="myBeanPostProcessor" class="com.heihu577.process.MyBeanPostProcessor"/>
当然了, 之前在学习该部分内容时, <bean>
标签中有一个init-method
属性, 如果使用扫描包, 使用@PostConstructor
注解, 例如:
@Component // 也可以使用 @Controller 注解
@Scope("prototype")
public class UserAction {
@PostConstruct // 相当于 init-method 属性
public void init() {
System.out.println("UserAction init...");
}
}
那么我们查看运行结果:
public class T {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void t1() {
UserAction userAction = ioc.getBean("userAction", UserAction.class);
System.out.println(userAction);
UserService userService = ioc.getBean("userService", UserService.class);
System.out.println(userService);
UserDAO userDAO = ioc.getBean("userDAO", UserDAO.class);
System.out.println(userDAO);
/*
MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = userDAO
MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = userDAO
MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = userService
MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = userService
MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = userAction
UserAction init... (这里使用 @PostConstruct 指定了 init-method 方法)
MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = userAction
com.heihu577.component.UserAction@2b6856dd
com.heihu577.component.UserService@5db45159
com.heihu577.component.UserDAO@6107227e
*/
}
}
单例时, 在IOC容器
启动时, 会自动调用BeanPostProcessor
, 多例时, 当ioc.getBean
时, 会调用BeanPostProcessor
.
AOP 问题
定义如下接口:
package com.heihu577.aop;
public interface UsbInterface {
public void start();
}
定义类, 实现该接口:
package com.heihu577.aop;
import org.springframework.stereotype.Component;
@Component
public class Phone implements UsbInterface{
@Override
public void start() {
System.out.println("Phone Start...");
}
}
定义切面类:
@Aspect
@Component
public class UsbAspect {
@Pointcut("execution(void com.heihu577.aop.UsbInterface.start())")
public void myPointCut() {
}
@Before("myPointCut()")
public void startBefore(JoinPoint joinPoint) {
System.out.println("start Before...");
}
@After("myPointCut()")
public void startAfter(JoinPoint joinPoint) {
System.out.println("start After...");
}
@AfterThrowing("myPointCut()")
public void afterThrowing() {
System.out.println("start After Throwing...");
}
@AfterReturning("myPointCut()")
public void afterReturning() {
System.out.println("start After Returning...");
}
}
定义xml
文件, 并进行扫描:
<context:component-scan base-package="com.heihu577"/> <!-- 扫描 -->
<aop:aspectj-autoproxy/> <!-- 开启 AOP -->
运行结果:
public class T {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
@Test
public void t1() {
UsbInterface bean = ioc.getBean(UsbInterface.class);
bean.start();
/*
MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = phone
MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = phone
MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = usbAspect
MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = usbAspect
MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = userDAO
MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = userDAO
MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = userService
MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = userService
start Before...
Phone Start...
start After Returning...
start After...
*/
}
}
那么下来来研究一下BeanPostProcessor
与AOP切面
之间的关系. 配置BeanPostProcessor
如下:
package com.heihu577.process;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = " + beanName + " " +
"class " + bean.getClass()); // 增加查看 class 的功能
return bean;
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = " + beanName + " class" +
" " + bean.getClass()); // 增加查看 class 的功能
return bean;
}
}
运行结果如图:
MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = phone class class com.heihu577.aop.Phone // 在进入 postProcessBeforeInitialization 之前还是 Phone 类型
MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = phone class class com.sun.proxy.$Proxy18 // 在进入 postProcessAfterInitialization 之后变为了代理类型
MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = usbAspect class class com.heihu577.aop.UsbAspect
MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = usbAspect class class com.heihu577.aop.UsbAspect
MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = userDAO class class com.heihu577.component.UserDAO
MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = userDAO class class com.heihu577.component.UserDAO
MyBeanPostProcessor postProcessBeforeInitialization 被调用, beanName = userService class class com.heihu577.component.UserService
MyBeanPostProcessor postProcessAfterInitialization 被调用, beanName = userService class class com.heihu577.component.UserService
start Before...
Phone Start...
start After Returning...
start After...
我们可以看到, 被切面的类, 在postProcessBeforeInitialization之前
是类本身的类型, 而在postProcessAfterInitialization
后则变为了代理类型, 下面我们可以揭开它们的面纱.
AOP 的底层是基于 BeanPostProcessor 机制的. 在 Bean 创建好后, 根据是否需要 AOP 处理, 决定返回代理对象, 还是原生 Bean 在返回代理对象时, 就可以根据要代理的类和方法返回
最终目标: 不使用 Spring 框架, 模拟 Spring 底层实现, 也能完成相同的功能.
Spring 整体架构分析
classpath 路径
在执行java.exe
时, 使用-classpath
参数可以进行指明classLoader
的路径, 例如:所以我们在使用
classloader
获取类资源时, 可以找到target
目录下的资源, 下面我们创建一个IDEA
子模块, 来对我们当前的项目进行并行.
然后根据我们之前所编写的项目, 进行分解. https://zhuanlan.zhihu.com/p/682619049
编写基础环境
阶段1: 实现扫描包
准备com.heihu577.annotations
包, 并准备如下注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value() default "";
}
以及:
@Target(ElementType.TYPE) // 该注解可以修饰类中的哪部分类型
@Retention(RetentionPolicy.RUNTIME) // 指定 ComponentScan 注解保留范围, 当前运行时生效
public @interface ComponentScan {
String value() default ""; // 可以传入 value 属性, 默认值为空字符串
}
准备完毕注解后, 我们创建com.heihu577.components
包, 并且准备一些测试Bean:
@Component
public class MonsterDAO {
}
@Component(value = "monsterService") // 将 MonsterService 注入到容器中
public class MonsterService {
}
public class NoBean {
}
并且在com.heihu577.ioc
准备如下核心逻辑代码:
public class HeihuSpringApplicationContext {
private Class<?> springConfig;
public HeihuSpringApplicationContext(Class<?> springConfig) {
this.springConfig = springConfig;
initApplicationContext();
}
public void initApplicationContext() {
ComponentScan annotation = springConfig.getAnnotation(ComponentScan.class); // 得到该注解
String componentScanValue = annotation.value(); // 得到注解中的配置值
String classPath = componentScanValue.replace('.', '/'); // 得到真正的路径信息
System.out.println("要扫描的包: " + classPath);
URL classPathResource = springConfig.getClassLoader().getResource(classPath); // 得到 URL, 路径存在不返回 null
if (classPathResource != null) {
File file = new File(classPathResource.getPath()); // 得到 file 资源
if (file.isDirectory()) {
File[] files = file.listFiles(); // 得到所有编译好的文件信息
for (File f : files) { // 扫描 .class 的文件信息
String fStr = f.toString(); // 字符串截取判断
if (fStr.substring(fStr.indexOf(".")).equals(".class")) { // 判断最后是否是 .class 结尾
String className = f.getName().replace(".class", "");
String fullCLassPath = (classPath + "/" + className).replace("/", "."); // 再转换回来, 得到包名+类名
try {
Class<?> clazz = Class.forName(fullCLassPath);
if (clazz.isAnnotationPresent(Component.class)) {
System.out.println("是 Bean: " + clazz);
} else {
System.out.println("不是 Bean: " + clazz);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
}
} else {
System.out.println("扫描包出现了问题");
}
}
}
以及我们所定义ComponentScan
注解:
@ComponentScan("com.heihu577.components")
public class HeihuSpringConfig {
}
运行结果:
public class T {
public static void main(String[] args) {
HeihuSpringApplicationContext heihuSpringApplicationContext = new HeihuSpringApplicationContext(HeihuSpringConfig.class);
/*
要扫描的包: com/heihu577/components
是 Bean: class com.heihu577.components.MonsterDAO
是 Bean: class com.heihu577.components.MonsterService
不是 Bean: class com.heihu577.components.SB
*/
}
}
阶段2: 将 bean 信息封装到容器
当我们getBean
时, 我们需要考虑两点.
-
getBean 时, 发现是单例 (singleton)的, 那么从另一个集合(单例集合)中直接获得到 Object -
getBean 时, 发现是多例 (prototype)的, 那么现创建一个 Object
那么, 既然考虑到判断单例多例, 在原生Spring中, 可以定义@Scope
, 所以在这里我们自己的框架中, 定义一个@Scope
注解即可.
@Target(ElementType.TYPE) // 该注解可以修饰类中的哪部分类型
@Retention(RetentionPolicy.RUNTIME) // 指定 ComponentScan 注解保留范围, 当前运行时生效
public @interface Scope {
String value() default "singleton"; // 默认单例
}
随后我们定义BeanDefinition
, 用来保存每个类是单例|多例
的.
package com.heihu577.ioc;
public class BeanDefinition {
private String scope; // 当前 bean 作用范围
private Class<?> clazz; // 当前 bean 的 Class 对象
public BeanDefinition(String scope, Class<?> clazz) {
this.scope = scope;
this.clazz = clazz;
}
public BeanDefinition() {
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public Class<?> getClazz() {
return clazz;
}
public void setClazz(Class<?> clazz) {
this.clazz = clazz;
}
@Override
public String toString() {
return "BeanDefinition{" +
"scope='" + scope + ''' +
", clazz=" + clazz +
'}';
}
}
随后我们将其定义为成员属性, 放到HeihuSpringApplicationContext
中, 并定义成为一个Map
集合, 例如:
private Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(); // 用于保存类原型
随后我们需要定义一个单例池, 用于保存所有单例.
private Map<String, Object> singletonObject = new ConcurrentHashMap<>(); // 用于保存单例对象
随后initApplicationContext
方法的定义如下:
public void initApplicationContext() {
ComponentScan annotation = springConfig.getAnnotation(ComponentScan.class); // 得到该注解
String componentScanValue = annotation.value(); // 得到注解中的配置值
String classPath = componentScanValue.replace('.', '/'); // 得到真正的路径信息
System.out.println("要扫描的包: " + classPath);
URL classPathResource = springConfig.getClassLoader().getResource(classPath); // 得到 URL, 路径存在不返回 null
if (classPathResource != null) {
File file = new File(classPathResource.getPath()); // 得到 file 资源
if (file.isDirectory()) {
File[] files = file.listFiles(); // 得到所有编译好的文件信息
for (File f : files) { // 扫描 .class 的文件信息
String fStr = f.toString(); // 字符串截取判断
if (fStr.substring(fStr.indexOf(".")).equals(".class")) { // 判断最后是否是 .class 结尾
String className = f.getName().replace(".class", "");
String fullCLassPath = (classPath + "/" + className).replace("/", "."); // 再转换回来, 得到包名+类名
try {
Class<?> clazz = Class.forName(fullCLassPath);
if (clazz.isAnnotationPresent(Component.class)) {
String beanName = clazz.getDeclaredAnnotation(Component.class).value();
if (beanName.equals("")) { // 如果是没配置的情况, 那么定义为首字母小写
beanName = className.substring(0, 1).toLowerCase() + className.substring(1);
}
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setClazz(clazz); // 保存当前 class 类型
if (clazz.isAnnotationPresent(Scope.class)) {
// 前面有 Scope 注解定义
String scopeValue = clazz.getAnnotation(Scope.class).value();
beanDefinition.setScope(scopeValue);
} else {
beanDefinition.setScope("singleton"); // 默认 singleton
}
beanDefinitionMap.put(beanName, beanDefinition); // 保存容器
System.out.println("是 Bean: " + clazz);
} else {
System.out.println("不是 Bean: " + clazz);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
}
} else {
System.out.println("扫描包出现了问题");
}
}
我们重点关注对BeanDefinition
集合类的一个操作. 最终效果如下:
阶段3: 初始化单例池
那么我们创建一个基于BeanDefinition
返回对象的方法.
private Object createBean(BeanDefinition beanDefinition) {
Class<?> clazz = beanDefinition.getClazz();
try {
Object o = clazz.getDeclaredConstructor().newInstance();
return o;
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
随后我们在initApplicationContext
方法之后, 进行遍历BeanDefinition
, 判断Scope
是单例还是多例, 如果是单例, 那么放入到我们的单例池.
public HeihuSpringApplicationContext(Class<?> springConfig) {
this.springConfig = springConfig;
initApplicationContext(); // 扫描包的代码
judgeBeanDefinition(); // 单例池初始化代码
}
public void judgeBeanDefinition() {
Set<Map.Entry<String, BeanDefinition>> entries = beanDefinitionMap.entrySet();
// 得到所有 entry
for (Map.Entry<String, BeanDefinition> entry : entries) { // 遍历 entry 信息
BeanDefinition beanDefinition = entry.getValue(); // 得到 beanDefinition 类
String scope = beanDefinition.getScope(); // 得到单例还是多例
if ("singleton".equalsIgnoreCase(scope)) { // 如果是单例
singletonObject.put(entry.getKey(), createBean(beanDefinition)); // 放入到单例容器中
}
}
}
下面我们就可以根据beanDefinitionMap, singletonObject
来定义一套自己的getBean
的逻辑.
-
如果该 Bean 不存在 (不在 beanDefinitionMap 中), 那么直接抛出异常. -
如果该 Bean 存在, 那么得到 BeanDefinition
, 进行判断Scope
类型. -
如果类型是单例, 那么从单例池中寻找 -
如果类型是多例, 直接调用 createBean
方法, 返回一个对象.
具体实现方法如下:
public Object getBean(String name) {
if (beanDefinitionMap.containsKey(name)) { // 如果 bean 存在
BeanDefinition beanDefinition = beanDefinitionMap.get(name); // 得到该 bean 的 class
String scope = beanDefinition.getScope(); // 拿到 单例|多例
if ("singleton".equalsIgnoreCase(scope)) { // 如果是 单例模式
return singletonObject.get(name); // 直接从单例容器中取
} else {
return createBean(beanDefinition); // 多例模式, 直接调用 createBean 进行创建
}
} else {
throw new NullPointerException("该 bean 不存在!");
}
}
运行结果:
public class T {
public static void main(String[] args) {
HeihuSpringApplicationContext heihuSpringApplicationContext = new HeihuSpringApplicationContext(HeihuSpringConfig.class);
// monsterDAO 是单例, monsterService 是多例
Object bean1 = heihuSpringApplicationContext.getBean("monsterDAO");
Object bean2 = heihuSpringApplicationContext.getBean("monsterDAO");
System.out.println(bean1); // com.heihu577.components.MonsterDAO@5e481248
System.out.println(bean2); // com.heihu577.components.MonsterDAO@5e481248
Object bean3 = heihuSpringApplicationContext.getBean("monsterService");
Object bean4 = heihuSpringApplicationContext.getBean("monsterService");
System.out.println(bean3); // com.heihu577.components.MonsterService@66d3c617
System.out.println(bean4); // com.heihu577.components.MonsterService@63947c6b
Object bean = heihuSpringApplicationContext.getBean("ABC");
/*
Exception in thread "main" java.lang.NullPointerException: 该 bean 不存在!
at com.heihu577.ioc.HeihuSpringApplicationContext.getBean(HeihuSpringApplicationContext.java:110)
at T.main(T.java:17)
*/
}
}
实现依赖注入
在前面的createBean
方法中, 我们已经得到了bean的class, 所以在这里我们依赖注入的位置放在createBean
中更加合适, 例如:
private Object createBean(BeanDefinition beanDefinition) {
Class<?> clazz = beanDefinition.getClazz();
try {
Object o = clazz.getDeclaredConstructor().newInstance();
// ... 依赖注入核心代码
return o;
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
创建一个@Autowired
注解, 如下:
package com.heihu577.annotations;
import java.lang.annotation.*;
@Target(ElementType.FIELD) // 作用于成员属性上
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
boolean required() default true;
}
创建后, 我们在MonsterDAO
中增加一个方法, 用于验证我们增加注解后是否成功, 例如:
@Component
public class MonsterDAO {
public void sayHello() {
System.out.println("MonsterDAO Hello World");
}
}
并且, 创建MonsterService
与MonsterDAO
之间的关系:
@Scope("prototype")
@Component(value = "monsterService") // 将 MonsterService 注入到容器中
public class MonsterService {
@Autowired
private MonsterDAO monsterDAO;
public void sayHello() {
monsterDAO.sayHello();
}
}
在这里直接调用即可, 下面我们准备核心代码:
private Object createBean(BeanDefinition beanDefinition) {
Class<?> clazz = beanDefinition.getClazz();
try {
Object o = clazz.getDeclaredConstructor().newInstance();
Field[] declaredFields = clazz.getDeclaredFields();
for (Field declaredField : declaredFields) {
if (declaredField.isAnnotationPresent(Autowired.class)) { // 如果该字段被 Autowired 所修饰
String fieldName = declaredField.getName(); // 得到字段名称
Object bean = getBean(fieldName); // 通过 getBean 得到该对象 (根据字段名称查找)
declaredField.setAccessible(true);
declaredField.set(o, bean); // 将该字段的值设置为该 bean 对象
}
}
return o;
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
public Object getBean(String name) {
if (beanDefinitionMap.containsKey(name)) {
BeanDefinition beanDefinition = beanDefinitionMap.get(name);
String scope = beanDefinition.getScope();
if ("singleton".equalsIgnoreCase(scope)) {
return singletonObject.get(name);
} else {
return createBean(beanDefinition);
}
} else {
throw new NullPointerException("该 bean 不存在!");
}
}
最终测试结果:
public class T {
public static void main(String[] args) {
HeihuSpringApplicationContext heihuSpringApplicationContext = new HeihuSpringApplicationContext(HeihuSpringConfig.class);
MonsterService monsterService = (MonsterService) heihuSpringApplicationContext.getBean("monsterService");
monsterService.sayHello();
/*
要扫描的包: com/heihu577/components
是 Bean: class com.heihu577.components.MonsterDAO
是 Bean: class com.heihu577.components.MonsterService
不是 Bean: class com.heihu577.components.NoBean
MonsterDAO Hello World
*/
}
}
实现后置处理器功能
实现初始化方法
定义如下接口:
package com.heihu577.processor;
public interface InitializingBean {
// 根据原生 spring, 定义了该接口, 该接口有一个方法
void afterPropertiesSet() throws Exception; // 在 Bean 的 setter 方法后执行
}
并且让Bean
进行实现:
@Component
public class MonsterDAO implements InitializingBean {
public void sayHello() {
System.out.println("MonsterDAO Hello World");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("afterPropertiesSet MonsterDAO Hi");
}
}
在接口编程中, 我们可以根据该类是否实现了某个接口, 来进行判断是否要执行某个业务逻辑. 我们在HeihuSpringApplicationContext
类中的createBean
方法中, 我们可以这样定义:
private Object createBean(BeanDefinition beanDefinition) {
Class<?> clazz = beanDefinition.getClazz();
try {
Object o = clazz.getDeclaredConstructor().newInstance();
Field[] declaredFields = clazz.getDeclaredFields();
for (Field declaredField : declaredFields) {
if (declaredField.isAnnotationPresent(Autowired.class)) { // 如果该字段被 Autowired 所修饰
String fieldName = declaredField.getName(); // 得到字段名称
Object bean = getBean(fieldName); // 通过 getBean 得到该对象 (根据字段名称查找)
declaredField.setAccessible(true);
declaredField.set(o, bean); // 将该字段的值设置为该 bean 对象
}
}
if (o instanceof InitializingBean) {
// 如果实现 InitializingBean 接口, 那么就直接调用 afterPropertiesSet
((InitializingBean) o).afterPropertiesSet();
}
return o;
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
最终测试结果:
public class T2 {
public static void main(String[] args) {
HeihuSpringApplicationContext heihuSpringApplicationContext = new HeihuSpringApplicationContext(HeihuSpringConfig.class);
MonsterDAO monsterDAO = (MonsterDAO) heihuSpringApplicationContext.getBean("monsterDAO");
monsterDAO.sayHello();
/*
要扫描的包: com/heihu577/components
是 Bean: class com.heihu577.components.MonsterDAO
是 Bean: class com.heihu577.components.MonsterService
不是 Bean: class com.heihu577.components.NoBean
afterPropertiesSet MonsterDAO Hi
MonsterDAO Hello World
*/
}
}
实现 BeanPostProcessor 功能
我们可以根据原生Spring
中, 来定义一个BeanPostProcessor
功能.
@Component
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
}
随后我们可以定义一个容器, 用于实现它.
@Component
public class HeihuBeanPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String beanName) {
System.out.println("HeihuBeanPostProcessor postProcessBeforeInitialization... " + beanName);
return bean;
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("HeihuBeanPostProcessor postProcessAfterInitialization... " + beanName);
return bean;
}
}
因为可能会有多个BeanPostProcessor
对象, 所以我们在我们的HeihuSpringApplicationContext
中, 可以定义一个ArrayList
, 并且类型是BeanPOstProcessor
池.
private List<BeanPostProcessor> beanPostProcessorList = new ArrayList<>();
并且我们在扫描包的时候增加如下代码:
String className = f.getName().replace(".class", "");
String fullCLassPath = (classPath + "/" + className).replace("/", "."); // 再转换回来, 得到包名+类名
try {
Class<?> clazz = Class.forName(fullCLassPath);
if (clazz.isAnnotationPresent(Component.class)) {
if (BeanPostProcessor.class.isAssignableFrom(clazz)) {
// 这个类实现了 BeanPostProcessor 接口
BeanPostProcessor beanPostProcessor = (BeanPostProcessor) clazz.getDeclaredConstructor().newInstance();
beanPostProcessorList.add(beanPostProcessor); // 加入到我们的 beanPostProcessorList 容器中
continue; // 直接进入下一跳. 单独放到ArrayList中即可
}
当然了, 我们的执行前后也是在初始化方法
前后进行执行, 我们可以在createBean
方法中的((InitializingBean) o).afterPropertiesSet()
前后加入如下代码:
for (BeanPostProcessor beanPostProcessor : beanPostProcessorList) {
o = beanPostProcessor.postProcessBeforeInitialization(o, "..."); // 前置
}
if (o instanceof InitializingBean) {
((InitializingBean) o).afterPropertiesSet(); // 初始化
}
for (BeanPostProcessor beanPostProcessor : beanPostProcessorList) {
o = beanPostProcessor.postProcessAfterInitialization(o, "..."); // 后置
}
return o;
运行结果:
public class T2 {
public static void main(String[] args) {
HeihuSpringApplicationContext heihuSpringApplicationContext = new HeihuSpringApplicationContext(HeihuSpringConfig.class);
MonsterDAO monsterDAO = (MonsterDAO) heihuSpringApplicationContext.getBean("monsterDAO");
monsterDAO.sayHello();
/*
HeihuBeanPostProcessor postProcessBeforeInitialization... com.heihu577.components.MonsterDAO@66d3c617
afterPropertiesSet MonsterDAO Hi
HeihuBeanPostProcessor postProcessAfterInitialization... com.heihu577.components.MonsterDAO@66d3c617
MonsterDAO Hello World
*/
}
}
根据 BeanDefinition 得到 beanName
当然了, 我们之前所定义的public Object postProcessBeforeInitialization(Object bean, String beanName)
中, beanName 我们不知道传什么, 在这里我们只需要提供一个根据BeanDefinition
得到beanName
的一个功能即可:
public String getKeyBybeanDefinitionMap(BeanDefinition beanDefinition) {
Set<Map.Entry<String, BeanDefinition>> entries = beanDefinitionMap.entrySet();
for (Map.Entry<String, BeanDefinition> entry : entries) {
BeanDefinition dstBeanDefinition = entry.getValue();
if (dstBeanDefinition == beanDefinition) { // 比较是否是同一对象
return entry.getKey();
}
}
return null;
}
那么我们BeanPostProcessor
调用的思路则是如下:
String beanName = getKeyBybeanDefinitionMap(beanDefinition);
for (BeanPostProcessor beanPostProcessor : beanPostProcessorList) {
o = beanPostProcessor.postProcessBeforeInitialization(o, beanName);
}
if (o instanceof InitializingBean) {
// 如果实现 InitializingBean 接口, 那么就直接调用 afterPropertiesSet
((InitializingBean) o).afterPropertiesSet();
}
for (BeanPostProcessor beanPostProcessor : beanPostProcessorList) {
o = beanPostProcessor.postProcessAfterInitialization(o, beanName);
}
当然了, 如果自定义的beanPostProcessor
返回NULL
, 会直接影响到o
变量, 此时再对o
进行操作会爆出空指针异常, 在这里我们可以使用如下代码逻辑:
for (BeanPostProcessor beanPostProcessor : beanPostProcessorList) {
Object o1 = beanPostProcessor.postProcessBeforeInitialization(o, beanName);
if (o1 == null) {
continue;
} else {
o = o1;
}
}
if (o instanceof InitializingBean) {
// 如果实现 InitializingBean 接口, 那么就直接调用 afterPropertiesSet
((InitializingBean) o).afterPropertiesSet();
}
for (BeanPostProcessor beanPostProcessor : beanPostProcessorList) {
Object o1 = beanPostProcessor.postProcessAfterInitialization(o, beanName);
if (o1 == null) {
continue;
} else {
o = o1;
}
}
return o;
这样的逻辑处理后就不会爆出异常.
实现 AOP 功能
在Spring框架中, 被@Aspect
注解所修饰的类, 在getBean
时, 返回的是$Proxy
代理类, 所以在这里我们需要使用动态代理技术. 在Spring框架中, BeanPostProcessor
可以对方法进行切面, 所以在这里我们需要使用BeanPostProcessor
技术.
下面我们准备一个IUsb
接口, 以及实现它的方法:
public interface IUsb {
public void start();
public void pringMsg(String msg);
public String getName();
}
并且定义一个实现它的类:
@Component
public class MyUsb implements IUsb{
@Override
public void start() {
System.out.println("MyUsb start...");
}
@Override
public void pringMsg(String msg) {
System.out.println(msg);
}
@Override
public String getName() {
return "手机";
}
}
当然了, 我们也需要制作一个切面类, 因为现在是设计初期, 我们先设计一个死的, 后面再设计成活的. 先死后活.
public class UsbAspect {
public static void beforeTest() { // 前置通知
System.out.println("UsbAspect before...");
}
public static void afterTest() { // 后置通知
System.out.println("UsbAspect after...");
}
}
那么接下来, 我们对我们之前的BeanPostProcessor
进行动手.
@Component
public class HeihuBeanPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// if (bean instanceof MonsterService) {
// System.out.println("这是 Monster Service");
// }
System.out.println("HeihuBeanPostProcessor postProcessBeforeInitialization... " + bean + " - " + beanName);
return bean;
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("HeihuBeanPostProcessor postProcessAfterInitialization... " + bean + " - " + beanName);
// 先死后活
if ("myUsb".equals(beanName)) { // 假设被切面的对象是 myUsb
Object proxyInstance = Proxy.newProxyInstance(HeihuBeanPostProcessor.class.getClassLoader(),
bean.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
if ("start".equals(method.getName())) { // 假设要切面 start 方法
UsbAspect.beforeTest();
method.invoke(bean, args);
UsbAspect.afterTest();
} else {
result = method.invoke(bean, args);
}
return result;
}
});
return proxyInstance;
}
return bean;
}
}
注意我们这里主要是针对于postProcessAfterInitialization
方法进行操作. 最终运行结果:
public class T2 {
public static void main(String[] args) {
HeihuSpringApplicationContext heihuSpringApplicationContext = new HeihuSpringApplicationContext(HeihuSpringConfig.class);
IUsb myUsb = (IUsb) heihuSpringApplicationContext.getBean("myUsb");
myUsb.start();
/*
UsbAspect before...
MyUsb start...
UsbAspect after...
*/
}
}
到现在已经有了一个基本的实现, 就没必要继续往下深究了, 理解到AOP的机制即可.
JDBCTemplate
在这里我们的手撕Spring
已经完毕了, 接下来还是Spring
中的内容.
在之前, 我们使用的是JDBCByDruid
类, 是我们自己开发的, 而Spring
提供了一个JDBCTemplate
, 也是用于操作数据库的. 准备如下pom.xml:
<dependencies>
<!-- c3p0 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<!-- MySQL Connector/J -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<!-- Spring Core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.8</version>
</dependency>
<!-- Spring Expression Language (SpEL) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.8</version>
</dependency>
<!-- Spring JDBC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.8</version>
</dependency>
<!-- Spring ORM -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.8</version>
</dependency>
<!-- Spring Transaction Management -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.8</version>
</dependency>
<dependency> <!-- 加入 Spring 开发的基本包 -->
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.8</version>
</dependency>
<dependency> <!-- 加入 Spring 开发切面编程需要的包 -->
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.8</version>
</dependency>
<dependency> <!-- 引入 Junit 单元测试 -->
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>compile</scope>
</dependency>
</dependencies>
那么我们准备如下表:
CREATE DATABASE `spring`;
use `spring`;
CREATE TABLE `monster`(
id int primary key,
`name` varchar(64) not null default '',
skill varchar(64) not null default ''
)charset=utf8;
INSERT INTO `monster` VALUES(100, '黄牛怪', '吐火');
INSERT INTO `monster` VALUES(200, '黄袍怪', '吐烟');
INSERT INTO `monster` VALUES(300, '蜘蛛怪', '吐丝');
随后我们配置/jdbc.properties
文件:
jdbc.userName=root
jdbc.password=root
jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring
随后准备bean.xml
, 内容如下:
<context:property-placeholder location="classpath:jdbc.properties"/> <!-- 引入 JDBC 配置文件 -->
<bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
<!-- 配置 c3p0 的数据源, 是属于 c3p0 开发的配置 -->
<property name="user" value="${jdbc.userName}"/>
<property name="password" value="${jdbc.password}"/>
<property name="driverClass" value="${jdbc.driverClass}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<!-- 配置 jdbcTemplate, 是属于 spring 开发的配置对象, 给 jdbcTemplate 配置数据池 -->
<property name="dataSource" ref="dataSource"/>
</bean>
运行结果:
public class T {
private ApplicationContext ioc2 = new ClassPathXmlApplicationContext("jdbcTemplateIoc.xml");
@Test
public void t2() throws SQLException {
DataSource dataSource = (DataSource) ioc2.getBean("dataSource");
System.out.println(dataSource); // 链接成功信息
Connection connection = dataSource.getConnection();
System.out.println("连接信息: " + connection); // com.mchange.v2.c3p0.impl.NewProxyConnection@3b2cf7ab
}
}
添加数据 && 修改数据
public void t3() throws SQLException {
JdbcTemplate jdbcTemplate = (JdbcTemplate) ioc2.getBean("jdbcTemplate");
// System.out.println(jdbcTemplate); // org.springframework.jdbc.core.JdbcTemplate@143640d5
// 添加方式1: 无预处理
jdbcTemplate.execute("INSERT INTO `spring`.`monster` VALUES(400, '张三', '吃饭')"); // 无返回值
// 添加方式2: 预处理
int affected = jdbcTemplate.update("INSERT INTO `spring`.`monster` VALUES(?,?,?)", 500, "李四", "吃饭"); // 返回受影响的行数
System.out.println(affected); // 1
}
批量处理
@Test
public void t4() {
JdbcTemplate jdbcTemplate = ioc2.getBean(JdbcTemplate.class);
String sql = "INSERT INTO `monster` VALUES(?,?,?)"; // 准备 SQL 语句
List<Object[]> param_list = new ArrayList<>(); // 准备好预处理数组
param_list.add(new Object[]{600, "王五", "打架"}); // 第一组数据
param_list.add(new Object[]{700, "赵六", "挨打"}); // 第二组数据
int[] result = jdbcTemplate.batchUpdate(sql, param_list); // 批量执行
System.out.println(Arrays.toString(result)); // [1, 1] 执行成功
}
数据查询
@Test
public void t5() {
JdbcTemplate bean = ioc2.getBean(JdbcTemplate.class);
String sql = "SELECT * FROM `monster` WHERE id > ?";
BeanPropertyRowMapper<Monster> monsterBeanPropertyRowMapper = new BeanPropertyRowMapper<>(Monster.class);
// 准备好 Mapper 才可以进行绑定数据到 Bean 上
List<Monster> result = bean.query(sql, monsterBeanPropertyRowMapper, 200);
// SQL语句, Mapper, 绑定的参数
System.out.println(result);
/*
[Monster{id=300, name='蜘蛛怪', skill='吐丝'}, Monster{id=400, name='张三', skill='吃饭'}, Monster{id=500, name='李四', skill='吃饭'}, Monster{id=600, name='王五', skill='打架'}, Monster{id=700, name='赵六', skill='挨打'}]
*/
}
查询单行单列值
@Test
public void t6() {
JdbcTemplate bean = ioc2.getBean(JdbcTemplate.class);
String sql = "SELECT name FROM `monster` WHERE id = ?";
String name = bean.queryForObject(sql, String.class, 100);
System.out.println(name); // 黄牛怪
}
使用 MAP 传递具体参数完成操作
在这里我们需要在beans.xml
文件中配置namedParameterJdbcTemplate
, 例如:
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<!-- 配置 namedParameterJdbcTemplate, 设置为 c3p0 的连接池 -->
<constructor-arg name="dataSource" ref="dataSource"/>
</bean>
配置好之后, 我们可以使用该JdbcTemplate
进行插入数据.
@Test
public void t7() {
NamedParameterJdbcTemplate namedParameterJdbcTemplate = ioc2.getBean(NamedParameterJdbcTemplate.class);
// 这次不用 JdbcTemplate, 而是 NamedParameterJdbcTemplate 需要注意
String sql = "INSERT INTO `monster` VALUES(:my_id, :name, :skill)";
HashMap<String, Object> map_parameter = new HashMap<>();
map_parameter.put("my_id", 1000); // 放置的KEY与占位符名称相同
map_parameter.put("name", "黑客"); // 放置的KEY与占位符名称相同
map_parameter.put("skill", "进局子"); // 放置的KEY与占位符名称相同
int result = namedParameterJdbcTemplate.update(sql, map_parameter);
System.out.println(result); // 1
}
通过已有对象来操作数据库
@Test
public void t8() {
NamedParameterJdbcTemplate namedParameterJdbcTemplate = ioc2.getBean(NamedParameterJdbcTemplate.class);
Monster monster = new Monster(900, "牛逼怪物", "大相机");
int update = namedParameterJdbcTemplate.update("INSERT INTO `monster` VALUES(:id,:name,:skill)",
// 这里要与monster类中的字段对应
new BeanPropertySqlParameterSource(monster)); // 传入我们准备的对象
System.out.println(update);
}
DAO 中使用 JdbcTemplate
准备如下DAO:
@Repository // DAO 使用 Repository
public class MonsterDAO {
@Autowired // JdbcTemplate 使用 Autowired 自动装配
private JdbcTemplate jdbcTemplate;
public boolean save(Monster monster) {
String sql = "INSERT INTO `monster` VALUES(?,?,?)";
return jdbcTemplate.update(sql, monster.getId(), monster.getName(), monster.getSkill()) > 0;
}
public List<Monster> showMonsters() {
String sql = "SELECT * FROM `monster`";
List<Monster> monsters = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Monster.class));
return monsters;
}
}
准备如下xml文件:
<context:component-scan base-package="com.heihu577.JdbcTemplate"/> <!-- 扫描到我们的 monsterDAO -->
<context:property-placeholder location="classpath:jdbc.properties"/> <!-- 引入 JDBC 配置文件 -->
<bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
<!-- 配置 c3p0 的数据源, 是属于 c3p0 开发的配置 -->
<property name="user" value="${jdbc.userName}"/>
<property name="password" value="${jdbc.password}"/>
<property name="driverClass" value="${jdbc.driverClass}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<!-- 配置 jdbcTemplate, 是属于 spring 开发的配置对象, 给 jdbcTemplate 配置数据池 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<!-- 配置 namedParameterJdbcTemplate, 设置为 c3p0 的连接池 -->
<constructor-arg name="dataSource" ref="dataSource"/>
</bean>
测试结果:
@Test
public void t9() {
MonsterDAO monsterDAO = ioc2.getBean(MonsterDAO.class);
List<Monster> monsters = monsterDAO.showMonsters();
System.out.println(monsters);
/*
[Monster{id=100, name='黄牛怪', skill='吐火'}, Monster{id=200, name='黄袍怪', skill='吐烟'}, Monster{id=300, name='蜘蛛怪', skill='吐丝'}, Monster{id=400, name='张三', skill='吃饭'}, Monster{id=500, name='李四', skill='吃饭'}, Monster{id=700, name='赵六', skill='挨打'}, Monster{id=600, name='王五', skill='打架'}, Monster{id=1000, name='黑客', skill='进局子'}, Monster{id=900, name='牛逼怪物', skill='大相机'}]
*/
}
Spring 声明式事务处理
基本使用
创建如下表:
CREATE TABLE `user_account`(
user_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(32) NOT NULL DEFAULT '',
money DOUBLE NOT NULL DEFAULT 0.0
)CHARSET=utf8;
INSERT INTO `user_account` VALUES(NULL,'张三', 1000);
INSERT INTO `user_account` VALUES(NULL,'李四', 2000);
CREATE TABLE `goods`(
goods_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
goods_name VARCHAR(32) NOT NULL DEFAULT '',
price DOUBLE NOT NULL DEFAULT 0.0
)CHARSET=utf8 ;
INSERT INTO `goods` VALUES(NULL,'小风扇', 10.00);
INSERT INTO `goods` VALUES(NULL,'小台灯', 12.00);
INSERT INTO `goods` VALUES(NULL,'可口可乐', 3.00);
CREATE TABLE `goods_amount`(
goods_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
goods_num INT UNSIGNED DEFAULT 0
)CHARSET=utf8 ;
INSERT INTO `goods_amount` VALUES(1,200);
INSERT INTO `goods_amount` VALUES(2,20);
INSERT INTO `goods_amount` VALUES(3,15);
并且准备如下DAO, 专门针对于Goods
表进行处理:
@Repository
public class GoodsDAO {
@Autowired
private JdbcTemplate jdbcTemplate;
public Float queryPriceById(Integer id) { // 根据ID查询出商品价格
String sql = "SELECT price From goods Where goods_id=?";
Float price = jdbcTemplate.queryForObject(sql, Float.class, id);
return price;
}
public void updateBalance(Integer user_id, Float money) { // 减少用户余额
String sql = "UPDATE user_account SET money=money-? Where user_id=?";
jdbcTemplate.update(sql, money, user_id);
}
public void updateAmount(Integer goods_id, int amount) { // 修改商品库存
String sql = "UPDATE goods_amount SET goods_num=goods_num-? Where goods_id=?";
jdbcTemplate.update(sql, amount, goods_id);
}
}
并且准备如下配置:
<context:property-placeholder location="classpath:jdbc.properties"/> <!-- 加载配置文件, 得用户名, 密码等信息 -->
<context:component-scan base-package="com.heihu577.JdbcTemplate02"/> <!-- 将 GoodsDAO 等 Spring Bean 扫描到 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!-- 配置数据库连接池 -->
<property name="user" value="${jdbc.userName}"/>
<property name="password" value="${jdbc.password}"/>
<property name="driverClass" value="${jdbc.driverClass}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<!-- 配置 jdbcTemplate, 是属于 spring 开发的配置对象, 给 jdbcTemplate 配置数据池 -->
<property name="dataSource" ref="dataSource"/>
</bean>
测试数据:
public class TestGoodsDAO {
private ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("JdbcTemplateIoc02.xml");
private GoodsDAO goodsDAO = ioc.getBean(GoodsDAO.class);
@Test
public void testQueryPriceById() { // 测试查询根据商品ID得到价格信息
/*
* +----------+--------------+-------+
| goods_id | goods_name | price |
+----------+--------------+-------+
| 1 | 小风扇 | 10 |
| 2 | 小台灯 | 12 |
| 3 | 可口可乐 | 3 |
+----------+--------------+-------+
* */
Float price = goodsDAO.queryPriceById(1);
System.out.println("ID 为 1 的商品价格: " + price); // ID 为 1 的商品价格: 10.0
}
@Test
public void testUpdateAmount() { // 测试减少商品数量
/*
* 原本:
* +----------+-----------+
| goods_id | goods_num |
+----------+-----------+
| 1 | 200 |
| 2 | 20 |
| 3 | 15 |
+----------+-----------+
* */
goodsDAO.updateAmount(1, 10); // goods_id 为 1 的商品数量减少10个
/*
* 执行结果后
* +----------+-----------+
| goods_id | goods_num |
+----------+-----------+
| 1 | 190 |
| 2 | 20 |
| 3 | 15 |
+----------+-----------+
* */
}
@Test
public void testUpdateBalance() {
/* 原本:
* +---------+-----------+-------+
| user_id | user_name | money |
+---------+-----------+-------+
| 1 | 张三 | 1000 |
| 2 | 李四 | 2000 |
+---------+-----------+-------+
* */
goodsDAO.updateBalance(1, 10.0f); // user_id 为 1 的 money 字段减少 10
/* 现在:
* +---------+-----------+-------+
| user_id | user_name | money |
+---------+-----------+-------+
| 1 | 张三 | 990 |
| 2 | 李四 | 2000 |
+---------+-----------+-------+
* */
}
}
创建Service:
@Service
public class GoodsService {
@Resource
private GoodsDAO goodsDAO;
public void buyGood(int userId, int goods_id, int amount) { // 用户ID 商品ID 购买数量
System.out.println("用户ID号为 " + userId + " 购买了商品ID为 " + goods_id + " 数量为 " + amount);
Float price = goodsDAO.queryPriceById(userId); // 得到当前用户余额
goodsDAO.updateBalance(userId, price * amount); // 用户的余额 - 商品单价 * 购买数量
goodsDAO.updateAmount(goods_id, amount); // 商品减少库存量
System.out.println("用户购买成功~");
}
}
测试结果:
public class TestGoodsDAO {
private ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("JdbcTemplateIoc02.xml");
private GoodsDAO goodsDAO = ioc.getBean(GoodsDAO.class);
private GoodsService goodsService = ioc.getBean(GoodsService.class);
@Test
public void testBuyGoods() {
/*
* mysql> SELECT * FROM goods;
+----------+--------------+-------+
| goods_id | goods_name | price |
+----------+--------------+-------+
| 1 | 小风扇 | 10 |
| 2 | 小台灯 | 12 |
| 3 | 可口可乐 | 3 |
+----------+--------------+-------+
3 rows in set (0.00 sec)
mysql> SELECT * FROM goods_amount;
+----------+-----------+
| goods_id | goods_num |
+----------+-----------+
| 1 | 190 |
| 2 | 20 |
| 3 | 15 |
+----------+-----------+
3 rows in set (0.00 sec)
mysql> SELECT * FROM user_account;
+---------+-----------+-------+
| user_id | user_name | money |
+---------+-----------+-------+
| 1 | 张三 | 990 |
| 2 | 李四 | 2000 |
+---------+-----------+-------+
2 rows in set (0.00 sec)
* */
goodsService.buyGood(2, 2, 2); // 用户ID号为 2 购买了商品ID为 2 数量为 2
/*
* mysql> SELECT * FROM goods;
+----------+--------------+-------+
| goods_id | goods_name | price |
+----------+--------------+-------+
| 1 | 小风扇 | 10 |
| 2 | 小台灯 | 12 |
| 3 | 可口可乐 | 3 |
+----------+--------------+-------+
3 rows in set (0.00 sec)
mysql> SELECT * FROM goods_amount;
+----------+-----------+
| goods_id | goods_num |
+----------+-----------+
| 1 | 190 |
| 2 | 18 |
| 3 | 15 |
+----------+-----------+
3 rows in set (0.00 sec)
mysql> SELECT * FROM user_account;
+---------+-----------+-------+
| user_id | user_name | money |
+---------+-----------+-------+
| 1 | 张三 | 990 |
| 2 | 李四 | 1976 |
+---------+-----------+-------+
2 rows in set (0.00 sec)
* */
}
}
但是当前的情况并没有增加事务控制, 也就是提交|回滚
, 当其中存在一条SQL语句出错时, 那么数据将错乱, 下面我们看一下Spring
声明式事务处理的应用.
声明式事务应用 [Mysql - Innodb]
定义Service
中操作DAO
的方法如下:
@Service
public class GoodsService {
@Resource
private GoodsDAO goodsDAO;
@Transactional // 加入想要一并执行的注解
public void buyGoodByTx(int userId, int goods_id, int amount) { // 用户ID 商品ID 购买数量
System.out.println("用户ID号为 " + userId + " 购买了商品ID为 " + goods_id + " 数量为 " + amount);
Float price = goodsDAO.queryPriceById(userId); // 得到当前用户余额
goodsDAO.updateBalance(userId, price * amount); // 用户的余额 - 商品单价 * 购买数量
goodsDAO.updateAmount(goods_id, amount); // 商品减少库存量
System.out.println("用户购买成功~");
}
}
但是此时@Transactional
注解还没有生效, 定义如下xml
文件, 让其生效:
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="dataSourceTransactionManager">
<!-- 必须配置该bean, 否则事务不生效 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/> <!-- 并且配置启用标签, 类似于AOP中的aop标签 -->
而DAO
中的方法如下:
@Repository
public class GoodsDAO {
@Autowired
private JdbcTemplate jdbcTemplate;
public Float queryPriceById(Integer id) { // 根据ID查询出商品价格
String sql = "SELECT price From goods Where goods_id=?";
Float price = jdbcTemplate.queryForObject(sql, Float.class, id);
return price;
}
public void updateBalance(Integer user_id, Float money) { // 减少用户余额
String sql = "UPDATE user_account SET money=money-? Where user_id=?";
jdbcTemplate.update(sql, money, user_id);
}
public void updateAmount(Integer goods_id, int amount) { // 修改商品库存
String sql = "UPDATEX goods_amount SET goods_num=goods_num-? Where goods_id=?"; // 这里故意搞错
jdbcTemplate.update(sql, amount, goods_id);
}
}
执行完毕后, 数据不会被影响.
@Transaction, 底层用的是AOP, 是在执行我们Service
中定义的方法前增加了前置通知, 其中调用了DataSourceTransactionManager
类中的doBegin方法
, 如图:当代码没出错, 那么我们调用
doCommit
方法, 如图:反之, 则调用
doRollback
方法:
事务传播机制
REQUIRED (默认)
当然, 我们也可以使用
@Transactional
注解, 显式声明事务:
@Transactional(propagation=Propagation.REQUIRED)
这也是Spring默认使用的传播机制, 我们可以创建如下测试环境: DAO重新复制一份方法, 如:
@Repository
public class GoodsDAO {
@Autowired
private JdbcTemplate jdbcTemplate;
public Float queryPriceById(Integer id) { // 根据ID查询出商品价格
String sql = "SELECT price From goods Where goods_id=?";
Float price = jdbcTemplate.queryForObject(sql, Float.class, id);
return price;
}
public void updateBalance(Integer user_id, Float money) { // 减少用户余额
String sql = "UPDATE user_account SET money=money-? Where user_id=?";
jdbcTemplate.update(sql, money, user_id);
}
public void updateAmount(Integer goods_id, int amount) { // 修改商品库存
String sql = "UPDATE goods_amount SET goods_num=goods_num-? Where goods_id=?";
jdbcTemplate.update(sql, amount, goods_id);
}
public Float queryPriceById2(Integer id) { // 根据ID查询出商品价格
String sql = "SELECT price From goods Where goods_id=?";
Float price = jdbcTemplate.queryForObject(sql, Float.class, id);
return price;
}
public void updateBalance2(Integer user_id, Float money) { // 减少用户余额
String sql = "UPDATE user_account SET money=money-? Where user_id=?";
jdbcTemplate.update(sql, money, user_id);
}
public void updateAmount2(Integer goods_id, int amount) { // 修改商品库存
String sql = "UPDATEX goods_amount SET goods_num=goods_num-? Where goods_id=?"; // 这里放置一个错误的SQL语句
jdbcTemplate.update(sql, amount, goods_id);
}
}
在原有updateBalance|updateBalance|queryPriceById
方法上, 复制粘贴出updateBalance2|updateBalance2|queryPriceById2
, 其中updateAmount2
方法中我们故意放置一个错误的SQL语句, 随后我们准备GoodsService
, 如下:
@Service
public class GoodsService {
@Resource
private GoodsDAO goodsDAO;
public void buyGood(int userId, int goods_id, int amount) { // 用户ID 商品ID 购买数量
System.out.println("用户ID号为 " + userId + " 购买了商品ID为 " + goods_id + " 数量为 " + amount);
Float price = goodsDAO.queryPriceById(userId); // 得到当前用户余额
goodsDAO.updateBalance(userId, price * amount); // 用户的余额 - 商品单价 * 购买数量
goodsDAO.updateAmount(goods_id, amount); // 商品减少库存量
System.out.println("用户购买成功~");
}
@Transactional
public void buyGoodByTx(int userId, int goods_id, int amount) { // 用户ID 商品ID 购买数量
System.out.println("用户ID号为 " + userId + " 购买了商品ID为 " + goods_id + " 数量为 " + amount);
Float price = goodsDAO.queryPriceById(userId); // 得到当前用户余额
goodsDAO.updateBalance(userId, price * amount); // 用户的余额 - 商品单价 * 购买数量
goodsDAO.updateAmount(goods_id, amount); // 商品减少库存量
System.out.println("用户购买成功~");
}
@Transactional
public void buyGoodByTx2(int userId, int goods_id, int amount) {
System.out.println("用户ID号为 " + userId + " 购买了商品ID为 " + goods_id + " 数量为 " + amount);
Float price = goodsDAO.queryPriceById2(userId); // 得到当前用户余额
goodsDAO.updateBalance2(userId, price * amount); // 用户的余额 - 商品单价 * 购买数量
goodsDAO.updateAmount2(goods_id, amount); // 商品减少库存量
System.out.println("用户购买成功~");
}
}
其中包含了buyGoodByTx
以及buyGoodByTx2
方法, 通过代码我们可以知道的是, buyGoodByTx
是肯定不会报错的, SQL语句等都是正确的, 而buyGoodByTx2
会报错, 其中包含了肯定会报错的updateAmount2
方法. 那么我们准备一个类, 来并行执行GoodsService::buyGoodByTx
以及GoodsService::buyGoodByTx2
, 并且使用@Transactional
注解进行修饰, 如下:
@Component
public class MultiplyService {
@Autowired
private GoodsService goodsService;
@Transactional
public void multiBuyGoodsTx() {
goodsService.buyGoodByTx(1, 1, 1); // 正常运行
goodsService.buyGoodByTx2(2, 1, 2); // 报错, 运行不了
}
}
测试类:
@Test
public void t10() {
MultiplyService multiplyService = ioc2.getBean(MultiplyService.class);
multiplyService.multiBuyGoodsTx(); // JAVA控制台显示出错, 但 Mysql 的数据并没被 buyGoodByTx 影响, 也就是, multiBuyGoodsTx 若出现错误, 那么该方法中的所有方法统一进行回滚.
}
REQUIRED_NEW
在上面的案例我们可以看出来,
required
模式是一个巨大的事务, 将其中所有的事务统一为一个事务, 要成功都commit, 要失败都rollback. 那么接下来我们要观察的是REQUIRED_NEW
, 我们对GoodsService
中已经被@Transacitonal
所修饰的注解进行修改为如下:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void buyGoodByTx(int userId, int goods_id, int amount) { // 用户ID 商品ID 购买数量
System.out.println("用户ID号为 " + userId + " 购买了商品ID为 " + goods_id + " 数量为 " + amount);
Float price = goodsDAO.queryPriceById(userId); // 得到当前用户余额
goodsDAO.updateBalance(userId, price * amount); // 用户的余额 - 商品单价 * 购买数量
goodsDAO.updateAmount(goods_id, amount); // 商品减少库存量
System.out.println("用户购买成功~");
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void buyGoodByTx2(int userId, int goods_id, int amount) {
System.out.println("用户ID号为 " + userId + " 购买了商品ID为 " + goods_id + " 数量为 " + amount);
Float price = goodsDAO.queryPriceById2(userId); // 得到当前用户余额
goodsDAO.updateBalance2(userId, price * amount); // 用户的余额 - 商品单价 * 购买数量
goodsDAO.updateAmount2(goods_id, amount); // 商品减少库存量
System.out.println("用户购买成功~");
}
增加了传播机制的设置后, 我们再进行测试, 会buyGoodByTx
执行成功, 而buyGoodByTx2
会执行失败. 当然了, 其他的还有事务的隔离级别, 超时回滚, 在这里就不做过多演示了.
原文始发于微信公众号(Heihu Share):开发基础 | Spring 基本使用总结 && 手动实现 Spring
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论