# Spring

# 第 1 章 Spring 基本介绍

# 1.1 官方资料

# 1.2 Spring 学习的核心内容

(1)Spring 核心学习内容 IOC、AOP、JDBCTemplate、声明式事务

(2)IOC:控制反转,可以管理 Java 对象

(3)AOP:切面编程

(4)JDBCTemplate:是 Spring 提供的一套访问数据库的技术

(5)声明式事务:基于 IOC/AOP 实现事务管理

# 1.3 Spring 几个重要概念

(1)Spring 可以整合其他的框架(Spring 是管理框架的框架)

(2)Spring 有两个核心的概念:IOC 和 AOP

(3)IOC(Inversion Of Control 反转控制)

(4)传统的开发模式(比如 JDBCUtils / 反射)

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

​ 以连接到数据库为例说明:

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

​ 2)创建对象,new XxxObject()

​ 3)使用对象完成任务

(5)IOC 的开发模式

​ 程序 <--- 容器 //容器创建好对象数据,程序直接使用(IOC 控制反转)

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

​ 2)当需要使用某个对象实例的时候,传统的方法是 new Xxxx(),IOC 的开发模式在需要使用某个对象实例时直接从容器中获取即可

(6)DI(Dependency Injection 依赖注入),可以理解成是 IOC 的另外的叫法

(7)Spring 最大的价值就是通过配置给程序提供需要使用的各个对象,可以实现解耦

# 1.4 Spring 快速入门

# 1.4.1 需求说明

通过 Spring 的方式获取 JavaBean 中的 Monster 对象,并给该对象的属性赋值,输出该对象的信息

# 1.4.2 完成步骤

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">

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

    public Monster(Integer monsterId, String name, String skill) {
        this.monsterId = monsterId;
        this.name = name;
        this.skill = skill;
    }

    //无参构造器一定要写,Spring 反射创建对象时需要使用
    public Monster() {
    }

    public Integer getMonsterId() {
        return monsterId;
    }

    public void setMonsterId(Integer monsterId) {
        this.monsterId = monsterId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSkill() {
        return skill;
    }

    public void setSkill(String skill) {
        this.skill = skill;
    }

    @Override
    public String toString() {
        return "Monster{" +
                "monsterId=" + monsterId +
                ", name='" + name + '\'' +
                ", skill='" + skill + '\'' +
                '}';
    }
}
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
public class SpringBeanTest {
    @Test
    public void getMonster() {
        //(1) 创建容器 ApplicationContext
        //(2) 该容器和容器配置文件关联
        //(3) ClassPathXmlApplicationContext 类的作用可以简单理解成用于解析 XML 文件,获取 bean 的信息,然后根据类的全路径通过反射创建对象后放入到 HashMap 中,并且提供方法可以通过 id 获取到该对象
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        //(3) 通过 getBean 获取对应的对象,此方法获取到的对象是 Object 类型的
        //Object monster01 = ioc.getBean("monster01");
        Monster monster01 = (Monster) ioc.getBean("monster01");
        //(4) 输出
        System.out.println("monster01" + monster01 + " 运行类型 = " + monster01.getClass());
        System.out.println("monster01" + monster01 + " 获取对应的属性 name = " + monster01.getName() + " monsterid = " + monster01.getMonsterId());
        //(5) 也可以在获取的时候,直接指定 Class 类型
        Monster monster011 = ioc.getBean("monster01", Monster.class);
        System.out.println("monster011 = " + monster011);
        System.out.println("monster011.name = " + monster011.getName());
        System.out.println("ok~~~");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 1.4.3 注意事项和细节

(1)ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml"); 是怎么读取到 beans.xml 的?

//验证类加载路径
    public void classPath() {
        File file = new File(this.getClass().getResource("/").getPath());
        //看到类的加载路径
        System.out.println("file = " + file); //file = D:\Study\Code\IDEA\hspedu_spring\spring\out\production\spring
    }
1
2
3
4
5
6

(2)debug 看 Spring 容器结构/机制

beanDefinitionMap 类型是 ConcurrentHashMap 集合,存放 beans.xml 中的 bean 节点配置的 bean 对象的信息

在 beanDefinitionMap 中有属性 table,table 是数组,类型是 ConcurrentHashMap$Node,因为是数组,所以可以存放很多的 bean 对象信息,就是 beans.xml 配置,初始化大小为 512,当超过时会自动扩容

通过 hash 算法,我们的 Monster01 对象的信息就保存在 index=217 的位置,保存方式是以 ConcurrentHashMap$Node 类型保存,key 就是 beans.xml 中配置的 monster01,value 就是 monster01 对象的信息(比如属性/属性值/类信息/是不是懒加载)

这里就是记录 beans.xml 中配置的 monster01 对象的属性名/属性值

在 beanfactory 中,属性 singletonObjects 类型是 ConcurrentHashMap,还有一个属性 table,类型是 ConcurrentHashMap$Node,如果在 beans.xml 文件中配置的对象是单例的就会初始化放在 table 中

在 beanfactory 中,还有一个属性 beanDefinitionNames,记录了在 beans.xml 中配置的 bean 的名称,方便查找

(3)案例:查看容器注入了哪些 bean 对象,并输出 bean 的 id

//(6) 查看容器注入了哪些 bean 对象,并输出 bean 的 id
        String[] beanDefinitionNames = ioc.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println("beanDefinitionName = " + beanDefinitionName);
        }
1
2
3
4
5

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

# 1.5.1 需求说明

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

# 1.5.2 思路分析

# 1.5.3 代码实现

beans.xml

<bean class="com.hspedu.spring.bean.Monster" id="monster01">
	<property name="monsterId" value="100"/>
    <property name="name" value="牛魔王"/>
    <property name="skill" value="芭蕉扇"/>
</bean>
1
2
3
4
5
/**
 * (1) 这个程序用于实现 Spring 的一个简单容器机制
 * (2) 这里实现如何将 beans.xml 文件进行解析,并生成对象,放入到容器中
 * (3) 提供一个方法 getBean(id) 返回对应的对象
 * */
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();
        System.out.println("path = " + path);

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

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

        //4. 得到 rootDocument,得到根元素
        Element rootElement = document.getRootElement();

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

        //6. 获取到第一个 bean 的相关属性
        String id = bean.attributeValue("id");
        String classFullPath = bean.attributeValue("class");
        System.out.println(id);
        System.out.println(classFullPath);
        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 对象赋值
        //利用反射来赋值,这里简化一下,直接赋值,先理解流程
        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
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
public class HspApplicationContextTest {
    public static void main(String[] args) throws Exception {
        HspApplicationContext ioc = new HspApplicationContext("beans.xml");
        Monster monster01 = (Monster) ioc.getBean("monster01");
        System.out.println("monster01 = " + monster01);
        System.out.println("monster01.name = " + monster01.getName());
        System.out.println("ok");
    }
}
1
2
3
4
5
6
7
8
9

# 1.6 Spring 原生容器底层结构

# 1.6.1 图解

# 1.6.2 课后作业

(1)在 beans.xml 中,我们注入 2 个 Monster 对象,但是不指定 id,如下:

(2)问题1:运行会不会报错

答:不会报错,会正常运行

(3)问题2:如果不报错,能否找到分配的 id,并获取到该对象

答:系统会默认分配 id,分配 id 的规则是 全类名#0、全类名#1,按这样的规则来分配 id,可以通过 Debug 方式来查看

public class Homework01 {
    @Test
    public void getMonster() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        Monster monster01 = ioc.getBean("com.hspedu.spring.bean.Monster#0", Monster.class);
        System.out.println("monster01 = " + monster01);
        System.out.println("monster01.monsterId = " + monster01.getMonsterId());

        Monster monster02 = ioc.getBean("com.hspedu.spring.bean.Monster#1", Monster.class);
        System.out.println("monster02 = " + monster02);
        System.out.println("monster02.monsterId = " + monster02.getMonsterId());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 1.7 Spring 课堂练习

需求:创建一个 Car 类(id,name,price),具体要求如下:

(1)创建 ioc 容器文件,并配置一个 Car 对象(bean)

(2)通过 Java 程序到 ioc 容器获取该 bean 对象并输出

public class Car {
    private Integer id;
    private String name;
    private Integer price;

    public Car() {
    }

    public Car(Integer id, String name, Integer price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Car{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}
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
<?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">

    <!--配置 carbean-->
    <bean class="com.hspedu.spring.bean.Car" id="car01">
        <property name="id" value="100"/>
        <property name="name" value="宝马"/>
        <property name="price" value="1000000"/>
    </bean>
</beans>
1
2
3
4
5
6
7
8
9
10
11
12
public class Homework02 {
    public static void main(String[] args) {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("car_beans.xml");
        Car car01 = ioc.getBean("car01", Car.class);
        System.out.println("car01 = " + car01);
        System.out.println("car01.name = " + car01.getName());
    }
}
1
2
3
4
5
6
7
8

# 第 2 章 Spring 管理 Bean - IOC

# 2.1 Spring 配置/管理 bean 介绍

(1)Bean 管理包括两方面:

​ 1)创建 bean 对象

​ 2)给 bean 注入属性

(2)Bean 的配置方式:

​ 1)基于 xml 文件配置方式

​ 2)基于注解方式

# 2.2 基于 XML 文件获取和配置 bean

# 2.2.1 通过 id 来获取 bean

之前用的一直是通过 id 来获取 bean

# 2.2.2 通过类型来获取 bean

# 2.2.2.1 应用案例

案例说明:

(1)通过 Spring 的 ioc 容器,获取一个 bean 对象

(2)说明:通过类型来获取 bean

<!--配置 Monster,通过类型来获取-->
    <bean class="com.hspedu.spring.bean.Monster">
        <property name="monsterId" value="1001"/>
        <property name="name" value="牛魔王~"/>
        <property name="skill" value="芭蕉扇~"/>
    </bean>
1
2
3
4
5
6
//通过 Bean 的类型来获取对象
    public void getBeanByType() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        //这里直接传入 class 对象/类型
        Monster bean = ioc.getBean(Monster.class);
        System.out.println("bean = " + bean);
    }
1
2
3
4
5
6
7
# 2.2.2.2 细节说明

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

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

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

# 2.2.3 通过构造器配置 bean

# 2.2.3.1 说明

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

# 2.2.3.2 代码实现
public class Monster {
    private Integer monsterId;
    private String name;
    private String skill;

    public Monster(Integer monsterId, String name, String skill) {
        this.monsterId = monsterId;
        this.name = name;
        this.skill = skill;
    }

    //无参构造器一定要写,Spring 反射创建对象时需要使用
    public Monster() {
    }

    public Integer getMonsterId() {
        return monsterId;
    }

    public void setMonsterId(Integer monsterId) {
        this.monsterId = monsterId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSkill() {
        return skill;
    }

    public void setSkill(String skill) {
        this.skill = skill;
    }

    @Override
    public String toString() {
        return "Monster{" +
                "monsterId=" + monsterId +
                ", name='" + name + '\'' +
                ", skill='" + skill + '\'' +
                '}';
    }
}
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
<!--通过构造器配置 bean-->
    <!--
        (1) constructor-arg 标签可以指定使用构造器的参数
        (2) index 表示构造器的第几个参数,从 0 开始计算的
        (3) 除了可以通过 index 还可以通过 name / type 来指定参数方式
    -->
    <bean id="monster03" class="com.hspedu.spring.bean.Monster">
        <constructor-arg value="200" index="0"/>
        <constructor-arg value="白骨精" index="1"/>
        <constructor-arg value="吸人血" index="2"/>
    </bean>

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

    <bean id="monster05" class="com.hspedu.spring.bean.Monster">
        <constructor-arg value="400" type="java.lang.Integer"/>
        <constructor-arg value="白骨精3" type="java.lang.String"/>
        <constructor-arg value="吸人血3" type="java.lang.String"/>
    </bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2.2.4 通过 p 名称空间配置 bean

# 2.2.4.1 说明

在 Spring 的 ioc 容器中,可以通过 p 名称空间来配置 bean 对象

# 2.2.4.2 代码实现
public class Monster {
    private Integer monsterId;
    private String name;
    private String skill;

    public Monster(Integer monsterId, String name, String skill) {
        this.monsterId = monsterId;
        this.name = name;
        this.skill = skill;
    }

    //无参构造器一定要写,Spring 反射创建对象时需要使用
    public Monster() {
    }

    public Integer getMonsterId() {
        return monsterId;
    }

    public void setMonsterId(Integer monsterId) {
        this.monsterId = monsterId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSkill() {
        return skill;
    }

    public void setSkill(String skill) {
        this.skill = skill;
    }

    @Override
    public String toString() {
        return "Monster{" +
                "monsterId=" + monsterId +
                ", name='" + name + '\'' +
                ", skill='" + skill + '\'' +
                '}';
    }
}
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
<!--通过 p 名称空间来配置 bean-->
    <bean id="monster06" class="com.hspedu.spring.bean.Monster"
          p:monsterId="500"
          p:name="红孩儿"
          p:skill="吐火"
    />
1
2
3
4
5
6
//通过 Id 获取 bean
    public void setBeanByP() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        Monster monstor06 = ioc.getBean("monster06", Monster.class);
        System.out.println("monstor06 = " + monstor06);
    }
1
2
3
4
5
6

# 2.2.5 引用/注入其它 bean 对象

# 2.2.5.1 说明

在 Spring 的 ioc 容器中,可以通过 ref 来实现 bean 对象的相互引用

# 2.2.5.2 代码实现
public class MemberDAOImpl {
    public MemberDAOImpl() {
        System.out.println("MemberDAOImpl 构造器被调用");
    }

    public void add() {
        System.out.println("MemberDAOImpl add()方法");
    }
}
1
2
3
4
5
6
7
8
9
public class MemberServiceImpl {
    private MemberDAOImpl memberDAO;

    public MemberDAOImpl getMemberDAO() {
        return memberDAO;
    }

    public void setMemberDAO(MemberDAOImpl memberDAO) {
        this.memberDAO = memberDAO;
    }

    public void add() {
        System.out.println("MemberServiceImpl add() 被调用");
        memberDAO.add();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--配置 MemberDAOImpl 对象-->
    <bean class="com.hspedu.spring.dao.MemberDAOImpl" id="memberDAO"/>

    <!--配置 MemberServiceImpl 对象-->
    <!--
        (1) ref = "memberDAO" 表示 MemberServiceImpl 对象的属性 memberDAO 引用的是 id="memberDAO" 的对象
        (2) 这里体现出 Spring 容器的依赖注入
        (3) 注意在 Spring 容器中,它是作为一个整体来执行的,即如果引用到一个bean对象,对配置的顺序没有要求
    -->
    <bean class="com.hspedu.spring.service.MemberServiceImpl" id="memberService">
        <property name="memberDAO" ref="memberDAO"/>
    </bean>
1
2
3
4
5
6
7
8
9
10
11
12
public void setBeanByRef() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        MemberServiceImpl memberService = ioc.getBean("memberService", MemberServiceImpl.class);
        memberService.add();
    }
1
2
3
4
5

# 2.2.6 引用/注入内部 bean 对象

# 2.2.6.1 说明

在 Spring 的 ioc 容器,可以直接配置内部 bean 对象

# 2.2.6.2 代码实现
<!--配置 MemberDAOImpl 对象 - 使用内部 bean -->
    <bean class="com.hspedu.spring.service.MemberServiceImpl" id="memberService2">
        <!--自己配置一个内部 bean-->
        <property name="memberDAO">
            <bean class="com.hspedu.spring.dao.MemberDAOImpl" />
        </property>
    </bean>
1
2
3
4
5
6
7
//通过内部 bean 设置属性
    @Test
    public void setBeanByPro() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        MemberServiceImpl memberService2 = ioc.getBean("memberService2", MemberServiceImpl.class);
        memberService2.add();
    }
1
2
3
4
5
6
7

# 2.2.7 引用/注入集合/数组类型

# 2.2.7.1 应用实例

在 Spring 的 ioc 容器中,看看如何给 bean 对象的集合/数组类型的属性赋值

# 2.2.7.2 代码实现
public class Monster {
    private Integer monsterId;
    private String name;
    private String skill;

