本博客参照了韩顺平老师的 Spring 课程讲义!

1 Spring 基本介绍

1.1 官方资料

1.1.1 官网 https://spring.io/

1.1.2 Spring5 下载

……

1.1.3 在线文档: https://docs.spring.io/spring-framework/docs/current/reference/html/

1.1.4 离线文档 : 解压 spring-5.3.8-dist.zipspring-framework-5.3.8\docs\reference\html\index.html

1.1.5 离线API: 解压 spring-5.3.8-dist.zipspring-framework-5.3.8\docs\javadoc-api\index.html

1.2 Spring 学习的核心内容

一图胜千言

image-20230716213809556

老韩解读上图:

1、Spring 核心学习内容 IOC、AOP, jdbcTemplate, 声明式事务

2、IOC: 控制反转 , 可以管理 java 对象

3、AOP : 切面编程

4、 JDBCTemplate : 是 spring 提供一套访问数据库的技术, 应用性强,相对好理解

5、声明式事务: 基于 ioc/aop 实现事务管理, 理解需要小伙伴花时间

6、IOC, AOP 是重点同时难点

1.3 Spring 几个重要概念

  1. Spring 可以整合其他的框架(老韩解读: Spring 是管理框架的框架)
  2. Spring 有两个核心的概念: IOCAOP
  3. IOC [Inversion Of Control 反转控制]

● 传统的开发模式[JdbcUtils / 反射]

程序——>环境 //程序读取环境配置,然后自己创建对象.

image-20230716214351358

老韩解读上图(以连接到数据库为例说明)

1、程序员编写程序, 在程序中读取配置信息

2、**(程序员)创建对象**, new Object???() || 反射方式

3、使用对象完成任务

● IOC 的开发模式 [EmpAction EmpService EmpDao Emp]

程序<—–容器 //容器创建好对象,程序直接使用

image-20230716214640935

老韩解读上图

1、Spring 根据配置文件 xml || 注解, 创建对象, 并放入到容器(如:ConcurrentHashMap)中,并且可以完成对象之间的依赖

2、**当需要使用某个对象实例的时候, 就直接从容器中获取即可**

3、程序员可以更加关注如何使用对象完成相应的业务 (以前是 new … ==> 注解/配置方式)

4、DI—Dependency Injection 依赖注入,可以理解成是 IOC 的另外叫法.

5、Spring最大的价值,通过配置,给程序提供需要使用的web 层[Servlet(Action/Controller)]/Service/Dao/[JavaBean/entity]对象,这个是核心价值所在,也是 ioc 的具体体现, 实现了解耦

image-20230716220144385

1.4 Spring 快速入门

1.4.1 需求说明

  1. 通过 Spring 的方式[配置文件], 获取 JavaBean: Monster 的对象, 并给该的对象属性赋值, 输出该对象信息

1.4.2 完成步骤

……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<beans>
<!--
1. 配置monster对象/javabean
2. 在beans中可以配置多个bean
3. bean表示就是一个java对象
4. class属性是用于指定类的全路径->spring底层使用反射创建
5. 🌟id属性表示该java对象在spring容器中的id, 通过id可以获取到对象
6. <property name="monsterId" value="100"> 用于给该对象的属性赋值
-->
<bean class="com.study.spring.bean.Monster" id="monster01">
<property name="monsterId" value="100"/>
<property name="name" value="牛魔王"/>
<property name="skill" value="芭蕉扇"/>
</bean>

<bean class="com.study.spring.bean.Monster" id="monster02">
<property name="monsterId" value="1001"/>
<property name="name" value="牛魔王~"/>
<property name="skill" value="芭蕉扇~"/>
</bean>
</beans>

1.4.3 注意事项和细节

1、 说明

ClassPathXmlApplicationContext ioc =new ClassPathXmlApplicationContext(“beans.xml”);

——为什么读取到 beans.xml

2、解释一下类加载路径. 可以给学员输出一下:

// 获取「类加载路径

// File f = new File(this.getClass().getResource(“/“).getPath());

// System.out.println(f);

—输出——项目文件夹\工程名\out\production\工程名

3、debug 看看 spring 容器结构/机制, 记住你是 OOP 程序员,重要!

  • 老韩说明: 注意配置 debugger, 否则你看到的 debug 视图和老师不一样

image-20230717215641546

image-20230717215726473

image-20230717215817159

image-20230717215902732

image-20230717215925529

image-20230717220000530

image-20230717220027499

4、ioc structure summary:

ioc容器(ClassPathXmlApplicationContext):

——beanFactory(DefaultListableBeanFactory)

————beanDefinitionMap(ConcurrentHashMap)

——————table(ConcurrentHashMap$Node[512])

——————可以存放beans.xml中的bean节点配置的bean对象信息

——————key是 在beans.xml中配置的bean的id,value是这个bean的各种信息(如:属性、属性值、类信息、是不是懒加载等信息)

————singletonObjects(ConcurrentHashMap)

——————table(ConcurrentHashMap$Node[512])

——————在beans.xml文件中配置的对象默认是单例的,会初始化在该table中;还有一些spring初始化的——————单例对象

————beanDefinitionNames(ArrayList)

————存放了beans.xml中,所有配置的bean的id,方便查看

用户想要一个对象—>先去beanDefinitionMap的table中找其定义的位置看看:如果是多例,则直接创建一个对象并返回;如果是单例的,则去singletonObjects的table中取出事先创建好的对象返回给用户

1.5 手动开发- 简单的 Spring 基于 XML 配置的程序

1.5.1 需求说明

  1. 自己写一个简单的 Spring 容器, 通过读取 beans.xml, 获取第 1 个 JavaBean: Monster 的对象, 并给该的对象属性赋值, 放入到容器中, 输出该对象信息.

  2. 也就是说,不使用 Spring 原生框架,我们自己简单模拟实现

  3. 可以让小伙伴了解 Spring 容器的简单机制

1.5.2 思路分析

  1. 思路分析/图解

image-20230718083350279

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.study.spring.hspapplicationcontext;

import com.study.spring.bean.Monster;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
* @author 韩顺平
* @version 1.0
* 老师解读
* 1. 这个程序用于实现Spring的一个简单容器机制
* 2. 后面我们还会详细的实现
* 3. 这里我们实现如何将beans.xml文件进行解析,并生成对象,放入容器中
* 4. 提供一个方法 getBean(id) 返回对应的对象
* 5. 这里就是一个开胃小点心, 理解Spring容器的机制
*/
public class HspApplicationContext {

private ConcurrentHashMap<String, Object> singletonObjects =
new ConcurrentHashMap<>();


//构造器
//接收一个容器的配置文件 比如 beans.xml, 该文件默认在src
public HspApplicationContext(String iocBeanXmlFile) throws Exception {

//1. 得到类加载路径
String path = this.getClass().getResource("/").getPath();

//2. 创建 Saxreader
SAXReader saxReader = new SAXReader();

//3. 得到Document对象
Document document =
saxReader.read(new File(path + iocBeanXmlFile));

//4. 得到rootDocument
Element rootElement = document.getRootElement();

//5. 得到第一个bean-monster01
Element bean = (Element) rootElement.elements("bean").get(0);

//6. 获取到第一个bean-monster01的相关属性
String id = bean.attributeValue("id");
String classFullPath = bean.attributeValue("class");
List<Element> property = bean.elements("property");
//遍历->老师简化直接获取
Integer monsterId =
Integer.parseInt(property.get(0).attributeValue("value"));

String name = property.get(1).attributeValue("value");
String skill = property.get(2).attributeValue("value");

//7. 使用反射创建对象.=> 回顾反射机制
Class<?> aClass = Class.forName(classFullPath);
//这里o对象就是Monster对象
Monster o = (Monster) aClass.newInstance();
//给o对象赋值
//反射来赋值=> 这里老师就简化,直接赋值->目的就是先理解流程
//这里的方法就是setter方法
//Method[] declaredMethods = aClass.getDeclaredMethods();
//for (Method declaredMethod : declaredMethods) {
// declaredMethod.invoke();
//}
//赋值
o.setMonsterId(monsterId);
o.setName(name);
o.setSkill(skill);

//8. 将创建好的对象放入到singletonObjects
singletonObjects.put(id, o);

}

public Object getBean(String id) {
//这里小伙伴可以再处理
return singletonObjects.get(id);
}
}

1.6 🌟Spring 原生容器底层结构

1.6.1 一图胜千言

image-20230718105740683

1.6.2 课后作业题

  1. 在 beans.xml 中, 我们注入 2 个 Monster 对象, 但是不指定 id,如下
1
2
3
4
5
6
7
8
9
10
<bean class="com.study.spring.beans.Monster" >
<property name="monsterId" value="1010"/>
<property name="name" value="牛魔王~"/>
<property name="skill" value="芭蕉扇~"/>
</bean>
<bean class="com.study.spring.beans.Monster">
<property name="monsterId" value="666"/>
<property name="name" value="牛魔王~~!!"/>
<property name="skill" value="芭蕉扇~~"/>
</bean>
  1. 问题 1:运行会不会报错
  • 答:不会报错,会正常运行
  1. 问题 2:如果不报错, 你能否找到分配的 id, 并获得到该对象.
  • 答:系统会默认分配 id ,分配 id 的规则是 全类名#0 , 全类名#1 这样的规则来分配 id, 我们可以通过 debug 方式来查看.

2 Spring 管理 Bean-IOC

2.1 Spring 配置/管理 bean 介绍

2.1.1 Bean 管理包括两方面

2.1.1.1 创建 bean 对象

2.1.1.2 给 bean 注入属性

2.1.2 Bean 配置方式

2.1.2.1 基于 xml 文件配置方式

2.1.2.2 基于注解方式

2.2 基于 XML 配置 bean

2.2.1 通过类型来获取 bean

2.2.1.1 应用案例

​ ● 案例说明:

  1. 通过 spring 的 ioc 容器, 获取一个 bean 对象
  2. 说明:获取 bean 的方式:按类型
1
Monster monster = ioc.getBean(Monster.class);

2.2.1.2 细节说明

  1. 按类型来获取 bean, 要求 ioc 容器中的同一个类的 bean 只能有一个, 否则会抛出异常 NoUniqueBeanDefinitionException

  2. 这种方式的应用场景:比如 XxxAction/Servlet/Controller, 或 XxxService 在一个线程中只需要一个对象实例(单例)的情况

  3. 老师这里在说明一下: 在容器配置文件(比如 beans.xml)中给属性赋值, 底层是通过 setter 方法完成的, 这也是为什么我们需要提供 setter 方法的原因

