Spring01_IOC、DI和Beans配置

发布时间 2023-04-12 10:06:16作者: Purearc

一、Spring概述

(一)Spring简介

​ Spring 为企业应用的开发提供了一个轻量级的解决方案。该解决方案包括:基于依赖注入的核心机制、基于 AOP (Aspect Oriented Programming,面向切面的程序设计)的声明式事务管理、与各种持久层技术的整合,以及优 秀的Web MVC框架等。Spring致力于JavaEE应用各层的解决方案,而不是仅仅专注于某一层的方案。可以说:Spring 是企业应用开发的“一站式”解决方案,Spring 贯穿表现层、业务层、持久层。然而,Spring 并不想取代那些已有的框架,而是以高度的开放性与它们无缝整合。

(二)Spring的优点

​ 1.低侵入式设计,代码的污染极低。

​ 2.独立于各种应用服务器,基于 Spring 框架的应用,可以实现 Write Once,Run Anywhere。

​ 3.Spring 的 IoC 容器降低了业务对象替换的复杂性,提高了组件之间的解耦。

​ 4.Spring 的 AOP 支持允许将一些通用任务如安全、事务、日志等进行集中处理,从而提供了更好的复用。

​ 5.Spring 的 ORM 和 DAO 提供了与第三方持久层框架的良好整合,并简化了底层的数据库访问。

​ 6.Spring 的高度开放性,并不强制应用完全依赖 Spring,开发者可以自由选用 Spring 框架的部分或全部。

(三)Spring体系结构

image-20230411103056026

​ 1.Spring核心容器

​ Core模块:框架的核心,封装框架的最底层依赖,包括资源访问、类型转换以及一些工具。

​ Beans模块:Spring的基础,包括控制反转和依赖注入,以 BeanFactory 为核心(工厂模式、单例模式);所有的应用程序对象和对象间的关系由Spring框架管理。

​ Context模块:以 Core 和 beans为基础,继承 Beans 功能并添加资源绑定、验证数据、国际化等,核心接口是 ApplicationContext。

​ El模块:表达式语言支持。

​ 2.AOP和ASpects

​ Aop模块:提供面向切面编程的实现,提供比如日志记录、权限控制、性能统计等通用功能和业务逻辑分离的技术,降低业务逻辑和通用功能的耦合。

​ Aspects模块:AspectJ 是一个面向切面的框架。

​ 3.数据访问/集成模块

​ 事务模块:用于Spring管理事务。

​ JDBC模块:提供JDBC的样例模板,消除传统JDBC编码。

​ ORM模块:提供 “对象--关系” 的无缝集成。

​ 4.Web模块

​ Web模块 :提供基础的 web 功能。

​ Web-Servlet模块:提供 SpringMVC 框架实现。

二、Spring 容器

(一)控制反转IoC

​ 1.概念:Ioc(Inversion of control)直译过来就是控制反转,控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导 框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在 没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流 程通过框架来控制。流程的控制权从程序员“反转”给了框架。在 Spring中最能体现 IOC 的就是由Spring框架去创建对象并放在Spring容器中管理。**

​ 2.案例

​ (1)所需依赖

        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>${spring.context-version}</version>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit-version}</version>
                <scope>test</scope>
            </dependency>
        </dependencies>

​ 由于Spring的各个模块是相互依赖的,所以只需要一个context就可以了。

image-20230411110054244

​ (2)Spring的核心配置文件

​ 主要是这个xsd的校验,里面的配置稍后再说

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

   
</beans>

​ (3)包结构如下

com/qlu/dao/impl/UserDaoImpl.java
com/qlu/dao/UserDao.java
com/qlu/service/impl/UserServiceImpl.java
com/qlu/service/UserService.java

​ (4)配置bean并加载Spring的配置文件

​ 在spring-beans.xml添加配置

<bean id="userService" class="com.qlu.service.impl.UserServiceImpl"></bean>

​ 在测试类中进行测试

package com.qlu.test1;

import com.qlu.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class BeanTest {
    public static void main(String[] args) {
        //加载核心配置文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-beans.xml");
        //获得bean对象getBean默认返回Object
        UserService userService = (UserService) applicationContext.getBean("userService");
        userService.say();
    }
}