    public Monster(Integer monsterId, String name, String skill) {
        this.monsterId = monsterId;
        this.name = name;
        this.skill = skill;
    }

    //无参构造器一定要写,Spring 反射创建对象时需要使用
    public Monster() {
    }

    public Integer getMonsterId() {
        return monsterId;
    }

    public void setMonsterId(Integer monsterId) {
        this.monsterId = monsterId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSkill() {
        return skill;
    }

    public void setSkill(String skill) {
        this.skill = skill;
    }

    @Override
    public String toString() {
        return "Monster{" +
                "monsterId=" + monsterId +
                ", name='" + name + '\'' +
                ", skill='" + skill + '\'' +
                '}';
    }
}
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
public class Master {
    private String name; //主人名
    private List<Monster> monsterList;
    private Map<String, Monster> monsterMap;
    private Set<Monster> monsterSet;
    private String[] monsterName;
    //这个 Properties 是 Hashtable 的子类,是 key-value 的形式
    //这里的 Properties 的 key 和 value 都是 String
    private Properties pros;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Monster> getMonsterList() {
        return monsterList;
    }

    public void setMonsterList(List<Monster> monsterList) {
        this.monsterList = monsterList;
    }

    public Map<String, Monster> getMonsterMap() {
        return monsterMap;
    }

    public void setMonsterMap(Map<String, Monster> monsterMap) {
        this.monsterMap = monsterMap;
    }

    public Set<Monster> getMonsterSet() {
        return monsterSet;
    }

    public void setMonsterSet(Set<Monster> monsterSet) {
        this.monsterSet = monsterSet;
    }

    public String[] getMonsterName() {
        return monsterName;
    }

    public void setMonsterName(String[] monsterName) {
        this.monsterName = monsterName;
    }

    public Properties getPros() {
        return pros;
    }

    public void setPros(Properties pros) {
        this.pros = pros;
    }

    @Override
    public String toString() {
        return "Master{" +
                "name='" + name + '\'' +
                ", monsterList=" + monsterList +
                ", monsterMap=" + monsterMap +
                ", monsterSet=" + monsterSet +
                ", monsterName=" + Arrays.toString(monsterName) +
                ", pros=" + pros +
                '}';
    }
}
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
<!--配置 Master 对象-->
    <bean class="com.hspedu.spring.bean.Master" id="master">
        <property name="name" value="太上老君"/>
        <!--给 list 属性赋值-->
        <property name="monsterList">
            <list>
                <!--引用的方式-->
                <ref bean="monster01"/>
                <ref bean="monster02"/>
                <!--使用内部bean的方式创建对象放到这里面-->
                <bean class="com.hspedu.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>
                    <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.hspedu.spring.bean.Monster">
                    <property name="name" value="金角大王"/>
                    <property name="skill" value="吐水"/>
                    <property name="monsterId" value="666"/>
                </bean>
            </set>
        </property>
        <!--给数组属性赋值-->
        <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>
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
public void setBeanByCollection() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        Master master = ioc.getBean("master", Master.class);
        System.out.println("master = " + master);
    }
1
2
3
4
5
# 2.2.7.3 使用细节

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

(2)Properties 集合的特点

​ 1)这个 Properties 是 Hashtable 的子类,是 key-value 的形式

​ 2)key 是 string 而 value 也是 string

# 2.2.8 通过 util 名称空间创建 list

public class BookStore {
    private List<String> bookList;

    public BookStore() {
    }

    public List<String> getBookList() {
        return bookList;
    }

    public void setBookList(List<String> bookList) {
        this.bookList = bookList;
    }

    @Override
    public String toString() {
        return "BookStore{" +
                "bookList=" + bookList +
                '}';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

之前的

<bean class="com.hspedu.spring.bean.BookStore" id="bookStore">
        <property name="bookList">
            <list>
                <value>三国演义</value>
                <value>红楼梦</value>
                <value>西游记</value>
                <value>水浒传</value>
            </list>
        </property>
    </bean>
1
2
3
4
5
6
7
8
9
10

如果还有一个书店卖同样的书,那样的话还得再写一遍,可以把同样的代码提取出来

现在的

<bean class="com.hspedu.spring.bean.BookStore" id="bookStore">
        <property name="bookList" ref="myBookList"/>
    </bean>
    <!--如果还有一个书店卖同样的书,那样的话还得再写一遍,可以把同样的代码提取出来-->
    <!--定义一个 util:list 并且指定 id 可以达到数据复用的作用-->
    <util:list id="myBookList">
        <value>三国演义</value>
        <value>红楼梦</value>
        <value>西游记</value>
        <value>水浒传</value>
    </util:list>
1
2
3
4
5
6
7
8
9
10
11
public void setBeanByUtilList() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        BookStore bookStore = ioc.getBean("bookStore", BookStore.class);
        System.out.println("bookStore = " + bookStore);
    }
1
2
3
4
5

# 2.2.9 级联属性赋值

# 2.2.9.1 说明

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

举例:假如有 A 类和 B 类两个类,A 类中有个属性是 B,B 也有自己的属性,直接给 B 的属性赋值就是级联属性赋值

# 2.2.9.2 代码实现
//部门类
public class Dept {
    private String name;

    public Dept() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Dept{" +
                "name='" + name + '\'' +
                '}';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//员工类
public class Emp {
    private String name;
    private Dept dept;

    public Emp() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Dept getDept() {
        return dept;
    }

    public void setDept(Dept dept) {
        this.dept = dept;
    }

    @Override
    public String toString() {
        return "Emp{" +
                "name='" + name + '\'' +
                ", dept=" + dept +
                '}';
    }
}
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
<!--配置 Dept 对象-->
    <bean class="com.hspedu.spring.bean.Dept" id="dept"/>
    <!--配置 Emp 对象-->
    <bean class="com.hspedu.spring.bean.Emp" id="emp">
        <property name="name" value="jack"/>
        <property name="dept" ref="dept"/>
        <!--这里希望给 dept 的 name 属性指定值[级联属性赋值]-->
        <property name="dept.name" value="Java开发部门"/>
    </bean>
1
2
3
4
5
6
7
8
9
//给属性进行级联赋值
    @Test
    public void setBeanByRelation() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        Emp emp = ioc.getBean("emp", Emp.class);
        System.out.println("emp = " + emp);
    }
1
2
3
4
5
6
7

# 2.2.10 通过静态工厂配置 Bean 对象

# 2.2.10.1 说明

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

# 2.2.10.2 代码实现
//静态工厂类,可以返回 Monster 对象
public class MyStaticFactory {
    private static Map<String, Monster> monsterMap;

    //使用 static 代码块进行初始化
    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--配置 monster 对象,通过静态工厂获取-->
    <!--
        (1) 通过静态工厂配置 bean
        (2) class 是静态工厂类的全路径
        (3) factory-method 表示指定静态工厂类的哪个方法返回对象
        (4) constructor-arg value="monster02" 中 value 是指定要返回静态工厂的哪个对象
    -->
    <bean id="my_monster01" class="com.hspedu.spring.factory.MyStaticFactory" factory-method="getMonster">
        <constructor-arg value="monster02"/>
    </bean>
1
2
3
4
5
6
7
8
9
10
//通过静态工厂获取 bean
    @Test
    public void getBeanByStaticFactory() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        Monster my_monster01 = ioc.getBean("my_monster01", Monster.class);
        System.out.println("my_monster01 = " + my_monster01);
    }
1
2
3
4
5
6
7

# 2.2.11 通过实例工厂配置 Bean 对象

# 2.2.11.1 说明

在 Spring 的 ioc 容器中,可以通过实例工厂获取 bean 对象

# 2.2.11.2 代码实现
//实例工厂类
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--配置实例工厂对象-->
    <bean class="com.hspedu.spring.factory.MyInstanceFactory" id="myInstanceFactory"/>
    <!--通过实例工厂配置 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>
1
2
3
4
5
6
7
8
9
10
11
//通过实例工厂获取 bean
    @Test
    public void getBeanByInstanceFactory() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        Monster my_monster02 = ioc.getBean("my_monster02", Monster.class);
        System.out.println("my_monster02 = " + my_monster02);
    }
1
2
3
4
5
6
7

# 2.2.12 通过 FactoryBean 配置 Bean 对象

# 2.2.12.1 说明

在 Spring 的 ioc 的容器中,通过 FactoryBean 获取 Bean 对象

# 2.2.12.2 代码实现
//这是一个 FactoryBean
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(100, "牛魔王", "芭蕉扇"));
        monster_map.put("monster04", new Monster(200, "狐狸精啦啦啦", "美人计"));
    }

    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;
    }

    //这个方法判断是不是单例对象,默认返回 false,则每次都是一个新的对象
    @Override
    public boolean isSingleton() {
        return true;
    }
}
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
<!--通过 FactoryBean 获取配置 monster 对象-->
    <!--
        (1) class 指定使用的 FactoryBean
        (2) key 表示就是 MyFactoryBean 的属性 key
        (3) value 就是你要获取的对象对应的 key
    -->
    <bean id="my_monster05" class="com.hspedu.spring.factory.MyFactoryBean">
        <property name="key" value="monster04"/>
    </bean>
1
2
3
4
5
6
7
8
9
//通过 FactoryBean 获取 bean
    @Test
    public void getBeanByFactoryBean() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        Monster myMonster05 = ioc.getBean("my_monster05", Monster.class);
        System.out.println("myMonster05 = " + myMonster05);
    }
1
2
3
4
5
6
7

# 2.2.13 Bean 配置信息重用(继承)

# 2.2.13.1 说明

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

# 2.2.13.2 代码实现
<!--配置 Monster 对象-->
    <bean id="monster10" class="com.hspedu.spring.bean.Monster">
        <property name="monsterId" value="10"/>
        <property name="name" value="蜈蚣精"/>
        <property name="skill" value="蜇人"/>
    </bean>

    <!--
        如果还有一个 Monster 对象实例,内容和 monster10 的一致,笨方法是复制粘贴一份
        好方法是使用继承,parent="monster10" 表示指定当前这个配置的对象的属性从 id=monster10 中继承而来
    -->
    <bean id="monster11" class="com.hspedu.spring.bean.Monster" parent="monster10"/>

    <!--配置 Monster 对象-->
    <!--
        (1) 如果 bean 指定了 abstract="true", 表示该 bean 对象是专门用于被继承的
        (2) 本身这个 bean 就不能被获取/实例化
    -->
    <bean id="monster12" class="com.hspedu.spring.bean.Monster" abstract="true">
        <property name="monsterId" value="100"/>
        <property name="name" value="蜈蚣精~"/>
        <property name="skill" value="蜇人~"/>
    </bean>

    <bean id="monster13" class="com.hspedu.spring.bean.Monster" parent="monster12"/>
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
//通过继承获取 bean
    @Test
    public void getBeanByExtends() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        Monster monster11 = ioc.getBean("monster11", Monster.class);
        System.out.println("monster11 = " + monster11);

        Monster monster13 = ioc.getBean("monster13", Monster.class);
        System.out.println("monster13 = " + monster13);
    }
1
2
3
4
5
6
7
8
9
10

# 2.2.14 Bean 创建顺序

# 2.2.14.1 说明

(1)在 Spring 的 ioc 容器中,默认是按照配置的顺序创建 bean 的对象

比如有:

<bean id="student01" class="com.hspedu.bean.Student"/>
<bean id="department01" class="com.hspedu.bean.Department"/>
1
2

会先创建 student01 这个 bean 对象,然后创建 department01 这个 bean 对象

代码实现:

public class Student {
    public Student() {
        System.out.println("Student()构造器被执行...");
    }
}
1
2
3
4
5
public class Department {
    public Department() {
        System.out.println("Department() 被执行...");
    }
}
1
2
3
4
5
<!--测试 bean 对象的创建顺序-->
    <bean id="student01" class="com.hspedu.spring.bean.Student"/>
    <bean id="department01" class="com.hspedu.spring.bean.Department"/>
1
2
3
//测试 Bean 创建顺序
    @Test
    public void testBeanByCreate() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        System.out.println("ok");
    }
1
2
3
4
5
6

这里运行代码会输出:

Student()构造器被执行...
Department() 被执行...
ok
1
2
3

由此看出其顺序

(2)如果这样配置

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

会先创建 department01 对象,再创建 student01 对象

代码实现:

<!--测试 bean 对象的创建顺序-->
<!-- depends-on="department01" 表示 student01 要依赖于 department01 -->
    <bean id="student01" class="com.hspedu.spring.bean.Student" depends-on="department01"/>
    <bean id="department01" class="com.hspedu.spring.bean.Department"/>
1
2
3
4

这里运行代码会输出:

Department() 被执行...
Student()构造器被执行...
ok
1
2
3

由此看出其顺序

# 2.2.14.2 提出问题

(1)先分析下面两个 bean 创建的顺序是什么?

<!--配置 MemberDAOImpl 对象-->
<bean class="com.hspedu.spring.dao.MemberDAOImpl" id="memberDAOImpl"/>

<!--配置 MemberServiceImpl 对象-->
<bean class="com.hspedu.spring.service.MemberServiceImpl" id="memberServiceImpl">
    <property name="memberDAO" ref="memberDAOImpl"/>
</bean>
1
2
3
4
5
6
7

​ 1)先创建 id=memberDAOImpl

​ 2)再创建 id=memberServiceImpl

​ 3)调用 MemberServiceImpl.setMemberDAO() 完成引用

(2)再看下面这两个 bean 的创建顺序是什么?

<!--配置 MemberServiceImpl 对象-->
<bean class="com.hspedu.spring.service.MemberServiceImpl" id="memberServiceImpl">
    <property name="memberDAO" ref="memberDAOImpl"/>
</bean>

<!--配置 MemberDAOImpl 对象-->
<bean class="com.hspedu.spring.dao.MemberDAOImpl" id="memberDAOImpl"/>
1
2
3
4
5
6
7

​ 1)先创建 id=memberServiceImpl

​ 2)再创建 id=memberDAOImpl

​ 3)用 memberServiceImpl.setMemberDAO() 完成引用

# 2.2.15 bean 对象的单例和多例

# 2.2.15.1 说明

在 Spring 的 ioc 的容器中,在默认的情况下是按照单例创建对象的,即配置一个 bean 对象后,ioc 容器只会创建一个 bean 实例。如果我们希望 ioc 容器配置的某个 bean 对象是以多个实例形式创建的,则可以通过配置 scope="prototype" 来指定

# 2.2.15.2 代码实现
public class Cat {
    private Integer id;
    private String name;

    public Cat() {
        System.out.println("Cat() 被执行...");
    }

    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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--配置 Cat 对象-->
    <!--
        (1) 在默认情况下 scope 属性值是 singleton
        (2) 此时在 ioc 的容器中,只有一个这个 bean 对象
        (3) 也就是说当执行 ioc.getBean 时,返回的是同一个对象
        (4) 如果希望每次返回一个新的 bean 的对象,则可以指定 scope 的属性值是 prototype
    -->
    <bean id="cat" class="com.hspedu.spring.bean.Cat">
        <property name="id" value="100"/>
        <property name="name" value="小花猫"/>
    </bean>
1
2
3
4
5
6
7
8
9
10
11
//测试 Scope
    @Test
    public void testBeanScope() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        Cat cat = ioc.getBean("cat", Cat.class);
        Cat cat2 = ioc.getBean("cat", Cat.class);
        Cat cat3 = ioc.getBean("cat", Cat.class);
        System.out.println("cat = " + cat);
        System.out.println("cat2 = " + cat2);
        System.out.println("cat3 = " + cat3);
    }