2.2.2 通过构造器配置 bean

2.2.2.1 应用案例

● 案例说明:

在 spring 的 ioc 容器, 可以通过构造器来配置 bean 对象

● 完成步骤

  1. 在 beans.xml 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!--配置Monster对象,并且指定构造器
老师解读
1. constructor-arg标签可以指定使用构造器的参数
2. index表示构造器的第几个参数 从0开始计算的
3. 除了可以通过index 还可以通过 name / type 来指定参数方式
4. 解除大家的疑惑, 类的构造器,不能有完全相同类型和顺序的构造器,所以可以通过type来指定
-->

<!-- index -->
<bean id="monster03" class="com.study.spring.bean.Monster">
<constructor-arg value="200" index="0"/>
<constructor-arg value="白骨精" index="1"/>
<constructor-arg value="吸人血" index="2"/>
</bean>

<!-- name -->
<bean id="monster04" class="com.study.spring.bean.Monster">
<constructor-arg value="200" name="monsterId"/>
<constructor-arg value="白骨精" name="name"/>
<constructor-arg value="吸人血" name="skill"/>
</bean>

<!-- type -->
<bean id="monster05" class="com.study.spring.bean.Monster">
<constructor-arg value="300" type="java.lang.Integer"/>
<constructor-arg value="白骨精~" type="java.lang.String"/>
<constructor-arg value="吸人血~" type="java.lang.String"/>
</bean>

2.2.2.2 使用细节

  1. 通过 index 属性来区分是第几个参数
  2. 通过 type 属性来区分是什么类型(按照顺序)

2.2.3 通过 p 名称空间配置 bean

1
2
3
4
5
6
7
8
9
10
11
12
13
在beans标签中添加    xmlns:p="http://www.springframework.org/schema/p"


<!--通过p名称空间来配置bean
老韩解读
1. 将光标放在p , 输入alt+enter , 就会自动的添加xmlns
2. 有时需要多来几次
-->
<bean id="monster06" class="com.study.spring.bean.Monster"
p:monsterId="500"
p:name="红孩儿"
p:skill="吐火"
/>

2.2.4 引用/注入其它 bean 对象(某类中有其他类型的数据,也需要初始化)

2.2.4.1 实例演示

​ ● 案例说明:在 spring 的 ioc 容器, 可以通过 ref 来实现 bean 对象的相互引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--配置MemberServiceImpl对象
老韩解读
1. ref="memberDAO"表示 MemberServiceImpl对象属性memberDAO引用的对象是id=memberDAO
的对象
2. 这里就体现出spring容器的依赖注入
3. 注意再spring容器中, 他是「作为一个整体来执行的」即如果你引用到一个bean对象, 对你配置的顺序没有要求
4. 建议还是按顺序,好处是阅读的时候,比较方便
-->
<!--配置MemberDAOImpl对象-->
<bean class="com.study.spring.dao.MemberDAOImpl" id="memberDAO"/>

<bean class="com.study.spring.service.MemberServiceImpl" id="memberService">
<property name="memberDAO" ref="memberDAO"/>
</bean>

2.2.5 引用/注入内部 bean 对象

1
2
3
4
5
<bean id="memberServiceImpl02"class="com.study.spring.service.MemberServiceImpl">
<property name="memberDAO">
<bean class="com.study.spring.dao.MemberDAOImpl"/>
</property>
</bean>

2.2.6 引用/注入集合/数组类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!--配置Master对象
体会 spring 容器配置特点 依赖注入-非常灵活
-->
<bean class="com.study.spring.bean.Master" id="master">
<property name="name" value="太上老君"/>
<!--给list属性赋值-->
<property name="monsterList">
<list>
<!--引用的方法-->
<ref bean="monster01"/>
<ref bean="monster02"/>
<!--内部bean-->
<bean class="com.study.spring.bean.Monster">
<property name="name" value="老鼠精"/>
<property name="monsterId" value="100"/>
<property name="skill" value="吃粮食"/>
</bean>
</list>
</property>
<!--给map属性赋值-->
<property name="monsterMap">
<map>
<entry>
<key>
<value>monster03</value>
</key>
<!--这里老师使用的外部bean,引入-->
<ref bean="monster03"/>
</entry>
<entry>
<key>
<value>monster04</value>
</key>
<ref bean="monster04"/>
</entry>
</map>
</property>
<!--给set属性赋值-->
<property name="monsterSet">
<set>
<ref bean="monster05"/>
<ref bean="monster06"/>
<bean class="com.study.spring.bean.Monster">
<property name="name" value="金角大王"/>
<property name="skill" value="吐水"/>
<property name="monsterId" value="666"/>
</bean>
</set>
</property>
<!--给数组属性赋值
老师多说一句: array标签中使用 value 还是 bean , ref .. 要根据你的业务决定
-->
<property name="monsterName">
<array>
<value>小妖怪</value>
<value>大妖怪</value>
<value>老妖怪</value>
</array>
</property>
<!--给Properties属性赋值 结构k(String)-v(String)-->
<property name="pros">
<props>
<prop key="username">root</prop>
<prop key="password">123456</prop>
<prop key="ip">127.0.0.1</prop>
</props>
</property>
</bean>

2.2.6.2 使用细节

  1. 主要掌握 List/Map/Properties 三种集合的使用.

  2. Properties 集合的特点

    • 这个 Properties 是 Hashtable 的子类 , 是 key-value 的形式

    • key 是 string 而 value 也是 string

2.2.7 通过 util 名称空间创建 list

2.2.7.1 实例演示

​ ● 案例说明

​ spring 的 ioc 容器, 可以通过 util 名称空间创建 list 集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xmlns:util="http://www.springframework.org/schema/util"

<!--定义一个util:list 并且指定id 可以达到数据复用
老师说明: 在使用util:list 名称空间时候,需要引入相应的标签, 一般来说通过alt+enter会自动加入
, 如果没有就手动添加一下即可.
-->
<util:list id="myBookList">
<value>三国演义</value>
<value>红楼梦</value>
<value>西游记</value>
<value>水浒传</value>
</util:list>
<!--配置BookStore对象-->

<bean class="com.study.spring.bean.BookStore" id="bookStore">
<property name="bookList" ref="myBookList"/>
</bean>

2.2.8 级联属性赋值

2.2.8.1 实例演示

​ ● 案例说明

​ spring 的 ioc 容器, 可以直接给对象属性的属性赋值, 即级联属性赋值

1
2
3
4
5
6
7
8
9
<!--配置Dept对象-->
<bean class="com.study.spring.bean.Dept" id="dept"/>
<!--配置Emp对象-->
<bean class="com.study.spring.bean.Emp" id="emp">
<property name="name" value="jack"/>
<property name="dept" ref="dept"/>
<!--这里我希望给dept的name属性指定值[级联属性赋值]-->
<property name="dept.name" value="Java开发部门"/>
</bean>

2.2.9 通过静态工厂获取对象

2.2.9.1 实例演示

​ ● 案例说明

​ 在 spring 的 ioc 容器, 可以通过静态工厂获取 bean 对象

  1. 创 建com\study\spring\factory\MyStaticFactory.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyStaticFactory {
private static Map<String, Monster> monsterMap;

//使用 static代码块 进行初始化
//在java基础的时候,讲过的
static {
monsterMap = new HashMap<>();
monsterMap.put("monster01", new Monster(100,"牛魔王","芭蕉扇"));
monsterMap.put("monster02", new Monster(200,"狐狸精","美人计"));
}

//提供一个方法,返回Monster对象
public static Monster getMonster(String key) {
return monsterMap.get(key);
}

}
  1. 修改 beans.xml , 增加配置
1
2
3
4
5
6
7
8
9
10
11
12
<!--配置monster对象,通过静态工厂获取
老师解读
1. 通过静态工厂获取/配置bean
2. class 是静态工厂类的全路径
3. factory-method 表示是指定静态工厂类的哪个方法返回对象
4. constructor-arg value="monster02" value是指定要返回静态工厂的哪个对象
-->
<bean id="my_monster01"
class="com.study.spring.factory.MyStaticFactory"
factory-method="getMonster">
<constructor-arg value="monster02"/>
</bean>

2.2.10 通过实例工厂获取对象

2.2.10.1 实例演示

​ ● 案例说明在 spring 的 ioc 容器, 可以通过实例工厂获取 bean 对象

​ ● 完成步骤1. 创 建com\study\spring\factory\MyInstanceFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyInstanceFactory {
private Map<String, Monster> monster_map;

//通过普通代码块进行初始化
{
monster_map = new HashMap<>();
monster_map.put("monster03", new Monster(300, "牛魔王~", "芭蕉扇~"));
monster_map.put("monster04", new Monster(400, "狐狸精~", "美人计~"));
}

//写一个方法返回Monster对象
public Monster getMonster(String key) {
return monster_map.get(key);
}
}
  1. 配置 beans.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--配置2个实例工厂对象-->
<bean class="com.study.spring.factory.MyInstanceFactory" id="myInstanceFactory"/>
<bean class="com.study.spring.factory.MyInstanceFactory" id="myInstanceFactory2"/>
<!--配置monster对象, 通过实例工厂
老韩解读
1. factory-bean 指定使用 '哪个实例工厂' 对象返回bean
2. factory-method 指定使用实例工厂对象的 '哪个方法' 返回bean
3. constructor-arg value="monster03" 指定获取到实例工厂中的哪个monster
-->
<bean id="my_monster02" factory-bean="myInstanceFactory" factory-method="getMonster">
<constructor-arg value="monster03"/>
</bean>

<bean id="my_monster03" factory-bean="myInstanceFactory2" factory-method="getMonster">
<constructor-arg value="monster03"/>
</bean>

2.2.12 通过 FactoryBean 获取对象(重点)

2.2.12.1 应用实例

​ ● 案例说明