​ 在上面的测试类中我们用ClassPathXmlApplicationContext的父类来接受子类对象,实际上这个 ApplicationContext 就是一个 BeanFactory 的子接口,而 BeanFactory 就是 Spring 容器类的一个最基本接口,所以联合一下就能知道这儿就把对象创建了。

image-20230411111224649

​ 其实在上面加载核心配置文件的时候对应的对象就已经被创建了,我们可以重写 UserServiceImpl 的构造方法来证明一下

    
public UserServiceImpl() {
        System.out.println("这里是UserServiceImpl的构造方法");
    }

@Override
public void say() {
        System.out.println("这里是UserServiceImpl");
    }

image-20230411114116588

​ 此时对象就放在了Spring的容器里面,下一步就是取出对象,使用applicationContext的getBean(String name),此方法默认返回一个Object对象(或者你也可以使用getBean(Class type)方法),参数name是在配置文件中 bean 中的 id 或 name,为了返回UserService的对象我们再传一个class对象进去(即getBean(String var1,Class var2)方法),就能把获取到的 Object 对象转换成传入的 class 对象。

​ 获得了该对象就可以调用对象的方法了,如下。

image-20230411115249801

​ 在实际开发中我们都是使用服务(service)来调用数据持久层(dao)来操作数据,所以下一步就是要在UserServiceImpl中来调用Dao的方法。

​ 在 UserServiceImpl 中声明一个 userDao 并为 userDao 提供 set 方法,UserDaoImpl 中有一个 sayHi() 方法,我们在 UserServiceImp 类的 say() 方法中调用它。

public class UserServiceImpl implements UserService {
    //set注入
    private UserDao userDao;
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    //重写构造器
    public UserServiceImpl() {
        System.out.println("这里是UserServiceImpl的构造方法");
    }
    @Override
    public void say() {
        System.out.println("这里是UserServiceImpl的say方法");
        userDao.sayHi();
    }
}

​ 同样在 spring-beans.xml 中需要把 dao 配置到 service 中

 <bean id="userDao" class="com.qlu.dao.impl.UserDaoImpl"></bean>

    <bean id="userService" class="com.qlu.service.impl.UserServiceImpl">
        <property name="userDao" ref="userDao"></property>
    </bean>

image-20230411141859034

(二)依赖注入DI

​ 1.概念:依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等 方式传递(或注入)给类来使用

​ 2.依赖注入的方式

20200521235846480

(1)set 注入

​ 基于set方法实现的,底层会通过反射机制调用属性对应的set方法然后给属性赋值。这种方式要求属性必须对外提供set方法。

image-20230411151311051

(2)构造注入

​ 通过调用构造方法来给属性赋值。

​ 我们需要更改构造函数的内容,在 UserService 的构造器中把 UserDao 作为参数传入;

    //重写构造器
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
        System.out.println("这里是UserServiceImpl的构造方法");
    }

​ 同时 xml 文件中的配置也需要更改

<!--        构造注入-->
<!--        这里的第一个userDao就是java类中构造器的参数,后面的ref就是配置文件上面声明的userDao对象-->
        <constructor-arg name="userDao" ref="userDao"></constructor-arg>

image-20230411152445667

​ 当然你也可以在构造器中写多个参数,并在 xml 文件中注入多个值,无论是 index 方式 还是 name 方式都可以。

<!--        这里的第一个userDao就是java类中构造器的参数,后面的ref就是配置文件上面声明的userDao对象-->
        <constructor-arg name="userDao" ref="userDao"></constructor-arg>
        <constructor-arg name="mesg" value="这里是通过构造注入的megs值"></constructor-arg>

<!--        <constructor-arg index="0" ref="userDao"></constructor-arg>-->
<!--        <constructor-arg name="mesg" value="这里是通过构造注入的megs值"></constructor-arg>-->
    //重写构造器
    public UserServiceImpl(UserDao userDao,String mesg) {
        this.userDao = userDao;
        //上面别忘记声明
        this.mesg = mesg;
        System.out.println("这里是UserServiceImpl的构造方法");
        System.out.println(mesg);
    }

image-20230411153355937

(3)注入集合

​ 如果需要 Spring 容器来管理集合对象,则可以使用集合元素标签:list、set、map 和 props 来创建 List 对象、 Set 对象、Map 对象和 Properties 对象。以下面的 UtilBean 类为例,该类中包含了常用的集合。

import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