1
2
3
4
5
6
7
8
9
10
11

输出:

Cat() 被执行...
cat = com.hspedu.spring.bean.Cat@58fdd99
cat2 = com.hspedu.spring.bean.Cat@58fdd99
cat3 = com.hspedu.spring.bean.Cat@58fdd99
1
2
3
4
<bean id="cat" class="com.hspedu.spring.bean.Cat" scope="prototype">
        <property name="id" value="100"/>
        <property name="name" value="小花猫"/>
    </bean>
1
2
3
4

输出:

Cat() 被执行...
Cat() 被执行...
Cat() 被执行...
cat = com.hspedu.spring.bean.Cat@6b1274d2
cat2 = com.hspedu.spring.bean.Cat@7bc1a03d
cat3 = com.hspedu.spring.bean.Cat@70b0b186
1
2
3
4
5
6
# 2.2.15.3 使用细节

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

(2)当 <bean scope="prototype"> 设置为多实例机制后,该 bean 只有在调用 ioc.getBean 方法时才创建

(3)如果一个 bean 的配置既是单例的,又是懒加载,这时的 ioc 容器就不会提前创建该对象,而是当执行 ioc.getBean 方法的时候才会创建对象

懒加载也被叫作延迟价值,它的核心思想是把对象的实例化延迟到真正调用该对象的时候,这样做的好处是可以减轻大量对象在实例化时对资源的消耗,而不是在程序初始化的时候就预先将对象实例化。另外懒加载可以将对象的实例化代码从初始化方法中独立出来,从而提高代码的可读性,以便于代码能够更好地组织。

(4)通常情况下,lazy-init 就使用默认值 false,在开发看来,用空间换时间是值得的

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

# 2.2.16 Bean 的生命周期

# 2.2.16.1 说明

Bean 对象的创建是由 JVM 完成的,然后执行如下方法:

​ 1)执行构造器

​ 2)执行 set 相关方法

​ 3)调用 Bean 的初始化的方法(需要配置)

​ 4)使用 Bean

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

# 2.2.16.2 代码实现

House 类

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;
    }

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

    //这个方法是程序员来编写的
    //根据自己的业务逻辑来写
    public void destroy() {
        System.out.println("House destroy()...");
    }
}
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
<!--配置 House 对象,演示整个 Bean 的生命周期
        可以指定 bean 初始化时执行哪个方法,可以指定 bean 销毁时执行哪个方法
        (1) init-method="init" 指定 bean 的初始化方法为 House 类中的 init() 方法,初始化方法在 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>
1
2
3
4
5
6
7
8
9
10
11
12
//测试 Bean 的生命周期
    @Test
    public void testBeanLife() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans.xml");
        House house = ioc.getBean("house", House.class);
        System.out.println("使用 house = " + house);

        //关闭容器
        //(1) ioc 的编译类型是 ApplicationContext,运行类型是 ClassPathXmlApplicationContext
        //(2) 因为 ClassPathXmlApplicationContext 实现了 ConfigurableApplicationContext, ClassPathXmlApplicationContext 有 close 方法
        //(3) 所以将 ioc 转成 ClassPathXmlApplicationContext 再调用 close
        //(4) 这里 close 方法调用我们自己配置的 destroy 方法
        ((ConfigurableApplicationContext) ioc).close();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

输出:

House() 构造器...
House setName() = 北京豪宅
House init()...
使用 house = com.hspedu.spring.bean.House@70b0b186
House destroy()...
1
2
3
4
5

# 2.2.17 配置 Bean 的后置处理器

# 2.2.17.1 说明

(1)在 Spring 的 ioc 容器中,可以配置 Bean 的后置处理器

(2)该处理器会在 bean 初始化方法调用前和初始化方法调用后被调用

(3)程序员可以在后置处理器中编写自己的代码

# 2.2.17.2 代码实现
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;
    }

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

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

    @Override
    public String toString() {
        return "House{" +
                "name='" + name + '\'' +
                '}';
    }
}
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
public class MyBeanPostProcessor implements BeanPostProcessor {
    /**
     * 什么时候被调用? 在 Bean 的 init 方法前被调用
     * bean: 传入的是在 ioc 容器中创建/配置的 Bean
     * beanName: 传入的是在 ioc 容器中创建/配置的 Bean 的 Id
     * */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("postProcessBeforeInitialization().. bean = " + bean + " beanName = " + beanName);
        return bean;
    }

    /**
     * 什么时候被调用? 在 Bean 的 init 方法后被调用
     * bean: 传入的是在 ioc 容器中创建/配置的 Bean
     * beanName: 传入的是在 ioc 容器中创建/配置的 Bean 的 Id
     * */
    @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
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--配置 House 对象-->
    <bean class="com.hspedu.spring.bean.House" id="house"
          init-method="init"
          destroy-method="destroy">
        <property name="name" value="大豪宅"/>
    </bean>

    <!--配置后置处理器对象
        (1) 当我们在 beans02.xml 容器配置文件,配置了 MyBeanPostProcessor
        (2) 这时后置处理器对象就会作用在该容器创建的 Bean 对象中
    -->
    <bean class="com.hspedu.spring.bean.MyBeanPostProcessor" id="myBeanPostProcessor"></bean>
1
2
3
4
5
6
7
8
9
10
11
12
//测试 Bean 的生命周期
    @Test
    public void testBeanPostProcessor() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans02.xml");
        House house = ioc.getBean("house", House.class);
        System.out.println("使用 house = " + house);

        //关闭容器
        //(1) ioc 的编译类型是 ApplicationContext,运行类型是 ClassPathXmlApplicationContext
        //(2) 因为 ClassPathXmlApplicationContext 实现了 ConfigurableApplicationContext, ClassPathXmlApplicationContext 有 close 方法
        //(3) 所以将 ioc 转成 ClassPathXmlApplicationContext 再调用 close
        ((ConfigurableApplicationContext) ioc).close();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
House() 构造器...
House setName() = 大豪宅
postProcessBeforeInitialization().. bean = House{name='大豪宅'} beanName = house
House init()...
postProcessAfterInitialization().. bean = House{name='大豪宅'} beanName = house
使用 house = House{name='大豪宅'}
House destroy()...
1
2
3
4
5
6
7
# 2.2.17.3 后置处理器的其它说明

(1)怎么执行到这个方法?

使用 AOP(反射 + 动态代理 + IO + 容器 + 注解)

(2)有什么用?

可以对 IOC 容器中所有的对象进行统一处理,比如日志处理/权限的校验/安全的验证/事务管理

(3)针对容器的所有对象吗?

是的,符合切面编程的特点

# 2.2.18 通过属性文件给 Bean 注入值

# 2.2.18.1 说明

在 Spring 的 ioc 容器中,可以通过属性文件给 bean 注入值

# 2.2.18.2 代码实现
monsterId=1000
name=jack
skill=hello
1
2
3
<!--指定属性文件
        location="classpath:my.properties"
        需要带上 classpath 否则不知道去哪读文件
    -->
    <context:property-placeholder location="classpath:my.properties"/>

    <!--配置 Monster 对象
        通过属性文件给 monster 对象的属性赋值
        这时属性值可以通过 ${属性名} 获取
        这里说的属性名就是 my.properties 文件中的 K=V 的 K
    -->
    <bean class="com.hspedu.spring.bean.Monster" id="monster1000">
        <property name="monsterId" value="${monsterId}"/>
        <property name="skill" value="${skill}"/>
        <property name="name" value="${name}"/>
    </bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//通过属性文件给 bean 属性赋值
    @Test
    public void setBeanByFile() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans03.xml");
        Monster monster1000 = ioc.getBean("monster1000", Monster.class);
        System.out.println("monster1000 = " + monster1000);
    }
1
2
3
4
5
6
7

如果属性配置文件中有中文,需要将其转为 Unicode 编码进行配置

# 2.2.19 基于 XML 的 bean 的自动装配

# 2.2.19.1 说明

在 Spring 的 ioc 容器中,可以实现自动装配 bean

# 2.2.19.2 代码实现
//DAO 类
public class OrderDAO {
    public void saveOrder() {
        System.out.println("保存一个订单...");
    }
}
1
2
3
4
5
6
//Service 类
public class OrderService {
    //OrderDAO 属性
    private OrderDAO orderDAO;

    public OrderDAO getOrderDAO() {
        return orderDAO;
    }

    public void setOrderDAO(OrderDAO orderDAO) {
        this.orderDAO = orderDAO;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class OrderServlet {
    private OrderService orderService;

    public OrderService getOrderService() {
        return orderService;
    }

    public void setOrderService(OrderService orderService) {
        this.orderService = orderService;
    }
}
1
2
3
4
5
6
7
8
9
10
11

通过 byType 方式自动装配

<!--配置 OrderDAO 对象-->
    <bean class="com.hspedu.spring.dao.OrderDAO" id="orderDAO"/>
    <!--配置 OrderService 对象
        (1) autowire="byType" 表示在创建 OrderService 时
            通过类型的方式给对象的属性自动完成赋值或引用
        (2) 比如 OrderService 对象有 private OrderDAO orderDAO 的属性,那么就会自动在容器中去找有没有 OrderDAO 类型的对象
        (3) 如果有,就会自动的装配,如果是按照 byType 的方式来装配,那么这个容器中不能存在两个相同类型的对象
    -->
    <bean autowire="byType" class="com.hspedu.spring.service.OrderService" id="orderService"/>
    <!--配置 OrderServlet 对象-->
    <bean autowire="byType" class="com.hspedu.spring.Servlet.OrderServlet" id="orderServlet"/>
1
2
3
4
5
6
7
8
9
10
11

通过 byName 方式自动装配

<!--配置 OrderDAO 对象-->
    <bean class="com.hspedu.spring.dao.OrderDAO" id="orderDAO"/>
    <!--配置 OrderService 对象
        (1) autowire="byType" 表示在创建 OrderService 时
            通过类型的方式给对象的属性自动完成赋值或引用
        (2) 比如 OrderService 对象有 private OrderDAO orderDAO 的属性,那么就会自动在容器中去找有没有 OrderDAO 类型的对象
        (3) 如果有,就会自动的装配,如果是按照 byType 的方式来装配,那么这个容器中不能存在两个相同类型的对象

        (4) 如果设置的是 autowire="byName" 表示是通过名字来完成自动装配
        (5) 比如: autowire="byName" class="com.hspedu.spring.service.OrderService"
            1) 先看 OrderService 里有个属性 private OrderDAO orderDAO,那么就是给这个 orderDAO 完成自动装配,通过 byName 的方式的话就是要从 OrderService 类中通过 setter 方法来找到类名,此时找到有个 setOrderDAO 的方法名,这时就会把 set 去掉把首字母小写后获得 orderDAO,这时再去 ioc 容器中根据 id 查找是否有符合此 id 的类,这里就找有没有 id 是 orderDAO 的,有则自动装配,没有则报错 
    -->
    <bean autowire="byName" class="com.hspedu.spring.service.OrderService" id="orderService"/>
    <!--配置 OrderServlet 对象-->
    <bean autowire="byName" class="com.hspedu.spring.Servlet.OrderServlet" id="orderServlet"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void setBeanByAutowire() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans03.xml");
        OrderServlet orderServlet = ioc.getBean("orderServlet", OrderServlet.class);
        //验证是否自动装配上 OrderService
        System.out.println(orderServlet.getOrderService());
        //验证是否自动装配上 OrderDAO
        System.out.println(orderServlet.getOrderService().getOrderDAO());
        System.out.println("orderServlet = " + orderServlet);
    }
1
2
3
4
5
6
7
8
9
# 2.2.20 Spring el 表达式
# 2.2.20.1 说明

(1)Spring Expression Language 是 Spring 的表达式语言,简称 SpEL。支持运行时查询并可以操作对象

(2)和 EL 表达式一样,SpEL 根据 JavaBean 风格的 getXxx()、setXxx() 方法定义的属性访问对象

(3)SpEL 使用 #{...} 作为定界符,所有在大括号中的字符都将被认为是 SpEL 表达式

# 2.2.20.2 代码实现
public class SpELBean {
    private String name;
    private Monster monster;
    private String monsterName;
    private String crySound;
    private String bookName;
    private Double result;

    public SpELBean() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Monster getMonster() {
        return monster;
    }

    public void setMonster(Monster monster) {
        this.monster = monster;
    }

    public String getMonsterName() {
        return monsterName;
    }

    public void setMonsterName(String monsterName) {
        this.monsterName = monsterName;
    }

    public String getCrySound() {
        return crySound;
    }

    public void setCrySound(String crySound) {
        this.crySound = crySound;
    }

    public String getBookName() {
        return bookName;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

    public Double getResult() {
        return result;
    }

    public void setResult(Double result) {
        this.result = result;
    }

    @Override
    public String toString() {
        return "SpELBean{" +
                "name='" + name + '\'' +
                ", monster=" + monster +
                ", monsterName='" + monsterName + '\'' +
                ", crySound='" + crySound + '\'' +
                ", bookName='" + bookName + '\'' +
                ", result=" + result +
                '}';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<!--spring el 表达式的使用-->
    <!--通过 spel 给 bean 的属性赋值-->
    <bean id="spELBean" class="com.hspedu.spring.bean.SpELBean">
        <!--spel 给字面量-->
        <property name="name" value="止束"/>
        <!--spel 引用其它 bean-->
        <property name="monster" value="#{monster01}"/>
        <!--spel 引用其它 bean 的属性值-->
        <property name="monsterName" value="#{monster02.name}"/>
        <!--spel 调用普通方法赋值-->
        <property name="crySound" value="#{spELBean.cry('喵喵的...')}"/>
        <!--spel 调用静态方法赋值-->
        <property name="bookName" value="#{T(com.hspedu.spring.bean.SpELBean).read('天龙八部')}"/>
        <!--spel 通过运算赋值-->
        <property name="result" value="#{89*1.2}"/>
    </bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void setBeanBySpel() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans04.xml");
        SpELBean spELBean = ioc.getBean("spELBean", SpELBean.class);
        System.out.println("spELBean = " + spELBean);
    }
1
2
3
4
5

# 第 3 章 基于注解配置 bean

# 3.1 基本使用

# 3.1.1 说明

(1)基于注解的方式配置 bean,主要是项目开发中的组件,比如 Controller、Service 和 DAO

(2)组件注解的形式有:

​ 1)@Component 表示当前注解标识的是一个组件

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

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

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

# 3.1.2 快速入门

# 3.1.2.1 应用实例

使用注解的方式来配置 Controller、Service、Respository、Component

# 3.1.2.2 代码实现

(1)引入 spring-aop-5.3.8.jar,在 spring 的 libs 的目录下拷贝即可

UserDAO

//使用 @Repository 标识该类是一个 DAO 类,是一个持久化层的类/对象
@Repository
public class UserDAO {
}
1
2
3
4

UserService

//@Service 标识该类是一个 Service 类/对象
@Service
public class UserService {
}
1
2
3
4

UserAction

//@Controller 标识该类是一个控制器,通常这个类是一个 Servlet
@Controller
public class UserAction {
}
1
2
3
4

MyComponent

//@Component 标识该类是一个组件,是一个通用的注解
@Component
public class MyComponent {
}
1
2
3
4
<!--配置容器要扫描的包
        (1) component-scan 要对指定包下的类进行扫描,并创建对象到容器中
        (2) base-package 指定要扫描的包
        (3) 含义是当 spring 容器创建/初始化时,就会扫描 com.hspedu.spring.component 包下的所有的有 @Controller / @Service / @Respository / @Component 注解的类并将其实例化,生成对象放入到 ioc 容器中
    -->
    <context:component-scan base-package="com.hspedu.spring.component"/>
1
2
3
4
5
6
//通过注解来配置 Bean
    @Test
    public void setBeanByAnnotation() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans05.xml");