​ 在 spring 的 ioc 容器,通过 FactoryBean 获取 bean 对象(重点)

  1. 创 建com\study\spring\factory\MyFactoryBean.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class MyFactoryBean implements FactoryBean<Monster> {

//这个就是你配置时候,指定要获取的对象对应key
private String key;
private Map<String, Monster> monster_map;

{ //代码块,完成初始化
monster_map = new HashMap<>();
monster_map.put("monster03", new Monster(300, "牛魔王~", "芭蕉扇~"));
monster_map.put("monster04", new Monster(400, "狐狸精~", "美人计~"));
}

public void setKey(String key) {
this.key = key;
}

@Override
public Monster getObject() throws Exception {
return monster_map.get(key);
}

@Override
public Class<?> getObjectType() {
return Monster.class;
}

@Override
public boolean isSingleton() {//这里指定是否返是单例
return false;
}
}
  1. 配置 beans.xml
1
2
3
4
5
6
7
8
9
<!--配置monster对象,通过FactoryBean获取
老师解读
1. class 指定使用的FactoryBean
2. key表示就是 MyFactoryBean 属性key
3. value就是你要获取的对象对应key
-->
<bean id="my_monster05" class="com.study.spring.factory.MyFactoryBean">
<property name="key" value="monster04"/>
</bean>

2.2.13 bean 配置信息重用(继承)

2.2.13.1 应用实例

​ ● 说明

​ 在 spring 的 ioc 容器, 提供了一种继承的方式来实现 bean 配置信息的重用

​ ● 应用实例演示

  1. 配置 beans.xml
1
2
3
4
5
6
7
8
9
10
11
<!--配置Monster对象
1. 如果bean指定了 abstract="true", 表示该bean对象, 是用于被继承
2. 本身这个bean就不能被获取/实例化
-->
<bean id="monster12" class="com.study.spring.bean.Monster" abstract="true">
<property name="monsterId" value="100"/>
<property name="name" value="蜈蚣精~"/>
<property name="skill" value="蜇人~"/>
</bean>

<bean id="monster13" class="com.study.spring.bean.Monster" parent="monster12"/>

2.2.14 bean 创建顺序

2.2.14.1 实例演示

​ ● 说明

  1. 在 spring 的 ioc 容器, 默认是按照配置的顺序创建 bean 对象
1
2
3
4
5
6
7
8
<!--测试bean对象的创建顺序
老师解读
1. 在默认情况下, bean创建的顺序是按照配置顺序来的
2. 但是如果我们增加了 depends-on="department01" 这时就会先创建id= department01对象
-->

<bean id="student01" class="com.hspedu.bean.Student" depends-on="department01"/>
<bean id="department01" class="com.hspedu.bean.Department" />

2.2.14.2 一个问题

​ ● 问题说明

  1. 先看下面的配置, 请问两个 bean 创建的顺序是什么? 并分析执行流
    1. 先创建 id=memberDAOImpl
    2. 再创建 id = memberServiceImpl
    3. 调用 memberServiceImpl.setMemberDAO() 完成引用

image-20230720095126833

  1. 先看下面的配置, 请问两个 bean 创建的顺序是什么, 并分析执行流程

    1. 先创建 id = memberServiceImpl
    2. 再创建 id=memberDAOImpl
    1. 用 memberServiceImpl.setMemberDAO() 完成引用. (像是栈

image-20230720095309694

2.2.15 bean 对象的单例和多例

2.2.15.1 应用实例

​ ● 说明

在 spring 的 ioc 容器, 在默认是按照单例创建的, 即配置一个 bean 对象后, ioc 容器只会创建一个 bean 实例。

​ 如果,我们希望 ioc 容器配置的某个 bean 对象, 是以多个实例形式创建的则可以通过配置scope=”prototype” 来指定

配置 beans.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--配置Cat对象
老师解读
1. 在默认情况下 scope属性是 singleton
2. 在ioc容器中, 只要有一个这个bean对象
3. 当程序员执行getBean时, 返回的的是同一个对象
4. 如果我们希望每次getBean返回一个新的Bean对象,则可以scope="prototype"
5. 如果bean的配置是 scope="singleton" lazy-init="true" 这时,ioc容器就不会提前创建该对象
, 而是当执行getBean方法的时候,才会创建对象
-->
<bean id="cat" class="com.study.spring.bean.Cat" scope="prototype" lazy-init="false">
<property name="id" value="100"/>
<property name="name" value="小花猫"/>
</bean>

2.2.15.2 使用细节

  1. 默认是单例 singleton, 在启动容器时, 默认就会创建 , 并放入到 singletonObjects 集合

  2. 设置为多实例机制后, 该 bean 是在 getBean()时才创建

  3. 如果是单例 singleton, 同时希望在 getBean 时才创建 , 可以指定懒加载lazy-init=”true” (注意默认是 false)

  4. 通常情况下, lazy-init 就使用默认值 false , 在开发看来, 用空间换时间是值得的, 除非有特殊的要求.

  5. 如果 scope=”prototype” 这时你的 lazy-init 属性的值不管是 ture, 还是 false 都是在getBean 时候,才创建对象.

2.2.16 bean 的生命周期

2.2.16.1 应用实例

​ ● 说明: bean 对象创建是由 JVM 完成的,然后执行如下方法

1. 执行构造器

2. 执行 set 相关方法

3. 调用 bean 的初始化的方法(需要配置)

4. 使用 bean

5. 当容器关闭时候,调用 bean 的销毁方法(需要配置)

​ ● 应用实例演示

  1. 创建 com.hspedu.spring.beans;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class House {
private String name;

public House() {
System.out.println("House() 构造器...");
}

public String getName() {
return name;
}

public void setName(String name) {
System.out.println("House setName()=" + name);
this.name = name;
}

//老师解读
//1. 这个方法是程序员来编写的.
//2. 根据自己的业务逻辑来写.
public void init() {
System.out.println("House init()..");
}

//老师解读
//1. 这个方法是程序员来编写的.
//2. 根据自己的业务逻辑来写.
//3. 名字也不是固定的
public void destroy() {
System.out.println("House destroy()..");
}

@Override
public String toString() {
return "House{" +
"name='" + name + '\'' +
'}';
}
}
  1. 配置 beans.xml
1
2
3
4
5
6
7
8
9
10
11
12
<!--配置House对象,演示整个Bean的生命周期
老师解读
1. init-method="init" 指定bean的初始化方法 , 在setter方法后执行
2. init方法执行的时机,由spring容器来控制
3. destroy-method="destroy" 指定bean的销毁方法, 在容器关闭的时候执行
4. destroy方法执行的时机,也由spring容器来控制
-->
<bean class="com.hspedu.spring.bean.House" id="house"
init-method="init"
destroy-method="destroy">
<property name="name" value="北京豪宅"/>
</bean>

2.2.16.2 使用细节

  1. 初始化 init 方法和 destory 方法, 是程序员来指定

  2. 销毁方法就是当关闭容器时,才会被调用.

2.2.17 配置 bean 的后置处理器 【这个比较难】

2.2.17.1 应用实例

● 说明

  1. 在 spring 的 ioc 容器,可以配置 bean 的后置处理器
  2. 该处理器/对象会在 bean 初始化方法调用前和初始化方法调用后被调用
  3. 程序员可以在后置处理器中编写自己的代码

后置处理器创建好后,也注册在beans.xml中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 什么时候被调用: 在Bean的init方法前被调用
* @param bean 传入的在IOC容器中创建/配置Bean
* @param beanName 传入的在IOC容器中创建/配置Bean的id
* @return Object 程序员对传入的bean 进行修改/处理【如果有需要的话】 ,返回
* @throws BeansException
*/

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("postProcessBeforeInitialization().. bean="
+ bean + " beanName=" + beanName);

//初步体验案例: 如果类型是House的统一改成 上海豪宅
//对多个对象进行处理/编程==>切面编程
if(bean instanceof House) {
((House)bean).setName("上海豪宅~");
}
return null;
}

/**
* 什么时候被调用: 在Bean的init方法后被调用
* @param bean 传入的在IOC容器中创建/配置Bean
* @param beanName 传入的在IOC容器中创建/配置Bean的id
* @return 程序员对传入的bean 进行修改/处理【如果有需要的话】 ,返回
* @throws BeansException
*/

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("postProcessAfterInitialization().. bean="
+ bean + " beanName=" + beanName);
return bean;
}
1
2
3
4
5
6
7
<!--配置后置处理器对象
老师解读
1. 当我们在beans02.xml 容器配置文件 配置了 MyBeanPostProcessor
2. 这时后置处理器对象,就会作用在该容器创建的Bean对象
3. 已经是针对所有对象编程->切面编程AOP
-->
<bean class="com.hspedu.spring.bean.MyBeanPostProcessor" id="myBeanPostProcessor"/>

2.2.17.2 其它说明

1、 怎么执行到这个方法?=> 使用 AOP(反射+动态代理+IO+容器+注解)

2、有什么用?=> 可以对 IOC 容器中所有的对象进行统一处理 ,比如 日志处理/权限的校验/安全的验证/事务管理.

​ -初步体验案例: 如果类型是 House 的统一改成 上海豪宅

3、针对容器的所有对象吗? 是的=>切面编程特点

4、后面我们会自己实现这个底层机制,这个是一个比较难理解的知识点, 现在不做过多的纠结,后面我会带小伙伴实现这个机制

2.2.18 通过属性文件给 bean 注入值

2.2.18.1 应用实例

​ ● 说明

在 spring 的 ioc 容器,通过属性文件给 bean 注入值

​ ● 应用实例演示

  1. src/ 创建 my.properties
1
2
3
monsterId=1000
name=\u4e4c\u9f9f\u7cbe
skill=\u7f29\u8116\u5b50
  1. 修改 src\beans.xml , 继续完成配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
xmlns:context="http://www.springframework.org/schema/context"
<!--指定属性文件
1. 先把这个文件修改成提示All Problem
2. 提示错误,将光标放在context 输入alt+enter 就会自动引入namespace
3. location="classpath:my.properties" 表示指定属性文件的位置
4. 提示,需要带上 classpath
5. 属性文件有中文,需要将其转为unicode编码-> 使用工具
-->
<context:property-placeholder location="classpath:my.properties"/>
<bean id="monster100" class="com.hspedu.spring.beans.Monster">
<property name="monsterId" value="${id}"/>
<property name="name" value="${name}"/>
<property name="skill" value="${skill}"/>
</bean>

2.2.19 基于 XML 的 bean 的自动装配

2.2.19.1 应用实例

​ ● 说明

​ 在 spring 的 ioc 容器,可以实现自动装配 bean

​ ● 应用实例演示