@Data
public class UtilBean {
    private List<String> schools;
    private Map<String, Integer> scores;
    private Properties health;
    private Set<Object> set;
    private String[] books;
}

​ 在上面的类中使用 lombok 提供了 get、set方法,Spring默认是构造注入的,所以下面的 property name="schools" 相当于调用了 utilBean.setSchools() 方法,而 value 中的值就是 set 方法中的参数值了,在外层标签我们可以指定泛型(放入什么)。

    <bean id="utilBean" class="com.qlu.bean.UtilBean">
<!--测试向容器中注入集合,Untilbean放在容器中进行管理-->
        <property name="schools">
            <list value-type="java.lang.String">
                <value>齐鲁工业大学</value>
                <value>齐鲁工业大学(长清校区)</value>
                <value>齐鲁工业大学(千佛山校区)</value>
            </list>
        </property>
<!--注入集合-->
        <property name="scores">
            <map value-type="java.lang.Integer">
                <entry key="yuwen" value="99"></entry>
                <entry key="shuxue" value="90"></entry>
                <entry key="yingyu" value="96"></entry>
            </map>
        </property>
<!--注入properties-->
        <property name="health">
            <props>
                <prop key="血压">正常</prop>
                <prop key="身高">正常</prop>
            </props>
        </property>
<!--注入map-->
        <property name="set">
            <set>
                <value>1</value>
                <value>2</value>
                <value>3</value>
            </set>
        </property>
<!--注入String数组-->
        <property name="books">
            <array>
                <value>java入门到入坟</value>
                <value>TCP/IP实战</value>
                <value>OS实战</value>
            </array>
        </property>
    </bean>

​ 编写测试(这里可能不规范,你完全可以使用快捷键ctrl +shift +t 进行生成测试)

public class BeanTest {

    @Test
    public void test1() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-beans.xml");
        UtilBean utilBean = applicationContext.getBean(UtilBean.class);
        //属性一 List
        List<String> schools = utilBean.getSchools();
        for (String school : schools) {
            System.out.println(school);
        }
        //属性二 map
        System.out.println(utilBean.getScores());
        //属性三 properties
        System.out.println(utilBean.getHealth());
        //属性四 set
        System.out.println(utilBean.getSet());
        //属性五 arrays
        for (String book : utilBean.getBooks()) {
            System.out.print(book+"-------");
        }

    }
}

image-20230411161037474

(三)创建 Bean 的方式

1.构造器创建

​ 默认通过类的无参构造器生成,这点在上面已经证实。

2.静态工厂方法

​ 下面的程序定义了工厂类 CarFactory 和 工厂产品 Car,其中 Car 有三个属性,当创建时我们指定其中的 carName 和 color。

public class CarFactory {
     /**
     * 静态工厂方法
     * @param carName
     * @param color
     * @return
     */
    public static Car getCar(String carName , String color) {
        return new Car().setCarName(carName).setColor(color);
    }
}

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class Car {
    private String carName;
    private String color;
    private int money;
}

​ 使用静态工厂方法创建 Bean 实例时,class 属性指定并不是指定 Bean 实例的实现类,而是静态工厂类。除此之外,还需要使用 factory-method 属性来指定静态工厂方法,Spring 将调用静态工厂方法返回一个 Bean 实例,得到 Bean 实例后,Spring 后面的处理步骤与采用普通方法创建 Bean 完全一样。

<!--配置工厂方法-->
    <bean id="car" class="com.qlu.bean.CarFactory" factory-method="getCar">
        <constructor-arg name="carName" value="高级轿车"></constructor-arg>
        <constructor-arg name="color" value="黑色"></constructor-arg>
        <property name="money" value="30"></property>
    </bean>

​ 需要注意的是,当工厂方法 factory-method 需要参数时,应该用 constructor-arg 来指定参数的值,而对于想给对象的属性注入信息时可以使用 property 来完成值的注入。在本示例中,工厂方法需要我们传入两个参数 carName 和 color ,而 money 并为在工厂方法中出现。为方便记忆,你可以把工厂方法当成构造器来看。

public class BeanTest02 {
    @Test
    public void test02 () {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-beans.xml");
        Car car = applicationContext.getBean(Car.class);
        System.out.println(car);
    }
}

image-20230411191922709

3.实例工厂方法