        UserDAO userDAO = ioc.getBean(UserDAO.class);
        UserService userService = ioc.getBean(UserService.class);
        UserAction userAction = ioc.getBean(UserAction.class);
        MyComponent myComponent = ioc.getBean(MyComponent.class);

        System.out.println("userDAO = " + userDAO);
        System.out.println("userService = " + userService);
        System.out.println("userAction = " + userAction);
        System.out.println("myComponent = " + myComponent);

        System.out.println("ok");
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3.1.2.3 注意事项和细节说明

(1)需要导入 spring-aop-5.3.8.jar

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

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

可以使用通配符 * 来指定,比如 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" 表示只扫描满足类的名字的开头是 User 的类

(5)如果希望排除某个包或者子包下的某个类被扫描,可以通过指定以某种类型的注解的方式进行过滤,通过 exclude-filter 来指定

<!--
        context:exclude-filter type="annotation" 指定要排除哪些类,type 指定排除的方式,annotation 表示按照注解的方式来排除
        expression="org.springframework.stereotype.Service" 用来指定要排除的注解的全路径
    -->
    <context:component-scan base-package="com.hspedu.spring.component">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>
1
2
3
4
5
6
7
8

(6)指定自动扫描哪些注解类

<!-- 如果希望按照自己的规则来扫描包或者子包下的某些注解,可以通过 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:component-scan>
1
2
3
4
5
6
7
8
9
10

(7)默认情况下标记注解后,id 的值为类名首字母小写,也可以使用注解的 value 属性指定 id 值,并且 value 可以省略

@Repository(value = "hspUserDAO")
public class UserDAO {
}
1
2
3

彻底弄懂@Controller 、@Service、@Component - 知乎 (zhihu.com) (opens new window)

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

# 3.2.1 需求说明

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

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

# 3.2.2 思路分析

(1)使用注解方式完成,这里不用 XML 来配置

(2)程序框架图

# 3.2.3 代码实现

# 3.2.3.1 搭建基本结构并获取扫描包

HspSpringConfig

/**
 * 这是一个配置类,作用类似我们原生 Spring 的 beans.xml 容器配置文件
 * */
@ComponentScan(value = "com.hspedu.spring.component")
public class HspSpringConfig {
}
1
2
3
4
5
6

ComponentScan

/**
 * (1) @Target(ElementType.TYPE) 指定我们的 ComponentScan 注解可以修饰 Type 程序元素,包括 class 等
 * (2) @Retention(RetentionPolicy.RUNTIME) 指定 ComponentScan 注解的保留范围
 * (3) String value() default ""; 表示 ComponentScan 可以传入 value
 * */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ComponentScan {
    String value() default "";
}
1
2
3
4
5
6
7
8
9
10

HspSpringApplicationContext

/**
 * 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); //this.configClass = class com.hspedu.spring.annotation.HspSpringConfig
        //获取注解信息,从注解信息里的 value 值中获取包的路径
        //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); // com.hspedu.spring.component
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 3.2.3.2 获取扫描包下所有的 .class 文件

HspSpringApplicationContext

/**
 * 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); //this.configClass = class com.hspedu.spring.annotation.HspSpringConfig
        //获取注解信息,从注解信息里的 value 值中获取包的路径
        //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); // com.hspedu.spring.component
        //3. 得到要扫描的包下的所有资源(.class 文件等,注意是在 out 目录下,而不是 src 下)
        //(1) 得到类的加载器
        ClassLoader classLoader = HspApplicationContext.class.getClassLoader();
        //(2) 通过类的加载器获取到要扫描的包的资源 url
        //因为类加载器加载包时的路径的写法是以 / 分隔,所以要把上面 path 中的 . 换成 / 例如:com/hspedu/spring/component
        path = path.replace(".", "/");
        URL resource = classLoader.getResource(path);
        System.out.println("resource = " + resource); //resource = file:/D:/Study/Code/IDEA/hspedu_spring/spring/out/production/spring/com/hspedu/spring/component
        //4. 将要加载的资源路径下的文件进行遍历
        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()); //得到绝对路径
            }
        }
    }
}
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
# 3.2.3.3 获取全类名、反射对象、放入容器

HspSpringApplicationContext

/**
 * 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); //this.configClass = class com.hspedu.spring.annotation.HspSpringConfig
        //获取注解信息,从注解信息里的 value 值中获取包的路径
        //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); // com.hspedu.spring.component

        //3. 得到要扫描的包下的所有资源(.class 文件等,注意是在 out 目录下,而不是 src 下)
        //(1) 得到类的加载器
        ClassLoader classLoader = HspApplicationContext.class.getClassLoader();
        //(2) 通过类的加载器获取到要扫描的包的资源 url
        //因为类加载器加载包时的路径的写法是以 / 分隔,所以要把上面 path 中的 . 换成 / 例如:com/hspedu/spring/component
        path = path.replace(".", "/");
        URL resource = classLoader.getResource(path);
        System.out.println("resource = " + resource); //resource = file:/D:/Study/Code/IDEA/hspedu_spring/spring/out/production/spring/com/hspedu/spring/component

        //4. 将要加载的资源路径下的文件进行遍历
        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()); //得到绝对路径
                //5. 从绝对路径中获取全类名
                //从 D:\Study\Code\IDEA\hspedu_spring\spring\out\production\spring\com\hspedu\spring\component\MyComponent.class
                //获取到 com\hspedu\spring\component\MyComponent
                //并且要将 \ 换成 .
                //最终要得到 com.hspedu.spring.component.MyComponent
                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) 获取类的完整的路径(全类名)
                    String classFullName = path.replace("/", ".") + "." + className;
                    System.out.println("classFullName = " + classFullName);
                    //(3) 判断该类是不是需要注入到容器中,就看该类是不是含有这些注解:@Component @Service 等
                    try {
                        //通过类的全类名获得该类的类对象
                        /*
                        * Class.forName(classFullName) 和 classLoader.loadClass(classFullName) 的区别
                        * 两者都可以反射加载类,前者会调用类的静态方法,后者不会,所以前者更加完整,后者更加轻量
                        * */
                        Class<?> aClass = classLoader.loadClass(classFullName);
                        // aClass.isAnnotationPresent(Component.class) 用于判断该类是否含有 @Component 注解
                        if (aClass.isAnnotationPresent(Component.class) || aClass.isAnnotationPresent(Controller.class) || aClass.isAnnotationPresent(Service.class) || aClass.isAnnotationPresent(Repository.class)) {
                            //6. 这时就可以反射对象,并放入到容器中
                            Class<?> clazz = Class.forName(classFullName);
                            Object instance = clazz.newInstance();
                            //放入到容器中
                            //StringUtils.uncapitalize() 可以将类名首字母改成小写
                            ioc.put(StringUtils.uncapitalize(className), instance);
                        }
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }

    //编写方法返回容器中的对象
    public Object getBean(String name) {
        return ioc.get(name);
    }
}
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

# 3.2.4 注意事项和细节说明

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

//@Component 标识该类是一个组件,是一个通用的注解
@Component(value = "hsp1")
public class MyComponent {
}
1
2
3
4
/**
 * 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); //this.configClass = class com.hspedu.spring.annotation.HspSpringConfig
        //获取注解信息,从注解信息里的 value 值中获取包的路径
        //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); // com.hspedu.spring.component

        //3. 得到要扫描的包下的所有资源(.class 文件等,注意是在 out 目录下,而不是 src 下)
        //(1) 得到类的加载器
        ClassLoader classLoader = HspApplicationContext.class.getClassLoader();
        //(2) 通过类的加载器获取到要扫描的包的资源 url
        //因为类加载器加载包时的路径的写法是以 / 分隔,所以要把上面 path 中的 . 换成 / 例如:com/hspedu/spring/component
        path = path.replace(".", "/");
        URL resource = classLoader.getResource(path);
        System.out.println("resource = " + resource); //resource = file:/D:/Study/Code/IDEA/hspedu_spring/spring/out/production/spring/com/hspedu/spring/component

        //4. 将要加载的资源路径下的文件进行遍历
        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()); //得到绝对路径
                //5. 从绝对路径中获取全类名
                //从 D:\Study\Code\IDEA\hspedu_spring\spring\out\production\spring\com\hspedu\spring\component\MyComponent.class
                //获取到 com\hspedu\spring\component\MyComponent
                //并且要将 \ 换成 .
                //最终要得到 com.hspedu.spring.component.MyComponent
                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) 获取类的完整的路径(全类名)
                    String classFullName = path.replace("/", ".") + "." + className;
                    System.out.println("classFullName = " + classFullName);
                    //(3) 判断该类是不是需要注入到容器中,就看该类是不是含有这些注解:@Component @Service 等
                    try {
                        //通过类的全类名获得该类的类对象
                        /*
                        * Class.forName(classFullName) 和 classLoader.loadClass(classFullName) 的区别
                        * 两者都可以反射加载类,前者会调用类的静态方法,后者不会,所以前者更加完整,后者更加轻量
                        * */
                        Class<?> aClass = classLoader.loadClass(classFullName);
                        // aClass.isAnnotationPresent(Component.class) 用于判断该类是否含有 @Component 注解
                        if (aClass.isAnnotationPresent(Component.class) || aClass.isAnnotationPresent(Controller.class) || aClass.isAnnotationPresent(Service.class) || aClass.isAnnotationPresent(Repository.class)) {
                            //这里演示一个用 Component 注解指定 value 来当作 id 时,怎样取出这个 id
                            if (aClass.isAnnotationPresent(Component.class)) {
                                //获取到该注解
                                Component component = aClass.getDeclaredAnnotation(Component.class);
                                String id = component.value();
                                if (!"".endsWith(id)) {
                                    className = id; //替换
                                }
                            }

                            //6. 这时就可以反射对象,并放入到容器中
                            Class<?> clazz = Class.forName(classFullName);
                            Object instance = clazz.newInstance();
                            //放入到容器中
                            //StringUtils.uncapitalize() 可以将类名首字母改成小写
                            ioc.put(StringUtils.uncapitalize(className), instance);
                        }
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }

    //编写方法返回容器中的对象
    public Object getBean(String name) {
        return ioc.get(name);
    }
}
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

# 3.3 自动装配

# 3.3.1 基本说明

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

(2)@AutoWired 的规则说明

​ 1)在 IOC 容器中查找待装配的组件的类型,如果有唯一的 bean 匹配,则使用该 bean 装配

​ 2)如果待装配的类型对应的 bean 在 IOC 容器中有多个,则使用待装配的属性的属性名作为 id 值再进行查找,找到就装配,找不到就抛异常,例如:xml 文件中又配置了两个 UserService 类型的对象,id 分别为 userService200、userService300,而注解的方式又会创建一个,那么这时候就会根据 UserServlet 中定义的 UserService 的属性名进行装配,如果 UserServlet 定义的是 private UserService userService 则会装配注解创建的,因为通过注解方式创建的对象实例的 id 是类名首字母小写,如果 UserServlet 定义的是 private UserService userService200 则会装配 id 是 userService200 的实例,如果如果 UserServlet 定义的是 private UserService userService300 则会装配 id 是 userService300 的实例,如果如果 UserServlet 定义的是 private UserService userService400 则会报错

​ 3)注意:假设只有一个 UserService 实例,是通过注解配置的,但是 UserServlet 中定义的是 private UserService userService400 此时不会报错,因为程序会从第一条规则通过,即在 IOC 容器中查找待装配的组件的类型,如果有唯一的 bean 匹配,则使用该 bean 装配,就不会管你定义的属性的名字是什么

beans06.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.hspedu.spring.component"/>
</beans>
1
2
3
4
5
6
7
8

UserService

//@Service 标识该类是一个 Service 类/对象
@Service
public class UserService {
    public void hi() {
        System.out.println("UserService hi()~");
    }
}
1
2
3
4
5
6
7

UserAction

//@Controller 标识该类是一个控制器,通常这个类是一个 Servlet
@Controller
public class UserAction {
    @Autowired
    private UserService userService;
    public void sayOk() {
        System.out.println("UserAction 的 sayOk()");
        userService.hi();
    }
}
1
2
3
4
5
6
7
8
9
10

SpringBeanTest

@Test
    public void setProByAutowired() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans06.xml");
        UserAction userAction = ioc.getBean("userAction", UserAction.class);
        System.out.println("userAction = " + userAction);
        userAction.sayOk();
    }
1
2
3
4
5
6
7

(3)@Resource 的规则说明

​ 1)@Resource 有两个属性是比较重要的,分别是 name 和 type,Spring 将 @Resource 注解的 name 属性解析为 bean 的名字,而 type 属性则解析为 bean 的类型,所以如果使用 name 属性,则使用 byName 的自动注入策略,而使用 type 属性时则使用 byType 自动注入策略

​ 2)如果 @Resource 没有指定 name 和 type,则先使用 byName 注入策略,如果匹配不上,再使用 byType 策略,如果都不成功,就会报错

//@Controller 标识该类是一个控制器,通常这个类是一个 Servlet
@Controller
public class UserAction {
    //@Resource(name = "userService") //表示装配 id = userService 的对象
    @Resource(type = UserService.class)
    private UserService userService;
    public void sayOk() {
        System.out.println("UserAction 的 sayOk()");
        userService.hi();
    }
}
1
2
3
4
5
6
7
8
9
10
11

(4)不管是 @Autowire 还是 @Resource 都保证属性名是规范的写法就可以注入

# 3.3.2 注意事项和细节说明

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

(2)@Autowired@Qualifier(value = "xxx") 可以一起使用,就可以指定属性名

# 3.4 泛型依赖注入

# 3.4.1 泛型依赖解释

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

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

# 3.4.2 应用实例

(1)各个类关系图

(2)传统方法是将 PhoneDAO / BookDAO 自动装配到 PhoneService / BookService 中,当这种继承关系多时,就比较麻烦,可以使用 Spring 提供的泛型依赖注入

3.4.3 代码实现

Book

public class Book {
}
1
2

Phone

public class Phone {
}
1
2

BaseDAO

//自定义的泛型类
public abstract class BaseDAO<T> {
    public abstract void save();
}
1
2
3
4

PhoneDAO

@Repository
public class PhoneDAO extends BaseDAO<Phone> {
    @Override
    public void save() {
        System.out.println("PhoneDAO save()");
    }
}
1
2
3
4
5
6
7

BookDAO

@Repository
public class BookDAO extends BaseDAO<Book> {
    @Override
    public void save() {
        System.out.println("BookDAO 的 save 方法");
    }
}
1
2
3
4
5
6
7

BaseService

public class BaseService<T> {
    @Autowired
    private BaseDAO<T> baseDAO;

    public void save() {
        baseDAO.save();
    }
}
1
2
3
4
5
6
7
8

BookService

@Service
public class BookService extends BaseService<Book> {
}
1
2
3

PhoneService

@Service
public class PhoneService extends BaseService<Phone> {
}
1
2
3

Test

//通过泛型依赖来配置 Bean
    @Test
    public void setProByDependencyInjection() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans07.xml");
        PhoneService phoneService = ioc.getBean("phoneService", PhoneService.class);
        phoneService.save(); //PhoneDAO save()
    }
1
2
3
4
5
6
7

