找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5182

积分

0

好友

708

主题
发表于 4 小时前 | 查看: 4| 回复: 0

在深入探讨了IoC容器的基本概念后,我们不可避免地要面对其最核心的两大工作方式:依赖查找与依赖注入。

应该说IoC的工作方式有两种,一种是依赖查找,通过资源定位,把对应的资源查找出来,例如通过JNDI找到数据源。依赖查找被广泛使用在第三方的资源注入上,比如在Web项目中,数据源往往是通过服务器配置的,例如Tomcat的数据源配置,这种场景下便可以用JNDI的形式通过接口将其注入Spring IoC容器;另一种则是依赖注入,其主要是在容器内通过类型或者名称查找资源来管理Bean之间的依赖关系。而就依赖注入而言又可分为构造器注入和setter注入两种方式,setter的形式是Spring推荐的也是应用更广泛的形式。

构造器注入

构造器注入依赖于构造方法的实现,而构造方法可以是有参数的或者是无参数的。在大部分情况下,我们通过类的构造方法创建类对象,Spring也可以采用反射的方式,通过使用构造方法完成注入,这便是构造器注入的原理。

要让Spring更好地完成对应的构造注入,有必要描述具体的类、构造方法、设置对应的参数,如此Spring就会通过对应的信息用反射的形式创建对象。

如下代码所示,首先定义一个新的 Role pojo:

package com.ssm.pojo;

/**
 * 角色类,用于表示角色信息
 */
public class RoleII {
    private Long id; // 角色编号
    private String roleName; // 角色名称
    private String note; // 备注信息

    /**
     * 无参数构造方法,用于创建一个空的角色对象。
     */
    public RoleII() {
    }

    /**
     * 带参数的构造方法,用于创建一个具有指定角色信息的角色对象。
     *
     * @param id 角色的唯一标识符。
     * @param roleName 角色的名称。
     * @param note 对角色的备注说明。
     */
    public RoleII(Long id, String roleName, String note) {
        this.id = id;
        this.roleName = roleName;
        this.note = note;
    }

    // 提供对id属性的访问和修改
    public Long getId() {
        return id;
    }

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

    // 提供对roleName属性的访问和修改
    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    // 提供对note属性的访问和修改
    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

要使用带参数的构造方法创建对象,需要在配置文件 spring-cfg.xml 中进行适当的配置,如下所示:

 <!-- 定义一个RoleII类型的bean,通过构造器进行初始化 -->
    <bean id="role1" class="com.ssm.pojo.RoleII">
        <!-- 构造器参数1:角色ID,这里设置为1 -->
        <constructor-arg index="0" value="1" />
        <!-- 构造器参数2:角色名称,这里设置为"总经理" -->
        <constructor-arg index="1" value="总经理" />
        <!-- 构造器参数3:角色描述,这里设置为"公司管理者" -->
        <constructor-arg index="2" value="公司管理者" />
    </bean>

其中 constructor-arg 元素用于定义构造方法的参数,index 用于定位参数的位置,从0开始。通过这样的定义,Spring便会使用 RoleII(Long id, String roleName, String note) 方法创建对象。

但参数少的时候还好,参数多了这种构造形式的可读性会比较差。

setter注入

setter注入是Spring中最主流的注入方式,它利用JavaBean规范定义的setter方法完成注入,不但灵活而且可读性高,消除了使用构造器注入时出现多个参数可读性变差的问题。在 RoleII 的pojo里有个无参数的构造函数,通过配置Spring也可以通过Java反射技术注入。

<!-- 定义一个RoleII类型的bean实例,id为2,角色名为"高级工程师",备注为"重要人员" -->
    <bean id="role2" class="com.ssm.pojo.RoleII" scope="prototype">
        <!-- 设置角色id为2 -->
        <property name="id" value="2" />
        <!-- 设置角色名为"高级工程师" -->
        <property name="roleName" value="高级工程师" />
        <!-- 设置备注为"重要人员" -->
        <property name="note" value="重要人员" />
    </bean>

这里 scope 属性定义了Spring容器管理的Bean的实例化策略,即Bean的作用域。不同的作用域会影响Bean的创建和生命周期管理。以下是Spring支持的主要作用域:

  • Singleton (单例):这是默认的作用域。当一个Bean被配置为单例时,Spring容器只会创建该Bean的一个实例,并在第一次请求时初始化。之后的每次请求都将返回同一个实例。适合那些在整个应用中需要共享同一状态的对象。
  • Prototype (原型):原型作用域表示每次请求都会创建一个新的Bean实例。这适用于那些需要有各自独立状态的对象,比如用户会话或者并发操作中的对象。
  • Request:在Web应用中,每个HTTP请求都会创建一个新的Bean实例。这意味着对于每个请求,都会有一份独立的Bean副本。
  • Session:也是在Web应用中,每个HTTP session会创建一个新的Bean实例。这意味着每个用户session都有自己的Bean副本,可以用于存储用户特定的信息。
  • Application:在Web应用中,这个作用域的Bean在整个ServletContext(应用上下文)中是唯一的,类似于单例,但作用于整个Web应用而不是容器本身。
  • Global Session:在Portlet应用中,全局session作用域的Bean在一个全局portlet session中是唯一的。这在多portlet环境中用于跨portlet共享状态。

选择正确的scope有助于优化应用性能并确保正确管理对象的状态。

这样Spring就会通过反射调用没有参数的构造方法生成对象,同时通过反射对应的setter方法注入配置的值。

propertyconstructor-arg 是Spring框架中用于XML配置文件中进行依赖注入的两个不同方式。

  • constructor-arg 用于通过构造函数来注入依赖。它指定调用构造函数时传递给构造函数的参数。
    • 当类有无参构造函数或有参构造函数时,可以使用此标签来指定哪个构造函数应该被调用,并提供对应的参数值。
    • 参数可以通过 valuerefindex 等属性来设置,value 用于直接注入基本类型或字符串,ref 用于引用其他bean,indexname 用于指定构造函数参数的位置或名称(如果构造函数有多个相同类型的参数)。
  • property 用于通过setter方法来注入依赖。它对应于Java对象的属性,即调用setter方法来设置对象的属性值。
    • 这种方式适用于类中有getter/setter方法的属性。
    • constructor-arg 一样,property 也可以通过 valueref 来设置值。

主要区别

  • 注入方式constructor-arg 是构造函数注入,而 property 是setter方法注入。
  • 初始化顺序:构造函数注入通常在对象实例化时发生,而setter注入可以在对象创建后任何时候进行。
  • 强制性:构造函数注入可能是强制性的,如果类只有一个无参构造函数,那么必须使用setter注入;如果有带参数的构造函数,Spring会根据提供的 constructor-arg 调用相应的构造函数。
  • 安全性:构造函数注入通常被认为更安全,因为它保证了对象在创建时就处于一致和完整状态,而setter注入的对象可能在初始化后才被完全设置好。

依赖查找

有些资源并非来自系统,例如数据库连接资源完全可以在Tomcat下配置,然后通过JNDI形式获取。这种数据库连接资源属于工程外资源,就可以采用接口注入的形式获取它。

顺便讲一下Tomcat配置JNDI(Java Naming and Directory Interface),主要涉及在Tomcat的配置文件中定义资源和资源引用。

方式1:在 context.xml 中配置,找到 conf/context.xml 文件,添加以下 <Resource> 元素来定义JNDI资源,例如一个数据源:

<Resource name="jndi/ssm" auth="Container"
          type="javax.sql.DataSource"
          driverClassName="com.mysql.jdbc.Driver"
          url="jdbc:mysql://localhost:3306/ssm?useUnicode=true;characterEncoding=utf8""
          username="root" password="xxxx"
          maxActive="20" maxIdle="10" maxWait="10000"/>

方式2:在 server.xml 中配置全局资源,打开 conf/server.xml 文件,在 <GlobalNamingResources> 元素内添加 <Resource> 元素,这样配置的资源可以被所有应用共享。

<GlobalNamingResources>
  ...
  <Resource name="jndi/globalSsm" auth="Container"
            type="javax.sql.DataSource"
            driverClassName="oracle.jdbc.driver.OracleDriver"
            url="jdbc:oracle:thin:@127.0.0.1:1521:ORCL"
            username="scott" password="tiger"
            maxActive="20" maxIdle="10" maxWait="10000"/>
  ...
</GlobalNamingResources>

然后在每个应用的 context.xml 文件中,通过 <ResourceLink> 元素引用全局资源,如下所示:

<ResourceLink name="jndi/localDemo" global="jndi/globalSsm" type="javax.sql.DataSource"/>

也可以在应用的 WEB-INF/web.xml 文件中,添加 <resource-ref> 元素来声明应用将要使用的资源,完成资源引用:

<resource-ref>
  <description>DB Connection</description>
  <res-ref-name>jndi/localDemo</res-ref-name>
  <res-type>javax.sql.DataSource</res-type>
  <res-auth>Container</res-auth>
</resource-ref>

一旦在全局或某个应用的 context.xml 中定义了资源,接下来需要在具体的应用上下文中引用这些资源。这通常通过在应用的 WEB-INF/web.xml 文件中添加 <resource-ref> 元素来完成,或者在应用的特定 context.xml(如果有的话)使用 <ResourceLink> 来链接到全局资源。

返回来看,假设在Tomcat的 context.xml 文件中有如下JNDI配置:

<Resource name="jndi/ssm" auth="Container"
          type="javax.sql.DataSource"
          driverClassName="com.mysql.cj.jdbc.Driver"
          url="jdbc:mysql://localhost:3306/ssm?useUnicode=true;characterEncoding=utf8""
          username="root" password="xxxx"
          maxActive="20" maxIdle="10" maxWait="10000"/>

如果Tomcat的Web项目使用了Spring,那么可以通过Spring的机制,用JNDI获取Tomcat启动的数据库连接池。在 webapp -> WEB-INF -> applicationContext.xml 文件中加入配置:

<!-- 定义一个数据源bean,通过JNDI技术从应用程序服务器中获取数据源 -->
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <!-- 设置JNDI名称,用于在Java EE服务器中查找对应的DataSource -->
    <property name="jndiName">
        <value>java:comp/env/jdbc/ssm</value>
    </property>
</bean>

这样就可以在Spring的IoC容器中获得Tomcat管理的数据库连接池了,这就是一种接口注入的方式。

装配Bean

Spring提供了3种配置方式:在XML中显示配置、在Java的接口和类中实现配置、隐式Bean的发现机制和自动装配原则。在实际工作中,这三种方式都会被用到混合使用。