​ 实例工厂方法与静态工厂方法只有一点不同:调用静态工厂方法只需要工厂类即可,而调用实例工厂方法则需要工厂实例。所以配置实例工厂方法与配置静态工厂方法基本相似,只有一点区别:配置静态工厂方法使用 class 指定静态工厂类,而配置实例工厂方法则使用 factory-bean 指定工厂实例。以下为实例工厂的实例。

public class StudentFactory {

    public Student newStudent(String name , String address) {
        Student student = new Student().setName(name).setAddress(address);
        return student;
    }
}

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class Student {
    private String name;
    private String address;
    private int age;
}

​ 采用实例工厂方法创建 Bean 的 bean 元素时需要指定如下两个属性:

​ factory-bean:该属性的值为工厂 Bean 的 id。

​ factory-method:该属性指定实例工厂的工厂方法。

​ 同样区分 constructor-arg 和 property 分别用于指定和赋值。

<!--配置实例工厂方法-->
    <bean id="studentFactory" class="com.qlu.bean.StudentFactory"/>
    <bean id="student" factory-bean="studentFactory" factory-method="newStudent">
        <constructor-arg name="name" value="烟芜镜"></constructor-arg>
        <constructor-arg name="address" value="yahoooo"></constructor-arg>
        <property name="age" value="500"></property>
    </bean>
    @Test
    public void test03() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-beans.xml");
        Student student = applicationContext.getBean(Student.class);
        System.out.println(student);
    }

image-20230411202639540

(四)命名空间简化

​ 从 Spring 2.0 版本开始,Spring 允许使用基于 XML Schema 的约束来简化 Spring 配置。

​ 1.P 命名空间

​ P命名空间不需要特定的 Schema 定义,直接存在 Spring 内核中。主要用于简化设值注入,相当于把这个当成 property。

xmlns:p="http://www.springframework.org/schema/p"
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class Person {
    private String name;
    private int age;
}
<!--p命名空间-->
    <bean id="person" class="com.qlu.bean.Person" p:name="烟芜镜" p:age="500"></bean>
@Test
//不写了,反正都知道啥意思
//下面就是结果

image-20230411205242803

​ 2.C 命名空间

​ C命名空间同样不需要特定的 Schema 定义,直接存在 Spring 内核中。主要用于简化构造注入,相当于写 constructor-arg。

xmlns:c="http://www.springframework.org/schema/c"
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("这里是重写的构造方法");
    }
}
<!--c命名空间-->
    <bean id="person" class="com.qlu.bean.Person" c:name="烟芜呼镜" c:age="500"></bean>
@Data
//那自然是不写了,下面是结果,重写了有参构造
//或许能c和p混用?
//破案了,混用相当于property 和 constructor-arg 各算各的,截图放上,代码不写了
//你也可以用_index的方式获取

image-20230411210129320

image-20230411210409735

​ 3.Util 命名空间

​ 使用 util 命名空间主要是简化对集合类 List、Set、Map 等 Java 对象的配置,util 命名空间对应的 XML Schema 校验并不在 Spring 的内核中,需要在 Spring 配置文件的 beans 标签中引入 util 命名空间的 XML Schema 文件。

xmlns:util="http://www.springframework.org/schema/util"
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd"

​ 我们以上面的 [UtilBean](# (3)注入集合) 类为实例。

​ 使用p:schools-ref 来参照上面的bean

    <util:list id="schools" value-type="java.lang.String">
        <value>小学</value>
        <value>中学</value>
        <value>高中</value>
        <value>大学</value>
    </util:list>

    <bean id="utilBean" class="com.qlu.bean.UtilBean" p:schools-ref="schools"></bean>
    @Test
    public void test() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-beans2.xml");
        UtilBean utilBean = applicationContext.getBean(UtilBean.class);
        System.out.println(utilBean);
    }

image-20230411212559560

(五)Bean 作用域

​ 当通过 Spring 容器创建一个 Bean 实例时,不仅可以完成 Bean 实例的实例化,还可以为 Bean 指定特定的作用域。Spring 支持如下 4 中作用域。

​ 1.singleton:单例模式,在整个 Spring IoC 容器中,singleton 作用域的 Bean 将只生成一个实例。

​ 在 bean 标签中有 scop 属性,值默认是 singleton,我们可以测试一下在 singleton 下是否为同一个对象

<bean id="utilBean" class="com.qlu.bean.UtilBean" p:schools-ref="schools" scope="singleton"></bean>