解析:使用注解自动装配 BookDAO、PhoneDAO、BookService、PhoneService 后,因为 DAO 都带着泛型继承了 BaseDAO、Service 都带着泛型继承了 BaseService,而 BaseService 自动装配了 BaseDAO,通过泛型依赖注入,BookService 类中不用声明 BookDAO 属性就可以注入 BookDAO 属性,这里的步骤就是:BookService 继承了 BaseService 就带着泛型 Book 进入到 BaseService,因为 BaseService 装配了 BaseDAO 就带着泛型 Book 进入到 BaseDAO,看到 BaseDAO 下有个子类的泛型也是 Book 即 BookDAO 后就把 BookDAO 注入到 BookService 中了。

# 第 4 章 AOP 切面编程

# 4.1 动态代理

# 4.1.1 需求说明

(1)有 Vehicle(交通工具接口,有一个 run 方法),下面有两个实现类 Car 和 Ship

(2)当运行 Car 对象的 run 方法和 Ship 对象的 run 方法时,输出如下内容,注意观察前后有统一的输出

# 4.1.2 传统方式解决

public interface Vehicle {
    public void run();
}
1
2
3
public class Car implements Vehicle{
    @Override
    public void run() {
        System.out.println("交通工具开始运行了");
        System.out.println("小汽车在路上 running");
        System.out.println("交通工具停止运行了");
    }
}
1
2
3
4
5
6
7
8
public class Ship implements Vehicle{
    @Override
    public void run() {
        System.out.println("交通工具开始运行了");
        System.out.println("轮船在水上 running");
        System.out.println("交通工具停止运行了");
    }
}
1
2
3
4
5
6
7
8
public class TestVehicle {
    @Test
    public void run() {
        Vehicle car = new Car();
        car.run();
        Vehicle ship = new Ship();
        ship.run();
    }
}
1
2
3
4
5
6
7
8
9

# 4.1.3 动态代理方式解决

(1)动态代理解决思路,在调用方法时,使用反射机制,根据方法去决定调用哪个对象方法

Vehicle 接口

public interface Vehicle {
    public void run();
}
1
2
3

Car

public class Car implements Vehicle{
    @Override
    public void run() {
        System.out.println("小汽车在路上 running");
    }
}
1
2
3
4
5
6

Ship

public class Ship implements Vehicle{
    @Override
    public void run() {
        System.out.println("轮船在水上 running");
    }
}
1
2
3
4
5
6

VehicleProxyProvider

//该类可以返回一个代理对象
public class VehicleProxyProvider {
    //定义一个属性
    //target_vehicle 表示真正要执行的对象
    //该对象的类实现了 Vehicle 才可以调用
    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();

        //创建 InvocationHandler 对象
        /* InvocationHandler 的源代码
            public interface InvocationHandler {
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
            }
        */
        //因为 InvocationHandler 是接口不能被实例化,所以我们可以通过匿名对象的方式来创建该对象
        InvocationHandler invocationHandler = new InvocationHandler() {
            /**
             * invoke 方法会在执行 target_vehicle 的方法时被调用
             * proxy 表示代理对象
             * method 就是通过代理对象调用方法时的那个方法,比如当有 代理对象.run() 那么 method 就指的是 run 方法
             * args 表示调用 代理对象.run() 时传入的参数
             * return 返回的是 代理对象.run() 执行后的结果
             * */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("交通工具开始运行了");
                //这里是反射,方法.对象
                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
46
47
48
49
50
51
52
53
54
55
56
57
58
59

TestVehicle

@Test
    public void proxyRun() {
        Vehicle vehicle = new Ship();
        //传入要代理的对象
        VehicleProxyProvider vehicleProxyProvider = new VehicleProxyProvider(vehicle);
        //获取代理对象,该对象可以代理执行方法
        //此时 proxy 编译类型是 Vehicle。运行类型是 代理类型,即 class com.sun.proxy.$Proxy9
        Vehicle proxy = vehicleProxyProvider.getProxy();
        //当执行 run 方法时,会执行到 代理对象的 invoke
        //如何体现动态 [1. 被代理的对象 2. 方法]
        proxy.run();
    }
1
2
3
4
5
6
7
8
9
10
11
12
交通工具开始运行了
轮船在水上 running
交通工具停止运行了
1
2
3

# 4.2 动态代理深入 - 横切关注点

# 4.2.1 需求说明

(1)有一个 SmartAnimal 接口,可以完成简单的加减法,要求在执行 getSum() 和 getSub() 时,输出执行前、执行过程、执行后的日志输出

# 4.2.2 使用传统方法完成

传统的解决思路,在各个方法的 [前、执行过程、后] 输出日志 OOP

//接口
public interface SmartAnimalable {
    //求和
    float getSum(float i, float j);
    //求查
    float getSub(float i, float j);
}
1
2
3
4
5
6
7
public class SmartDog implements SmartAnimalable{
    @Override
    public float getSum(float i, float j) {
        System.out.println("日志-方法名-getSum-参数 " + i + " " + j);
        float result = i + j;
        System.out.println("方法内部打印 result = " + result);
        System.out.println("日志-方法名-getSum-结果 result = " + result);
        return result;
    }

    @Override
    public float getSub(float i, float j) {
        System.out.println("日志-方法名-getSub-参数 " + i + " " + j);
        float result = i - j;
        System.out.println("方法内部打印 result = " + result);
        System.out.println("日志-方法名-getSub-结果 result = " + result);
        return result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AopTest {
    @Test
    public void smartDogTest() {
        SmartDog smartDog = new SmartDog();
        smartDog.getSum(10.78f, 89.7f);
        System.out.println("============");
        smartDog.getSub(10.78f, 89.7f);
    }
}
1
2
3
4
5
6
7
8
9

传统方法解决的优点:实现简单直接;缺点:日志代码维护不方便,代码复用性差

解决思路:

(1)使用动态代理来更好的处理日志记录问题

(2)其它比如封装函数,或者类的继承在这里都不是特别的合适

# 4.2.3 使用动态代理完成

要求考虑代理对象调用方法时可能出现的异常

SmartAnimalable 接口

//接口
public interface SmartAnimalable {
    //求和
    float getSum(float i, float j);
    //求查
    float getSub(float i, float j);
}
1
2
3
4
5
6
7

SmartDog 类实现了 SmartAnimalable 接口

public class SmartDog implements SmartAnimalable{
    @Override
    public float getSum(float i, float j) {
        //System.out.println("日志-方法名-getSum-参数 " + i + " " + j);
        float result = i + j;
        System.out.println("方法内部打印 result = " + result);
        //System.out.println("日志-方法名-getSum-结果 result = " + result);
        return result;
    }

    @Override
    public float getSub(float i, float j) {
        //System.out.println("日志-方法名-getSub-参数 " + i + " " + j);
        float result = i - j;
        System.out.println("方法内部打印 result = " + result);
        //System.out.println("日志-方法名-getSub-结果 result = " + result);
        return result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

MyProxyProvider 类,该类返回代理对象

//可以返回一个动态代理对象,可以执行 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));
                    //使用反射调用方法
                    //result 就是将来执行那个方法时的结果
                    result = method.invoke(target_obj, args);
                    //方法执行后的代码也叫作横切关注点,也叫作返回通知
                    System.out.println("方法执行正常结束-日志-方法名-"+ method.getName() +"-结果 result = " + result);

                } catch (Exception e) {
                    //如果在使用反射执行方法时出现异常,就会进入到 catch{}
                    e.printStackTrace();
                    //这里也是一个横切关注点,也叫作异常通知
                    System.out.println("方法执行异常-日志-方法名-"+ method.getName() +"-异常类型 = " + e.getClass().getName());
                } finally { //不管是否出现异常最终都会执行到 finally{}
                    //这里又是一个横切关注点,也叫作最终通知
                    System.out.println("方法最终结束-日志-方法名" + method.getName());
                }
                return result;
            }
        };

        //创建代理对象
        SmartAnimalable proxy = (SmartAnimalable) 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
46
47
48
49
50

AopTest 类,该类进行测试

@Test
    public void smartDogTestByProxy() {
        SmartAnimalable smartAnimalable = new SmartDog();
        MyProxyProvider myProxyProvider = new MyProxyProvider(smartAnimalable);
        //返回代理对象
        SmartAnimalable proxy = myProxyProvider.getProxy();

        proxy.getSum(10, 2);
        System.out.println("=============");
        proxy.getSub(10, 2);
    }
1
2
3
4
5
6
7
8
9
10
11
方法执行前-日志-方法名-getSum-参数 [10.0, 2.0]
方法内部打印 result = 12.0
方法执行正常结束-日志-方法名-getSum-结果 result = 12.0
方法最终结束-日志-方法名getSum
=============
方法执行前-日志-方法名-getSub-参数 [10.0, 2.0]
方法内部打印 result = 8.0
方法执行正常结束-日志-方法名-getSub-结果 result = 8.0
方法最终结束-日志-方法名getSub
1
2
3
4
5
6
7
8
9

# 4.3 问题再次出现

# 4.3.1 提出问题

(1)在 MyProxyProvider 类中,我们的输出语句功能比较弱,而且还是硬编码,在实际开发中,我们希望是以一个方法的形式嵌入到反射执行的方法前,该怎么办?,意思就是把那些输出语句可以封装到方法中,然后通过方法调用

# 4.3.2 用土方法解决

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

MyProxyProvider 类

//可以返回一个动态代理对象,可以执行 SmartDog 对象的方法
public class MyProxyProvider {
    //定义我们要执行的目标对象,该对象需要实现 SmartAnimalable 接口
    private SmartAnimalable target_obj;

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

    //自己定义的方法,在反射执行方法(目标方法)执行前执行
    public void before(Object proxy, Method method, Object[] args) {
        //下面这句代码也就是方法执行前的的代码就叫做横切关注点,也叫作前置通知
        System.out.println("before-方法执行前-日志-方法名-" + method.getName() + "-参数 " + Arrays.asList(args));
    }

    //自己定义的方法,在反射执行方法(目标方法)执行后执行
    public void after(Method method, Object result) {
        //方法执行后的代码也叫作横切关注点,也叫作返回通知
        System.out.println("after-方法执行正常结束-日志-方法名-"+ method.getName() +"-结果 result = " + result);
    }

    //方法,可以返回代理对象,该代理对象可以执行目标对象
    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 {
                    before(proxy, method, args);
                    
                    //使用反射调用方法
                    //result 就是将来执行那个方法时的结果
                    result = method.invoke(target_obj, args);

                    after(method, result);

                } catch (Exception e) {
                    //如果在使用反射执行方法时出现异常,就会进入到 catch{}
                    e.printStackTrace();
                    //这里也是一个横切关注点,也叫作异常通知
                    System.out.println("方法执行异常-日志-方法名-"+ method.getName() +"-异常类型 = " + e.getClass().getName());
                } finally { //不管是否出现异常最终都会执行到 finally{}
                    //这里又是一个横切关注点,也叫作最终通知
                    System.out.println("方法最终结束-日志-方法名" + method.getName());
                }
                return result;
            }
        };

        //创建代理对象
        SmartAnimalable proxy = (SmartAnimalable) 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

问题分析:耦合度高

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

HspAOP 类

//我们自己编写的一个极简的 AOP 类
public class HspAOP {
    //自己定义的方法,在反射执行方法(目标方法)执行前执行
    public static void before(Object proxy, Method method, Object[] args) {
        //下面这句代码也就是方法执行前的的代码就叫做横切关注点,也叫作前置通知
        System.out.println("HspAOP-方法执行前-日志-方法名-" + method.getName() + "-参数 " + Arrays.asList(args));
    }

    //自己定义的方法,在反射执行方法(目标方法)执行后执行
    public static void after(Method method, Object result) {
        //方法执行后的代码也叫作横切关注点,也叫作返回通知
        System.out.println("HspAOP-方法执行正常结束-日志-方法名-"+ method.getName() +"-结果 result = " + result);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

MyProxyProvider 类

//可以返回一个动态代理对象,可以执行 SmartDog 对象的方法
public class MyProxyProvider {
    //定义我们要执行的目标对象,该对象需要实现 SmartAnimalable 接口
    private SmartAnimalable target_obj;

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

    /*//自己定义的方法,在反射执行方法(目标方法)执行前执行
    public void before(Object proxy, Method method, Object[] args) {
        //下面这句代码也就是方法执行前的的代码就叫做横切关注点,也叫作前置通知
        System.out.println("before-方法执行前-日志-方法名-" + method.getName() + "-参数 " + Arrays.asList(args));
    }*/

    /*//自己定义的方法,在反射执行方法(目标方法)执行后执行
    public void after(Method method, Object result) {
        //方法执行后的代码也叫作横切关注点,也叫作返回通知
        System.out.println("after-方法执行正常结束-日志-方法名-"+ method.getName() +"-结果 result = " + result);
    }*/

    //方法,可以返回代理对象,该代理对象可以执行目标对象
    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 {
                    //before(proxy, method, args);
                    HspAOP.before(proxy, method, args);

                    //使用反射调用方法
                    //result 就是将来执行那个方法时的结果
                    result = method.invoke(target_obj, args);

                    //after(method, result);
                    HspAOP.after(method, result);

                } catch (Exception e) {
                    //如果在使用反射执行方法时出现异常,就会进入到 catch{}
                    e.printStackTrace();
                    //这里也是一个横切关注点,也叫作异常通知
                    System.out.println("方法执行异常-日志-方法名-"+ method.getName() +"-异常类型 = " + e.getClass().getName());
                } finally { //不管是否出现异常最终都会执行到 finally{}
                    //这里又是一个横切关注点,也叫作最终通知
                    System.out.println("方法最终结束-日志-方法名" + method.getName());
                }
                return result;
            }
        };

        //创建代理对象
        SmartAnimalable proxy = (SmartAnimalable) 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

# 4.3.4 再次分析 - 提出 Spring AOP

(1)土方法不够灵活

(2)土方法复用性差

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

(4)Spring AOP 闪亮登场,底层是 ASPECTJ

# 4.4 AOP 的基本介绍

# 4.4.1 什么是 AOP

AOP 的全称是 Aspect Oriented Programming,即面向切面的编程

面向对象编程

面向切面编程

# 4.5 AOP 编程快速入门

# 4.5.1 基本说明

(1)需要引入核心的 aspect 包

(2)在切面类中声明通知方法:

​ 1)前置通知:@Before

​ 2)返回通知:@AfterReturning

​ 3)异常通知:@AfterThrowing

​ 4)后置通知:@After

​ 5)环绕通知:@Around

# 4.5.2 快速入门实例

(1)需求说明:使用 AOP 编程的方式,来实现手写的动态代理案例效果,就以上一个案例为例来讲解

(2)导入 AOP 编程需要的包

(3)代码实现

SmartAnimalable 接口

//接口
public interface SmartAnimalable {
    //求和
    float getSum(float i, float j);
    //求查
    float getSub(float i, float j);
}
1
2
3
4
5
6
7

SmartDog实现类

@Component //使用 @Component 当 Spring 容器启动时,将 SmartDog 注入到容器
public class SmartDog implements SmartAnimalable{
    @Override
    public float getSum(float i, float j) {
        float result = i + j;
        System.out.println("方法内部打印 result = " + result);
        return result;
    }