  • 基于约定优于配置的原则,最优先的应该是通过隐式Bean的发现机制和基于自动装配的原则。这样的好处是减小程序开发者的决定权,简单又不失灵活。
  • 在没有办法使用自动装配原则的情况下,应该优先考虑在Java接口和类中实现配置。这样的好处是减少XML配置的泛滥。例如一个父类有多个子类,通过IoC容器初始化一个父类,容器将无法知道使用哪个子类初始化,这个时候使用Java的注解配置指定。
  • 在上述方法都无法使用的情况下,可以使用XML进行配置,也可以使用Java配置文件的 new 关键字创建对象配置。例如在实际工作中常常会用到第三方类库,常常无法修改里面的代码,这个时候可以通过这样的方式配置。

如果配置的类是开发者自身正在开发的项目,那么应该考虑以Java配置为主。而Java配置又分为自动装配和Bean名称配置,优先使用自动装配可以减少大量的XML配置。如果所需配置的类是第三方的,建议使用XML或者Java配置文件的方式。

通过XML配置装备Bean

在使用XML装配Bean需要定义对应的XML,这里需要引入对应的XML模式(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">

下面是一个包含了多种配置方式的完整 spring-cfg.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">
    <!-- 定义一个BeanPostProcessor的实现类,用于bean创建后的处理 -->
    <bean id="beanPostProcessor" class="com.ssm.bean.BeanPostProcessorImpl" />
    <!-- 定义一个DisposableBean的实现类,用于bean销毁前的处理 -->
    <bean id="disposableBean" class="com.ssm.bean.DisposableBeanImpl" />
    <!-- 配置一个具有属性的bean,表示豆浆的原材料配置 -->
    <bean id="source" class="com.ssm.pojo.Source">
        <property name="bean" value="黑豆" />
        <property name="sugar" value="少糖" />
        <property name="size" value="大杯" />
    </bean>
    <!-- 配置一个延迟初始化的bean,表示豆浆机II,使用了初始化和销毁方法 -->
    <bean id="soybeanMilkMakerII" class="com.ssm.pojo.SoybeanMilkMakerII" lazy-init="true" init-method="init" destroy-method="destroy">
        <property name="beverageShop" value="贡茶" />
        <property name="source" ref="source" />
    </bean>
    <!-- 定义一个RoleII类型的bean,通过构造器进行初始化 -->
    <bean id="role1" class="com.ssm.pojo.RoleII">
        <!-- 构造器参数1:角色ID,这里设置为1 -->
        <constructor-arg index="0" value="1" />
        <!-- 构造器参数2:角色名称,这里设置为"总经理" -->
        <constructor-arg index="1" value="总经理" />
        <!-- 构造器参数3:角色描述,这里设置为"公司管理者" -->
        <constructor-arg index="2" value="公司管理者" />
    </bean>
    <!-- 定义一个RoleII类型的bean实例,id为2,角色名为"高级工程师",备注为"重要人员" -->
    <bean id="role2" class="com.ssm.pojo.RoleII" scope="prototype">
        <!-- 设置角色id为2 -->
        <property name="id" value="2" />
        <!-- 设置角色名为"高级工程师" -->
        <property name="roleName" value="高级工程师" />
        <!-- 设置备注为"重要人员" -->
        <property name="note" value="重要人员" />
    </bean>
</beans>

在文件的头部,引入了一个 <beans> 元素的定义,它是一个根元素,同时XSD文件也被引入,使用该文件所定义的元素可以定义对应的Spring Bean。

装配简单值
<!-- 定义一个RoleII类型的bean实例,id为2,角色名为"高级工程师",备注为"重要人员" -->
    <bean id="role2" class="com.ssm.pojo.RoleII">
        <!-- 设置角色id为2 -->
        <property name="id" value="2" />
        <!-- 设置角色名为"高级工程师" -->
        <property name="roleName" value="高级工程师" />
        <!-- 设置备注为"重要人员" -->
        <property name="note" value="重要人员" />
    </bean>

对应pojo里实体类的构造函数,就很清楚配置项的涵义:

 /**
     * 带参数的构造方法,用于创建一个具有指定角色信息的角色对象。
     *
     * @param id 角色的唯一标识符。
     * @param roleName 角色的名称。
     * @param note 对角色的备注说明。
     */
    public RoleII(Long id, String roleName, String note) {
        this.id = id;
        this.roleName = roleName;
        this.note = note;
    }

关于Bean的定义细节:

  • id,它是Spring找bean的时候会找它的编号,也就是这个id。但id不是一个必须的属性,如果没有声明它,Spring将采用“全限定名#{number}”的格式生成编号。例如上边这个例子,如果没有 id="role2",那么Spring将会为这个bean生成一个编号 "com.ssm.pojo.RoleII#0",那么下一个没有声明id的bean就是 "com.ssm.pojo.RoleII#1"。很显然自动生成id并没有自己定义更清晰。
  • class 是pojo类的全限定名。

这样定义很简单,稍微复杂一点的例如,在bean的配置中,需要注入一些自定义的类:

  <!-- 配置一个具有属性的bean,表示豆浆的原材料配置 -->
    <bean id="source" class="com.ssm.pojo.Source">
        <property name="bean" value="黑豆" />
        <property name="sugar" value="少糖" />
        <property name="size" value="大杯" />
    </bean>
    <!-- 配置一个延迟初始化的bean,表示豆浆机II,使用了初始化和销毁方法 -->
    <bean id="soybeanMilkMakerII" class="com.ssm.pojo.SoybeanMilkMakerII" lazy-init="true" init-method="init" destroy-method="destroy">
        <property name="beverageShop" value="贡茶" />
        <property name="source" ref="source" />
    </bean>

如代码所示,首先定义了一个id为 source 的bean,然后再通过 ref 被id为 soybeanMilkMakerII 的bean引入。看一下对应的Pojo类:

package com.ssm.pojo;

/**
 * Source类用于表示饮品的来源信息。
 */
public class Source {
    private String bean; // 饮品的类型
    private String sugar; // 饮品的糖分描述
    private String size; // 饮品的大小杯

    /**
     * 无参构造方法,用于创建一个新的Source对象。
     */
    public Source() {
        System.out.println("构造方法.....");
    }

    public Source(String bean, String sugar, String size) {
        this.bean = bean;
        this.sugar = sugar;
        this.size = size;
    }

    /**
     * 获取饮品类型。
     *
     * @return 返回当前饮品的类型。
     */
    public String getBean() {
        return bean;
    }

    /**
     * 设置饮品类型。
     *
     * @param bean 需要设置的饮品类型。
     */
    public void setBean(String bean) {
        this.bean = bean;
    }

    /**
     * 获取饮品的糖分描述。
     *
     * @return 返回当前饮品的糖分描述。
     */
    public String getSugar() {
        return sugar;
    }

    /**
     * 设置饮品的糖分描述。
     *
     * @param sugar 需要设置的饮品糖分描述。
     */
    public void setSugar(String sugar) {
        this.sugar = sugar;
    }

    /**
     * 获取饮品的大小杯。
     *
     * @return 返回当前饮品的大小杯。
     */
    public String getSize() {
        return size;
    }

    /**
     * 设置饮品的大小杯。
     *
     * @param size 需要设置的饮品大小杯。
     */
    public void setSize(String size) {
        this.size = size;
    }
}
package com.ssm.pojo;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
 * SoybeanMilkMakerII类实现了多种Spring接口,包括BeanNameAware, BeanFactoryAware,
 * ApplicationContextAware, InitializingBean,用于展示如何与Spring容器交互。
 * 这个类模拟了一个豆奶制作机,能够制作豆奶并提供自定义初始化和销毁逻辑。
 */
public class SoybeanMilkMakerII implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean{
    private String beverageShop = null; // 饮品店品牌
    private Source source = null; // 果汁原料描述

    // Getter 方法
    public String getBeverageShop() {
        return beverageShop;
    }

    // Setter 方法,确保参数类型与getter返回类型匹配
    public void setBeverageShop(String beverageShop) {
        this.beverageShop = beverageShop;
    }

    //Getter method
    public Source getSource() {
        return this.source;
    }

    // Setter method
    public void setSource(Source source) {
       this.source = source;
    }

    /**
     * SoybeanMilkMakerII的构造方法,打印构造信息。
     */
    public SoybeanMilkMakerII() {
        System.out.println("SoybeanMilkMakerII的构造方法");
    }

    /**
     * 制作豆奶的方法。
     * @return 返回一杯具有特定品牌、大小、糖分和水果的豆奶描述。
     */
    public String makeSoybeanMilk() {
        String soybeanMilk = "这是一杯由" + beverageShop + "饮品店,提供的" + source.getSize() + source.getSugar() + source.getBean();
        return soybeanMilk;
    }

    /**
     * 自定义初始化方法,展示如何在Bean初始化后执行特定逻辑。
     */
    public void init() {
        System.out.println("【" + this.getClass().getSimpleName() + "】执行自定义初始化方法");
    }

    /**
     * 自定义销毁方法,展示如何在Bean销毁前执行特定逻辑。
     */
    public void destroy() {
        System.out.println("【" + this.getClass().getSimpleName()  + "】执行自定义销毁方法");
    }

    /**
     * 实现BeanNameAware接口的方法,用于获取Bean的名称。
     * @param beanName Bean的名称。
     */
    @Override
    public void setBeanName(String beanName) {
        System.out.println("【" + this.getClass().getSimpleName() + "】调用BeanNameAware接口的setBeanName方法");
    }

    /**
     * 实现BeanFactoryAware接口的方法,用于获取BeanFactory。
     * @param bf BeanFactory实例。
     * @throws BeansException 如果设置过程中发生错误。
     */
    @Override
    public void setBeanFactory(BeanFactory bf) throws BeansException {
        System.out.println("【" + this.getClass().getSimpleName() + "】调用BeanFactoryAware接口的setBeanFactory方法");
    }

    /**
     * 实现ApplicationContextAware接口的方法,用于获取ApplicationContext。
     * @param ctx ApplicationContext实例。
     * @throws BeansException 如果设置过程中发生错误。
     */
    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        System.out.println("【" + this.getClass().getSimpleName()  + "】调用ApplicationContextAware接口的setApplicationContext方法");
    }

    /**
     * 实现InitializingBean接口的方法,用于在所有属性设置完成后执行初始化操作。
     * @throws Exception 如果初始化过程中发生错误。
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("【" + this.getClass().getSimpleName() + "】调用InitializingBean接口的afterPropertiesSet方法");
    }
}
装配集合

有些时候要做复杂的装配工作,比如 SetMapListArrayProperties 等,定义如下pojo:

package com.ssm.pojo;

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

/**
 * ComplexAssembly类用于演示装配复杂数据类型。
 * 该类包含了一系列属性,分别是:id、list、map、props、set和array。
 * 这些属性通过对应的setter和getter方法进行访问和设置。
 */
public class ComplexAssembly {
    private Long id; // 唯一标识
    private List<String> list; // 字符串列表
    private Map<String, String> map; // 字符串键值对映射
    private Properties props; // 属性集合,常用于配置信息存储
    private Set<String> set; // 不含重复元素的集合
    private String[] array; // 字符串数组

    /**
     * 获取id属性。
     * @return 返回当前对象的唯一标识。
     */
    public Long getId() {
        return id;
    }

    /**
     * 设置id属性。
     * @param id 欲设置的唯一标识。
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * 获取list属性。
     * @return 返回当前对象的字符串列表。
     */
    public List<String> getList() {
        return list;
    }

    /**
     * 设置list属性。
     * @param list 欲设置的字符串列表。
     */
    public void setList(List<String> list) {
        this.list = list;
    }

    /**
     * 获取map属性。
     * @return 返回当前对象的字符串键值对映射。
     */
    public Map<String, String> getMap() {
        return map;
    }

    /**
     * 设置map属性。
     * @param map 欲设置的字符串键值对映射。
     */
    public void setMap(Map<String, String> map) {
        this.map = map;
    }

    /**
     * 获取props属性。
     * @return 返回当前对象的属性集合。
     */
    public Properties getProps() {
        return props;
    }

    /**
     * 设置props属性。
     * @param props 欲设置的属性集合。
     */
    public void setProps(Properties props) {
        this.props = props;
    }

    /**
     * 获取set属性。
     * @return 返回当前对象的不含重复元素的集合。
     */
    public Set<String> getSet() {
        return set;
    }

    /**
     * 设置set属性。
     * @param set 欲设置的不含重复元素的集合。
     */
    public void setSet(Set<String> set) {
        this.set = set;
    }

    /**
     * 获取array属性。
     * @return 返回当前对象的字符串数组。
     */
    public String[] getArray() {
        return array;
    }

    /**
     * 设置array属性。
     * @param array 欲设置的字符串数组。
     */
    public void setArray(String[] array) {
        this.array = array;
    }
}

要装配这个pojo类,其xml配置如下:

<!-- 定义一个复杂组件装配的bean -->
<bean id="complexAssembly"
      class="com.ssm.pojo.ComplexAssembly">
    <!-- 设置组件的id为1 -->
    <property name="id" value="1" />
    <!-- 设置一个字符串列表,包含多个值 -->
    <property name="list">
        <list>
            <value>value-list-1</value>
            <value>value-list-2</value>
            <value>value-list-3</value>
        </list>
    </property>
    <!-- 设置一个键值对映射,包含多个键值对 -->
    <property name="map">
        <map>
            <entry key="key1" value="value-map-1" />
            <entry key="key2" value="value-map-2" />
            <entry key="key3" value="value-map-3" />
        </map>
    </property>
    <!-- 设置一个属性集合,每个属性包含键和值 -->
    <property name="props">
        <props>
            <prop key="prop1">value-prop-1</prop>
            <prop key="prop2">value-prop-2</prop>
            <prop key="prop3">value-prop-3</prop>
        </props>
    </property>
    <!-- 设置一个无序的值集合 -->
    <property name="set">
        <set>
            <value>value-set-1</value>
            <value>value-set-2</value>
            <value>value-set-3</value>
        </set>
    </property>
    <!-- 设置一个值数组 -->
    <property name="array">
        <array>
            <value>value-array-1</value>
            <value>value-array-2</value>
            <value>value-array-3</value>
        </array>
    </property>
</bean>

上面代码是对字符串的各个集合的装载。有些时候可能需要更复杂的装载,例如一个 List 可以是一个系列类的对象,又如一个 Map 集合类,键是一个类对象,值也是一个类对象。相关的pojo类定义如下:

package com.ssm.pojo;

public class RoleIII {
    private Long id; // 角色编号
    private String roleName; // 角色名称
    private String note; // 备注信息

    /**
     * 无参数构造方法,用于创建一个空的角色对象。
     */
    public RoleIII() {
    }

    /**
     * 带参数的构造方法,用于创建一个具有指定属性的角色对象。
     * @param id 角色的唯一标识符。
     * @param roleName 角色的名称。
     * @param note 关于角色的备注信息。
     */
    public RoleIII(Long id, String roleName, String note) {
        this.id = id;
        this.roleName = roleName;
        this.note = note;
    }

    /**** setters and getters ****/
    // 获取角色的ID
    public Long getId() {
        return id;
    }

    // 设置角色的ID
    public void setId(Long id) {
        this.id = id;
    }

    // 获取角色的名称
    public String getRoleName() {
        return roleName;
    }

    // 设置角色的名称
    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    // 获取角色的备注信息
    public String getNote() {
        return note;
    }

    // 设置角色的备注信息
    public void setNote(String note) {
        this.note = note;
    }
}
package com.ssm.pojo;

/**
 * User类用于表示用户信息。
 */
public class User {
    private Long id; // 用户的唯一标识
    private String userName; // 用户名
    private String note; // 用户备注信息

    /**
     * 获取用户的唯一标识。
     * @return 用户的ID。
     */
    public Long getId() {
        return id;
    }

    /**
     * 设置用户的唯一标识。
     * @param id 要设置的用户ID。
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * 获取用户的用户名。
     * @return 用户的用户名。
     */
    public String getUserName() {
        return userName;
    }

    /**
     * 设置用户的用户名。
     * @param userName 要设置的用户名。
     */
    public void setUserName(String userName) {
        this.userName = userName;
    }

    /**
     * 获取用户的备注信息。
     * @return 用户的备注信息。
     */
    public String getNote() {
        return note;
    }

    /**
     * 设置用户的备注信息。
     * @param note 要设置的用户备注信息。
     */
    public void setNote(String note) {
        this.note = note;
    }
}
package com.ssm.pojo;

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

/**
 * UserRoleAssembly 类用于组装和管理用户角色关系。
 * 它包含了对用户角色关系的不同类型集合的管理,如列表、映射和集合。
 */
public class UserRoleAssembly {
    private Long id; // 唯一标识符
    private List<RoleIII> list; // 角色列表
    private Map<RoleIII, User> map; // 角色到用户的映射
    private Set<RoleIII> set; // 角色集合

    /**** setters and getters ****/
    /**
     * 获取UserRoleAssembly的唯一标识符。
     * @return 返回此实例的唯一标识符。
     */
    public Long getId() {
        return id;
    }

    /**
     * 设置UserRoleAssembly的唯一标识符。
     * @param id 要设置的唯一标识符。
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * 获取角色列表。
     * @return 返回此实例的角色列表。
     */
    public List<RoleIII> getList() {
        return list;
    }

    /**
     * 设置角色列表。
     * @param list 要设置的角色列表。
     */
    public void setList(List<RoleIII> list) {
        this.list = list;
    }

    /**
     * 获取角色到用户的映射。
     * @return 返回此实例的角色到用户映射。
     */
    public Map<RoleIII, User> getMap() {
        return map;
    }

    /**
     * 设置角色到用户的映射。
     * @param map 要设置的角色到用户映射。
     */
    public void setMap(Map<RoleIII, User> map) {
        this.map = map;
    }

    /**
     * 获取角色集合。
     * @return 返回此实例的角色集合。
     */
    public Set<RoleIII> getSet() {
        return set;
    }

    /**
     * 设置角色集合。
     * @param set 要设置的角色集合。
     */
    public void setSet(Set<RoleIII> set) {
        this.set = set;
    }
}

则XML需要如下配置:

<!-- 定义Role实体 Bean -->
<bean id="role_1" class="com.ssm.pojo.RoleIII">
    <property name="id" value="1" />
    <property name="roleName" value="role_name_1" />
    <property name="note" value="role_note_1" />
</bean>
<bean id="role_2" class="com.ssm.pojo.RoleIII">
    <property name="id" value="2" />
    <property name="roleName" value="role_name_2" />
    <property name="note" value="role_note_2" />
</bean>
<!-- 定义User实体 Bean -->
<bean id="user_1" class="com.ssm.pojo.User">
    <property name="id" value="1" />
    <property name="userName" value="user_name_1" />
    <property name="note" value="role_note_1" />
</bean>
<bean id="user_2" class="com.ssm.pojo.User">
    <property name="id" value="2" />
    <property name="userName" value="user_name_2" />
    <property name="note" value="role_note_1" />
</bean>
<!-- 定义UserRoleAssembly实体 Bean,用于组装用户和角色的关系 -->
<bean id="userRoleAssembly"
      class="com.ssm.pojo.UserRoleAssembly">
    <property name="id" value="1" />
    <property name="list">
        <list>
            <ref bean="role_1" />
            <ref bean="role_2" />
        </list>
    </property>
    <property name="map">
        <map>
            <entry key-ref="role_1" value-ref="user_1" />
            <entry key-ref="role_2" value-ref="user_2" />
        </map>
    </property>
    <property name="set">
        <set>
            <ref bean="role_1" />
            <ref bean="role_2" />
        </set>
    </property>
</bean>
命名空间装配

Spring还提供了对应的命名空间的定义,在使用命名空间的时候要先引入对应的命名空间和XML模式(XSD)文件,如下代码所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
  xmlns:c="http://www.springframework.org/schema/c"
  xmlns:p="http://www.springframework.org/schema/p"

这两行定义了XML的命名空间,这样才能在内容里面使用 pc 这样的前缀,如下代码所示:

<!-- 使用构造方法注入创建Role对象 -->
    <bean id="c_role" class="com.ssm.pojo.RoleIII" c:_0="8"
          c:_1="role_name_c" c:_2="role_note_c" />
    <!-- 使用setter注入创建Role对象 -->
    <bean id="p_role" class="com.ssm.pojo.RoleIII" p:id="9"
          p:roleName="role_name_p" p:note="role_note_p" />

在这段XML配置中,我们通过 <bean> 标签创建了两个Role对象。这里涉及到了两种不同的属性注入方式:C风格和P风格。

  • C风格的属性注入:c:_0="8" 表示注入第一个属性,c:_1="role_name_c" 表示注入第二个,c:_2="role_note_c" 表示注入第三个。
  • P风格的属性注入:p:id="9" 明确指出了注入的属性名为id,p:roleNamep:note 同理。这种方式在配置时更加直观,可以根据属性名直接确定注入的值,便于理解和维护。

如下是通过命名空间定义 UserRoleAssembly 类实例:

<?xml version='1.0' encoding='UTF-8' ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:util="http://www.springframework.org/schema/util"
       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-4.0.xsd
           http://www.springframework.org/schema/util
           http://www.springframework.org/schema/util/spring-util.xsd">
    <!-- 使用构造方法注入创建RoleIII对象,通过构造函数参数初始化对象 -->
    <bean id="c_role" class="com.ssm.pojo.RoleIII"
          c:_0="8" c:_1="role_name_c" c:_2="role_note_c" />
    <!-- 使用setter方法注入创建RoleIII对象,通过调用setter方法初始化对象属性 -->
    <bean id="p_role" class="com.ssm.pojo.RoleIII"
          p:id="9" p:roleName="role_name_p" p:note="role_note_p" />
    <!-- 装配多个RoleIII和User对象示例,演示了通过构造方法和setter方法的混合注入 -->
    <bean id="role1" class="com.ssm.pojo.RoleIII"
          c:_0="1" c:_1="role_name_1" c:_2="role_note_1" />
    <bean id="role2" class="com.ssm.pojo.RoleIII"
          p:id="2" p:roleName="role_name_2" p:note="role_note_2" />
    <bean id="user1" class="com.ssm.pojo.User"
          p:id="1" p:userName="role_name_1" p:note="user_note_1" />
    <bean id="user2" class="com.ssm.pojo.User"
          p:id="2" p:userName="role_name_2" p:note="user_note_2" />
    <!-- 装配一个List对象,包含已定义的role1和role2两个Bean -->
    <util:list id="list">
        <ref bean="role1" />
        <ref bean="role2" />
    </util:list>
    <!-- 装配一个Map对象,将role和user Bean以键值对形式装配 -->
    <util:map id="map">
        <entry key-ref="role1" value-ref="user1" />
        <entry key-ref="role2" value-ref="user2" />
    </util:map>
    <!-- 装配一个Set对象,包含已定义的role1和role2两个Bean -->
    <util:set id="set">
        <ref bean="role1" />
        <ref bean="role2" />
    </util:set>
    <!-- 创建UserRoleAssembly对象,并将其与之前定义的List、Map、Set对象关联 -->
    <bean id="userRoleAssembly"
          class="com.ssm.pojo.UserRoleAssembly"
          p:id="1" p:list-ref="list" p:map-ref="map" p:set-ref="set" />
</beans>

通过注解装配Bean

实际上并不推荐使用XML的形式装配Bean,避免XML文件泛滥。使用注解的方式可以减少XML,注解的方式既能够实现XML的功能,也能够提供自动装配的功能。采用自动装配后,开发者所需做的决策就少了,更有利于程序的开发,这便是“约定优于配置”的理念。

Spring提供了两种方式让Spring IoC容器发现Bean,大部分的项目都可以用Java配置完成,而不是XML,这样可以有效减少配置并避免引入大量XML,解决了在 Spring Boot 3之前的版本需要配置大量XML的问题。

  • 组件扫描:通过定义资源的方式,让Spring IoC容器扫描对应的包,从而把Bean装配起来。
  • 自动装配:通过注解定义,使一些依赖关系可以通过注解完成。

不过,使用注解为主XML为辅的方式更加合理。比如当系统存在多个公共的配置文件时候(properties和xml),如果都写到注解里,显然代码里就分散了各种公共配置,不利于管理,这个时候XML的配置就更合理一些;又或者一些类来自第三方,而不是系统开发的配置文件,这时利用XML的方式会更加明确。

使用注解 @Component 装配Bean

package com.ssm.pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * RoleIV类表示一个角色实体,它包含了角色的基本信息。
 * 通过Spring的@Component注解将其标记为一个Bean,名称为"role"。
 */
@Component(value = "role")
public class RoleIV {
    // 通过@Value注解注入角色ID,这里硬编码为1。
    @Value("1")
    private Long id;
    // 通过@Value注解注入角色名称,这里硬编码为"admin"。
    @Value("admin")
    private String roleName;
    // 通过@Value注解注入角色备注信息,这里硬编码为"administrator"。
    @Value("administrator")
    private String note;

    /**
     * 无参构造函数,打印构造函数名称。
     */
    public RoleIV()
    {
        System.out.println("RoleIV()");
    }

    /**
     * 带参数的构造函数,用于初始化角色信息。
     *
     * @param id 角色ID
     * @param roleName 角色名称
     * @param note 角色备注信息
     */
    public RoleIV(Long id, String roleName, String note)
    {
        System.out.println("RoleIV(Long id, String roleName, String note)");
        this.id = id;
        this.roleName = roleName;
        this.note = note;
    }

    /**
     * 获取角色名称。
     *
     * @return 角色名称
     */
    public String getRoleName()
    {
        return roleName;
    }

    /**
     * 设置角色名称。
     *
     * @param roleName 要设置的角色名称
     */
    public void setRoleName(String roleName)
    {
        this.roleName = roleName;
    }

    /**
     * 获取角色备注信息。
     *
     * @return 角色备注信息
     */
    public String getNote()
    {
        return note;
    }

    /**
     * 设置角色备注信息。
     *
     * @param note 要设置的角色备注信息
     */
    public void setNote(String note)
    {
        this.note = note;
    }

    /**
     * 获取角色ID。
     *
     * @return 角色ID
     */
    public Long getId()
    {
        return id;
    }

    /**
     * 设置角色ID。
     *
     * @param id 要设置的角色ID
     */
    public void setId(Long id)
    {
        this.id = id;
    }

    /**
     * 重写toString方法,便于打印角色信息。
     *
     * @return 描述角色信息的字符串
     */
    @Override
    public String toString()
    {
        return "RoleIV [id=" + id + ", roleName=" + roleName + ", note=" + note + "]";
    }
}

@Component 是 Spring 框架中的一个注解,它是 Spring 的核心组件之一,用于标记一个 Java 类作为 Spring 容器管理的 Bean。当 Spring 容器启动时,它会扫描带有 @Component 注解的类,并将这些类实例化为 Bean,然后存储在 Spring 容器中,以便在需要时可以自动注入到其他依赖于它们的类中。

在代码中,@Component(value = "role") 告诉 Spring 容器这个 RoleIV 类是一个 Bean,并且它的名称是 "role"。这样,其他类可以通过 @Autowired 或者 @Resource 注解来注入这个名为 "role" 的 Bean 实例,也可以简写成 @Component("role"),甚至不给value值,直接写成 @Component。如果不写,Spring IoC容器就默认类名,以首字母小写的形式作为id,为其生成对象,装配到容器中。

例如,如果你有一个其他类需要使用 RoleIV,可以这样做:

import org.springframework.beans.factory.annotation.Autowired;
import com.ssm.pojo.RoleIV;

public class SomeClass {
    private RoleIV role;

    @Autowired
    public void setRole(RoleIV role) {
        this.role = role;
    }
    // 其他方法...
}

Spring 会自动将 "role" Bean 注入到 SomeClassrole 字段中,无需手动创建 RoleIV 实例。

然而有了这个pojo类还不够,Spring IoC并不知道去哪里扫描对象,这个时候可以使用一个Java Config告诉他:

package com.ssm.pojo;

import org.springframework.context.annotation.ComponentScan;

/**
 * PojoConfig类用于配置POJO相关的设置。
 * 该类通过使用@ComponentScan注解来自动扫描并注册所有的组件,使得这些组件能够被Spring容器管理。
 *
 */
@ComponentScan
public class PojoConfig{
}

@ComponentScan 表示启动扫描,默认扫描当前包的路径。然后便可以通过Spring定义好的Spring IoC容器的实现类 AnnotationConfigApplicationContext 初始化容器并生成Bean了:

package com.ssm.main;

import com.ssm.pojo.RoleIV;
import com.ssm.pojo.PojoConfig;
import com.ssm.config.ApplicationConfig;
import java.sql.SQLException;
import com.ssm.service.RoleIVService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * 主函数类,用于演示基于注解的IoC容器的使用。
 */
public class AnnotationMain {
    /**
     * 程序入口点。
     * @param args 命令行参数
     * @throws SQLException 抛出SQLException异常
     */
    public static void main(String[] args) throws SQLException {
        testAnnotation();
    }

    /**
     * 测试注解配置的IoC容器功能。
     * 该方法不接受参数,也不返回任何值。
     * 主要步骤包括:
     * 1. 创建基于注解的IoC容器,并指定配置类;
     * 2. 通过容器获取RoleIV类型的Bean实例;
     * 3. 打印获取的Bean实例的id属性;
     * 4. 关闭容器。
     */
    public static void testAnnotation() {
        // 创建基于注解的IoC容器,并指定配置类
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PojoConfig.class);
        // 通过容器获取RoleIV类型的Bean实例
        RoleIV role = context.getBean(RoleIV.class);
        // 打印获取的Bean实例的id属性
        System.out.println(role.getId());
        // 关闭容器
        context.close();
    }
}

以上代码只是一个简单的例子,@ComponentScan 存在几个非常实用的配置项,例如 basePackages,用于配置一个Java包的数组,Spring会根据它扫描对应的包和子包;例如 basePackageClasses,用于配置多个Java类,Spring会根据这个配置扫描。通过如下代码可以看到效果:

首先定义接口:

package com.ssm.service;

import com.ssm.pojo.RoleIV;

/**
 * RoleService接口定义了角色服务的相关操作。
 */
public interface RoleIVService {
    /**
     * 打印角色信息。
     * @param roleIV 角色信息实体,包含角色相关的详细信息。
     */
    public void printRoleIVInfo(RoleIV roleIV);
}

Spring推荐使用接口定义方法,它可以将定义和实现分离,然后用具体的实现类实现:

package com.ssm.service.iml;

import com.ssm.pojo.RoleIV;
import com.ssm.service.RoleIVService;
import org.springframework.stereotype.Component;

/**
 * RoleIVService的实现类,提供具体的角色信息打印服务。
 */
@Component
public class RoleIVServiceImpl implements RoleIVService{
    /**
     * 打印角色信息。
     * @param roleIV 角色信息对象,包含角色的ID、名称和备注。
     */
    @Override
    public void printRoleIVInfo(RoleIV roleIV)
    {
        // 打印角色信息的方法实现
        System.out.println("RoleIVServiceImpl.printRoleInfo");
        System.out.println(roleIV.getId());
        System.out.println(roleIV.getRoleName());
        System.out.println(roleIV.getNote());
    }
}

这里注解 @Component 表明它是一个Spring需要装配的Bean,而且也实现了对应的接口方法。然后使用 @ComponentScan 加上对应的扫描配置,如下代码所示:

package com.ssm.config;

/**
 * ApplicationConfig类用于配置Spring框架的组件扫描。
 * 通过@ComponentScan注解来告诉Spring容器去扫描指定包下的所有组件。
 */
import org.springframework.context.annotation.ComponentScan;
import com.ssm.pojo.RoleIV; // 引入RoleIV类,用作示例
import com.ssm.service.iml.RoleIVServiceImpl; // 引入RoleIVServiceImpl类,用作示例

@ComponentScan(basePackageClasses = {RoleIV.class, RoleIVServiceImpl.class})
// 通过指定具体的类,来让Spring容器扫描包含这些类的包及其子包下的所有组件。
@ComponentScan(basePackages = {"com.ssm.pojo", "com.ssm.service"})
// 通过指定包的名称,来让Spring容器扫描这些包及其子包下的所有组件。
@ComponentScan(basePackages = {"com.ssm.pojo"}, basePackageClasses = {RoleIVServiceImpl.class})
// 组合使用basePackages和basePackageClasses,让Spring容器扫描指定包及其子包下以及指定类所在包的组件。
public class ApplicationConfig {
}
// ApplicationConfig类结束

应该尽量避免多个注解配置,扫描混乱,尤其重复扫描,避免没必要的异常出现;另外如果包名经常变动的场景应尽量避免使用包来扫,往往包名改了如果配置没跟着都改IDE不报错,这时候用类来扫会更好IDE会报错。

使用如下代码来验证效果:

  /**
     * 测试组件扫描功能。
     * 该方法通过注解配置的应用上下文来启动Spring容器,从容器中获取RoleIV和RoleIVService的实例,
     * 然后调用服务方法处理角色信息,最后关闭应用上下文。
     */
    public static void testComponentScan() {
        // 创建注解配置的应用上下文,并指定应用配置类ApplicationConfig
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        // 从应用上下文中获取RoleIV类型的Bean实例
        RoleIV role = context.getBean(RoleIV.class);
        // 从应用上下文中获取RoleIVService类型的Bean实例
        RoleIVService roleIVService = context.getBean(RoleIVService.class);
        // 调用服务方法,打印角色信息
        roleIVService.printRoleIVInfo(role);
        // 关闭应用上下文
        context.close();
    }

自动装配 @Autowired

Spring IoC先完成Bean的定义,再初始化和寻找需要注入的资源。所谓自动装配就是由Spring发现对应的Bean,自动完成装配工作的方式,如下代码所示:

package com.ssm.service;

/**
 * RoleIVServiceII接口定义了角色IV的相关服务操作
 */
public interface RoleIVServiceII {
    /**
     * 打印角色IV的信息
     * 该方法没有参数和返回值
     */
    public void printRoleIVInfo();
}

其实现类代码如下:

package com.ssm.service.iml;

import com.ssm.service.RoleIVServiceII;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ssm.pojo.RoleIV;

/**
 * RoleIVServiceIIImpl类实现了RoleIVServiceII接口,用于打印RoleIV对象的信息。
 * 通过@Autowired注解自动注入RoleIV对象。
 */
@Component(value = "roleIVServiceII")
public class RoleIVServiceIIImpl implements RoleIVServiceII{
    @Autowired
    private RoleIV roleIV; // 自动注入的RoleIV对象