​ 这里说的 Action 就是我们前面讲过的 Servlet->充当 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!--配置OrderDao对象-->
<bean class="com.hspedu.spring.dao.OrderDao" id="orderDao"/>
<!--配置OrderService对象
老师解读
1. autowire="byType" 表示 在创建 orderService时
通过类型的方式 给对象属性 自动完成赋值/引用
2. 比如OrderService 对象有 private OrderDao orderDao
3. 就会在容器中去找有没有 OrderDao类型对象
4. 如果有,就会自动的装配, 老师提示如果是按照 byType 方式来装配, 这个容器中,不能有两个
的OrderDao类型对象
5. 如果你的对象没有属性, autowire就没有必要写
6. 其它类推..

7. 如果我们设置的是 autowire="byName" 表示通过名字完成自动装配
8. 比如下面的 autowire="byName" class="com.hspedu.spring.service.OrderService"
1) 先看 OrderService 属性 private OrderDao orderDao
2) 再根据这个属性的setXxx()方法的 xxx 来找对象id
3) public void setOrderDao() 就会找id=orderDao对象来进行自动装配
4) 如果没有就装配失败

-->
<bean autowire="byName" class="com.hspedu.spring.service.OrderService"
id="orderService"/>

<!--配置OrderAction-->
<bean autowire="byName" class="com.hspedu.spring.web.OrderAction" id="orderAction"/>

2.2.19.2 其它说明

1. 这个知识点作为了解即可, 后面我们主要还是使用基于注解的方式(重点.)
1. 但是机制和原理类似

2.3 🌟基于注解配置 bean

2.3.18 基本使用

2.3.18.1 说明

​ ● 基本介绍基于注解的方式配置 bean, 主要是项目开发中的组件, 比如 Controller、 Service、 和 Dao.

​ ● 组件注解的形式有

  1. @Component 表示当前注解标识的是一个组件最宽泛,只是把该类注册到spring ioc容器中

  2. @Controller 表示当前注解标识的是一个控制器, 通常用于 Servlet

  3. @Service 表示当前注解标识的是一个处理业务逻辑的类, 通常用于 Service 类

  4. @Repository 表示当前注解标识的是一个持久化层的类, 通常用于 Dao 类

2.3.18.2 快速入门

​ ● 应用实例使用注解的方式来配置 Controller / Service / Respository / Component

​ ● 代码实现

​ 1. 引入 spring-aop-5.3.8.jar , 在 spring/libs 下拷贝即可

​ 2.创建 UserAction.java UserService.java, UserDao.java MyComponent.java

​ 3.配置 beans.xml(配置自动扫描的包)

1
2
<!-- 配置自动扫描的包,注意需要加入 context 名称空间 -->
<context:component-scan base-package="com.study.spring.component" />

2.3.18.3 注意事项和细节说明

  1. 需要导入 spring-aop-5.3.8.jar , 别忘了

  2. 必须在 Spring 配置文件中指定”自动扫描的包”,IOC 容器才能够检测到当前项目中哪些类被标识了注解, 注意到导入 context 名称空间

    1
    2
    <!-- 配置自动扫描的包 -->
    <context:component-scan base-package="com.hspedu.spring.component" />

    可以使用通配符 * 来指定 ,比如 com.hspedu.spring.* 表示
    –提问: com.hspedu.spring.component 会不会去扫描它的子包? –答:会的

  3. Spring 的 IOC 容器不能检测一个使用了@Controller 注解的类到底是不是一个真正的控制器。注解的名称是用于程序员自己识别当前标识的是什么组件。其它的@Service@Repository 也是一样的道理 [也就是说 spring 的 IOC 容器只要检查到注解就会生成对象,但是这个注解的含义 spring 不会识别,注解是给程序员编程方便看的]

  4. <context:component-scan base-package=”com.hspedu.spring.component”resource-pattern=”User* .class” />resource-pattern=”User*.class”: 表示只扫描满足要求的类.[使用的少,不想扫描,不写注解就可以, 知道这个知识点即可]

  5. <context:component-scan base-package=”com.hspedu.spring.component” >

    <context:exclude-filter type=”annotation”expression=”org.springframework.stereotype.Service”/>

1
2
3
4
5
1) <context:exclude-filter> 放在<context:component-scan>内,表示扫描过滤掉当前包的某些类
2) type="annotation" 按照注解类型进行过滤.
3) expression :就是注解的全类名,比如 org.springframework.stereotype.Service 就是@Service 注解的全类名,其它比@Controller @Repository 等 依次类推
4) 上面表示过滤掉 com.hspedu.spring.component 包下,加入了@Service 注解的类
5) 完成测试, 修改 beans.xml, 增加 exclude-filter , 发现 UserService, 不会注入到容器.
  1. 指定自动扫描哪些注解类
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--
需求:如果我们希望按照自己的规则,来扫描包/子包下的某些注解, 可以通过 include-filter
1. use-default-filters="false" 表示不使用默认的过滤机制/扫描机制
2. context:include-filter 表示要去扫描哪些类
3. type="annotation" 按照注解方式来扫描/过滤
4. expression="org.springframework.stereotype.Service" 指定要扫描的注解的全路径
-->

<context:component-scan base-package="com.hspedu.spring.component" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
</context:component-scan>
  1. 默认情况:标记注解后,默认把类名首字母小写作为 id 的值。也可以使用注解的 value 属性指定 id 值,并且 value 可以省略。

  2. 扩展-@Controller 、@Service、@Component 区别 : (回去看看一下老师的讲解的注解基础) https://zhuanlan.zhihu.com/p/454638478

2.3.19 🌟手动开发简单的 Spring 基于注解配置的程序

-老韩要求: 小伙伴要至少独立写 2 遍

2.3.19.1 需求说明

  1. 自 己 写 一 个 简 单 的 Spring 容 器 , 通 过 读 取 类 的 注 解 (@Component @Controller@Service @Reponsitory), 将对象注入到 IOC 容器

  2. 也就是说,不使用 Spring 原生框架,我们自己使用 IO+Annotaion+反射+集合 技术实现, 打通 Spring 注解方式开发的技术痛点

2.3.19.2 思路分析

  1. 思路分析+程序结构

    1)我们使用注解方式完成, 这里老韩不用 xml 来配

    2)程序框架图

image-20230725093807908

2.3.19.3 代码实现

​ ● 应用实例

  1. 手动实现注解的方式来配置 Controller / Service / Respository / Component

  2. 我们使用自定义注解来完成.

    ● 代码实现

  3. 仍然使用前面的 \com\hspedu\spring\component\ 包下的类

  4. 创建 ComponentScan.java