    @Override
    public float getSub(float i, float j) {
        float result = i - j;
        System.out.println("方法内部打印 result = " + result);
        return result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

SmartAnimalAspect 切面类

@Aspect //表示是一个切面类 [底层有切面编程的支撑(动态代理+反射+动态绑定)]
@Component //会注入 SmartAnimalAspect 到容器
public class SmartAnimalAspect {
    //自己定义的方法,在反射执行方法(目标方法)执行前执行
    /**
     * (1) @Before 表示前置通知,即在我们的目标对象执行方法前执行
     * (2) value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))" 表示要把前置通知放在 value = 的这个方法之前,即指定切入到哪个类的哪个方法,形式是:访问修饰符 返回类型 全类名.方法名(形参列表)
     * (3) f1 方法可以理解成就是一个切入方法,这个方法名是可以程序员指定的
     * (4) JoinPoint joinPoint 在底层执行时,由 AspectJ 切面框架,会给该切入方法传入 joinPoint 对象,joinPoint 对象里有 Object proxy, Method method, Object[] args 这些信息
     * */
    @Before(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))")
    public void f1(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        //下面这句代码也就是方法执行前的的代码就叫做横切关注点,也叫作前置通知
        System.out.println("切面类 f1() -方法执行前-日志-方法名-" + signature.getName() + "-参数 " + Arrays.asList(joinPoint.getArgs()));
    }

    //自己定义的方法,在反射执行方法(目标方法)执行后执行
    @AfterReturning(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))")
    public void f2(JoinPoint joinPoint) {
        //方法执行后的代码也叫作横切关注点,也叫作返回通知
        Signature signature = joinPoint.getSignature();
        System.out.println("切面类 f2() -方法执行正常结束后-日志-方法名-" + signature.getName() + "-结果 ");
    }

    //异常通知
    @AfterThrowing(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))")
    public void f3(JoinPoint joinPoint) {
        //方法执行后的代码也叫作横切关注点,也叫作返回通知
        Signature signature = joinPoint.getSignature();
        System.out.println("切面类 f3() -方法执行异常后-日志-方法名-" + signature.getName());
    }

    //最终通知:即把 f4 方法切入到目标方法执行后(不管是否发生异常,都要执行)
    @After(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))")
    public void f4(JoinPoint joinPoint) {
        //方法执行后的代码也叫作横切关注点,也叫作返回通知
        Signature signature = joinPoint.getSignature();
        System.out.println("切面类 f4() -方法最终执行完毕-日志-方法名-" + signature.getName());
    }
}
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

配置文件

<context:component-scan base-package="com.hspedu.spring.aop.aspectj"/>
    <!--开启基于注解的 AOP 功能-->
    <aop:aspectj-autoproxy/>
1
2
3

测试类

public class AopAspectjTest {
    @Test
    public void smartDogTestByProxy() {
        //得到 Spring 容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans08.xml");
        //这里我们需要通过接口类型来获取到注入的 SmartDog 对象 - 就是代理对象
        SmartAnimalable smartAnimalable = ioc.getBean(SmartAnimalable.class);
        smartAnimalable.getSum(10, 2);
        System.out.println("smartAnimalable 的运行类型 = " + smartAnimalable.getClass()); //smartAnimalable 的运行类型 = class com.sun.proxy.$Proxy13
    }
}
1
2
3
4
5
6
7
8
9
10
11
切面类 f1() -方法执行前-日志-方法名-getSum-参数 [10.0, 2.0]
方法内部打印 result = 12.0
切面类 f2() -方法执行正常结束后-日志-方法名-getSum-返回的结果是 = 12.0
切面类 f4() -方法最终执行完毕-日志-方法名-getSum
smartAnimalable 的运行类型 = class com.sun.proxy.$Proxy19
1
2
3
4
5

# 4.5.3 细节说明

(1)关于切面类方法命名规范:showBeginLog(),showSuccessEndLog(),showExceptionLog(),showFinallyEndLog()

(2)切入表达式的更多配置,比如使用模糊配置

@Before(value="execution(* com.hspedu.aop.proxy.SmartDog.*(..))")

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

@Before(value="execution(* *.*(..))")

(4)当 Spring 容器开启了基于注解的 AOP 的功能 <aop:aspectj-autoproxy/> ,我们获取注入的对象,需要以接口的类型来获取,因为注入的对象.getClass() 已经是代理类型了

(5)当 Spring 容器开启了基于注解的 AOP 的功能 <aop:aspectj-autoproxy/> ,我们获取注入的对象,也可以通过 id 来获取,但是也要转成接口类型

# 4.5.4 课后作业

(1)有接口 UsbInterface 里有方法 work

(2)实现子类 Phone 和 Camera,实现 UsbInterface

(3)在 SmartAnimalAspect 切面类中写一个方法(可输出日志信息)等作为前置通知,在 Phone 和 Camera 对象执行 work 方法前调用

(4)其它返回通知,异常通知,后置通知也可以加入

接口

public interface UsbInterface {
    public void work();
}
1
2
3

实现类

@Component //把 Phone 对象当做一个组件注入到容器中
public class Phone implements UsbInterface{
    @Override
    public void work() {
        System.out.println("手机开始工作了...");
    }
}
1
2
3
4
5
6
7
@Component //将 Camera 注入到 Spring 容器中
public class Camera implements UsbInterface{
    @Override
    public void work() {
        System.out.println("相机开始工作了...");
    }
}
1
2
3
4
5
6
7

切面类

@Before(value = "execution(public void com.hspedu.spring.aop.aspectj.Phone.work()) || execution(public void com.hspedu.spring.aop.aspectj.Camera.work())")
    public void hi(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        System.out.println("切面类的 hi() - 执行的目标方法- " +signature.getName());
    }
1
2
3
4
5

测试类

@Test
    public void smartDogTestByProxy2() {
        //得到 Spring 容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans08.xml");
        //这里我们需要通过 id 来获取到注入的 Phone 对象,但是要转成接口类型
        UsbInterface phone = (UsbInterface) ioc.getBean("phone");
        UsbInterface camera = (UsbInterface) ioc.getBean("camera");

        phone.work();

        System.out.println("========");

        camera.work();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4.6 AOP 切入表达式

# 4.6.1 具体使用

(1)作用:通过表达式的方式定位一个或多个具体的连接点

(2)语法细节:

​ 1)切入点表达式的语法格式:execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表]))

​ 2)举例说明:

​ 3)在 AspectJ 中,切入点表达式可以通过 &&、||、! 等操作符结合起来

# 4.6.2 注意事项和细节

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

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

(3)切入表达式可以对没有实现接口的类进行切入

Car 类

@Component //把 Car 作为一个组件,注入到 Spring 容器中
public class Car {
    public void run() {
        System.out.println("小汽车在 running");
    }
}
1
2
3
4
5
6

切面类

//给 Car 配置一个前置通知
    @Before(value = "execution(public void com.hspedu.spring.aop.aspectj.Car.run())")
    public void ok1(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        System.out.println("切面类的 ok1() - 执行的目标方法- " +signature.getName());
    }
1
2
3
4
5
6

测试类

@Test
    public void test3() {
        //得到 Spring 容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans08.xml");
        Car car = ioc.getBean(Car.class);
        car.run();
    }
1
2
3
4
5
6
7
切面类的 ok1() - 执行的目标方法- run
小汽车在 running
1
2

# 4.6.3 动态代理中 JDK 的 Proxy 的代理和 Spring 的 CGlib 的代理的特点和区别

(1)JDK 的 Proxy 动态代理是面向接口的动态代理

特点:

​ 1)一定要有接口和实现类的存在,代理对象增强的是实现类在实现接口时重写的方法

​ 2)生成的代理对象只能转换成接口的不能转换成被代理类

​ 3)代理对象只能增强接口中定义的方法,实现类中的其它的和接口无关的方法是无法增强的

​ 4)代理对象只能读取到实现类实现接口中的方法上的注解,不能读取到实现类中特有方法上的注解

(2)Spring 的 CGlib 动态代理是面向父类的动态代理

特点:

​ 1)面向父类的和接口没有直接关系

​ 2)不仅可以增强接口中定义的方法还可以增强其它方法

​ 3)可以读取父类中方法上的所有注解

(3)两个动态代理的区别

​ 1)JDK 动态代理是面向接口的,只能增强实现类中接口中存在的方法;CGlib 是面向父类的,可以增强父类的所有方法

​ 2)JDK 得到的对象是代理对象实例,而 CGlib 得到的对象是被代理对象的子类

# 4.7 JoinPoint

# 4.7.1 应用实例

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

应用实例需求:在调用前置通知获取到调用方法的签名和其它相关信息

代码实现:前面已经写过了

其它常用方法:

# 4.8 返回通知获取结果

/**
     * (1) 如果我们希望把目标方法执行的结果返回给切入方法,可以在 @AfterReturning 里增加属性,比如 returning = "res",同时在切入方法中增加形参 Object res
     * (2) 注意 returning = "res" 和 Object res 的 res 要一致
     * */
    @AfterReturning(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))", returning = "res")
    public void f2(JoinPoint joinPoint, Object res) {
        //方法执行后的代码也叫作横切关注点,也叫作返回通知
        Signature signature = joinPoint.getSignature();
        System.out.println("切面类 f2() -方法执行正常结束后-日志-方法名-" + signature.getName() + "-返回的结果是 = " + res);
    }
1
2
3
4
5
6
7
8
9
10

# 4.9 异常通知中获取异常

//异常通知
    @AfterThrowing(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))", throwing = "throwable")
    public void f3(JoinPoint joinPoint, Throwable throwable) {
        //方法执行后的代码也叫作横切关注点,也叫作返回通知
        Signature signature = joinPoint.getSignature();
        System.out.println("切面类 f3() -方法执行异常后-日志-方法名-" + signature.getName() + " 异常信息 = " + throwable);
    }
1
2
3
4
5
6
7

# 4.10 环绕通知(了解)

# 4.11 切入点表达式重用

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

@Aspect //表示是一个切面类 [底层有切面编程的支撑(动态代理+反射+动态绑定)]
@Component //会注入 SmartAnimalAspect 到容器
public class SmartAnimalAspect {

    //定义一个切入点,在后面使用时可以直接引用,提高了复用性
    @Pointcut(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))")
    public void myPointCut() {

    }

    //自己定义的方法,在反射执行方法(目标方法)执行前执行
    //@Before(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))")
    //这里我们使用定义好的切入点
    @Before(value = "myPointCut()")
    public void f1(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        //下面这句代码也就是方法执行前的的代码就叫做横切关注点,也叫作前置通知
        System.out.println("切面类 f1() -方法执行前-日志-方法名-" + signature.getName() + "-参数 " + Arrays.asList(joinPoint.getArgs()));
    }

    //自己定义的方法,在反射执行方法(目标方法)执行后执行
    //@AfterReturning(value = "execution(public float com.hspedu.spring.aop.aspectj.SmartDog.getSum(float, float))", returning = "res")
    //使用切入点
    @AfterReturning(value = "myPointCut()", returning = "res")
    public void f2(JoinPoint joinPoint, Object res) {
        //方法执行后的代码也叫作横切关注点,也叫作返回通知
        Signature signature = joinPoint.getSignature();
        System.out.println("切面类 f2() -方法执行正常结束后-日志-方法名-" + signature.getName() + "-返回的结果是 = " + res);
    }
}
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

# 4.12 切面优先级问题

# 4.12.1 基本概念

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

基本语法:@order(value=n) 来控制 n 值,n 值越小优先级越高

# 4.12.2 注意事项和细节说明

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

(2)流程图

# 4.13 基于 XML 配置 AOP

(1)前面我们是通过注解来配置 AOP 的,在 Spring 中,我们也可以通过 XML 的方式来配置 AOP

(2)代码实现

SmartAnimalable 接口

//接口
public interface SmartAnimalable {
    //求和
    float getSum(float i, float j);
    //求查
    float getSub(float i, float j);
}
1
2
3
4
5
6
7

SmartDog 实现类

public class SmartDog implements SmartAnimalable {
    @Override
    public float getSum(float i, float j) {
        float result = i + j;
        System.out.println("方法内部打印 result = " + result);
        return result;
    }

    @Override
    public float getSub(float i, float j) {
        float result = i - j;
        System.out.println("方法内部打印 result = " + result);
        return result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

SmartAnimalAspect 切面类

public class SmartAnimalAspect {
    public void f1(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        //下面这句代码也就是方法执行前的的代码就叫做横切关注点,也叫作前置通知
        System.out.println("切面类 f1() -方法执行前-日志-方法名-" + signature.getName() + "-参数 " + Arrays.asList(joinPoint.getArgs()));
    }

    public void f2(JoinPoint joinPoint, Object res) {
        //方法执行后的代码也叫作横切关注点,也叫作返回通知
        Signature signature = joinPoint.getSignature();
        System.out.println("切面类 f2() -方法执行正常结束后-日志-方法名-" + signature.getName() + "-返回的结果是 = " + res);
    }

    public void f3(JoinPoint joinPoint, Throwable throwable) {
        //方法执行后的代码也叫作横切关注点,也叫作返回通知
        Signature signature = joinPoint.getSignature();
        System.out.println("切面类 f3() -方法执行异常后-日志-方法名-" + signature.getName() + " 异常信息 = " + throwable);
    }

    public void f4(JoinPoint joinPoint) {
        //方法执行后的代码也叫作横切关注点,也叫作返回通知
        Signature signature = joinPoint.getSignature();
        System.out.println("切面类 f4() -方法最终执行完毕-日志-方法名-" + signature.getName());
    }
}
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

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: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/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--使用 XML 配置,完成 AOP 编程-->
    <!--注入一个切面类对象-->
    <bean class="com.hspedu.spring.aop.xml.SmartAnimalAspect" id="smartAnimalAspect"/>
    <!--注入一个 SmartDog 对象-->
    <bean class="com.hspedu.spring.aop.xml.SmartDog" id="smartDog"/>

    <!--配置切面类-->
    <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">
            <!--配置前置通知-->
            <!--解读:意思就是把 smartAnimalAspect 类中的 f1 方法,作为前置通知切入到 myPointCut 的这个点的位置,这个点的位置就是上面配置的位置-->
            <aop:before method="f1" pointcut-ref="myPointCut"/>
            <!--返回通知-->
            <!--解读:f2 方法切入到 myPointCut 这个点的位置,myPointCut 这个点的位置的方法执行完毕后将结果返回给 res,res 又作为 f2 的形参,从而 f2 可以取得结果-->
            <aop:after-returning method="f2" pointcut-ref="myPointCut" returning="res"/>
            <!--异常通知-->
            <aop:after-throwing method="f3" pointcut-ref="myPointCut" throwing="throwable"/>
            <!--最终通知-->
            <aop:after method="f4" pointcut-ref="myPointCut"/>
        </aop:aspect>
    </aop:config>
</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

AopAspectjXMLTest 测试类

public class AopAspectjXMLTest {
    @Test
    public void testAspectByXML() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("beans09.xml");
        SmartAnimalable smartAnimalable = ioc.getBean(SmartAnimalable.class);
        smartAnimalable.getSum(10, 2);
    }
}
1
2
3
4
5
6
7
8

# 4.14 课后作业

# 4.14.1 作业布置

(1)编写一个 Cal 接口

有方法 cal1(int n) 计算 1+2+..+n

有方法 cal2(int n) 计算 1*2*..*n

(2)编写类 MyCal 实现接口 Cal

(3)分别使用注解方式、XML 配置方式完成 AOP 编程

​ 1)在执行 cal1 前打印开始执行的时间,在执行完后打印时间

​ 2)在执行 cal2 前打印开始执行的时间,在执行完后打印时间

# 第 6 章 JDBCTemplate

# 6.1 实际需求

如果程序员使用 Spring 框架来做项目,Spring 框架如何处理对数据库的操作呢?

(1)使用前面做项目开发的 JDBCUtils 类

(2)使用 Spring 提供的一个操作数据库的功能强大的类 JDBCTemplate,我们可以通过 ioc 容器来配置一个 JDBCTemplate 对象,使用它来完成对数据库表的各种操作

# 6.2 JDBCTemplate 基本介绍

(1)通过 Spring 可以配置数据源,从而完成对数据表的操作