    /**
     * 打印RoleIV对象的信息,包括ID、角色名和备注。
     * 该方法没有参数和返回值。
     */
    @Override
    public void printRoleIVInfo() {
        System.out.println(roleIV.getId() + " " + roleIV.getRoleName() + " " + roleIV.getNote());
    }
}

这里使用了 @Autowired,表示在Spring IoC定位所有的Bean后,这个字段需要按类型注入,然后Spring IoC容器会寻找资源找到后将其注入,也就是将实例 roleIV 注入进来。这里需要注意的是 @Autowired 会按照类型进行注入。

Spring IoC容器有时候会寻找失败,在默认情况下会抛异常,因为Spring IoC容器认为一定要找到对应的Bean注入这个属性。如果要改变这个特性,可以通过 @Autowired 的配置项 required 改变它,类似这样 @Autowired(required=false)。也就是说默认情况下值必须注入成功的,也就是这个 required 的值默认为 true。声明修改为 false 后,表明如果在已经定义好的Bean中找不到对应的类型,则允许不注入,也不会抛异常,这个时候这个字段可能为空值,需要开发者自己校验,从而避免类似空指针异常等异常出现。

注解 @Autowired 除可以配置在属性外,还允许方法配置,常见的Bean的setter方法也可以使用它来完成注入,如下代码所示:

package com.ssm.service.iml;

import com.ssm.pojo.RoleIV;
import com.ssm.service.RoleIVServiceII;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 这是一个实现RoleIVServiceII接口的服务类,用于操作RoleIV实体。
 */
@Component(value = "roleIVServiceII")
public class RoleIVServiceIIImplII implements RoleIVServiceII{
    private RoleIV roleIV; // RoleIV实体对象

    /**
     * 通过@Autowired注解自动注入RoleIV实体。
     * @param roleIV 要注入的RoleIV实体
     */
    @Autowired
    public void setRoleIV(RoleIV roleIV){
        this.roleIV = roleIV;
    }

    /**
     * 打印RoleIV实体的信息。
     * 该方法没有参数和返回值。
     */
    public void printRoleIVInfo() {
        System.out.println(roleIV.getId() + " " + roleIV.getRoleName() + " " + roleIV.getNote());
    }
}

使用 @Autowired 这是Spring IoC自动装配完成的,使得配置大幅度减少,满足约定优于配置的原则,也不会降低程序的健壮性。

自定义装配的歧义性 (注解 @Primary@Qualifier)

Spring建议在大部分情况下使用接口编程,但定义一个接口并不一定只有一个实现类。例如接口:

package com.ssm.service;

import com.ssm.pojo.RoleIV;

/**
 * RoleService接口定义了角色服务的相关操作。
 */
public interface RoleIVService {
    /**
     * 打印角色信息。
     * @param roleIV 角色信息实体,包含角色相关的详细信息。
     */
    public void printRoleIVInfo(RoleIV roleIV);
}

有了一个实现类:

package com.ssm.service.iml;

import com.ssm.pojo.RoleIV;
import com.ssm.service.RoleIVService;
import org.springframework.stereotype.Component;

/**
 * RoleIVService的实现类,提供具体的角色信息打印服务。
 */
@Component
public class RoleIVServiceImpl implements RoleIVService{
    /**
     * 打印角色信息。
     * @param roleIV 角色信息对象,包含角色的ID、名称和备注。
     */
    @Override
    public void printRoleIVInfo(RoleIV roleIV)
    {
        // 打印角色信息的方法实现
        System.out.println("RoleIVServiceImpl.printRoleInfo");
        System.out.println("id="+roleIV.getId());
        System.out.println("roleName="+roleIV.getRoleName());
        System.out.println("note="+roleIV.getNote());
    }
}

还有另一个实现类:

package com.ssm.service.iml;

import com.ssm.pojo.RoleIV;
import com.ssm.service.RoleIVService;
import org.springframework.stereotype.Component;

/**
 * RoleIVServiceImplII类实现了RoleIVService接口,
 * 用于打印RoleIV对象的相关信息。
 */
@Component("RoleIVServiceImplII")
public class RoleIVServiceImplII implements RoleIVService {
    /**
     * 打印RoleIV对象的信息。
     *
     * @param roleIV RoleIV对象,包含角色的id、角色名和备注信息。
     *               该方法会打印出这些信息的字符串表示。
     */
    @Override
    public void printRoleIVInfo(RoleIV roleIV) {
        // 通过字符串拼接的方式,将roleIV对象的id、roleName和note属性打印出来
        System.out.println("{id=" + roleIV.getId() + ", roleName=" + roleIV.getRoleName() + ", note=" + roleIV.getNote() + "}");
    }
}

这个时候新建一个类,代码如下:

package com.ssm.controller;

import com.ssm.pojo.RoleIV;
import com.ssm.service.RoleIVService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * RoleIVController类用于处理与RoleIV相关的请求。
 * 它通过注入RoleIVService来实现对RoleIV业务逻辑的调用。
 */
@Component
public class RoleIVController {
    // 自动注入RoleIVService,以便在controller中使用
    @Autowired
    private RoleIVService roleIVService;

    /**
     * 打印RoleIV的信息。
     * 该方法会调用roleIVService中的printRoleIVInfo方法,将roleIV的信息打印出来。
     *
     * @param roleIV 角色信息对象,包含角色的详细信息。
     */
    public void printRoleIVInfo(RoleIV roleIV)
    {
        roleIVService.printRoleIVInfo(roleIV);
    }
}

它有一个字段是 RoleService 类型,并且自动注入的属性是 roleServiceRoleService 是接口类型,但有两个实现类,这个时候Spring IoC容器就会混乱了,它是无法判断注入哪个对象的,于是就会抛出异常,这样通过 @Autowired 注入就会失败。

究其原因,发生这样的情况是因为注解 @Autowired 采用的是按类型注入对象的方式。一个接口可以有多个实现类,一个抽象类也可以有多个非抽象子类,但类型相同,无法获取唯一的实例,导致了异常情况。

在Spring IoC底层容器接口 BeanFactory 的定义中,存在一个通过类型获取Bean的方法即:
<T> T getBean(Class<T> requiredType) throws BeansException;
这样仅仅通过类型(RoleIVService.class)作为参数无法判断使用哪个具体类实例返回,这便是自动装备的歧义。为了解决歧义,Spring提供了注解 @Primary@Qualifier 用于消除歧义。

@Primary

@Primary 注解代表优先的,表示当Spring IoC通过一个接口或者抽象类注入对象,发现多个实现类,不能做出决策时,注解 @Primary 会告诉Spring IoC容器,优先将该类实例注入,如下代码所示:

package com.ssm.service.iml;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import com.ssm.pojo.RoleIV;
import com.ssm.service.RoleIVService;

/**
 * 这是一个实现RoleIVService接口的类,用于处理关于RoleIV实体的相关业务逻辑。
 * 通过实现printRoleIVInfo方法,打印RoleIV实体的信息。
 *
 * @Component("roleIVServiceIII") 标注此对象是一个Bean,并且在Spring容器中的名称为"roleIVServiceIII"。
 * @Primary 标注此Bean在同一个接口的多个实现中具有优先权。
 */
@Component("roleIVServiceIII")
@Primary
public class RoleIVServiceImplIII implements RoleIVService{
    /**
     * 打印RoleIV实体的信息。
     *
     * @param roleIV RoleIV实体,包含id、角色名和备注信息。
     * 该方法不返回任何内容,仅将实体信息以特定格式打印到控制台。
     */
    public void printRoleIVInfo(RoleIV roleIV)
    {
        // 格式化输出RoleIV实体的详细信息
        System.out.println("{id=" + roleIV.getId() + ", roleName=" + roleIV.getRoleName() + ", note=" + roleIV.getNote() + "}");
    }
}

@Primary 告诉Spring IoC容器,当存在多个 RoleIVService 的实现类时,优先将 RoleIVServiceImplIII 的实例注入。然而 @Primary 只能解决优先问题,解决不了选择问题。例如同一个接口两个实现类,都挂上 @Primary 的话,代码可以这样写,但是注入的时候就会抛异常了。

@Qualifier

出现歧义的一个重要原因是Spring在注入时是按类型。除了按类型查找Bean,Spring IoC容器底层的接口 BeanFactory,也定义了按名称查找的方法 <T> T getBean(String name, Class<T> requiredType) throws BeansException;。注解 @Qualifier 就是用来按名称查找的,修改 RoleIVController 代码如下:

package com.ssm.controller;

import com.ssm.pojo.RoleIV;
import com.ssm.service.RoleIVService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

/**
 * RoleIVController类用于处理与RoleIV相关的请求。
 * 它通过注入RoleIVService来实现对RoleIV业务逻辑的调用。
 */
@Component
public class RoleIVController {
    // 自动注入RoleIVService,以便在controller中使用
    @Autowired
    @Qualifier("roleIVServiceIII")
    private RoleIVService roleIVService;

    /**
     * 打印RoleIV的信息。
     * 该方法会调用roleIVService中的printRoleIVInfo方法,将roleIV的信息打印出来。
     *
     * @param roleIV 角色信息对象,包含角色的详细信息。
     */
    public void printRoleIVInfo(RoleIV roleIV)
    {
        roleIVService.printRoleIVInfo(roleIV);
    }
}

这个时候IoC容器不会再按照类型的方式注入,而是按照名称的方式注入,就不会存在歧义。再明确一下 @Autowired 的注入规则:先按照类型匹配,如果只有一个满足的Bean,则直接将其注入,结束;如果找到多个匹配的Bean类型,那么会按属性名查找Bean。例如上边这个代码 private RoleIVService roleIVService;,就会先找 roleIVService 这个字符串,如果可以找到则结束注入过程。在这两个情况都找不到,并且又不允许注入为空时则抛异常。

装载带有参数的构造方法类

通常构造方法都是带参数的,而带参数的构造方法也允许通过注解注入,如下代码所示:

package com.ssm.controller;

import com.ssm.pojo.RoleIV;
import com.ssm.service.RoleIVService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

/**
 * RoleIVController类,负责处理RoleIV相关的控制器逻辑。
 * 依赖于RoleIVService来执行具体的服务逻辑。
 */
@Component
public class RoleIVController {
    // RoleIVService的实例,用于执行角色IV相关的服务操作。
    private RoleIVService roleIVService;