1
2
3
4
5
6
7
8
9
10
11
/**
* 解读
* 1. @Target(ElementType.TYPE)指定我们的ComponentScan注解可以修饰 Type程序元素
* 2. @Retention(RetentionPolicy.RUNTIME) 指定ComponentScan注解 保留范围
* 3. String value() default ""; 表示ComponentScan 可以传入 value
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ComponentScan {
String value() default "";
}
  1. 创 建 HspSpringConfig.java
1
2
3
4
5
6
7
8
9
10
package com.hspedu.spring.annotation;

/**
* @author 韩顺平
* @version 1.0
* 这是一个配置类, 作用类似我们原生spring的 beans.xml 容器配置文件
*/
@ComponentScan(value = "com.hspedu.spring.component")
public class HspSpringConfig {
}
  1. 创 建 HspSpringApplicationContext.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/**
* HspSpringApplicationContext 类的作用类似Spring原生ioc容器
*/
public class HspSpringApplicationContext {
private Class configClass;
//ioc我存放的就是通过反射创建的对象(基于注解方式)
private final ConcurrentHashMap<String, Object> ioc =
new ConcurrentHashMap<>();

//构造器
public HspSpringApplicationContext(Class configClass) {

this.configClass = configClass;
System.out.println("this.configClass=" + this.configClass);
//获取要扫描的包
//1. 先得到HspSpringConfig配置的的@ComponentScan(value = "com.hspedu.spring.component")
ComponentScan componentScan =
(ComponentScan) this.configClass.getDeclaredAnnotation(ComponentScan.class);
//2. 通过componentScan的value=> 即要扫描的包
String path = componentScan.value();
System.out.println("要扫描的包= " + path);

//得到要扫描的包下的所有资源(类 .class) ⚠️要去out目录下,而不是src目录
//1.得到类的加载器
ClassLoader classLoader =
HspApplicationContext.class.getClassLoader();
//2. 通过类的加载器获取到要扫描的包的资源 url=》类似一个路径
path = path.replace(".", "/");//一定要把. 替换成 /
URL resource =
classLoader.getResource(path);
System.out.println("resource=" + resource);
//3. 将要加载的资源(.class) 路径下的文件进行遍历=>io
File file = new File(resource.getFile());
if (file.isDirectory()) {
File[] files = file.listFiles();
for (File f : files) {
System.out.println("=====================");
System.out.println("=" + f.getAbsolutePath());
//D:\hspedu_spring\spring\out\production\spring\com\hspedu\spring\component\UserService.class
//获取到 com.hspedu.spring.component.UserService
String fileAbsolutePath = f.getAbsolutePath();

//这里我们只处理.class文件
if (fileAbsolutePath.endsWith(".class")) {

//1. 获取到类名
String className =
fileAbsolutePath.substring(fileAbsolutePath.lastIndexOf("\\") + 1, fileAbsolutePath.indexOf(".class"));
//System.out.println("className=" + className);
//2. 获取类的完整的路径(全类名)
//老师解读 path.replace("/",".") => com.hspedu.spring.component.
String classFullName = path.replace("/", ".") + "." + className;
//System.out.println("classFullName=" + classFullName);

//3. 判断该类是不是需要注入容器, 就看该类是不是有注解 @Component @Service..
try {
//这时,我们就得到老该类的Class对象
//Class clazz = Class.forName(classFullName)
//老师说一下
//1. Class clazz = Class.forName(classFullName) 可以反射加载类
//2. classLoader.loadClass(classFullName); 可以反射类的Class
//3. 区别是 : 上面方式后调用来类的静态方法, 下面方法不会
//4. aClass.isAnnotationPresent(Component.class) 判断该类是否有 @Component
Class<?> aClass = classLoader.loadClass(classFullName);
if (aClass.isAnnotationPresent(Component.class) ||
aClass.isAnnotationPresent(Controller.class) ||
aClass.isAnnotationPresent(Service.class) ||
aClass.isAnnotationPresent(Repository.class)) {

//这里老师演示一个Component注解指定value,分配id
//老师就是演示了一下机制.
if(aClass.isAnnotationPresent(Component.class)) {
//获取到该注解
Component component = aClass.getDeclaredAnnotation(Component.class);
String id = component.value();
if(!"".endsWith(id)) {
className = id;//替换
}
}

//这时就可以反射对象,并放入到容器中
Class<?> clazz = Class.forName(classFullName);
Object instance = clazz.newInstance();
//放入到容器中, 将类名的首字母小写作为id
//StringUtils

ioc.put(StringUtils.uncapitalize(className) , instance);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

}

//编写方法返回对容器中对象
public Object getBean(String name) {
return ioc.get(name);
}
}

2.3.19.4 注意事项和细节说明

还可以通过@Component(value = “xx”) @Controller(value = “yy”) @Service(value = “zz”)中指定的 value, 给 bean 分配 id

2.3.20 自动装配

2.3.20.1 应用实例

​ ● 基本说明

  1. 基于注解配置 bean,也可实现自动装配,使用的注解是:@AutoWired 或者 @Resource

  2. @AutoWired 的规则说明

    1. 🌟在 IOC 容器中查找待装配的组件的类型,如果有唯一的 bean 匹配,则使用该 bean 装配
    2. 🌟如待装配的类型对应的 bean 在 IOC 容器中有多个,则使用待装配的属性的属性名作为 id 值再进行查找, 找到就装配,找不到就抛异常
  3. @Resource 的规则说明

    1. 🌟@Resource 有两个属性是比较重要的,分是 name 和 type,Spring 将@Resource 注解的name 属性解析为 bean 的名字,而 type 属性则解析为 bean 的类型.所以如果使用 name 属性,则使用 byName 的自动注入策略,而使用 type 属性时则使用 byType 自动注入策略(同样,容器中只能有一个该类型的对象)
    2. 🌟如果@Resource 没有指定 name 和 type ,则先使用byName注入策略(用你的对象名字找ioc容器中的id), 如果匹配不上,再使用 byType 策略(ioc容器中该类型对象必须唯一), 如果都不成功,就会报错
  4. 总结:不管是@Autowired 还是 @Resource都保证属性名是规范的写法就可以注入.

2.3.20.2 注意事项和细节说明

  1. 如待装配的类型对应的 bean 在 IOC 容器中有多个, 则使用待装配的属性的属性名作为 id 值再进行查找, 找到就装配, 找不到就抛异常

2.3.21 泛型依赖注入

2.3.21.1 泛型依赖解释

​ ● 基本说明

  1. 为了更好的管理有继承和相互依赖的 bean 的自动装配, spring 还提供基于泛型依赖的注入机制

  2. 在继承关系复杂情况下, 泛型依赖注入就会有很大的优越性

2.3.21.2 应用实例

​ ● 应用实例需求

  1. 各个类关系图

image-20230725145512754

  1. 传统方法是将 PhoneDao /BookDao 自动装配到 BookService/PhoneSerive 中,当这种继承关系多时,就比较麻烦,可以使用 spring 提供的泛型依赖注入

(跟配置mybatis plus的service层、impl层、mapper层之间的关系很像)

3 AOP

3.1 官方文档

3.1.1 AOP 讲解:

spring-framework-5.3.8/docs/reference/html/core.html#aop

3.1.2 AOP APIs :

spring-framework-5.3.8/docs/reference/html/core.html#aop-api

3.2 动态代理-精致小案例

3.2.1 需求说明

● 需求说明

  1. 有 Vehicle(交通工具接口, 有一个 run 方法), 下面有两个实现类 Car 和 Ship
  2. 当运行 Car 对象 的 run 方法和 Ship 对象的 run 方法时,输入如下内容, 注意观察前后有统一的输出.
image-20230725153717459

3.2.2 解决方案-传统方式

  1. 传统的解决思路, 在各个方法的[前, 执行过程, 后]输出日志提示信息

  2. 来思考一下, 解决方案好吗? ===> 代码冗余, 其实就是单个对象的调用,并没有很好的解决

3.2.3 解决方案-🌟动态代理方式 !!!!!!!!

● 解决方案 2-代码实现

  1. 动态代理解决思路,在调用方法时,使用反射机制,根据方法去决定调用哪个对象方法
1
2
3
4
5
6
7
8
9
10
11
package com.hspedu.spring.proxy2;

/**
* @author 韩顺平
* @version 1.0
* 接口,该接口有run方法
*/
public interface Vehicle {
public void run();
public String fly(int height);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package com.hspedu.spring.proxy2;



import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
* VehicleProxyProvider 该类可以返回一个代理对象.
*/
public class VehicleProxyProvider {

//定义一个属性
//target_vehicle 表示真正要执行的对象
//该对象实现了Vehicle接口
private Vehicle target_vehicle;

//构造器
public VehicleProxyProvider(Vehicle target_vehicle) {
this.target_vehicle = target_vehicle;
}

//编写一个方法,可以返回一个代理对象, 该代理对象可以通过反射机制调用到被代理对象的方法
//老师解读
//1. 这个方法非常重要, 理解有一定难度
public Vehicle getProxy() {

//得到类加载器
ClassLoader classLoader =
target_vehicle.getClass().getClassLoader();

//得到要代理的对象/被执行对象 的接口信息,底层是通过接口来完成调用
Class<?>[] interfaces = target_vehicle.getClass().getInterfaces();


//创建InvocationHandler 对象
//因为 InvocationHandler 是接口,所以我们可以通过匿名对象的方式来创建该对象
/**
*
* public interface InvocationHandler {
* public Object invoke(Object proxy, Method method, Object[] args)
* throws Throwable;
* }
* invoke 方法是将来执行我们的target_vehicle的方法时,会调用到
*
*/

InvocationHandler invocationHandler = new InvocationHandler() {
/**
* invoke 方法是将来执行我们的target_vehicle的方法时,会调用到
* @param o 表示代理对象
* @param method 就是通过代理对象调用方法时,的哪个方法 代理对象.run()
* @param args : 表示调用 代理对象.run(xx) 传入的参数
* @return 表示 代理对象.run(xx) 执行后的结果.
* @throws Throwable
*/
@Override
public Object invoke(Object o, Method method, Object[] args)
throws Throwable {

System.out.println("交通工具开始运行了....");
//这里是我们的反射基础 => OOP
//method 是?: public abstract void com.hspedu.spring.proxy2.Vehicle.run()
//target_vehicle 是?: Ship对象
//args 是null
//这里通过反射+动态绑定机制,就会执行到被代理对象的方法
//执行完毕就返回
Object result = method.invoke(target_vehicle, args);
System.out.println("交通工具停止运行了....");
return result;
}
};

/*

public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)

老师解读
1. Proxy.newProxyInstance() 可以返回一个代理对象
2. ClassLoader loader: 类的加载器.
3. Class<?>[] interfaces 就是将来要代理的对象的接口信息
4. InvocationHandler h 调用处理器/对象 有一个非常重要的方法invoke
*/
Vehicle proxy =
(Vehicle)Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);

return proxy;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.hspedu.spring.proxy2;


import org.junit.Test;

/**
* @author 韩顺平
* @version 1.0
*/
public class TestVehicle {

@Test
public void run() {
//OOP基础=>java基础
Vehicle vehicle = new Ship();
//动态绑定
vehicle.run();
}

@Test
public void proxyRun() {
//创建Ship对象
Vehicle vehicle = new Car();

//创建VehicleProxyProvider对象, 并且我们传入的要代理的对象
VehicleProxyProvider vehicleProxyProvider =
new VehicleProxyProvider(vehicle);

//获取代理对象, 该对象可以代理执行方法
//老师解读
//1. porxy 编译类型 Vehicle
//2. 运行类型 是代理类型 class com.sun.proxy.$Proxy9
Vehicle proxy = vehicleProxyProvider.getProxy();

System.out.println("proxy的编译类型是 Vehicle");
System.out.println("proxy的运行类型是 " + proxy.getClass());
//下面老韩就要给大家解读/debug怎么 执行到 代理对象的 public Object invoke(Object o, Method method, Object[] args)
//梳理完毕. proxy的编译类型是 Vehicle, 运行类型是 class com.sun.proxy.$Proxy9
//所以当执行run方法时,会执行到 代理对象的invoke
//如何体现动态 [1. 被代理的对象 2. 方法]
//proxy.run();
String result = proxy.fly(10000);
System.out.println("result=" + result);
}
}

3.3 动态代理深入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.hspedu.spring.aop.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
* @author 韩顺平
* @version 1.0
* 可以返回一个动态代理对象, 可以执行SmartDog对象的方法
*/
public class MyProxyProvider {

//定义我们要执行的目标对象, 该对象需要实现SmartAnimalable
private SmartAnimalable target_obj;

//构造器
public MyProxyProvider(SmartAnimalable target_obj) {
this.target_obj = target_obj;
}

//方法, 可以返回代理对象,该代理对象可以执行目标对象
public SmartAnimalable getProxy() {

//1. 先到的类加载器/对象
ClassLoader classLoader = target_obj.getClass().getClassLoader();

//2. 得到要执行的目标对象的接口信息
Class<?>[] interfaces = target_obj.getClass().getInterfaces();

//3. 创建InvocationHandler
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
System.out.println("方法执行前-日志-方法名-" + method.getName() + "-参数 "
+ Arrays.asList(args)); //这里从AOP看,就是一个横切关注点-前置通知
//使用反射调用方法
result = method.invoke(target_obj, args);
System.out.println("方法执行正常结束-日志-方法名-" + method.getName() + "-结果result= "
+ result);//从AOP看, 也是一个横切关注点-返回通知

} catch (Exception e) {
e.printStackTrace();
//如果反射执行方法时,出现异常,就会进入到catch{}
System.out.println("方法执行异常-日志-方法名-" + method.getName()
+ "-异常类型=" + e.getClass().getName());//从AOP看, 也是一个横切关注点-异常通知
} finally {//不管你是否出现异常,最终都会执行到finally{}
//从AOP的角度看, 也是一个横切关注点-最终通知
System.out.println("方法最终结束-日志-方法名-" + method.getName());
}

return result;
}
};

//创建代理对象
SmartAnimalable proxy =
(SmartAnimalable)Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
return proxy;
}
}

3.4 老韩分析: 问题再次出现

3.4.1 问题提出

​ ● 问题提出

  1. 在 MyProxyProvider.java 中, 我们的输出语句功能比较弱,在实际开发中,我们希望是以一个方法的形式,嵌入到真正执行的目标方法前,怎么办?
  2. 也就是如图分析

image-20230725164754136

3.4.2 用老韩的土方法解决

  1. 需求分析: 使用老韩的土方法解决前面的问题 => 后面使用 Spring 的 AOP 组件完成, 先过苦日子, 再过甜日子

  2. 先新建一个包,把相关文件拷贝过来,进行修改完成,思路更加清晰.

老师学习小技巧:新建一个包,保留原来的代码

具体实现方式省略…………把红框里的代码在同一类中自行封装成一个个方法即可

  1. 该方法问题分析:耦合度高

3.4.3 对土方法解耦-开发简易的 AOP 类

把这些方法抽出,放到一个类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author 韩顺平
* @version 1.0
* 我们自己编写一个一个极简的AOP类
*/
public class HspAOP {

//我们一个方法,在目标对象执行前执行
public static void before(Object proxy, Method method, Object[] args) {
System.out.println("HspAOP-方法执行前-日志-方法名-" + method.getName() + "-参数 "
+ Arrays.asList(args)); //这里从AOP看,就是一个横切关注点-前置通知
}

//我们一个方法,在目标对象执行后执行
public static void after(Method method, Object result) {
System.out.println("HspAOP-方法执行正常结束-日志-方法名-" + method.getName() + "-结果result= " + result);//从AOP看, 也是一个横切关注点-返回通知
}
}

3.4.4 再次分析-提出 Spring AOP

3.4.4.1 土方法 不够灵活

3.4.4.2 土方法 复用性差

3.4.4.3 土方法 还是一种硬编码(因为没有注解和反射支撑)

3.4.4.4 Spring AOP 闪亮登场-底层是 ASPECTJ

3.4.4.5 有了前面的技术引导, 理解 Spring AOP 就水到渠成

3.5 AOP 的基本介绍

​ ● 什么是 AOP

​ AOP 的全称(aspect oriented programming) ,面向切面编程

​ ● 2 张示意图说明 AOP 的相关概念

  1. 一张简易图说明 AOP

image-20230725224710622

  1. 一张详细图说明 AOP

image-20230725225233953

● AOP 实现方式

  1. 基于动态代理的方式[内置 aop 实现]

  2. 使用框架 aspectj 来实现

3.6 AOP 编程快速入门

3.6.1 基本说明

​ ● 说明

  1. 需要引入核心的 aspect 包

  2. 在切面类中声明通知方法

    1. 前置通知:@Before
    2. 返回通知:@AfterReturning
    3. 异常通知:@AfterThrowing
    4. 后置通知:@After
    5. 环绕通知:@Around

3.6.2 快速入门实例

​ ● 需求说明

​ 我们使用 aop 编程的方式,来实现手写的动态代理案例效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.hspedu.spring.aop.aspectj;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
* 切面类 , 类似于我们以前自己写的MyProxyProvider,但是功能强大很多
*/
@Order(value = 1)
@Aspect //表示是一个切面类[底层切面编程的支撑(动态代理+反射+动态绑定...)]
@Component //会将SmartAnimalAspect3注入到容器
public class SmartAnimalAspect3 {
//希望将f1方法切入到SmartDog-getSum前执行-前置通知

/**
* 老师解读
* 1. @Before 表示前置通知:即在我们的目标对象执行方法前执行
* 2. value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float)
* 指定切入到哪个类的哪个方法 形式是: 访问修饰符 返回类型 全类名.方法名(形参列表)
* 3. showBeginLog方法可以理解成就是一个切入方法, 这个方法名是可以程序员指定 比如:showBeginLog
* 4. JoinPoint joinPoint 在底层执行时,由AspectJ切面框架,会给该切入方法传入joinPoint对象, 通过 * 该方法,程序员可以获取到相关信息
*
* @param joinPoint
*/
@Before(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))")
public void showBeginLog(JoinPoint joinPoint) {
//通过连接点对象joinPoint 可以获取方法签名
Signature signature = joinPoint.getSignature();
System.out.println("SmartAnimalAspect3-切面类showBeginLog()-方法执行前-日志-方法名-" + signature.getName() + "-参数 "
+ Arrays.asList(joinPoint.getArgs()));
}

//返回通知:即把showSuccessEndLog方法切入到目标对象方法正常执行完毕后的地方
//老韩解读
//1. 如果我们希望把目标方法执行的结果,返回给切入方法
//2. 可以再 @AfterReturning 增加属性 , 比如 returning = "res"
//3. 同时在切入方法增加 Object res
//4. 注意: returning = "res" 和 Object res 的 res名字一致
@AfterReturning(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))", returning = "res")
public void showSuccessEndLog(JoinPoint joinPoint, Object res) {
Signature signature = joinPoint.getSignature();
System.out.println("SmartAnimalAspect3-切面类showSuccessEndLog()-方法执行正常结束-日志-方法名-" + signature.getName() + " 返回的结果是=" + res);
}



//异常通知:即把showExceptionLog方法切入到目标对象方法执行发生异常的的catch{}
@AfterThrowing(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))", throwing = "throwable")
public void showExceptionLog(JoinPoint joinPoint, Throwable throwable) {
Signature signature = joinPoint.getSignature();
System.out.println("SmartAnimalAspect3-切面类showExceptionLog()-方法执行异常-日志-方法名-" + signature.getName() + " 异常信息=" + throwable);
}

//最终通知:即把showFinallyEndLog方法切入到目标方法执行后(不管是否发生异常,都要执行 finally{})
@After(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))")
public void showFinallyEndLog(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
System.out.println("SmartAnimalAspect3-切面类showFinallyEndLog()-方法最终执行完毕-日志-方法名-" + signature.getName());
}
}
1
2
<!-- 使用AOP时,记得在beans.xml中添加以下代码,以开启基于注解的 AOP 功能 -->
<aop:aspectj-autoproxy/>