(2)JDBCTemplate 是 Spring 提供的访问数据库的技术,可以将 JDBC 的常用操作封装为模板方法

# 6.3 JDBCTemplate 使用实例

# 6.3.1 需求说明

使用 Spring 的方式来完成 JDBCTemplate 的配置和使用

# 6.3.2 JDBCTemplate 的使用

(1)引入使用 JDBCTemplate 需要的 jar 包

(2)创建数据库 Spring 和表 monster

-- 创建数据库
create database spring;
use spring;

-- 创建表 monster
create table monster (
    id int primary key ,
    name varchar(64) not null default '',
    skill varchar(64) not null default ''
);

insert into monster values (100, '青牛怪', '吐火');
insert into monster values (200, '黄袍怪', '吐烟');
insert into monster values (300, '蜘蛛怪', '吐丝');
1
2
3
4
5
6
7
8
9
10
11
12
13
14

(3)创建配置文件 src/jdbc.properties

jdbc.user=root
jdbc.pwd=123456
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring
1
2
3
4

(4)创建配置文件 src/JdbcTemplate_ioc.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">

    <!--引入外部的 jdbc.properties 文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--配置数据源对象-DataSource-->
    <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
        <!--给数据源对象配置属性值-->
        <property name="user" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.pwd}"/>
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
    </bean>
</beans>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

(5)测试

public class JdbcTemplateTest {
    @Test
    public void testDatasourceByJdbcTemplate() throws SQLException {
        //获取到容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("JdbcTemplate_ioc.xml");
        DataSource dataSource = ioc.getBean(DataSource.class);
        Connection connection = dataSource.getConnection();
        System.out.println("获取到 connection = " + connection);
        connection.close();
        System.out.println("ok");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

(6)配置 JdbcTemplate_ioc.xml,将数据源分配给 JdbcTemplate bean

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns: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">

    <!--引入外部的 jdbc.properties 文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--配置数据源对象-DataSource-->
    <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
        <!--给数据源对象配置属性值-->
        <property name="user" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.pwd}"/>
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
    </bean>

    <!--配置 JdbcTemplate 对象-->
    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
        <!--给 JdbcTemplate 对象配置 dataSource 属性-->
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

(7)使用 JdbcTemplate 对象完成添加数据

//测试通过 JdbcTemplate 对象完成添加数据
    @Test
    public void addDataByJdbcTemplate() {
        //获取到容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("JdbcTemplate_ioc.xml");
        //获取 JdbcTemplate 对象
        JdbcTemplate jdbcTemplate = ioc.getBean(JdbcTemplate.class);

        //添加方式 1
        //String sql = "insert into monster values(400, '红孩儿', '枪法')";
        //jdbcTemplate.execute(sql);

        //添加方式 2
        String sql = "insert into monster values(?, ?, ?)";
        int affected = jdbcTemplate.update(sql, 500, "红孩儿2", "枪法2");

        System.out.println("添加成功 affected = " + affected);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

(8)使用 JdbcTemplate 对象修改数据

//测试通过 JdbcTemplate 对象完成修改数据
    @Test
    public void updateDataByJdbcTemplate() {
        //获取到容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("JdbcTemplate_ioc.xml");

        //获取 JdbcTemplate 对象
        JdbcTemplate jdbcTemplate = ioc.getBean(JdbcTemplate.class);

        //组织 SQL
        String sql = "update monster set skill=? where id=?";
        int affected = jdbcTemplate.update(sql, "美女计", 300);
        System.out.println("成功 affected = " + affected);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

(9)使用 JdbcTemplate 批量添加两个 monster

//批量添加两个 monster
    @Test
    public void addBatchDataByJdbcTemplate() {
        //获取到容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("JdbcTemplate_ioc.xml");

        //获取 JdbcTemplate 对象
        JdbcTemplate jdbcTemplate = ioc.getBean(JdbcTemplate.class);

        String sql = "insert into monster values(?, ?, ?)";
        List<Object[]> batchArgs = new ArrayList<>();
        batchArgs.add(new Object[]{600, "老鼠精", "偷吃粮食"});
        batchArgs.add(new Object[]{700, "老猫精", "抓老鼠"});
        int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs);

        for (int anInt : ints) {
            System.out.println("anInt = " + anInt);
        }
        System.out.println("成功");
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

(10)查询 id=100 的 monster 并封装到 Monster 实体对象

//查询 id=100 的 monster 并封装到 Monster 实体对象
    @Test
    public void selectDataByJdbcTemplate() {
        //获取到容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("JdbcTemplate_ioc.xml");

        //获取 JdbcTemplate 对象
        JdbcTemplate jdbcTemplate = ioc.getBean(JdbcTemplate.class);

        //组织 Sql
        String sql = "select id as monsterId, name, skill from monster where id = 100";
        //使用 RowMapper 接口来对返回的数据进行封装,底层使用反射
        RowMapper<Monster> rowMapper = new BeanPropertyRowMapper<>(Monster.class);
        Monster monster = jdbcTemplate.queryForObject(sql, rowMapper);
        System.out.println("monster = " + monster);
        System.out.println("查询成功");
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

(11)查询 id >= 200 的 monster 并封装到 Monster 实体对象

//查询 id >= 200 的 monster 并封装到 Monster 实体对象
    @Test
    public void selectMulDataByJdbcTemplate() {
        //获取到容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("JdbcTemplate_ioc.xml");

        //获取 JdbcTemplate 对象
        JdbcTemplate jdbcTemplate = ioc.getBean(JdbcTemplate.class);

        //组织 SQL
        String sql = "select id as monsterId, name, skill from monster where id >= ?";
        RowMapper<Monster> rowMapper = new BeanPropertyRowMapper<>(Monster.class);
        List<Monster> monsterList = jdbcTemplate.query(sql, rowMapper, 100);
        for (Monster monster : monsterList) {
            System.out.println("monster = " + monster);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

(12)查询返回结果只有一行一列的值,比如查询 id = 100 的怪物名

//查询返回结果只有一行一列的值,比如查询 id = 100 的怪物名
    @Test
    public void selectScalarByJdbcTemplate() {
        //获取到容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("JdbcTemplate_ioc.xml");

        //获取 JdbcTemplate 对象
        JdbcTemplate jdbcTemplate = ioc.getBean(JdbcTemplate.class);

        //组织 SQL
        String sql = "select name from monster where id = 100";

        String name = jdbcTemplate.queryForObject(sql, String.class);
        System.out.println("返回 name = " + name);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

(13)使用 Map 传入具名参数完成操作,具名参数形式需要使用 NamedParameterJdbcTemplate 类,语句形式:String sql = "insert into monster values(:my_id, :name, :skill)"

<!--配置 NameParameterJdbcTemplate 对象-->
    <bean class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate" id="namedParameterJdbcTemplate">
        <!--通过构造器设置数据源-->
        <constructor-arg name="dataSource" ref="dataSource"/>
    </bean>
1
2
3
4
5
//使用 Map 传入具名参数完成操作
    @Test
    public void testDataByNamedParameterJdbcTemplate() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("JdbcTemplate_ioc.xml");

        //得到 NamedParameterJdbcTemplate bean
        NamedParameterJdbcTemplate namedParameterJdbcTemplate = ioc.getBean(NamedParameterJdbcTemplate.class);

        //准备参数
        //具名参数就是把原来的 ? 换成具体的名称,这样赋值的时候就知道这里应该赋什么值
        String sql = "insert into monster values(:id, :name, :skill)";
        Map<String, Object> paramMap = new HashMap<>();
        //给 paramMap 填写数据
        paramMap.put("id", 800);
        paramMap.put("name", "蚂蚁精");
        paramMap.put("skill", "喜欢打洞");
        int affected = namedParameterJdbcTemplate.update(sql, paramMap);
        System.out.println("成功,affected = " + affected);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

(14)使用 SqlParameterSource 来封装具名参数

//使用 SqlParameterSource 来封装具名参数
    @Test
    public void operDataBySqlparametersource() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("JdbcTemplate_ioc.xml");

        //得到 NamedParameterJdbcTemplate bean
        NamedParameterJdbcTemplate namedParameterJdbcTemplate = ioc.getBean(NamedParameterJdbcTemplate.class);

        //准备参数
        String sql = "insert into monster values(:monsterId, :name, :skill)";
        Monster monster = new Monster(900, "大象精", "搬运木头");
        BeanPropertySqlParameterSource sqlParameterSource = new BeanPropertySqlParameterSource(monster);
        int affected = namedParameterJdbcTemplate.update(sql, sqlParameterSource);
        System.out.println("成功,affected = " + affected);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

(15)DAO 对象中使用 JdbcTemplate 完成对数据的操作

MonsterDAO

@Repository //将 MonsterDao 注入到 Spring 容器
public class MonsterDao {
    //注入一个属性
    @Resource
    private JdbcTemplate jdbcTemplate;

    //完成保存任务
    public void save(Monster monster) {
        //组织 sql
        String sql = "insert into monster values(?, ?, ?)";
        int update = jdbcTemplate.update(sql, monster.getMonsterId(), monster.getName(), monster.getSkill());
        System.out.println("成功 update = " + update);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

配置文件

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

测试

//测试 MonsterDAO
    @Test
    public void monsterDaoSave() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("JdbcTemplate_ioc.xml");
        MonsterDao monsterDao = ioc.getBean(MonsterDao.class);
        Monster monster = new Monster(1000, "小鸭精", "吃鱼");
        monsterDao.save(monster);
        System.out.println("保存成功");
    }
1
2
3
4
5
6
7
8
9

# 第 7 章 声明式事务

# 7.1 事务分类

(1)编程式事务:

传统方式:

Connection connection = JDBCUtils.getConnection();
try{
    //1. 先设置事务不要自动提交
    connection.setAutoCommit(false);
    //2. 进行各种 crud
    //多个表的修改,添加,删除
    //3. 提交
    connection.commit();
} catch(Exception e) {
    //4. 回滚
    connection.rollback();
}
1
2
3
4
5
6
7
8
9
10
11
12

(2)声明式事务

下面讲

# 7.2 声明式事务使用实例

# 7.2.1 需求说明 - 用户购买商品

我们需要去处理用户购买商品的业务逻辑,分析当一个用户要去购买商品应该包含三个步骤:

(1)通过商品 id 获取价格

(2)购买商品(某人购买商品,修改用户的余额)

(3)修改库存量

这时,我们需要涉及到三张表:商品表、用户表、商品存量表,应该使用事务处理

意思就是:当第(2)步成功但是第(3)步失败将会有影响,利用事务处理让三个步骤同时进行,所有成功才运行成功,只要有一个步骤错了就不运行,总而言之:要么全部成功,要么全部失败;不能部分成功,不能部分失败

# 7.2.2 解决方案分析

(1)使用传统的编程事务来处理,将代码写到一起【缺点:代码冗余、效率低、不利于扩展;优点:简单、好理解】

Connection connection = JDBCUtils.getConnection();
try{
    //1. 先设置事务不要自动提交
    connection.setAutoCommit(false);
    //2. 进行各种 crud
    //多个表的修改,添加,删除
    获取价格 => select from 商品表
    修改用户余额 => update...
    修改库存量 => update...
    //3. 提交
    connection.commit();
} catch(Exception e) {
    //4. 回滚
    connection.rollback();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

(2)使用 Spring 的声明式事务处理,可以将上面三个子步骤分别写成一个方法,然后统一管理【这个是 Spring 很牛的地方,在开发中使用的很多,优点:无代码冗余、效率高、扩展方便;缺点:难以理解】

# 7.2.3 代码实现

(1)先创建商品系统的数据库和表

-- 演示声明式事务创建的表
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
);

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
);

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
);

insert into goods_amount values (1, 200);
insert into goods_amount values (2, 20);
insert into goods_amount values (3, 15);
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

GoodsDao

@Repository //将 GoodsDao 对象注入到 Spring 容器
public class GoodsDao {
    @Resource
    private JdbcTemplate jdbcTemplate;

    /**
    * @Author: 止束
    * @Description: 根据商品 id,返回对应的价格
    * @DateTime: 19:27 2024/6/21
    * @Params: [id]
    * @Return java.lang.Float
    */
    public Float queryPriceById(Integer id) {
        String sql = "select price from goods where goods_id = ?";
        Float price = jdbcTemplate.queryForObject(sql, Float.class, id);
        return price;
    }

    /**
    * @Author: 止束
    * @Description: 减少用户的余额
    * @DateTime: 19:30 2024/6/21
    * @Params: [user_id, money]
    * @Return void
    */
    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);
    }

    /**
    * @Author: 止束
    * @Description: 减少商品库存
    * @DateTime: 19:33 2024/6/21
    * @Params: [goods_id, amount]
    * @Return void
    */
    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);
    }
}
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

GoodsService

@Service //将 GoodsService 对象注入到 Spring 容器
public class GoodsService {
    //定义属性 GoodsDao
    @Resource
    private GoodsDao goodsDao;


    /**
    * @Author: 止束
    * @Description: 编写一个方法,完成用户购买商品的业务
    * @DateTime: 19:53 2024/6/21
    * @Params: [userId:用户 id, goodsId:商品 id, amount:购买数量]
    * @Return void
    */
    public void buyGoods(int userId, int goodsId, int amount) {

        //输出购买的相关信息
        System.out.println("用户购买的信息 userId = " + userId + " goodsId = " + goodsId + " 购买数量 = " + amount);

        //1. 得到商品的价格
        Float price = goodsDao.queryPriceById(userId);
        //2. 减少用户的余额
        goodsDao.updateBalance(userId, price * amount);
        //3. 减少库存量
        goodsDao.updateAmount(goodsId, amount);

        System.out.println("用户购买成功");
    }

    /**
     * @Author: 止束
     * @Description: @Transactional 解读:使用 @Transactional 可以进行声明式事务控制,即可以将对数据库操作的方法作为一个事务管理
     * @DateTime: 12:14 2024/6/22
     * @Params: []
     * @Return void
     */
    @Transactional
    public void buyGoodsByTx(int userId, int goodsId, int amount) {

        //输出购买的相关信息
        System.out.println("用户购买的信息 userId = " + userId + " goodsId = " + goodsId + " 购买数量 = " + amount);

        //1. 得到商品的价格
        Float price = goodsDao.queryPriceById(userId);
        //2. 减少用户的余额
        goodsDao.updateBalance(userId, price * amount);
        //3. 减少库存量
        goodsDao.updateAmount(goodsId, amount);

        System.out.println("用户购买成功");
    }
}
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

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" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--配置要扫描的包-->
    <context:component-scan base-package="com.hspedu.spring.tx.dao"/>
    <context:component-scan base-package="com.hspedu.spring.tx.service"/>
    <!--引入外部的 jdbc.properties 文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--配置数据源对象-DataSource-->
    <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
        <!--给数据源对象配置属性值-->
        <property name="user" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.pwd}"/>
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
    </bean>

    <!--配置 JdbcTemplate 对象-->
    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
        <!--给 JdbcTemplate 对象配置 dataSource 属性-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置事务管理器-对象
        1. DataSourceTransactionManager 这个对象是进行事务管理的
        2. 一定要配置数据源属性,这样可以指定该事务管理器是对哪个数据源进行事务控制
    -->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置启动基于注解的声明式事务管理功能-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</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

测试

//测试用户购买商品业务
    @Test
    public void buyGoodsTest() {
        //获取到容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("tx_ioc.xml");

        GoodsService goodsService = ioc.getBean(GoodsService.class);
        goodsService.buyGoods(1, 1, 1);
    }