image-20230411213321449

​ 2.prototype:每次通过容器的 getBean 方法获取 prototype 作用域的 Bean 时,都将生成一个新的 Bean 实例。

​ 更改 scop 的值为prototype 即可

<bean id="utilBean" class="com.qlu.bean.UtilBean" p:schools-ref="schools" scope="prototype"></bean>

image-20230411213446517

​ 3.request:对于一次 HTTP 请求,request 作用域的 Bean 将只生成一个实例,这意味着,在同一次 HTTP 请求内,程序每次请求该 Bean,得到的是同一个实例。只有在 Web 应用中使用 Spring 时,该作用域才 有效。

​ 4.session:对于一次 HTTP 会话,session 作用域的 Bean 将只生成一个实例,这意味着,在同一次 HTTP 会话内,程序每次请求该 Bean,得到的是同一个实例。只有在 Web 应用中使用 Spring 时,该作用域才 有效。

​ 如果不指定 Bean 的作用域,Spring 默认使用 singleton 作用域。Java 在创建 Java 实例时,需要进行内存申请; 销毁实例时,需要完成垃圾回收,这些工作都会导致系统开销的增加。因此 prototype 作用域的 Bean 的创建、销毁代价比较大。而 singleton 作用域的 Bean 实例一旦创建完成可以反复使用。因此尽量避免将 Bean 设置成 prototype 作用域。

(六)Bean 的自动配置

​ Spring 能自动装配 Bean 与 Bean 之间的依赖关系,即无须使用 ref 显式指定依赖 Bean,而是由 Spring 容器检 查 XML 配置文件内容,根据某种规则,为调用者 Bean 注入被依赖的 Bean。

​ 1.ByName

​ 根据 setter 方法名进行自动装配。Spring 容器查找容器中的全部 Bean,找出其 id 与 setter 方法名去掉 set 前缀,并小写首字母后同名的 Bean 来完成注入。如果没有找到匹配的 Bean 实例,则 Spring 不会进行任何注入。

​ 开启 ByName 配置,需要在 beans 标签加入 default-autowire 设值为 byName

import lombok.Data;

@Data
public class User {
    private int age;
    private String name;
    private Role role;
}

import lombok.Data;

@Data
public class Role {
    private String roleName;
    private String roleCode;

    public Role(String roleName, String roleCode) {
        this.roleName = roleName;
        this.roleCode = roleCode;
    }
}

​ 在不使用自动装配时,我们会在 user 的 bean 中加入 role-ref 让其值等于被参照的那个 bean。

image-20230412094354212

<bean id="role" class="com.qlu.bean.Role" c:roleName="经验加三" c:roleCode="3"></bean>


<!--<bean id="user" class="com.qlu.bean.User" p:name="孙笑川" p:age="100" p:role-ref="role"></bean>-->
<bean id="user" class="com.qlu.bean.User" p:name="孙笑川" p:age="100"></bean>
@Test
//不写了

​ 我们可以看到即使没有让 user 的 bean 指向 role 仍然能获取到 role 的值,我们可以打开编译后的代码

image-20230412094629850

​ Bean 的 ByName 配置会在 User 中找到 setRole 方法,并把 set 前缀去掉 ,字母小写获得 role,进而找到一个叫 role 的bean

image-20230412094728326

​ 2.ByType

​ 根据 setter 方法的形参类型来自动装配。Spring 容器查找容器中全部的 Bean,如果正好有一个 Bean 类型与 setter 方法的形参类型匹配,就自动注入这个 Bean;如果找到多个这样的 Bean,就抛出一 个异常,如果没有找到这样的 Bean,则什么都不操作,setter 方法也不会被调用。

​ 同样需要在 beans 标签加入 default-autowire 设值为 byType。现在把 role 的 bean 改名为 role123.

<bean id="role123" class="com.qlu.bean.Role" c:roleName="经验加三" c:roleCode="3"></bean>


<!--<bean id="user" class="com.qlu.bean.User" p:name="孙笑川" p:age="100" p:role-ref="role"></bean>-->
<bean id="user" class="com.qlu.bean.User" p:name="孙笑川" p:age="100"></bean>

</beans>

​ 在更改后仍然能获得 role 的值

image-20230412095434323

​ 在指定 byType 后会在 User 中找到参数为 Role 的 set 方法,进而进行注入。

image-20230412095624331