3.6.3 细节说明

  1. 关于切面类方法命名可以自己规范一下, 比如 showBeginLog(), showSuccessEndLog(), showExceptionLog() , showFinallyEndLog()

  2. 切入表达式的更多配置,比如使用模糊配置@Before(value=”execution(* com.hspedu.aop.proxy.SmartDog.*(..))”)

  3. 表示所有访问权限,所有包的下所有有类的所方法,都会被执行该前置通知方法

​ @Before(value=”execution(* .(..))”)

  1. 当 spring 容器开启了 < aop:aspectj-autoproxy/> , 我们获取注入的对象, 需要以接口的类型来获取, 因为你注入的对象.getClass() 已经是代理类型了!
  2. 当 spring 容器开启了 < aop:aspectj-autoproxy/> , 我们获取注入的对象, 也可以通过 id 来获取, 但是也要转成接口类型

3.7 AOP-切入表达式

3.7.1 具体使用

image-20230726083322753

3.7.2 注意事项和细节

  1. 切入表达式可以指向类的方法, 这时切入表达式会对该类/对象生效 (类名.*)

  2. 切入表达式也可以指向接口的方法, 这时切入表达式会**对实现了接口的类/对象生效**

  3. 切入表达式也可以对没有实现接口的类,进行切入

  4. 老师补充: 动态代理 jdk 的 Proxy 与 Spring 的 CGlib

🌟https://www.cnblogs.com/threeAgePie/p/15832586.html

—对没有实现接口的类生成的代理是CGlib动态代理

两个动态代理的区别

  1. JDK动态代理是面向接口的,只能增强实现类中接口中存在的方法。CGlib是面向父类的,可以增强父类的所有方法
  2. JDK得到的对象是JDK代理对象实例,而CGlib得到的对象是被代理对象的子类

3.8 AOP-JoinPoint

3.8.1 应用实例

​ ● 通过 JoinPoint 可以获取到调用方法的签名

​ ● 应用实例需求

说明: 在调用前置通知获取到调用方法的签名, 和其它相关信息

​ ● 应用实例-代码实现

​ 前面我们已经举例说明过了

​ ● 其它常用方法一览

1
2
3
4
5
6
7
8
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(); // 获取被代理的对象joinPoint.getThis(); // 获取代理对象自己
}

3.9 AOP-返回通知获取结果

3.9.1 应用实例

​ ● 如何在返回通知方法获取返回结果

​ 看一个需求:

​ 在返回通知方法获取返回的结果

1
2
3
4
5
6
7
8
9
//1. 如果我们希望把目标方法执行的结果,返回给切入方法
//2. 可以再 @AfterReturning 增加属性 , 比如 returning = "res"
//3. 同时在切入方法增加 Object res
//4. 注意: returning = "res" 和 Object res 的 res名字一致
@AfterReturning(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))", returning = "res")
public void showSuccessEndLog(JoinPoint joinPoint, Object res) {
Signature signature = joinPoint.getSignature();
System.out.println("SmartAnimalAspect3-切面类showSuccessEndLog()-方法执行正常结束-日志-方法名-" + signature.getName() + " 返回的结果是=" + res);
}

3.10 AOP-异常通知中获取异常

同理……

1
2
3
4
5
6
//异常通知:即把showExceptionLog方法切入到目标对象方法执行发生异常的的catch{}
@AfterThrowing(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))", throwing = "throwable")
public void showExceptionLog(JoinPoint joinPoint, Throwable throwable) {
Signature signature = joinPoint.getSignature();
System.out.println("SmartAnimalAspect3-切面类showExceptionLog()-方法执行异常-日志-方法名-" + signature.getName() + " 异常信息=" + throwable);
}

3.11 AOP-环绕通知【了解】

3.11.1 应用实例

​ ● 环绕通知可以完成其它四个通知要做的事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//演示环绕通知的使用-了解
//老师解读
//1. @Around: 表示这是一个环绕通知[完成其它四个通知的功能]
//2. value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float)) 切入点表达式
//3. doAround 表示要切入的方法 - 调用结构 try-catch-finally
@Around(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object result = null;
String methodName = joinPoint.getSignature().getName();
try {
//1.相当于前置通知完成的事情
Object[] args = joinPoint.getArgs();
List<Object> argList = Arrays.asList(args);
System.out.println("AOP环绕通知[-前置通知]" + methodName + "方法开始了--参数有:" + argList);
//在环绕通知中一定要调用joinPoint.proceed()来执行目标方法
result = joinPoint.proceed();
//2.相当于返回通知完成的事情
System.out.println("AOP环绕通知[-返回通知]" + methodName + "方法结束了--结果是:" + result);
} catch (Throwable throwable) {
//3.相当于异常通知完成的事情
System.out.println("AOP环绕通知[-异常通知]" + methodName + "方法抛异常了--异常对象:" + throwable);
} finally {
//4.相当于最终通知完成的事情
System.out.println("AOP环绕通知[-后置通知]" + methodName + "方法最终结束了...");
}
return result;
}