    //测试用户购买商品业务
    @Test
    public void buyGoodsByTxTest() {
        //获取到容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("tx_ioc.xml");

        GoodsService goodsService = ioc.getBean(GoodsService.class);
        goodsService.buyGoodsByTx(1, 1, 1); //这里调用的是进行了事务声明的方法
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 7.3 事务的传播机制

# 7.3.1 事务的传播机制说明

(1)当有多个事务处理并存时,如何控制?比如用户去购买两次商品(使用不同的方法),每个方法都是一个事务,那么如何控制呢?

(2)这个就是事务的传播机制

(3)如上图中 multiTxTest 方法本身是一个事务,buyGoods 方法和 buyGoods2 方法也都是一个事务,那么当调用 buyGoods 方法时是使用 buyGoods 方法的事务还是使用 multiTxTest 方法的事务?这就是事务传播机制

# 7.3.2 事务传播机制种类

(1)事务传播的属性/种类一览图

(2)事务传播的属性/种类机制分析,重点分析了 REQUIRED 和 REQUIRES_NEW 两种事务传播属性,其它了解即可

(3)如果设置为 REQUIRED 时,buyGoods2 和 buyGoods 是一个整体,只要有方法的事务错误,那么两个方法都不会执行成功

如果设置为 REQUIRES_NEW 时,buyGoods2 如果错误,不会影响到 buyGoods,反之亦然,即它们的事务是独立的

# 7.3.3 事务的传播机制的应用实例

(1)需求说明:比如用户去购买两次商品(使用不同的方法),每个方法都是一个事务,那么如何控制呢?

@Repository //将 GoodsDao 对象注入到 Spring 容器
public class GoodsDao {
    @Resource
    private JdbcTemplate jdbcTemplate;

    /**
    * @Author: 止束
    * @Description: 根据商品 id,返回对应的价格
    * @DateTime: 19:27 2024/6/21
    * @Params: [id]
    * @Return java.lang.Float
    */
    public Float queryPriceById(Integer id) {
        String sql = "select price from goods where goods_id = ?";
        Float price = jdbcTemplate.queryForObject(sql, Float.class, id);
        return price;
    }

    /**
    * @Author: 止束
    * @Description: 减少用户的余额
    * @DateTime: 19:30 2024/6/21
    * @Params: [user_id, money]
    * @Return void
    */
    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);
    }

    /**
    * @Author: 止束
    * @Description: 减少商品库存
    * @DateTime: 19:33 2024/6/21
    * @Params: [goods_id, amount]
    * @Return void
    */
    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);
    }
    
    /**
     * @Author: 止束
     * @Description: 根据商品 id,返回对应的价格
     * @DateTime: 19:27 2024/6/21
     * @Params: [id]
     * @Return java.lang.Float
     */
    public Float queryPriceById2(Integer id) {
        String sql = "select price from goods where goods_id = ?";
        Float price = jdbcTemplate.queryForObject(sql, Float.class, id);
        return price;
    }

    /**
     * @Author: 止束
     * @Description: 减少用户的余额
     * @DateTime: 19:30 2024/6/21
     * @Params: [user_id, money]
     * @Return void
     */
    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);
    }

    /**
     * @Author: 止束
     * @Description: 减少商品库存
     * @DateTime: 19:33 2024/6/21
     * @Params: [goods_id, amount]
     * @Return void
     */
    public void updateAmount2(Integer goods_id, int amount) {
        String sql = "update goods_amount set goods_num = goods_num - ? where goods_id = ?";
        jdbcTemplate.update(sql, amount, goods_id);
    }
}
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
@Service //将 GoodsService 对象注入到 Spring 容器
public class GoodsService {
    //定义属性 GoodsDao
    @Resource
    private GoodsDao goodsDao;


    /**
    * @Author: 止束
    * @Description: 编写一个方法,完成用户购买商品的业务
    * @DateTime: 19:53 2024/6/21
    * @Params: [userId:用户 id, goodsId:商品 id, amount:购买数量]
    * @Return void
    */
    public void buyGoods(int userId, int goodsId, int amount) {

        //输出购买的相关信息
        System.out.println("用户购买的信息 userId = " + userId + " goodsId = " + goodsId + " 购买数量 = " + amount);

        //1. 得到商品的价格
        Float price = goodsDao.queryPriceById(userId);
        //2. 减少用户的余额
        goodsDao.updateBalance(userId, price * amount);
        //3. 减少库存量
        goodsDao.updateAmount(goodsId, amount);

        System.out.println("用户购买成功");
    }

    /**
     * @Author: 止束
     * @Description: @Transactional 解读:使用 @Transactional 可以进行声明式事务控制,即可以将对数据库操作的方法作为一个事务管理
     * @DateTime: 12:14 2024/6/22
     * @Params: []
     * @Return void
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void buyGoodsByTx(int userId, int goodsId, int amount) {

        //输出购买的相关信息
        System.out.println("用户购买的信息 userId = " + userId + " goodsId = " + goodsId + " 购买数量 = " + amount);

        //1. 得到商品的价格
        Float price = goodsDao.queryPriceById(userId);
        //2. 减少用户的余额
        goodsDao.updateBalance(userId, price * amount);
        //3. 减少库存量
        goodsDao.updateAmount(goodsId, amount);

        System.out.println("用户购买成功");
    }


    //这个方法是第二套进行商品购买的方法
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void buyGoodsByTx2(int userId, int goodsId, int amount) {

        //输出购买的相关信息
        System.out.println("用户购买的信息 userId = " + userId + " goodsId = " + goodsId + " 购买数量 = " + amount);

        //1. 得到商品的价格
        Float price = goodsDao.queryPriceById2(userId);
        //2. 减少用户的余额
        goodsDao.updateBalance2(userId, price * amount);
        //3. 减少库存量
        goodsDao.updateAmount2(goodsId, amount);

        System.out.println("用户购买成功");
    }
}
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
@Service
public class MultiplyService {
    @Resource
    private GoodsService goodsService;

    /**
     * multiBuyGoodsByTx 这个方法有两次购买商品的操作
     * buyGoodsByTx 和 buyGoodsByTx2 都是声明式事务
     * 当前 buyGoodsByTx 和 buyGoodsByTx2 使用的传播属性是默认的 REQUIRED,即会当做一个整体事务进行管理
     * 比如 buyGoodsByTx 方法成功,但是 buyGoodsByTx2 失败,会造成整个事务回滚,即会回滚 buyGoodsByTx
     * 如果 buyGoodsByTx 和 buyGoodsByTx2 事务传播属性修改成 REQUIRES_NEW,这时两个方法的事务是独立的,也就是说如果 buyGoodsByTx 成功但是 buyGoodsByTx2 失败,不会造成 buyGoodsByTx 回滚
     * */
    @Transactional
    public void multiBuyGoodsByTx() {
        goodsService.buyGoodsByTx(1,1,1);
        goodsService.buyGoodsByTx2(1,1,1);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--配置要扫描的包-->
    <context:component-scan base-package="com.hspedu.spring.tx.dao"/>
    <context:component-scan base-package="com.hspedu.spring.tx.service"/>
    <!--引入外部的 jdbc.properties 文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--配置数据源对象-DataSource-->
    <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
        <!--给数据源对象配置属性值-->
        <property name="user" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.pwd}"/>
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
    </bean>

    <!--配置 JdbcTemplate 对象-->
    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
        <!--给 JdbcTemplate 对象配置 dataSource 属性-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置事务管理器-对象
        1. DataSourceTransactionManager 这个对象是进行事务管理的
        2. 一定要配置数据源属性,这样可以指定该事务管理器是对哪个数据源进行事务控制
    -->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置启动基于注解的声明式事务管理功能-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</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
//测试事务的传播机制
    @Test
    public void multiBuyGoodsByTxTest() {
        //获取到容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("tx_ioc.xml");

        MultiplyService multiplyService = ioc.getBean(MultiplyService.class);

        multiplyService.multiBuyGoodsByTx();
    }
1
2
3
4
5
6
7
8
9
10

# 7.4 事务的隔离级别

# 7.4.1 事务隔离级别说明

(1)默认的隔离级别,就是 MySQL 数据库默认的隔离级别,一般为 Repeatable_read

# 7.4.2 事务隔离级别的设置和测试

(1)修改 GoodsService.java,先测默认隔离级别,增加方法 buyGoodsByTxISOLATION()

//在默认情况下,声明式事务的隔离级别是 REPEATABLE_READ
    //这里我们将 buyGoodsByTxISOLATION 的隔离级别设置为 Isolation.READ_COMMITTED,表示只要是提交了的数据,在当前事务是可以读取到最新数据的
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void buyGoodsByTxISOLATION() {
        //查询两次商品的价格
        Float price = goodsDao.queryPriceById(1);
        System.out.println("第一次查询的 price = " + price);

        Float price2 = goodsDao.queryPriceById(1);
        System.out.println("第一次查询的 price2 = " + price2);
    }
1
2
3
4
5
6
7
8
9
10
11
//测试声明式事务的隔离级别
    @Test
    public void buyGoodsByTxISOLATIONTest() {
        //获取到容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("tx_ioc.xml");

        GoodsService goodsService = ioc.getBean(GoodsService.class);
        goodsService.buyGoodsByTxISOLATION();
    }
1
2
3
4
5
6
7
8
9

# 7.4.3 事务的超时回滚

(1)如果一个事务执行的时间超过某个时间限制,就让该事务回滚

(2)可以通过设置事务超时回滚来实现

(3)修改 GoodsService.java,增加 buyGoodsByTxTimeout()

/**
    * timeout = 2 表示 buyGoodsByTxTimeout 如果执行时间超过了 2 秒,那么该事务就进行回滚,如果没有设置 timeout,默认是 -1,表示使用事务的默认超时时间或者不支持
    */
    @Transactional(timeout = 2)
    public void buyGoodsByTxTimeout(int userId, int goodsId, int amount) {
        //输出购买的相关信息
        System.out.println("用户购买的信息 userId = " + userId + " goodsId = " + goodsId + " 购买数量 = " + amount);

        //1. 得到商品的价格
        Float price = goodsDao.queryPriceById2(userId);
        //2. 减少用户的余额
        goodsDao.updateBalance2(userId, price * amount);

        //模拟超时
        System.out.println("=====超时开始4s=====");
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("=====超时开始4s=====");
        
        //3. 减少库存量
        goodsDao.updateAmount2(goodsId, amount);

        System.out.println("用户购买成功");
    }
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
//测试 timeout 属性
    @Test
    public void buyGoodsByTxTimeoutTest() {
        //获取到容器
        ApplicationContext ioc = new ClassPathXmlApplicationContext("tx_ioc.xml");

        GoodsService goodsService = ioc.getBean(GoodsService.class);
        goodsService.buyGoodsByTxTimeout(1,1,1);
    }
1
2
3
4
5
6
7
8
9

# 7.5 课后作业

模拟一个用户,进行银行转账购买淘宝商品的业务,分别有 数据表/dao/service,保证数据一致性

(1)数据表有:seller[卖家]、buyer[买家]、goods[商品表[库存量]]、taoBao[提取入账成交额的 10%]

(2)要求简单实现,使用声明式事务完成

(3)要求创建一个新的 Spring 容器配置文件 shopping_ioc.xml,完成测试

数据表:

-- 作业

-- 卖家表
create table seller (
    id int unsigned primary key auto_increment,
    uname varchar(32) not null default '',
    money double not null default 0.0
);
insert into seller values (null, '张三', 1000);
insert into seller values (null, '李四', 2000);

-- 买家
create table buyer (
    id int unsigned primary key auto_increment,
    uname varchar(32) not null default '',
    money double not null default 0.0
);
insert into buyer values (null, '王五', 1000);
insert into buyer values (null, '赵六', 2000);


-- 商品表
create table goods2 (
    goods_id int unsigned primary key auto_increment,
    goods_name varchar(32) not null default '',
    price double not null  default 0.0,
    amount int unsigned default 0
);
insert into goods2 values (null, '小风扇', 10.00, 10000);
insert into goods2 values (null, '小台灯', 12.00, 10000);
insert into goods2 values (null, '可口可乐', 3.00, 10000);

-- 淘宝表
create table taobao (
    id int unsigned primary key auto_increment,
    money double not null default 0.0
);
insert into taobao values (null, 0);
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

Dao层:

BuyerDao

@Repository
public class BuyerDao {
    @Resource
    private JdbcTemplate jdbcTemplate;

    //从买家表中扣除金额
    public void updateMoney(int id, double money) {
        String sql = "update buyer set money = money - ? where id = ?";
        jdbcTemplate.update(sql, money, id);
    }
}
1
2
3
4
5
6
7
8
9
10
11

SellerDao

@Repository
public class SellerDao {
    @Resource
    private JdbcTemplate jdbcTemplate;

    //对卖家的账号增加金额
    public void updateMoney(int id, double money) {
        String sql = "update seller set money = money + ? where id = ?";
        jdbcTemplate.update(sql, money, id);
    }
}
1
2
3
4
5
6
7
8
9
10
11

Goods2Dao

@Repository
public class Goods2Dao {
    @Resource
    private JdbcTemplate jdbcTemplate;

    //根据商品 id,返回价格
    //返回单行单列
    public Double queryPriceById(int goodsId) {
        String sql = "select price from goods2 where goods_id = ?";
        Double price = jdbcTemplate.queryForObject(sql, Double.class, goodsId);
        return price;
    }

    //根据商品 id,减去库存
    public void updateAmount(int goodsId, int amount) {
        String sql = "update goods2 set amount = amount - ? where goods_id = ?";
        jdbcTemplate.update(sql, amount, goodsId);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

TaobaoDao

@Repository
public class TaobaoDao {
    @Resource
    private JdbcTemplate jdbcTemplate;

    //给 id = 1 的 taobao 账号增加金额
    public void updateMoney(int id, double money) {
        String sql = "update taobao set money = money + ? where id = ?";
        jdbcTemplate.update(sql, money, id);
    }
}
1
2
3
4
5
6
7
8
9
10
11

Service层

Goods2Service

@Service
public class Goods2Service {
    @Resource
    private BuyerDao buyerDao;
    @Resource
    private SellerDao sellerDao;
    @Resource
    private Goods2Dao goods2Dao;
    @Resource
    private TaobaoDao taobaoDao;

    //用户的购买商品的行为会操作多张表,视为一个事务进行管理
    @Transactional
    public void shopping(int goodsId, int buyerId, int sellerId, int amount) {
        Double price = goods2Dao.queryPriceById(goodsId);
        //花费多少钱
        double money = price * amount;
        buyerDao.updateMoney(buyerId, money);
        //将成交额的 90% 转入卖家
        sellerDao.updateMoney(sellerId, money * 0.9);
        //将成交额的 10% 转入到淘宝账号
        taobaoDao.updateMoney(1, money * 0.1);
        //减去商品的库存
        goods2Dao.updateAmount(goodsId, amount);
    }
}
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

XML 配置文件

shopping_ioc.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" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--引入外部属性文件 jdbc.properties-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    
    <!--配置数据源 DataSource-->
    <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
        <!--设置属性值-->
        <property name="user" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.pwd}"/>
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
    </bean>

    <!--配置 JdbcTemplate 对象-->
    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
        <!--配置属性 DataSource-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置事务管理器-->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="dataSourceTransactionManager">
        <!--配置数据源-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置启动基于注解的声明式事务管理功能-->
    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>

    <!--配置进行扫描的包-->
    <context:component-scan base-package="com.hspedu.spring.tx.homework"/>
</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

测试

ShoppingTest

public class ShoppingTest {
    @Test
    public void shopping_() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("shopping_ioc.xml");
        Goods2Service goods2Service = ioc.getBean(Goods2Service.class);
        goods2Service.shopping(1,1,1,1);
        System.out.println("购买成功");
    }
}
1
2
3
4
5
6
7
8
9
最近更新: 3/24/2025, 2:07:09 PM
Spring

开发笔记   |