    /**
     * RoleIVController的构造函数,通过依赖注入的方式初始化roleIVService。
     *
     * @param roleIVService 一个标记为@Qualifier("roleIVServiceIII")的RoleIVService实例,用于具体的服务逻辑处理。
     */
    public RoleIVController(@Autowired @Qualifier("roleIVServiceIII") RoleIVService roleIVService){
        this.roleIVService = roleIVService;
    }

    /**
     * 打印RoleIV的信息。
     * 该方法会调用roleIVService中的printRoleIVInfo方法,将roleIV的信息打印出来。
     *
     * @param roleIV 角色信息对象,包含角色的详细信息。
     */
    public void printRoleIVInfo(RoleIV roleIV)
    {
        roleIVService.printRoleIVInfo(roleIV);
    }
}

使用注解 @Bean 装配

以上都是通过 @Component 装配Bean,但注解 @Component 只能注解在类上,不能注解到方法上。对于Java而言大部分的开发都需要引入第三方的包,而且往往并没有这些包的资源,这时候将无法为这些包的类加入注解 @Component, 从而让它成为开发环境的Bean。

这种情况开发者可以使用新类扩展(extends)其包内的类,然后在新类上使用注解 @Component,但这样又会显得很奇怪。这个场景中Spring给了一个注解 @Bean,它可以注解到方法上,并将方法返回的对象作为Spring的Bean,存放在IoC容器中。

例如我需要使用DBCP数据源,就要引入关于它的包,然后装配数据源的Bean,如下代码所示:

package com.ssm.config;

import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

/**
 * 数据源配置类,用于配置数据库连接池。
 */
public class DataSourceConfig {
    // 通过注解方式注入MySQL驱动类名
    @Value("com.mysql.jdbc.Driver")
    private String driverClassName = null;
    // 通过注解方式注入数据库URL
    @Value("jdbc:mysql://localhost:3306/ssm")
    private String url = null;
    // 通过注解方式注入数据库用户名
    @Value("root")
    private String username = null;
    // 通过注解方式注入数据库密码
    @Value("Ms123!@#")
    private String password = null;

    /**
     * 创建并返回DataSource Bean。
     *
     * @return 返回配置好的DataSource实例。
     */
    @Bean("dataSource")
    public DataSource getDataSource(){
        Properties props = new Properties();
        // 设置数据库连接池的属性
        props.setProperty("driverClassName", driverClassName);
        props.setProperty("url", url);
        props.setProperty("username", username);
        props.setProperty("password", password);
        DataSource datasource = null; // 初始化数据源变量
        try{
            // 通过属性创建数据源
            datasource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return datasource;
    }
}

这样就能够装配一个Bean。当Spring IoC容器扫描它的时候,就会为其生成对应的Bean,和其他的Bean一样,它可以通过 @Autowired 或者 @Qualifier 等注解注入别的Bean中。

注解自定义Bean的初始化和销毁方法

在Spring框架中,@Bean 注解告诉Spring容器这个方法将会返回一个需要被管理的对象,即Bean。在该注解中可以自定义初始化方法和销毁方法等一系列操作,只需要运用注解 @Bean 的配置项。

  • Bean的名称:默认情况下,@Bean 注解的方法名就是Bean的ID。如果需要自定义Bean的名称,可以使用 name 属性,如 @Bean(name= "myBean")。如果有多个 @Bean 方法返回相同类型,可以通过 @Qualifier 注解来区分它们。
  • 初始化和销毁方法initMethoddestroyMethod 属性可以用来指定Bean初始化和销毁时要调用的方法。例如,@Bean(initMethod="init", destroyMethod = "cleanup")
  • 依赖注入:Spring会自动处理 @Bean 方法之间的依赖关系,通过方法调用来注入。例如,如果Bean A需要Bean B,可以直接在A的 @Bean 方法中调用B的 @Bean 方法。如果需要注入其他已经注册的Bean,可以使用 @Autowired 注解,或者通过 @Qualifier 来指定特定的Bean。
  • 作用域:默认情况下,@Bean 创建的Bean是单例(Singleton)的。如果想创建原型(Prototype)或者其他作用域的Bean,可以使用 scope 属性,如 @Bean(scope=ConfigurableBeanFactory.SCOPE_PROTOTYPE)
  • autowireCandidate 属性是一个布尔值,它用于控制Spring容器在自动装配其他Bean时是否考虑当前Bean。默认情况下,autowireCandidatetrue,意味着这个Bean可以作为自动装配的候选者。如果设置为 false,那么这个Bean将被排除在自动装配的候选列表之外。
  • 属性值注入:可以使用 @Value 注解来注入属性值,比如常量或环境变量。对于复杂的属性配置,可以使用 @ConfigurationProperties 注解配合YAML或Properties文件来绑定。
  • Profile@Profile 注解允许你在特定的环境中激活或禁用Bean。例如,@Bean(@Profile("dev")) 将只在开发环境下创建Bean。
  • 懒加载:使用 @Lazy 注解标记的 @Bean 表示该Bean将在第一次请求时才创建,而不是在容器启动时立即创建。
  • 复合Bean@Bean 方法可以返回另一个 @Bean 方法的引用,实现Bean的组合。
  • 自定义初始化逻辑@Bean 方法体中的代码会作为Bean的初始化逻辑执行。
  • 代理模式:Spring支持JDK动态代理和CGLIB代理,你可以通过 proxyMode 属性来控制。

@Bean 注解通常与 @Configuration 注解一起使用,后者标识一个类作为配置源,提供了声明式的方式来定义Bean。这种方式比XML配置更简洁且易于维护。

    /**
     * 创建并配置 SoybeanMilkMakerII 实例的 Bean。
     *
     * @return SoybeanMilkMakerII 实例,配置了特定的饮料店名称和制作原料来源。
     * @see SoybeanMilkMakerII
     */
    @Bean(name = "soybeanMilkMakerII", initMethod = "init", destroyMethod = "Mydestroy")
    public SoybeanMilkMakerII getSoybeanMilkMakerII() {
        // 创建 SoybeanMilkMakerII 实例
        SoybeanMilkMakerII soybeanMilkMakerII = new SoybeanMilkMakerII();
        // 设置所属饮料店为"贡茶"
        soybeanMilkMakerII.setBeverageShop("贡茶");
        // 创建原料来源并配置其属性
        Source source = new Source("豆子", "少糖", "中杯");
        soybeanMilkMakerII.setSource(source);
        return soybeanMilkMakerII;
    }

混合使用装配Bean

在使用注解的前提下,有些场景使用XML会更合适,这就产生了两个共同存在的情况。例如当引入了第三方包或者服务的时候,如果使用注解,那么这个第三方的配置可能会散落在各个地方,难于管理,这种情况下用XML会更合适。

例如之前的Java配置数据源:

package com.ssm.config;

import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

/**
 * 数据源配置类,用于配置数据库连接池。
 */
public class DataSourceConfig {
    // 通过注解方式注入MySQL驱动类名
    @Value("com.mysql.jdbc.Driver")
    private String driverClassName = null;
    // 通过注解方式注入数据库URL
    @Value("jdbc:mysql://localhost:3306/ssm")
    private String url = null;
    // 通过注解方式注入数据库用户名
    @Value("root")
    private String username = null;
    // 通过注解方式注入数据库密码
    @Value("xxxxxx")
    private String password = null;