3.12 AOP-切入点表达式重用

3.12.1 应用实例

● 切入点表达式重用(其实就是把切入表达式提取出来,这样可以避免当一个方法需要多次切入时,每个切入方法都需要重写一遍切入表达式的情况)

为了统一管理切入点表达式,可以使用切入点表达式重用技术

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//定义一个切入点, 在后面使用时可以直接引用, 提高了复用性
@Pointcut(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float)))")
public void myPointCut() {
}

//希望将myPointCut方法切入到SmartDog-getSum前执行-前置通知

/**
* 老师解读
* 1. @Before 表示前置通知:即在我们的目标对象执行方法前执行
* 2. value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float)
* 指定切入到哪个类的哪个方法 形式是: 访问修饰符 返回类型 全类名.方法名(形参列表)
* 3. showBeginLog方法可以理解成就是一个切入方法, 这个方法名是可以程序员指定 比如:showBeginLog
* 4. JoinPoint joinPoint 在底层执行时,由AspectJ切面框架, 会给该切入方法传入 joinPoint对象
* , 通过该方法,程序员可以获取到 相关信息
*
* @param joinPoint
*/
//@Before(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))")
//这里我们使用定义好的切入点
@Before(value = "myPointCut()")
public void showBeginLog(JoinPoint joinPoint) {
//通过连接点对象joinPoint 可以获取方法签名
Signature signature = joinPoint.getSignature();
System.out.println("SmartAnimalAspect-切面类showBeginLog()[使用的myPointCut()]-方法执行前-日志-方法名-" + signature.getName() + "-参数 "
+ Arrays.asList(joinPoint.getArgs()));
}

//返回通知:即把showSuccessEndLog方法切入到目标对象方法正常执行完毕后的地方
//老韩解读
//1. 如果我们希望把目标方法执行的结果,返回给切入方法
//2. 可以再 @AfterReturning 增加属性 , 比如 returning = "res"
//3. 同时在切入方法增加 Object res
//4. 注意: returning = "res" 和 Object res 的 res名字一致
//@AfterReturning(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))", returning = "res")
//使用切入点
@AfterReturning(value = "myPointCut()", returning = "res")
public void showSuccessEndLog(JoinPoint joinPoint, Object res) {
Signature signature = joinPoint.getSignature();
System.out.println("SmartAnimalAspect-切面类showSuccessEndLog()-方法执行正常结束-日志-方法名-" + signature.getName() + " 返回的结果是=" + res);
}


//异常通知:即把showExceptionLog方法切入到目标对象方法执行发生异常的的catch{}
//@AfterThrowing(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))", throwing = "throwable")
//直接使用切入点表达式
@AfterThrowing(value = "myPointCut()", throwing = "throwable")
public void showExceptionLog(JoinPoint joinPoint, Throwable throwable) {
Signature signature = joinPoint.getSignature();
System.out.println("SmartAnimalAspect-切面类showExceptionLog()-方法执行异常-日志-方法名-" + signature.getName() + " 异常信息=" + throwable);
}

//最终通知:即把showFinallyEndLog方法切入到目标方法执行后(不管是否发生异常,都要执行 finally{})
//@After(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))")
//直接使用切入点
@After(value = "myPointCut()")
public void showFinallyEndLog(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
System.out.println("SmartAnimalAspect-切面类showFinallyEndLog()-方法最终执行完毕-日志-方法名-" + signature.getName());
}

3.13 AOP-切面优先级问题

3.13.1 应用实例

​ ● 切面优先级问题:

​ 如果同一个方法,有多个切面在同一个切入点切入,那么执行的优先级如何控制.

​ ● 基本语法:

​ @order(value=n) 来控制 n 值越小,优先级越高.

3.13.2 注意事项和细节说明

  1. 不能理解成: 优先级高的每个消息通知都先执行, 这个和方法调用机制(和 Filter 过滤器链式调用类似)

优先级越高,前置通知先执行(队列),但其他的通知都是最后执行(栈)

image-20230726092923563

3.14 AOP-基于 XML 配置 AOP

3.14.1 应用实例

​ ● 基本说明:

​ 前面我们是通过注解来配置 aop 的, 在 spring 中, 我们也可以通过 xml 的方式来配置 AOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

<!--使用XML配置,完成AOP编程-->
<!--配置一个切面类对象-bean-->
<bean class="com.hspedu.spring.aop.xml.SmartAnimalAspect" id="smartAnimalAspect"/>
<!--配置一个SmartDog对象-bean-->
<bean class="com.hspedu.spring.aop.xml.SmartDog" id="smartDog"/>
<!--配置切面类, 细节一定要引入 xmlns:aop-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="myPointCut" expression="execution(public float com.hspedu.spring.aop.xml.SmartDog.getSum(float, float)))"/>
<!--配置切面的前置,返回, 异常, 最终通知-->
<aop:aspect ref="smartAnimalAspect" order="10">
<!--配置前置通知-->
<aop:before method="showBeginLog" pointcut-ref="myPointCut"/>
<!--返回通知-->
<aop:after-returning method="showSuccessEndLog" pointcut-ref="myPointCut" returning="res"/>
<!--异常通知-->
<aop:after-throwing method="showExceptionLog" pointcut-ref="myPointCut" throwing="throwable"/>
<!--最终通知-->
<aop:after method="showFinallyEndLog" pointcut-ref="myPointCut"/>
<!--配置环绕通知-->
<!--<aop:around method=""/>-->
</aop:aspect>
</aop:config>
</beans>

4 手动实现 Spring 底层机制【 初始化 IOC容器+依赖注入+BeanPostProcessor 机制+AOP】

4.1.1 引言: 前面我们实际上已经用代码简单实现了

4.1.1.1 Spring XML 注入 bean

4.1.1.2 Spring 注解方式注入 bean

4.1.1.3 Spring AOP 动态代理实现

4.1.2 继续思考-原生 Spring 如何实现依赖注入和 singleton、 prototype

4.1.2.1 实例演示-Maven 项目

其实就是第二章,去看一下即可……

​ ● 快速给小伙伴完成这个小案例(提示: 创建新项目, 不和原来的 Spring 混在一起)

  1. 创建工程
  2. 修改 pom.xml, 引入需要的 jar 包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<!--加入spring开发的基本包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.8</version>
</dependency>
<!--加入spring开发切面编程需要的包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.8</version>
</dependency>
</dependencies>
  1. 创 建UserAction.java
1
2
3
4
5
6
7
8
9
10
11
/**
* 就是一个Controller
*/
//也可以使用@Controller
//在默认情况下 我们配置@Component @Controller @Service @Repository 是单例
//@Scope(value = "prototype") 表示以多实例形式,返回UserAction bean
//老师思考:Spring容器底层如何实现
@Component
//@Scope(value = "prototype")
public class UserAction {
}
  1. 创 建UserDao.java
1
2
3
4
5
6
7
8
//可以使用@Repository
@Component
public class UserDao {

public void hi() {
System.out.println("UserDao-hi()---");
}
}
  1. 创 建UserService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//也可以使用@Service
@Component
public class UserService {

//定义属性
//老师思考:加入 @Autowired , Spring容器时如何实现依赖注入?
//也可以使用@Resource
@Autowired
private UserDao userDao;

public void m1() {
userDao.hi();
}

//这里我们需要指定init() 是初始化方法
@PostConstruct
public void init() {
System.out.println("UserService-init()");
}

}
  1. 创建 beans.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">


<!--配置自动扫描的包, 同时引入对应的名称空间-->
<!--老师说明:
1. 如果我们是普通的java项目, beans.xml 放在src下
2. 如果我们是java maven 项目, beans.xml 放在 src/main/resources
-->
<context:component-scan base-package="com.hspedu.spring.component"/>
<context:component-scan base-package="com.hspedu.spring.aop"/>

<!--启用基于注解方式的AOP功能-->
<aop:aspectj-autoproxy/>

<!--配置后置处理器-->
<bean class="com.hspedu.spring.process.MyBeanPostProcessor" id="myBeanPostProcessor"/>
</beans>
  1. 创建 AppMain.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class AppMain {
public static void main(String[] args) {
//测试看看是否可以得到spring容器中的bean , 同时看看依赖注入是否OK

ApplicationContext ioc =
new ClassPathXmlApplicationContext("beans.xml");
UserAction userAction = (UserAction) ioc.getBean("userAction");
UserAction userAction2 = (UserAction) ioc.getBean("userAction");

System.out.println("userAction=" + userAction);
System.out.println("userAction2=" + userAction2);

UserDao userDao = (UserDao) ioc.getBean("userDao");
System.out.println("userDao=" + userDao);

UserService userService = (UserService) ioc.getBean("userService");
System.out.println("userService=" + userService);

//测试一下当前的依赖注入
userService.m1();

//测试一下AOP
SmartAnimalable smartDog = ioc.getBean(SmartAnimalable.class);
smartDog.getSum(10, 2);

}
}

4.1.2.2 思考问题

4.1.2.2.1 Spring 底层实现, 如何实现 IOC 容器创建和初始化【前面我们实现过, 现在要再深入】
4.1.2.2.2 Spring 底层实现, 如何实现 getBean, 根据 singleton 和 prototype 来返回 bean 实例

4.1.3 继续思考-原生 Spring 如何实现 BeanPostProcessor

4.1.3.1 实例演示

  1. 创 建MyBeanPostProcessor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//@Component
public class MyBeanPostProcessor implements BeanPostProcessor {

/**
* 在Bean的 init初始化方法前调用-> 这个知识点,在前面讲解后置处理器时讲过的
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

System.out.println("postProcessBeforeInitialization 被 调 用 " + beanName + " bean= " + bean.getClass());
return bean;
}

/**
* 在Bean的 init初始化方法后调用-> 这个知识点,在前面讲解后置处理器时讲过的
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("postProcessAfterInitialization 被 调 用 " + beanName + " bean= " + bean.getClass());
return bean;
}
}

4.1.3.2 思考问题

4.1.3.2.1 Spring 底层实现, 如何实现 Bean 后置处理器机制

——当然是动态代理呀

4.1.4 继续思考-原生 Spring 是如何实现 AOP

4.1.4.1 实例演示

看第三章……

4.1.4.2 简单分析 AOP 和 BeanPostProcessor 关系

……

老韩解读:

  • AOP 底层是基于 BeanPostProcessor 机制的.
  • 即在 Bean 创建好后,根据是否需要 AOP 处理,决定返回代理对象,还是原生 Bean
  • 在返回代理对象时,就可以根据要代理的类和方法来返回
  • 其实这个机制并不难,本质就是在 BeanPostProcessor 机制 + 动态代理技术

4.1.4.3 思考问题

4.1.4.3.1 Spring 底层实现, 如何实现 AOP 编程

4.1.5 我们的目标: 不用 Spring 框架, 模拟 Spring 底层实现, 也能完成相同的功能

4.2 🌟Spring 整体架构分析

4.2.1 🌟一图胜千言

image-20230727094752719

4.3 手动实现 Spring 底层机制

【初始化 IOC 容器+依赖注入+BeanPostProcessor 机制+AOP】

4.3.1 实现任务阶段 1- 编写自己 Spring 容器, 实现扫描包, 得到 bean 的 class 对象

4.3.1.1 知识扩展: 类加载器

​ ● java 的类加载器 3 种

​ Bootstrap 类加载器————–对应路径 jre/lib

​ Ext 类加载器——————–对应路径 jre/lib/ext

​ App 类加载器——————-对应路径 classpath

4.3.1.2 说明: 编写自己 Spring 容器, 实现扫描包, 得到 bean 的 class 对象

🌟看p12-P41

先了解spring底层机制 具体实现日后有空再说

JDBC等知识由于之后都用mybatis等工具,也先不学

事务先不学了,之后补!

5.Spring面试准备

IOC实现原理总结:

具体来说,Spring IOC 的实现过程如下:

  1. 读取配置文件或解析注解信息,将其转换为内部的对象定义和依赖关系。在 Spring 中,可以使用 XML 文件或注解来配置对象和依赖关系。Spring 通过解析配置文件或注解信息,将其转换为内部的对象定义和依赖关系(BeanDefinition)放到容器(BeanFactory)中。对象定义包括对象的类型、属性、构造函数等信息,依赖关系包括对象之间的依赖关系、依赖注入方式等信息。
  2. 实例化bean对象:Spring 会根据对象定义的类型和构造函数信息,使用反射机制来创建对象。
  3. 设置属性:实例化后的仍然是一个原生的状态,并没有进行依赖注入。 这一步Spring根据BeanDefinition中的信息进行属性填充,依赖注入。
  4. 调用Aware接口:Spring会检测该对象是否实现了xxxAware接口,如果有会在这里执行完成。Aware主要是能获取到Spring容器中的一些资源,然后可以供后续步骤,例如初始化阶段使用。
  5. BeanPostProcessor前置处理:postProcessBeforeInitialzation方法。上述几个步骤后,bean对象已经被正确构造,但如果想要对象被初始化前再进行一些自定义的处理,就可以通过BeanPostProcessor接口的该方法来实现。
  6. 初始化阶段:该阶段Spring首先会看是否是实现了InitializingBean接口的afterPropertiesSet方法以及是否有自定义的init-method等,如果有会进行调用执行。
  7. BeanPostProcessor后置处理:postProcessAfterInitialzation方法。当前正在初始化的bean对象会被传递进来,我们就可以对这个bean作任何处理,与前面前置处理相对的,这个函数会在InitialzationBean完成后执行,因此称为后置处理。
  8. bean初始化完成可以被使用了。

总的来说,Spring IOC 的实现原理是通过反射机制动态创建对象,依赖注入,对象初始化。通过解耦对象之间的依赖关系,使得应用程序更加灵活、可维护、可扩展。

BeanFactory和ApplicationContext的关系

BeanFactory和ApplicationContext是Spring框架中的两个重要的接口。它们都用于管理Spring Bean对象,但是它们在功能上有一些不同点。

BeanFactory是Spring框架中最基本的容器,它提供了最基础的IOC和DI的支持,它的主要功能是用于创建、管理和查找Bean对象。BeanFactory只是个接口,并不是IOC容器的具体实现, 它为其他具体的IOC容器提供了最基本的规范,例如DefaultListableBeanFactory,ApplicationContext 等容器实现或容器接口都是基于BeanFactory,再在其基础之上附加了其他的功能,原始的BeanFactory无法支持spring的许多插件,如AOP功能、Web应用等。原始BeanFactory是延时加载,也就是说在容器启动时不会注入bean,而是在需要使用bean的时候,才会对该bean进行加载实例化。

ApplicationContext接口是基于BeanFactory扩展而来,也是一个容器接口,具有BeanFactory所有的功能,同时继承了MessageSource,所以提供了更完整的框架功能,支持国际化、资源文件访问、载入多个上下文配置文件,使得每一个上下文都专注于一个特定层次,提供在监听器中注册bean事件。ApplicationContext 是预加载,在容器启动的时候一次性加载所有的bean,所以运行的时候速度相对BeanFactory比较快,缺点就是耗内存。

总的来说,BeanFactory是Spring框架中最基本的容器,提供最基础的IOC和DI的支持;而ApplicationContext是在BeanFactory的基础上扩展而来的,提供了更多的功能和特性。ApplicationContext是Spring框架中使用较为广泛的容器。

谈谈你对AOP的理解?

Spring AOP(面向切面编程)是 Spring 框架中的一个重要模块,用于解决系统中的横切关注点(cross-cutting concerns)问题。所谓横切关注点,指的是系统中分散在各个模块中、与主业务逻辑无关的代码,例如日志记录、事务管理、权限控制等。

Spring AOP 采用代理模式实现,它通过在运行期间动态代理目标对象,将横切关注点织入到系统中,从而实现了业务逻辑与横切关注点的分离。Spring AOP 主要由以下几个概念组成:

  1. 切面(Aspect):切面是一个类,它包含了一组横切关注点和相应的逻辑。一个切面通常会跨越多个对象,因此它不仅定义了横切关注点,还定义了横切关注点与业务逻辑的关系。
  2. 连接点(Join Point):连接点是在程序执行期间可以插入切面的点。例如方法调用、异常抛出等。
  3. 切入点(Pointcut):切入点是一组连接点的集合,它定义了在哪些连接点上应用切面。例如所有的方法调用、所有的异常抛出等。
  4. 通知(Advice):通知是切面在特定连接点执行的代码。Spring AOP 提供了五种类型的通知:前置通知(Before)、后置通知(After)、返回通知(After-returning)、异常通知(After-throwing)和环绕通知(Around)。
  5. 切面织入(Weaving):切面织入是将切面应用到目标对象并创建代理对象的过程。

Spring AOP 通过配置文件或注解的方式来定义切面、连接点、切入点和通知等信息,并使用代理模式将切面织入到目标对象中。通过 AOP 技术,可以有效地解耦业务逻辑和横切关注点,提高了系统的可维护性和可扩展性。

动态代理了解吗?

Java动态代理是Java中一种重要的代理模式,它允许在运行时动态地生成代理类和对象,无需编写静态代理类。

在Java中,动态代理可以通过Java自带的两种方式实现:基于接口的动态代理和基于类的动态代理。

  1. 基于接口的动态代理

基于接口的动态代理是Java官方提供的一种动态代理实现方式。在这种实现方式中,代理类必须实现一个或多个接口,然后在运行时动态创建代理对象。JDK中提供了一个Proxy类和一个InvocationHandler接口来实现基于接口的动态代理。

首先,需要定义一个实现InvocationHandler接口的代理类,该类实现了代理类的逻辑。这个类中有一个invoke方法,这个方法在代理类的方法被调用时被执行。在运行时通过Proxy类的静态方法newProxyInstance生成代理类对象。这个方法需要三个参数:ClassLoader、代理类需要实现的接口数组和InvocationHandler实现类的实例。当通过代理类对象调用方法时,这个方法首先被转发到InvocationHandler的invoke方法中。在invoke方法中,可以根据代理类方法的不同来执行不同的逻辑,包括调用被代理对象的方法和执行其他的逻辑。最终,代理类的方法被执行完毕,返回结果。

  1. 基于类的动态代理

基于类的动态代理是通过字节码生成技术实现的。在这种实现方式中,代理类不需要实现接口,而是通过继承一个已有的类来实现代理功能。在Java中,可以通过CGLIB库实现基于类的动态代理。

CGLIB(Code Generation Library)是一个高性能的代码生成库,它可以在运行时动态生成字节码来实现类的增强功能。通过CGLIB库,可以直接在运行时创建目标对象的子类,从而实现基于类的动态代理。

基于类的动态代理相比于基于接口的动态代理,可以代理那些没有实现任何接口的类,更加灵活。但是它的实现原理比较复杂,需要在运行时动态生成字节码,会带来一定的性能开销。

SpringMVC的执行流程了解吗?

image-20230815153451136

SpringMVC是基于MVC设计模式实现的Web框架,其工作流程如下:

  1. 客户端发送HTTP请求至前端控制器DispatcherServlet。
  2. DispatcherServlet根据请求信息调用HandlerMapping,解析请求对应的Handler即处理器(Controller)。
  3. HandlerMapping根据请求URL查找对应的Controller,同时生成用于执行该请求的HandlerExecutionChain对象(包含Interceptor链)。
  4. DispatcherServlet调用HandlerAdapter执行Handler。在执行过程中,HandlerAdapter将把ModelAndView对象传递给DispatcherServlet。
  5. Handler执行完成后,返回一个ModelAndView对象给HandlerAdapter。
  6. HandlerAdapter将ModelAndView对象传递给DispatcherServlet。
  7. DispatcherServlet调用ViewResolver解析视图(View)。
  8. ViewResolver解析出View对象后,将其返回给DispatcherServlet。
  9. DispatcherServlet调用View对象的render()方法进行视图渲染。
  10. DispatcherServlet将渲染后的视图返回给客户端。

在这个过程中,DispatcherServlet是整个SpringMVC的核心,它负责协调各个组件的工作。HandlerMapping负责将请求映射到对应的Controller,而HandlerAdapter负责执行Controller。ViewResolver则根据逻辑视图名(如JSP文件名)解析出View对象,最后由View渲染出实际的页面内容。通过这种分工协作的方式,SpringMVC可以实现灵活、高效、可扩展的Web应用程序开发。