    /**
     * 创建并返回DataSource Bean。
     *
     * @return 返回配置好的DataSource实例。
     */
    @Bean("dataSource")
    public DataSource getDataSource(){
        Properties props = new Properties();
        // 设置数据库连接池的属性
        props.setProperty("driverClassName", driverClassName);
        props.setProperty("url", url);
        props.setProperty("username", username);
        props.setProperty("password", password);
        DataSource datasource = null; // 初始化数据源变量
        try{
            // 通过属性创建数据源
            datasource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return datasource;
    }
}

完全可以用XML来替换:

<?xml version='1.0' encoding='UTF-8' ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
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-4.0.xsd
           http://www.springframework.org/schema/util 
           http://www.springframework.org/schema/util/spring-util.xsd
           http://www.springframework.org/schema/context  
            http://www.springframework.org/schema/context/spring-context-4.0.xsd ">
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/ssm" />
<property name="username" value="root" />
<property name="password" value="xxxxx" />
</bean>
</beans>

假设有这个xml,xml定义了一个bean,其内容是数据库连接配置,就可以将这个xml引入到注解的体系中,如下代码所示:

package com.ssm.config;

/**
 * ApplicationConfig类用于配置Spring框架的组件扫描。
 * 通过@ComponentScan注解来告诉Spring容器去扫描指定包下的所有组件。
 */
import org.springframework.context.annotation.ComponentScan;
import com.ssm.pojo.RoleIV; // 引入RoleIV类,用作示例
import com.ssm.service.iml.RoleIVServiceImpl; // 引入RoleIVServiceImpl类,用作示例
import org.springframework.context.annotation.ImportResource;

@ComponentScan(basePackageClasses = {RoleIV.class, RoleIVServiceImpl.class})
// 通过指定具体的类,来让Spring容器扫描包含这些类的包及其子包下的所有组件。
@ComponentScan(basePackages = {"com.ssm.pojo", "com.ssm.service"})
// 通过指定包的名称,来让Spring容器扫描这些包及其子包下的所有组件。
@ComponentScan(basePackages = {"com.ssm.pojo"}, basePackageClasses = {RoleIVServiceImpl.class})
// 组合使用basePackages和basePackageClasses,让Spring容器扫描指定包及其子包下以及指定类所在包的组件。
/**
 * 导入资源文件到Spring上下文中。
 * 该注解用于指定Spring配置文件的路径,以便Spring容器在启动时加载该配置文件。
 * 具体路径为classpath下的'spring-data.xml'文件。
 * 该注解通常用于配置与数据访问相关的bean,例如Hibernate的sessionFactory或JPA的entityManagerFactory。
 */
@ImportResource({"classpath:spring-data.xml"})
public class ApplicationConfig {
}
// ApplicationConfig类结束

@ImportResource 中配置的内容是个数组,它可以配置多个XML配置文件,这样就可以引入多个XML定义的Bean了。

先定义一个接口,如下所示:

/**
 * RoleDataSourceService接口定义了角色数据源服务的相关操作。
 * 该接口主要负责通过ID获取角色信息的具体实现。
 */
package com.ssm.service;

import com.ssm.pojo.RoleIV;

public interface RoleDataSourceService {
    /**
     * 根据指定ID获取角色信息。
     *
     * @param id 角色的唯一标识符,类型为long。
     * @return 返回对应ID的角色信息,类型为RoleIV。
     */
    public RoleIV getRoleIV(Long id);
}

其实现类代码如下:

package com.ssm.service.iml;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import com.ssm.pojo.RoleIV;
import com.ssm.service.RoleDataSourceService;

/**
 * 实现RoleDataSourceService接口,提供获取角色信息的功能
 */
@Service
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class RoleDataSourceServiceImpl implements RoleDataSourceService {
    // 通过自动装配获取数据源
    @Autowired
    DataSource dataSource = null;

    /**
     * 根据角色ID获取角色信息
     * @param id 角色的ID
     * @return 返回对应的角色信息,如果没有找到则返回null
     */
    @Override
    public RoleIV getRoleIV(Long id) {
        Connection conn = null;
        ResultSet rs = null;
        PreparedStatement ps = null;
        RoleIV roleIV = null;
        try {
            // 获取数据库连接
            conn = dataSource.getConnection();
            // 构造查询SQL语句
            String sql = "select id, role_name, note from t_role where id = ?";
            ps = conn.prepareStatement(sql);
            // 设置查询参数
            ps.setLong(1, id);
            // 执行查询
            rs = ps.executeQuery();
            // 处理查询结果
            while (rs.next()) {
                // 构建角色对象
                roleIV = new RoleIV();
                roleIV.setId(rs.getLong("id"));
                roleIV.setRoleName(rs.getString("role_name"));
                roleIV.setNote(rs.getString("note"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 关闭数据库连接及相关资源
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        return roleIV;
    }
}

通过这样的形式就把XML配置的 dataSource 注入 RoleIVDataSourceServiceImpl 了。而有的时候所有的配置都放在一个 ApplicationConfig 类里会造成配置过多且复杂,因此开发者可能希望有多个类似于 ApplicationConfig 的配置类,例如 ApplicationConfig2ApplicationConfig3 等。Spring也提供了这个机制,使用 @Import 的方式注入这些配置类:

package com.ssm.config;

/**
 * ApplicationConfig类用于配置Spring框架的组件扫描。
 * 通过@ComponentScan注解来告诉Spring容器去扫描指定包下的所有组件。
 */
import com.ssm.pojo.PojoConfig;
import org.springframework.context.annotation.ComponentScan;
import com.ssm.pojo.RoleIV; // 引入RoleIV类,用作示例
import com.ssm.service.iml.RoleIVServiceImpl; // 引入RoleIVServiceImpl类,用作示例
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;

@ComponentScan(basePackageClasses = {RoleIV.class, RoleIVServiceImpl.class})
// 通过指定具体的类,来让Spring容器扫描包含这些类的包及其子包下的所有组件。
@ComponentScan(basePackages = {"com.ssm.pojo", "com.ssm.service"})
// 通过指定包的名称,来让Spring容器扫描这些包及其子包下的所有组件。
@ComponentScan(basePackages = {"com.ssm.pojo"}, basePackageClasses = {RoleIVServiceImpl.class})
// 组合使用basePackages和basePackageClasses,让Spring容器扫描指定包及其子包下以及指定类所在包的组件。
/**
 * 导入资源文件到Spring上下文中。
 * 该注解用于指定Spring配置文件的路径,以便Spring容器在启动时加载该配置文件。
 * 具体路径为classpath下的'spring-data.xml'文件。
 * 该注解通常用于配置与数据访问相关的bean,例如Hibernate的sessionFactory或JPA的entityManagerFactory。
 */
@ImportResource({"classpath:spring-data.xml"})
/**
 * 使用@Import注解引入配置类
 * 该注解用于指定应用在启动时需要加载的配置类。在这里,我们引入了两个配置类:
 * 1. DataSourceConfig.class:用于数据源的配置,例如数据库连接池的配置。
 * 2. PojoConfig.class:用于POJO(Plain Old Java Object)的配置,例如实体类的映射配置。
 * 这两个配置类会被Spring上下文加载,以便应用在运行时可以使用其中定义的配置。
 */
@Import({DataSourceConfig.class, PojoConfig.class})
public class ApplicationConfig {
}
// ApplicationConfig类结束

在XML中也已使用这个机制,如下代码所示,在 spring-bean.xml 中引入 spring-datasource.xml,就可以在 spring-bean.xml 中使用 import 元素来加载,如代码所示 <import resource="spring-datasource.xml" />;Spring不支持使用XML加载Java配置类,但Spring支持通过XML的配置扫描注解的包,如代码所示 <context:component-scan base-package="com.ssm" />

使用Profile

实际开发中,都存在多个环境,这样就有了在不同的系统中进行切换的需求,Spring也支持这样的场景。在Spring中我们可以定义Bean的Profile。

使用注解 @Profile 配置

package com.ssm.bean;

import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

/**
 * ProfileDataSource类用于根据不同的环境配置数据源。
 * 通过@Profile注解,实现了根据Spring环境(dev或test)动态配置数据源。
 */
@Component
public class ProfileDataSource {
    /**
     * 当环境为dev时,创建并返回一个数据源。
     *
     * @return DataSource 返回一个数据源实例。
     */
    @Bean(name = "devDataSource")
    @Profile("dev")
    public DataSource getDevDataSource() {
        System.out.println("dev datasource");
        // 配置数据库连接属性
        Properties props = new Properties();
        props.setProperty("driver", "com.mysql.jdbc.Driver");
        props.setProperty("url", "jdbc:mysql://localhost:3306/ssm");
        props.setProperty("username", "root");
        props.setProperty("password", "123456");
        DataSource dataSource = null;
        // 尝试根据配置创建数据源
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

    /**
     * 当环境为test时,创建并返回一个数据源。
     *
     * @return DataSource 返回一个数据源实例。
     */
    @Bean(name = "testDataSource")
    @Profile("test")
    public DataSource getTestDataSource() {
        System.out.println("test datasource");
        // 配置数据库连接属性,此处与dev环境配置相同,实际应用中可能不同
        Properties props = new Properties();
        props.setProperty("driver", "com.mysql.jdbc.Driver");
        props.setProperty("url", "jdbc:mysql://localhost:3306/ssm");
        props.setProperty("username", "root");
        props.setProperty("password", "123456");
        DataSource dataSource = null;
        // 尝试根据配置创建数据源
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }
}

如代码所示,使用注解 @Profile 配置了两个环境的数据库连接池。

使用XML定义Profile

<?xml version='1.0' encoding='UTF-8' ?>
<!-- 定义Spring配置文件,指定配置文件的版本和命名空间 -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd"
       profile="dev">
    <!-- 定义数据源bean,使用Apache Commons DBCP2提供的BasicDataSource -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
        <!-- 设置数据库驱动类名 -->
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <!-- 设置数据库连接URL -->
        <property name="url" value="jdbc:mysql://localhost:3306/ssm" />
        <!-- 设置数据库用户名 -->
        <property name="username" value="root" />
        <!-- 设置数据库密码 -->
        <property name="password" value="123456" />
    </bean>
</beans>

如上代码配置了一个Profile为dev的数据源。也可以配置多个Profile:

<?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:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
    <!-- 定义在测试环境下的数据源配置 -->
    <beans profile="test">
        <bean id="devDataSource" class="org.apache.commons.dbcp2.BasicDataSource">
            <!-- 数据库驱动类名 -->
            <property name="driverClassName" value="com.mysql.jdbc.Driver" />
            <!-- 数据库连接URL -->
            <property name="url" value="jdbc:mysql://localhost:3306/ssm" />
            <!-- 数据库用户名 -->
            <property name="username" value="root" />
            <!-- 数据库密码 -->
            <property name="password" value="123456" />
        </bean>
    </beans>
    <!-- 定义在开发环境下的数据源配置,配置内容与测试环境相同 -->
    <beans profile="dev">
        <bean id="devDataSource" class="org.apache.commons.dbcp2.BasicDataSource">
            <property name="driverClassName" value="com.mysql.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/ssm" />
            <property name="username" value="root" />
            <property name="password" value="123456" />
        </bean>
    </beans>
</beans>

如果使用的IDE是Idea,会弹出通知,点开后便可以选择配置。

IDE中的Spring Profiles配置弹窗,可选择激活 dev 或 test 环境

启动Profile

当启动Java或者XML配置的Profile时,这些Bean并不会被加载到Spring IoC容器中,需要自行激活Profile。常见激活Profile的方法有5种:

  • 在使用SpringMVC的情况下,可以配置Web上下文参数或者配置DispatchServlet参数
  • 作为JNDI条目
  • 配置环境变量
  • 配置JVM启动参数
  • 在集成测试环境中使用注解 @ActiveProfile
package com.ssm;

import javax.sql.DataSource;
import com.ssm.config.ApplicationConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class) // 使用Spring提供的JUnit运行器
@ContextConfiguration(classes= ApplicationConfig.class) // 指定Spring配置类
@ActiveProfiles("test") // 指定使用的Spring Profile为"test"
public class ProfileTest {
    @Autowired
    private DataSource dataSource; // 自动注入数据源

    /**
     * 测试方法,用于验证在“test”环境下数据源是否能被正确注入。
     * 该方法没有参数和返回值。
     */
    @Test
    public void test() {
        System.out.println(dataSource.getClass().getName()); // 输出数据源类的名称
    }
}

以上代码是通过 @ActiveProfiles 注解来指定加载哪个Profile。有些时候程序要在一些服务器上运行,这个时候可以配置Java虚拟机的启动项,例如程序在Tomcat服务器上或者main方法上运行,Java虚拟机的参数如下:

  • spring.profiles.active:如果配置了该参数,那么 spring.profiles.default 配置项将失效。
  • spring.profiles.default:默认的配置,如果没有配置关于Profile的参数,就使用这个默认配置。

在上边这个例子中,配置应该是 JAVA_OPTS="-Dspring.profiles.active=test"

IDE中Maven Runner的JRE及VM Options配置界面

IDE中的Spring Profiles配置弹窗,可选择激活 dev 或 test 环境

都可以配置启动项,也可以根据自己的项目属性自行配置。

Run/Debug Configurations界面中Add VM options选项的高亮图示

如果是使用SpringMVC的Web项目,也可以设置Web环境参数或者DispatcherServlet参数,选择对应的Profile,例如修改web.xml配置Profile:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<!-- 配置Spring IoC配置文件路径 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<!-- 设置Spring环境配置参数 -->
<context-param>
<!-- 参数名称:指定Spring环境的活动配置文件 -->
<param-name>spring.profiles.active</param-name>
<!-- 参数值:定义当前活动的环境配置,此处为'test'环境 -->
<param-value>test</param-value>
</context-param>
<!-- 配置ContextLoaderListener用以初始化Spring IoC容器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 配置DispatcherServlet -->
<servlet>
<!-- 注意:Spring MVC框架会根据servlet-name配置,找到/WEB-INF/dispatcher-servlet.xml作为配置文件载入Web工程中 -->
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 初始化参数配置 -->
<init-param>
<!-- 参数名称,用于指定Spring环境配置的激活 profil -->
<param-name>spring.profiles.active</param-name>
<!-- 参数值,此处设置为"test",表示激活的环境配置为test -->
<param-value>test</param-value>
</init-param>
<!-- 使得Dispatcher在服务器启动的时候就初始化 -->
<load-on-startup>2</load-on-startup>
</servlet>
<!-- Servlet拦截配置 -->
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

注意顺序,顺序反了要飘红的。

加载属性文件

使用注解方式加载属性文件

同样是在 ApplicationConfig.java 文件中写入如下配置:

/**
 * 使用该注解来向Spring Boot应用添加一个属性源。它会尝试从类路径下的"database-config.properties"文件中加载属性,
 * 并将其命名为"database.properties"。如果文件找不到,则不会报错,因为设置了ignoreResourceNotFound为true。
 * 该属性源的编码格式为UTF-8。
 *
 * 该注解通常用于配置类上,以在Spring应用启动时加载额外的配置属性。
 */
@PropertySource(
        value = "classpath:database-config.properties",
        name = "database.properties",
        ignoreResourceNotFound = true,
        encoding = "UTF-8"
)

这样还不够,Spring还无法将属性读入,因为缺少属性文件的解析配置器,即 PropertySourcesPlaceHolderConfigurer,需在继续添加如下代码:

package com.ssm.config;

/**
 * ApplicationConfig类用于配置Spring框架的组件扫描。
 * 通过@ComponentScan注解来告诉Spring容器去扫描指定包下的所有组件。
 */
import com.ssm.condition.DataSourceCondition;
import com.ssm.pojo.PojoConfig;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import com.ssm.pojo.RoleIV; // 引入RoleIV类,用作示例
import com.ssm.service.iml.RoleIVServiceImpl; // 引入RoleIVServiceImpl类,用作示例
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import javax.sql.DataSource;
import java.util.Properties;

@ComponentScan(basePackageClasses = {RoleIV.class, RoleIVServiceImpl.class})
// 通过指定具体的类,来让Spring容器扫描包含这些类的包及其子包下的所有组件。
@ComponentScan(basePackages = {"com.ssm.pojo", "com.ssm.service"})
// 通过指定包的名称,来让Spring容器扫描这些包及其子包下的所有组件。
@ComponentScan(basePackages = {"com.ssm.pojo"}, basePackageClasses = {RoleIVServiceImpl.class})
// 组合使用basePackages和basePackageClasses,让Spring容器扫描指定包及其子包下以及指定类所在包的组件。
/**
 * 导入资源文件到Spring上下文中。
 * 该注解用于指定Spring配置文件的路径,以便Spring容器在启动时加载该配置文件。
 * 具体路径为classpath下的'spring-data.xml'文件。
 * 该注解通常用于配置与数据访问相关的bean,例如Hibernate的sessionFactory或JPA的entityManagerFactory。
 */
@ImportResource({"classpath:spring-data.xml"})
/**
 * 使用@Import注解引入配置类
 * 该注解用于指定应用在启动时需要加载的配置类。在这里,我们引入了两个配置类:
 * 1. DataSourceConfig.class:用于数据源的配置,例如数据库连接池的配置。
 * 2. PojoConfig.class:用于POJO(Plain Old Java Object)的配置,例如实体类的映射配置。
 * 这两个配置类会被Spring上下文加载,以便应用在运行时可以使用其中定义的配置。
 */
@Import({DataSourceConfig.class, PojoConfig.class})
/**
 * 使用该注解来向Spring Boot应用添加一个属性源。它会尝试从类路径下的"database-config.properties"文件中加载属性,
 * 并将其命名为"database.properties"。如果文件找不到,则不会报错,因为设置了ignoreResourceNotFound为true。
 * 该属性源的编码格式为UTF-8。
 *
 * 该注解通常用于配置类上,以在Spring应用启动时加载额外的配置属性。
 */
@PropertySource(
        value = "classpath:database-config.properties",
        name = "database.properties",
        ignoreResourceNotFound = true,
        encoding = "UTF-8"
)
public class ApplicationConfig {
    /**
     * 创建并返回一个PropertySourcesPlaceholderConfigurer的实例。
     * 这个配置器是用来处理属性文件占位符的,它可以在Spring配置文件中启用属性文件的引用。
     * 通过这个方法,可以在Spring Bean的配置中使用占位符来引用属性文件中的属性值。
     *
     * @return PropertySourcesPlaceholderConfigurer 返回一个配置了属性文件解析器的实例。
     */
    @Bean
    public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    /**
     * 创建并配置数据源 bean。
     *
     * @param driver JDBC驱动程序的类名。
     * @param url 数据库连接的URL。
     * @param username 连接数据库所需的用户名。
     * @param password 连接数据库所需的密码。
     * @return 配置好的数据源实例。
     */
    @Bean(name = "dataSource")
    public DataSource getDataSource(
            // 从属性文件中注入的JDBC配置
            @Value("${jdbc.database.driver}") String driver,
            @Value("${jdbc.database.url}") String url,
            @Value("${jdbc.database.username}") String username,
            @Value("${jdbc.database.password}") String password) {
        // 设置数据库连接的属性
        Properties props = new Properties();
        props.setProperty("driver", driver);
        props.setProperty("url", url);
        props.setProperty("username", username);
        props.setProperty("password", password);
        DataSource dataSource = null;
        // 尝试根据属性创建数据源
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            // 异常处理,打印堆栈跟踪
            e.printStackTrace();
        }
        return dataSource;
    }
}

测试配置代码如下:

  /**
     * 测试应用程序配置属性的方法。
     * 该方法通过IoC容器获取应用程序配置中的数据库连接信息,并打印出来。
     *
     * @throws SQLException 如果获取数据库连接或元数据时发生错误
     */
    public static void testProperties() throws SQLException {
        // 创建IoC容器,使用注解配置
        ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        // 获取应用程序的环境配置
        Environment env = context.getEnvironment();
        // 从环境配置中获取jdbc数据库url
        String url = env.getProperty("jdbc.database.url");
        // 从IoC容器中获取DataSource Bean
        DataSource ds = context.getBean(DataSource.class);
        // 打印数据库连接的URL
        System.out.println(ds.getConnection().getMetaData().getURL());
    }

在ApplicationConfig.java文件中写了如下方法,并加上了@Bean注解:

    /**
     * 创建并返回一个PropertySourcesPlaceholderConfigurer的实例。
     * 这个配置器是用来处理属性文件占位符的,它可以在Spring配置文件中启用属性文件的引用。
     * 通过这个方法,可以在Spring Bean的配置中使用占位符来引用属性文件中的属性值。
     *
     * @return PropertySourcesPlaceholderConfigurer 返回一个配置了属性文件解析器的实例。
     */
    @Bean
    public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

其作用是让Spring能够解析属性占位符,能够解析属性占位符,Spring提供了 @Value 注解和占位符的形式,来使用占位符。

先看一下属性文件 database-config.properties 情况:

jdbc.database.driver=com.mysql.cj.jdbc.Driver
jdbc.database.url=jdbc:mysql://localhost:3306/ssm
jdbc.database.username=root
jdbc.database.password=Ms123!@#
package com.ssm.config;

import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

/**
 * 数据源配置类,用于配置数据库连接池。
 */
@Component
public class DataSourceConfigII {
    // 通过注解方式注入MySQL驱动类名
    @Value("${jdbc.database.driver}")
    private String driverClassName = null;
    // 通过注解方式注入数据库URL
    @Value("${jdbc.database.url}")
    private String url = null;
    // 通过注解方式注入数据库用户名
    @Value("${jdbc.database.username}")
    private String username = null;
    // 通过注解方式注入数据库密码
    @Value("${jdbc.database.password}")
    private String password = null;

    /**
     * 创建并返回DataSource Bean。
     *
     * @return 返回配置好的DataSource实例。
     */
    @Bean("dataSource")
    public DataSource getDataSource(){
        Properties props = new Properties();
        // 设置数据库连接池的属性
        props.setProperty("driverClassName", driverClassName);
        props.setProperty("url", url);
        props.setProperty("username", username);
        props.setProperty("password", password);
        DataSource datasource = null; // 初始化数据源变量
        try{
            // 通过属性创建数据源
            datasource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return datasource;
    }
}

使用XML加载属性文件

用XML的方式也可以加载属性文件,只需要使用 <context:property-placeholder> 配置项,如下代码所示:

<?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:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    <!-- 配置属性占位符,指定属性文件位置并设置是否忽略未找到的资源 -->
    <context:property-placeholder
            ignore-resource-not-found="false"
            location="classpath:database-config.properties" />
    <!-- 数据源配置,使用Apache Commons DBCP2提供的BasicDataSource实现 -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
        <!-- 通过属性文件中的配置动态设置数据源的属性 -->
        <property name="driverClassName" value="${jdbc.database.driver}" />
        <property name="url" value="${jdbc.database.url}" />
        <property name="username" value="${jdbc.database.username}" />
        <property name="password" value="${jdbc.database.password}" />
    </bean>
</beans>

特别要注意 ignore-resource-not-found="false", 表示是否允许文件不存在,为 false 时不允许文件不存在,如果不存在Spring会抛出异常。location="classpath:database-config.properties"location 是文件路径,可以配置单个或多个,用逗号隔开即可。

也可以使用如下方式,避免配置多个属性文件的时候过长,可读性差:

<?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:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    <!-- 配置属性占位符,指定属性文件位置并设置是否忽略未找到的资源 -->
    <!--
    <context:property-placeholder
            ignore-resource-not-found="false"
            location="classpath:database-config.properties" />
           -->
    <!-- 数据源配置,使用Apache Commons DBCP2提供的BasicDataSource实现 -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
        <!-- 通过属性文件中的配置动态设置数据源的属性 -->
        <property name="driverClassName" value="${jdbc.database.driver}" />
        <property name="url" value="${jdbc.database.url}" />
        <property name="username" value="${jdbc.database.username}" />
        <property name="password" value="${jdbc.database.password}" />
    </bean>
    <!-- 定义一个bean来配置属性源占位符解析器 -->
    <bean id="propertySourcesPlaceholderConfigurer"
          class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
        <!-- 配置属性文件的位置 -->
        <property name="locations">
            <array>
                <!-- 指定数据库配置文件的位置 -->
                <value>classpath:database-config.properties</value>
                <!-- 指定日志配置文件的位置 -->
                <value>log4j.properties</value>
            </array>
        </property>
        <!-- 配置是否忽略资源未找到的错误 -->
        <property name="ignoreResourceNotFound" value="true" />
    </bean>
</beans>

条件化装配Bean

在某些条件下,不需要装配某些Bean,比如当没有 database-config.properties 属性配置时,就不需要创建数据源,需要有个条件判断。Spring提供了注解 @Conditional 去配置,通过它可以配置一个或者多个类,只需要这些类实现 Condition 接口即可( org.springframework.context.annotation.Condition ),代码如下:

package com.ssm.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.stereotype.Component;

/**
 * 数据源条件类,用于根据环境配置决定是否加载特定的Bean。
 * 实现了Spring的Condition接口,重写了matches方法来判断条件是否满足。
 */
@Component
public class DataSourceCondition implements Condition {
    /**
     * 判断条件是否满足。
     * @param context 条件上下文,提供了环境和类型元数据的信息。
     * @param metadata 注解类型元数据,用于获取类上的注解信息。
     * @return boolean 返回true表示条件满足,false表示条件不满足。
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        // 检查环境配置中是否包含了必要的jdbc属性
        return env.containsProperty("jdbc.database.driver")
                && env.containsProperty("jdbc.database.url")
                && env.containsProperty("jdbc.database.username")
                && env.containsProperty("jdbc.database.password");
    }
}

DataSourceCondition 实现了 Condition 接口的 matches 方法,该方法有两个参数,一个是 ConditionContext,它可以获得Spring的运行环境,另一个是 AnnotatedTypeMetadata,它可以获取关于该Bean的注解信息。这段代码优先获取了运行上下文的环境,然后判断在环境中属性文件是否配置了数据库的相关参数,如果配置了返回 true,Spring创建对应的Bean否则不创建。接着就可以配置数据源了:

package com.ssm.config;

/**
 * ApplicationConfig类用于配置Spring框架的组件扫描。
 * 通过@ComponentScan注解来告诉Spring容器去扫描指定包下的所有组件。
 */
import com.ssm.condition.DataSourceCondition;
import com.ssm.pojo.PojoConfig;
import org.apache.commons.dbcp2.BasicDataSourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import com.ssm.pojo.RoleIV; // 引入RoleIV类,用作示例
import com.ssm.service.iml.RoleIVServiceImpl; // 引入RoleIVServiceImpl类,用作示例
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import javax.sql.DataSource;
import java.util.Properties;

@ComponentScan(basePackageClasses = {RoleIV.class, RoleIVServiceImpl.class})
// 通过指定具体的类,来让Spring容器扫描包含这些类的包及其子包下的所有组件。
@ComponentScan(basePackages = {"com.ssm.pojo", "com.ssm.service"})
// 通过指定包的名称,来让Spring容器扫描这些包及其子包下的所有组件。
@ComponentScan(basePackages = {"com.ssm.pojo"}, basePackageClasses = {RoleIVServiceImpl.class})
// 组合使用basePackages和basePackageClasses,让Spring容器扫描指定包及其子包下以及指定类所在包的组件。
/**
 * 导入资源文件到Spring上下文中。
 * 该注解用于指定Spring配置文件的路径,以便Spring容器在启动时加载该配置文件。
 * 具体路径为classpath下的'spring-data.xml'文件。
 * 该注解通常用于配置与数据访问相关的bean,例如Hibernate的sessionFactory或JPA的entityManagerFactory。
 */
@ImportResource({"classpath:spring-data.xml"})
/**
 * 使用@Import注解引入配置类
 * 该注解用于指定应用在启动时需要加载的配置类。在这里,我们引入了两个配置类:
 * 1. DataSourceConfig.class:用于数据源的配置,例如数据库连接池的配置。
 * 2. PojoConfig.class:用于POJO(Plain Old Java Object)的配置,例如实体类的映射配置。
 * 这两个配置类会被Spring上下文加载,以便应用在运行时可以使用其中定义的配置。
 */
@Import({DataSourceConfig.class, PojoConfig.class})
/**
 * 使用该注解来向Spring Boot应用添加一个属性源。它会尝试从类路径下的"database-config.properties"文件中加载属性,
 * 并将其命名为"database.properties"。如果文件找不到,则不会报错,因为设置了ignoreResourceNotFound为true。
 * 该属性源的编码格式为UTF-8。
 *
 * 该注解通常用于配置类上,以在Spring应用启动时加载额外的配置属性。
 */
@PropertySource(
        value = "classpath:database-config.properties",
        name = "database.properties",
        ignoreResourceNotFound = true,
        encoding = "UTF-8"
)
public class ApplicationConfig {
    /**
     * 创建并返回一个PropertySourcesPlaceholderConfigurer的实例。
     * 这个配置器是用来处理属性文件占位符的,它可以在Spring配置文件中启用属性文件的引用。
     * 通过这个方法,可以在Spring Bean的配置中使用占位符来引用属性文件中的属性值。
     *
     * @return PropertySourcesPlaceholderConfigurer 返回一个配置了属性文件解析器的实例。
     */
    @Bean
    public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    /**
     * 创建并配置数据源 bean。
     *
     * @param driver JDBC驱动程序的类名。
     * @param url 数据库连接的URL。
     * @param username 连接数据库所需的用户名。
     * @param password 连接数据库所需的密码。
     * @return 配置好的数据源实例。
     */
    @Bean(name = "dataSource")
    // 根据特定条件决定是否创建该bean
    @Conditional({DataSourceCondition.class})
    public DataSource getDataSource(@Value("${jdbc.database.driver}") String driver, @Value("${jdbc.database.url}") String url, @Value("${jdbc.database.username}") String username, @Value("${jdbc.database.password}") String password) {
        // 设置数据库连接的属性
        Properties props = new Properties();
        props.setProperty("driver", driver);
        props.setProperty("url", url);
        props.setProperty("username", username);
        props.setProperty("password", password);
        DataSource dataSource = null;
        // 尝试根据属性创建数据源
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            // 异常处理,打印堆栈跟踪
            e.printStackTrace();
        }
        return dataSource;
    }
}

Bean的作用域

所谓Bean的作用域是指Bean在应用中的有效范围。默认情况下,Spring IoC容器只会对Bean创建唯一实例,然后在Spring IoC容器的生命周期中有效。用如下代码测试一下:

   /**
     * 测试作用域
     * 该方法通过创建一个注解配置的应用上下文,并从该上下文中获取RoleDataSourceService类型的bean实例,
     * 以此来验证Spring容器中bean的作用域特性。
     * 该方法不接受参数且没有返回值。
     */
    public static void testScope() {
        // 创建一个注解配置的应用上下文,并指定配置类ApplicationConfig
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        // 从上下文中获取RoleDataSourceService类型的bean实例
        RoleDataSourceService service1 = context.getBean(RoleDataSourceService.class);
        RoleDataSourceService service2 = context.getBean(RoleDataSourceService.class);
        // 打印两个bean实例是否为同一个对象的引用
        System.out.println(service1 == service2);
        // 关闭应用上下文
        context.close();
    }

这里Spring IoC容器通过类型的方式获取Bean,然后通过 == 比较两次获取的Bean的结果,这是一个位比较,比较 service1service2 是否为同一个对象。很显然结果会返回 true,换句话说在默认情况下,Spring IoC容器只会为配置的Bean生成一个实例,而不是多个。

而在互联网对性能有基本要求的场景下,有时候我们希望每请求一次就产生一个独立的对象,这样多个实例可以在不同的线程运行,在对性能有要求的场景中就能发挥作用。这些是由Spring的作用域决定的。Spring IoC容器提供了2种作用域(单例和原型),单例(singleton)是默认选项,在整个应用中,Spring只为其生成一个Bean的实例;原型(prototype) 当每次从Spring IoC容器获取Bean时,Spring都会为它创建一个新的实例。

在Spring中,可以用注解 @Scope 指定作用域,如下代码所示:

package com.ssm.service.iml;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import com.ssm.pojo.RoleIV;
import com.ssm.service.RoleDataSourceService;

/**
 * 实现RoleDataSourceService接口,提供获取角色信息的功能
 */
@Service
/**
 * RoleDataSourceServiceImpl 类实现了 RoleDataSourceService 接口,
 * 用于提供角色相关的数据源服务。该类的作用范围被注解为原型作用域(SCOPE_PROTOTYPE),
 * 意味着每次请求都会创建一个新的实例。
 */
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class RoleDataSourceServiceImpl implements RoleDataSourceService {
    // 通过自动装配获取数据源
    @Autowired
    DataSource dataSource = null;

    /**
     * 根据角色ID获取角色信息
     * @param id 角色的ID
     * @return 返回对应的角色信息,如果没有找到则返回null
     */
    @Override
    public RoleIV getRoleIV(Long id) {
        Connection conn = null;
        ResultSet rs = null;
        PreparedStatement ps = null;
        RoleIV roleIV = null;
        try {
            // 获取数据库连接
            conn = dataSource.getConnection();
            // 构造查询SQL语句
            String sql = "select id, role_name, note from t_role where id = ?";
            ps = conn.prepareStatement(sql);
            // 设置查询参数
            ps.setLong(1, id);
            // 执行查询
            rs = ps.executeQuery();
            // 处理查询结果
            while (rs.next()) {
                // 构建角色对象
                roleIV = new RoleIV();
                roleIV.setId(rs.getLong("id"));
                roleIV.setRoleName(rs.getString("role_name"));
                roleIV.setNote(rs.getString("note"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 关闭数据库连接及相关资源
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        return roleIV;
    }
}

然后再执行测试代码:

  /**
     * 测试作用域
     * 该方法通过创建一个注解配置的应用上下文,并从该上下文中获取RoleDataSourceService类型的bean实例,
     * 以此来验证Spring容器中bean的作用域特性。
     * 该方法不接受参数且没有返回值。
     */
    public static void testScope() {
        // 创建一个注解配置的应用上下文,并指定配置类ApplicationConfig
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        // 从上下文中获取RoleDataSourceService类型的bean实例
        RoleDataSourceService service1 = context.getBean(RoleDataSourceService.class);
        RoleDataSourceService service2 = context.getBean(RoleDataSourceService.class);
        // 打印两个bean实例是否为同一个对象的引用
        System.out.println(service1 == service2);
        // 关闭应用上下文
        context.close();
    }

就会返回 false 了,也就是当我们从Spring IoC容器中获取对象时,获取的都是新的实例,不同的对象。

在互联网应用中,Spring会使用实现了 WebApplicationContext 接口(该接口继承了ApplicationContext)的实现类作为IoC容器,此容器有两种常见的作用域(会话和请求):会话(Session) 在Web应用中使用,在会话过程中只创建一个实例;请求(request) 在Web应用中使用,在一次请求中Spring会创建一个实例。该配置可以在Java代码中进行,也可以在XML配置文件中进行,如下所示:

import org.springframework.web.context.WebApplicationContext;

/**
 * 实现RoleDataSourceService接口,提供获取角色信息的功能
 */
@Service
/**
 * 为Spring Web应用程序定义会话级别的作用域。
 * 使用此注解的bean将被存储在HTTP会话中,意味着它们在同一个会话内的所有HTTP请求中都是可用的。
 * 这是针对需要在多个相关请求之间共享状态的情况而设计的,例如用户的登录状态。
 *
 * 注意:此注解仅适用于Web应用程序上下文。
 */
@Scope(WebApplicationContext.SCOPE_SESSION)
public class RoleDataSourceServiceImpl implements RoleDataSourceService {
    // 通过自动装配获取数据源
    @Autowired
    DataSource dataSource = null;
<!-- 定义一个RoleII类型的bean实例,id为2,角色名为"高级工程师",备注为"重要人员" -->
    <bean id="role2" class="com.ssm.pojo.RoleII" scope="prototype">
        <!-- 设置角色id为2 -->
        <property name="id" value="2" />
        <!-- 设置角色名为"高级工程师" -->
        <property name="roleName" value="高级工程师" />
        <!-- 设置备注为"重要人员" -->
        <property name="note" value="重要人员" />
    </bean>

使用Spring表达式

Spring还提供了更灵活的方式,那就是Spring表达式(Spring EL), 它远比其他注入方式更强大,其大致内容如下:

Spring Expression Language (Spring EL) 是Spring框架中的一种表达式语言,它主要用于在运行时查询和操作对象图。Spring EL设计的目标是简化数据绑定和表达式评估,特别是在Spring应用程序上下文中。以下是一些关于Spring EL的关键点:

  • 对象导航:Spring EL允许你通过点号 . 来导航对象的属性,例如 person.name 获取 person 对象的 name 属性。
  • 方法调用:支持调用对象的方法,例如 list.sort()date.format('yyyy-MM-dd')
  • 集合操作:可以索引和遍历集合,如 list[0]list[0..2] 选取前三个元素。
  • 条件和逻辑运算:支持 ifandornot 等逻辑运算符,以及比较运算符(==>< 等)。
  • 算术运算:支持基本的数学运算,如加减乘除 (+, -, *, /) 和取余数 (%)。
  • 类型转换:可以显式地转换类型,例如 (int)number
  • 变量和参数:在表达式中可以引用变量和方法参数。
  • 上下文访问:可以访问Spring容器中的bean,例如 @myBean 引用名为 myBean 的bean。
  • 表达式结果:表达式的结果可以是任何Java类型,包括null。
  • spel:expression:在XML配置中,使用 #{} 语法来包含Spring EL表达式,例如 <property name="someProperty" value="#{myBean.someMethod()}" />

Spring EL相关的类

Spring EL 表达式解析器相关类的UML继承关系图

 /**
     * 测试表达式解析功能。
     * 该方法演示了如何使用Spring表达式语言(SpEL)解析一个简单的字符串表达式并获取其值。
     *
     */
    public static void testExpression(){
        // 创建SpEL表达式解析器
        ExpressionParser parser = new SpelExpressionParser();
        // 解析一个字符串表达式,并输出其值
        Expression expression = parser.parseExpression("'Hello World'");
        String str = (String) expression.getValue();
        System.out.println(str);
        // 解析并执行字符串的charAt方法,输出第一个字符
        expression = parser.parseExpression("'Hello World'.charAt(0)");
        char ch = (char) expression.getValue();
        System.out.println(ch);
        // 解析字符串并获取其字节表示
        expression = parser.parseExpression("'Hello World'.bytes");
        byte[] bytes = (byte[]) expression.getValue();
        System.out.println(bytes);
        // 获取字符串的字节长度
        expression = parser.parseExpression("'Hello World'.bytes.length");
        int length = (int) expression.getValue();
        System.out.println(length);
        // 使用SpEL构造一个新的字符串对象
        expression = parser.parseExpression("new String('abc')");
        String str2 = (String) expression.getValue();
        System.out.println(str2);
    }

如代码所示,是一个简单的使用Spring EL的例子, 通过表达式可以创建对象、调用对象的方法或者获取属性。

Spring EL还支持变量的解析,使用变量解析时常常用到一个接口 EvaluationContext, 可以有效解析表达式中的变量。它有一个实现类 StandardEvaluationContext,用变量解析表达式会使表达式更加灵活。在针对Spring IoC容器进行解析的时候,可以直接获取配置的属性值,但使用这些表达式会降低可读性,不适合处理复杂的问题。

    /**
     * 测试SpEL表达式的求值功能。
     * 该方法首先演示了如何使用SpEL获取和设置对象属性的值,接着展示了如何对列表元素进行操作。
     */
    public static void testEvaluation() {
        // 创建SpelExpressionParser实例以解析表达式
        ExpressionParser parser = new SpelExpressionParser();
        // 创建RoleIV实例,用于在表达式求值时提供属性值
        RoleIV roleIV = new RoleIV(1L, "admin", "administrator");
        // 解析表达式以获取"note"属性的值
        Expression expression = parser.parseExpression("note");
        String note = (String) expression.getValue(roleIV);
        System.out.println(note);
        // 创建表达式求值的上下文环境,并将roleIV设置为上下文中的根对象
        EvaluationContext ctx = new StandardEvaluationContext(roleIV);
        // 设置"note"属性的新值为"new_administrator"
        parser.parseExpression("note").setValue(ctx, "new_administrator");
        // 获取更新后的"note"属性值
        note = parser.parseExpression("note").getValue(ctx, String.class);
        System.out.println(note);
        // 获取RoleIV实例的"getRoleName()"方法返回值
        String roleName = parser.parseExpression("getRoleName()").getValue(ctx, String.class);
        System.out.println(roleName);
        // 初始化并创建一个字符串列表,用于演示如何在SpEL中操作集合
        List<String> list = new ArrayList<String>();
        list.add("value1");
        list.add("value2");
        // 将列表绑定到上下文中,以在表达式中使用
        ctx.setVariable("list", list);
        // 更新列表的第一个元素的值为"new_value1"
        parser.parseExpression("#list[0]").setValue(ctx, "new_value1");
        // 获取更新后的列表的第一个元素的值
        System.out.println(parser.parseExpression("#list[0]").getValue(ctx));
    }

EvaluationContext 使用了它的实现类 StandardEvaluationContext 进行实例化,在构造方法中将角色对象传递给它,它就会基于这个类进行解析。

Bean的属性和方法

使用注解的方式需要用到 @Value,在属性文件中的读取需要 $,在Spring EL中需要 #。在角色类中使用Spring EL初始化,如下代码所示:

package com.ssm.pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * ELRole类用于演示通过Spring的注解方式注入属性值。
 * 该类被标记为@Component,表示它是一个Spring组件,可以被其他组件依赖。
 * 其中使用的@Value注解用于注入具体的属性值。
 */
@Component("elRole")
public class ELRole {
    // 使用@Value注解注入id的值,此处使用SpEL表达式的方式注入,示例值为1。
    @Value("#{1}")
    private Long id;
    // 使用@Value注解注入roleName的值,此处使用字符串方式注入。
    @Value("#{'role_name_1'}")
    private String roleName;
    // 使用@Value注解注入note的值,此处使用字符串方式注入。
    @Value("#{'note_1'}")
    private String note;

    /**
     * 获取角色ID
     * @return 返回角色的ID值
     */
    public Long getId() {
        return id;
    }

    /**
     * 设置角色ID
     * @param id 角色的ID值
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * 获取角色名称
     * @return 返回角色名称字符串
     */
    public String getRoleName() {
        return roleName;
    }

    /**
     * 设置角色名称
     * @param roleName 角色名称字符串
     */
    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    /**
     * 获取角色备注信息
     * @return 返回角色的备注信息字符串
     */
    public String getNote() {
        return note;
    }

    /**
     * 设置角色备注信息
     * @param note 角色的备注信息字符串
     */
    public void setNote(String note) {
        this.note = note;
    }
}

这样就定义了一个BeanName为 elRole 的角色类,同时给它所有的属性都进行了初始化。然后可以通过另一个Bean引用它的属性或者调用它的方法,如下代码所示:

package com.ssm.pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * ELBean类用于演示和展示Spring Expression Language (EL)的使用。
 * 通过注解@Value从Spring表达式语言中获取值并注入到类的属性中。
 */
@Component("elBean")
public class ELBean {
    // 从Java Math类中获取PI值
    @Value("#{T(java.lang.Math).PI}")
    private double pi;
    // 获取一个随机数
    @Value("#{T(Math).random()}")
    private double random;
    // 根据elRole的id值加1赋值给num
    @Value("#{elRole.id+1}")
    private int num;
    // 将elRole的roleName和note拼接成一个字符串
    @Value("#{elRole.roleName + elRole.note}")
    private String str;
    // 判断elRole的id是否等于1
    @Value("#{elRole.id == 1}")
    private boolean equalNum;
    // 判断elRole的note是否等于'note_1'
    @Value("#{elRole.note eq 'note_1'}")
    private boolean eqaulString;
    // 判断elRole的id是否大于2
    @Value("#{elRole.id > 2}")
    private boolean greater;
    // 判断elRole的id是否小于2
    @Value("#{elRole.id < 2}")
    private boolean less;
    // 根据条件判断选择赋值5或1给max
    @Value("#{elRole.id > 1 ? 5 : 1}")
    private int max;
    // 如果elRole的note存在,则赋值note,否则赋值'hello'
    @Value("#{elRole.note?: 'hello'}")
    private String defaultString;
    // 通过beanName获取ELRole类型的bean并注入
    @Value("#{elRole}")
    private ELRole elRole;
    // 获取ELRole的id属性
    @Value("#{elRole.id}")
    private Long id;
    // 调用ELRole的getNote方法,获取note属性的值
    @Value("#{elRole.getNote().toString()}")
    private String note;

    /**
     * 获取ELRole对象
     * @return 返回ELRole对象
     */
    public ELRole getElRole() {
        return elRole;
    }

    /**
     * 设置ELRole对象
     * @param elRole 要设置的ELRole对象
     */
    public void setElRole(ELRole elRole) {
        this.elRole = elRole;
    }

    /**
     * 获取实体的ID
     * @return 返回实体的ID值
     */
    public Long getId() {
        return id;
    }

    /**
     * 获取圆周率π的值
     * @return 返回圆周率π的近似值
     */
    public double getPi() {
        return pi;
    }

    /**
     * 设置圆周率π的值
     * @param pi 要设置的圆周率π的近似值
     */
    public void setPi(double pi) {
        this.pi = pi;
    }

    /**
     * 获取一个随机数
     * @return 返回一个随机数值
     */
    public double getRandom() {
        return random;
    }

    /**
     * 设置一个随机数
     * @param random 要设置的随机数值
     */
    public void setRandom(double random) {
        this.random = random;
    }

    /**
     * 设置实体的ID
     * @param id 要设置的实体ID
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * 获取备注信息
     * @return 返回备注信息字符串
     */
    public String getNote() {
        return note;
    }

    /**
     * 设置备注信息
     * @param note 要设置的备注信息字符串
     */
    public void setNote(String note) {
        this.note = note;
    }
}

可以通过BeanName注入,也可以通过OGNL获取其属性或者调用其方法注入其他的Bean。注意表达式 "#{elRole.getNote().toString()}" 的注入,getNote() 方法可能返回null,然后导致 toString() 抛异常,可以写成 "#{elRole.getNote()?.toString()}",表达式中的问号事先判断是否非空,如果不是非空则不再调用 toString 方法。

使用类的静态常量和方法

  // 从Java Math类中获取PI值
    @Value("#{T(java.lang.Math).PI}")
    private double pi;
    // 获取一个随机数
    @Value("#{T(Math).random()}")
    private double random;

Mathjava.lang.* 包下的Math类,在Java代码中使用该类不需要import,对于Spring EL也是。也可以像代码所示给全限定名。




上一篇:智谱“降智”秘密复盘:GLM-5大规模推理的Scaling Pain与避坑指南
下一篇:千万级图片代理实战:Go语言搭配Redis与MySQL的高并发方案
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-2 06:12 , Processed in 0.690981 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表