购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第2章
IOC思想与实现

本章主要内容:

IOC思想的演绎推导;

IOC的两种实现方式;

IOC容器的两种核心模型;

基于注解驱动的IOC容器使用;

依赖注入的6种方式。

控制反转(Inverse of Control,IOC)这个概念乍一看不是那么容易理解,尤其是对于刚开始接触和学习Spring Framework的读者。为了能让读者更容易理解IOC的思想,并且能尽可能一次性理解透彻,本章将会从一个场景演绎开始推导IOC思想的实现过程。

2.1 IOC是怎么来的

下面我们来搭建一个基于原生Servlet时代的MVC三层架构工程,并以此作为基础。

小提示:

本书的所有代码均放置在总体工程spring6-boot3-projects-epudit中。

2.1.1 原生Servlet时代的三层架构

1.构建基于Maven的原生Servlet工程

我们采用Maven来完成工程构建,首先使用IDEA创建一个新的Maven模块,groupId声明为com.linkedbear.spring6,artifactId声明为 spring-00-introduction ,创建方式如图2-1所示。

随后在pom.xml文件中引入jakarta.servlet-api的坐标(注意此处引入的版本为6.0,对应的artifactId已不再是javax开头,而是jakarta开头);另外为了保证工程的编译级别为Java 17,还需要引入Maven的编译插件maven-compiler-plugin,并声明source、target、encoding属性;最后,将工程的打包方式改为war包,因为我们搭建的是一个Web工程。

图2-1 创建spring-00-introduction工程

该工程中的pom.xml配置文件内容如代码清单2-1所示。

代码清单2-1 spring-00-introduction工程中的pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project ......>
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>com.linkedbear.spring6</groupId>
    <artifactId>spring-00-introduction</artifactId>
    <version>1.0-RELEASE</version>
 
    <packaging>war</packaging>
 
    <dependencies>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
2.工程部署Servlet容器

工程创建完毕后,不要着急编写代码,而要先把工程部署到Servlet容器中,保证其能正常运行。本节选择使用Tomcat 10作为Servlet容器来运行工程。

小提示:

如无特殊说明和引用,本书所使用的所有Servlet容器均为Tomcat 10。

安装Tomcat 10可参考如下步骤:

(1)从Apache Tomcat的官方网站下载最新版的Tomcat 10,并解压到没有中文名的目录下;

(2)来到IDEA中,依次打开“File→Settings→Build, Execution, Deployment→Application Servers”,并在打开的配置页中单击左上角的+号,选择“Tomcat Server”,随后找到并选择上一步解压的Tomcat路径,即可在IDEA中添加Tomcat,如图2-2所示。

图2-2 将Tomcat添加到IDEA中

将Tomcat配置到IDEA后,接下来继续在IDEA中依次打开“File→Project Structure”,选中Artifacts标签,确认IDEA是否自动生成了图2-3所示的归档,如果没有自动生成,则点击左上角+号,手动添加Web Application:Exploded的输出类型,配置好对应的路径与名称,即可设置好编译打包输出配置。

图2-3 添加Web Application类型的归档

最后,在IDEA中依次打开“Run→Edit Configurations”,并在弹出的对话框中单击最上角的+号,选择“Tomcat Server→Local”,选择上一步添加的Tomcat 10,随后选择Tomcat的“Deployment”选项卡,单击+号添加Artifact,并选择带“exploded”后缀的归档,如图2-4所示。

图2-4 配置部署归档

上述操作完成后,即可完成工程的Servlet容器部署。

3.编写Servlet测试用例

在src/main/java中新建一个DemoServlet1,标注@WebServlet注解,并继承HttpServlet,重写它的doGet方法,如代码清单2-2所示。

代码清单2-2 DemoServlet1
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
 
import java.io.IOException;
 
@WebServlet(urlPatterns = "/demo1")
public class DemoServlet1 extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp throws Exception {
        response.getWriter().println("DemoServlet1 run ......");
    }
}

编写完毕后直接启动Tomcat,IDEA会自动编译工程并部署到Tomcat中。

打开浏览器,在地址栏输入http://localhost:8080/spring_00_introduction_war_exploded/demo1(每位读者搭建的工程名可能不一致,记得修改context-path),如果发现可以正常打印DemoServlet1 run ......,证明工程搭建并配置成功。

4.编写Service层与Dao层

由于在代码清单2-1中只导入了servlet-api的依赖,因此关于数据库访问的部分本节暂不予实现,不进行JDBC相关操作。下面在工程中分别新建servlet、service、dao三个包,并分别创建DemoService和DemoDao接口以及它们的实现类,如图2-5所示,在对应的三层架构中,组件及依赖的模型如图2-6所示。

图2-5 最简单的三层架构代码结构

图2-6 简单的三层架构示意

(1)Dao与DaoImpl

下面快速编写Dao层的代码,先定义一个简单的DemoDao接口,并声明一个findAll方法,模拟从数据库查询一组数据;之后编写它对应的实现类DemoDaoImpl,由于没有引入数据库的相关驱动,因此采用硬编码的临时数据模拟Dao与数据库的交互,如代码清单2-3所示。

代码清单2-3 Dao层代码
public interface DemoDao {
    List<String> findAll();
}
 
public class DemoDaoImpl implements DemoDao {
    
    @Override
    public List<String> findAll() {
        // 此处应该是访问数据库的操作,用硬编码的临时数据代替
        return Arrays.asList("aaa", "bbb", "ccc");
    }
}

至此,Dao层的接口与实现类定义完成。

(2)Service与ServiceImpl

再来编写Service层的代码,编写一个DemoService接口,并声明findAll方法;随后编写对应的实现类DemoServiceImpl,并在内部依赖DemoDao接口,调用DemoDao的findAll方法从数据库中查询数据,如代码清单2-4所示。

代码清单2-4 Service层代码
public interface DemoService {
    List<String> findAll();
}
 
public class DemoServiceImpl implements DemoService {
    
    private DemoDao demoDao = new DemoDaoImpl();
    
    @Override
    public List<String> findAll() {
        return demoDao.findAll();
    }
}

至此,Service层的接口与实现类定义完成。

5.修改DemoServlet

由于要模拟整体的三层架构,因此DemoServlet1要依赖DemoService,并在doGet方法触发时,执行DemoService的findAll方法并输出,如代码清单2-5所示。

代码清单2-5 DemoServlet1触发DemoService的findAll方法
@WebServlet(urlPatterns = "/demo1")
public class DemoServlet1 extends HttpServlet {
    
    DemoService demoService = new DemoServiceImpl();
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        resp.getWriter().println(demoService.findAll().toString());
    }
}
6.重新运行应用并测试效果

编码完毕后,我们将工程重新部署到Tomcat并运行,访问/demo1路径,浏览器中会输出['aaa', 'bbb', 'ccc'] ,说明代码编写正确且运行正常。

以上部分是读者在接触Java Web中最熟悉不过的基础内容。下面将模拟一个需求的变更,推演项目的变化。

2.1.2 需求变更

假设上述的项目由正在阅读的读者来负责,这个项目已经有一定规模,在数据库选型时使用了MySQL。在项目临近交付时,甲方给你打电话,要求将数据库由MySQL切换为Oracle,原因是Oracle的数据承载能力更强。

而挂断电话的你迫于甲方的要求,无奈只能将数据库实现改为Oracle,但是由于Dao层代码已经使用MySQL的语法实现,读者都知道,不同的数据库在SQL语法层面有一些细微的差别(例如分页),因此修改数据库实现时不能只修改数据库驱动和连接池的配置,而要针对不同的SQL语法特性进行重新适配,又由于该项目已经有一定规模,改动量非常大。

1.修改DaoImpl

下面演示项目中某一个DaoImpl的改动,我们使用返回不同数据的方式模拟SQL语句的修改,如代码清单2-6所示。

代码清单2-6 模拟DaoImpl的SQL语句修改
public class DemoDaoImpl implements DemoDao {
    
    @Override
    public List<String> findAll() {
        // 模拟修改SQL的动作
        return Arrays.asList("oracle", "oracle", "oracle");
    }
}
2.数据库再次变动

经过一星期的紧张修改,你终于完成了全部Dao层的SQL语句修改,马上就到项目交付的时候,甲方又因为一些“不可预见”的特殊原因,决定将数据库改回MySQL。

甲方提需求固然轻松,你作为开发者却非常痛苦,本来好不容易改完的代码又要再改一遍。你实在受够了这种频繁改动,那么是否有解决方案能破局呢?

小提示:

为了顺利演绎故事场景,推导出设计思想和原理,在没有特殊说明的前提下,本书的演绎推理场景均不考虑版本控制工具(如Git、SVN等)。

3.引入静态工厂

苦思良久,你终于想到一个办法: 如果在项目开发的初期就将适配不同数据库的Dao层实现全部制作完毕,在Service层引用时改为借助静态工厂创建特定的实现类 。如此设计后当需求发生变更时,只需要改变一处代码,而不用修改所有的Service层代码。

(1)构造静态工厂

下面改造代码,我们来声明一个静态工厂类,并给这个类起一个比较别致的名字:BeanFactory(之所以会选择这个名,是因为这是一个伏笔),如代码清单2-7所示。

代码清单2-7 最简单的BeanFactory
public class BeanFactory {
    public static DemoDao getDemoDao() {
        // return new DemoDaoImpl();
        return new DemoOracleDao();
    }
}

此处由BeanFactory负责实例化DemoDao的实现类,而为了区分基于MySQL和Oracle两个不同的数据库,DemoDaoImpl 类也要相应地复制出两份,分别命名为 DemoMySQLDao和DemoOracleDao,具体代码省略。

(2)改造ServiceImpl

DemoServiceImpl 中引用的 DemoDao 实现类不再使用 new 关键字创建,而是由BeanFactory的静态方法getDemoDao返回而获得,如代码清单2-8所示。

代码清单2-8 DemoServiceImpl使用BeanFactory依赖DemoDao接口
public class DemoServiceImpl implements DemoService {
    
    DemoDao demoDao = BeanFactory.getDemoDao();
    
    @Override
    public List<String> findAll() {
        return demoDao.findAll();
    }
}

如此改造后,即便Service层的实现类再多,Dao层的实现类再多,当发生需求更改时,只需要改动BeanFactory中静态方法的返回值。

问题解决了,皆大欢喜,甲方也很满意,项目顺利交付。

2.1.3 源码丢失

项目上线运行一段时间后,客户对系统中的一些功能提出了优化和扩展需求,自然而然又找到了你,毕竟你是这个项目的负责人。但是由于公司内项目众多,有一段时间你主要负责别的项目,维护工作改由你的同事负责。当你重新打开工程时,希望先在本地运行,以便确认要更新需求的功能位置,但是非常不幸,由于代码意外丢失,整个项目连编译都无法通过(为了演示无法编译的现象,我们手动删除DemoMySQLDao.java)。

此时的你百思不得其解,之前运行正常的项目为何无法运行?问题出现在哪里?通过定位报错的具体位置,发现BeanFactory中存在编译错误!这般现状让你更加费解,之前封装好的BeanFactory就是为了“偷懒”而设计的,为什么反而会出现编译出错的问题?当你打开代码观察后才得知,代码工程中DemoMySQLDao.java源文件意外丢失,导致整个工程无法编译。

场景演绎到这里暂停,请读者体会上述场景中出现的问题。

1.类之间的依赖关系——紧耦合
代码清单2-9 BeanFactory创建DemoMySQLDao
public class BeanFactory {
    public static DemoDao getDemoDao() {
        return new DemoMySQLDao(); // DemoMySQLDao.java不存在导致编译失败
    }
}

在代码清单2-9中,因为工程代码中真的缺少这个DemoMySQLDao类(当然是我们刚才手动删除模拟的),导致程序编译无法通过,这种现象可以描述为“ BeanFactory 强依赖于 DemoMySQLDao ”,也就是读者可能在其他地方听到过,也可能常说的“ 紧耦合 ”。

2.解决紧耦合

回到刚才的演绎场景中,因为工程中没有DemoMySQLDao.java文件,所以工程无法编译,工作无法正常进行。但是项目开发进度不能因为丢失一个类而被阻塞,请读者思考一下,根据现有的知识,有没有一种办法能解决这个无法编译的问题?

反射!反射可以通过声明一个类的全限定名,获取它的字节码描述,如此一来也能构造对象!

当引入反射机制后,BeanFactory就可以改造为代码清单2-10的内容。

代码清单2-10 引入反射机制的BeanFactory
public class BeanFactory {
    
    public static DemoDao getDemoDao() {
        try {
            return (DemoDao) Class.forName("com.linkedbear.spring00.c_reflect.dao.impl.DemoMySQLDao")
                    .getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
        }
    }
}

如此改造后,BeanFactory便可以正常编译,尽管在DemoService的初始化时还是会出现问题,但是工程可以正常启动。

3.弱依赖

使用反射机制之后,程序出现错误的现象不再是在编译时出现,而是在工程启动后,由于BeanFactory要构造DemoDaoImpl时确实还没有该类,因此抛出ClassNotFoundException异常。这样 BeanFactory对DemoMySQLDao的依赖程度就降低 了,这种情况可以被称作“ 弱依赖 ”( 松耦合 )。

2.1.4 硬编码问题

虽然利用反射机制暂时规避了工程无法正常启动的问题,但问题最终还是要解决。经过一番恢复工作,你终于把DemoMySQLDao.java找了回来,这样即便是程序运行时也不会抛出异常。但是在切换MySQL和Oracle数据库时还是会出现一个问题:由于类的全限定名被写死在BeanFactory的源码中,导致每次切换数据库后工程依然需要重新编译才可以正常运行,这种方式不是很理想,应该有更好的处理方案。

1.引入外部化配置文件

根据已有的Java SE知识,我们不难想到,可以 借助I/O机制实现文件存储配置 ,这样 每次 BeanFactory 被初始化时,让它读取指定的配置文件 ,就不会出现硬编码的现象。基于这个设计,可有如下改造。

(1)加入factory.properties文件

在src/main/resource目录下新建factory.properties文件,并在其中进行声明,如代码清单2-11所示。

代码清单2-11 factory.properties文件
demoService=com.linkedbear.spring00.d_properties.service.impl.DemoServiceImpl
demoDao=com.linkedbear.spring00.d_properties.dao.impl.DemoDaoImpl

为了方便接下来取出这些类的全限定名,示例代码中 给每一个类名都起一个“别名” ,这样就可以 根据别名来找到对应的全限定类名

(2)改造BeanFactory

既然配置文件是properties类型,在JDK中刚好也有一个API名为Properties,它可以解析properties文件。于是可以在BeanFactory中加入一个静态变量,并借助静态代码块,在工程启动的时候就初始化Properties。配置文件读取到之后,下面的getDemoDao和getDemoService方法也要一并修改,修改完成的代码如代码清单2-12所示。

代码清单2-12 使用Properties改良BeanFactory
public class BeanFactory {
    
    private static Properties properties;
    
    // 使用静态代码块初始化properties,加载factory.properties文件
    static {
        properties = new Properties();
        try {
            // 必须使用类加载器读取resource文件夹下的配置文件
            properties.load(BeanFactory.class.getClassLoader()
                      .getResourceAsStream("factory.properties"));
        } catch (IOException e) {
            // BeanFactory类的静态初始化失败,后续代码也没有必要继续执行,抛出异常
            throw new ExceptionInInitializerError("BeanFactory initialize error, cause: " + e.getMessage());
        }
    }
    
    public static DemoDao getDemoDao() {
        try {
            Class<?> beanClazz = Class.forName(properties.getProperty("demoDao"));
            return beanClazz.getDeclaredConstructor().newInstance();
        } // catch throw ex ......
    }
}

代码改造到这里,读者朋友是否会感到有些“不适”?既然代码已经抽象化到这种地步,像getDemoService、getDemoDao等方法就没有必要重复编写,而是制作一个通用的方法,这个方法 传入要获得对象的“别名”,由 BeanFactory 从配置文件中查找对应的全限定类名,反射构造对象返回 即可,所以我们可以将获取对象的方法继续抽象为 getBean 方法,即代码清单2-13的实现。

代码清单2-13 getBean方法获取任意对象
    public static Object getBean(String beanName) {
        try {
            // 从properties文件中读取指定name对应类的全限定名,并反射实例化
            Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
            return beanClazz.getDeclaredConstructor().newInstance();
        } // catch throw ex ......
    }

如此改造之后,DemoServiceImpl中就不再调用getDemoDao方法,而是转用getBean方法,并指定需要获取的对象名称为"demoDao",即可获得想要的DemoDao实现类对象,对应的代码略。

2.外部化配置

场景推演至此,读者朋友是否产生了一个大胆的想法:基于上述的设计,我们可以将 所有 需要抽取出来的 组件都做成外部化配置 !对于这种可能会变化的配置、属性等,通常不会直接硬编码在源码中,而是 抽取为一些配置文件的形式 (properties、XML、JSON、YML等),配合程序对配置文件的加载和解析,从而达到动态配置、降低配置耦合度的目的。 这种抽取配置文件的思想,称为“外部化配置”;抽取配置文件的动作,则称为“配置的外部化”。

2.1.5 多次实例化

BeanFactory演变至此依然存在问题,我们可以在ServiceImpl的构造方法中连续多次获取DemoDao的实现类对象,并观察这些对象的内存地址,如代码清单2-14所示。

代码清单2-14 重复获取DemoDao的实现类对象
public class DemoServiceImpl implements DemoService {
    
    DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");
    
    public DemoServiceImpl() {
        for (int i = 0; i < 5; i++) {
            System.out.println(BeanFactory.getBean("demoDao"));
        }
    }
}
com.linkedbear.spring00.d_properties.dao.impl.DemoDaoImpl@44548059
com.linkedbear.spring00.d_properties.dao.impl.DemoDaoImpl@5cab632f
com.linkedbear.spring00.d_properties.dao.impl.DemoDaoImpl@24943e59
com.linkedbear.spring00.d_properties.dao.impl.DemoDaoImpl@3f66e016
com.linkedbear.spring00.d_properties.dao.impl.DemoDaoImpl@5f50e9eb

重新运行程序,可以发现连续5次获取DemoDao的实现类对象时,每个对象打印的内存地址都不相同,这证明创建了5个不同的DemoDaoImpl!这样的设计并不合理,在一个工程中,具有功能的对象在没有特殊需求下,最好只存在一个(单实例)。

改良方法:引入缓存。

对于这些没有必要创建多个对象的组件,如果能有一种方法保证整个工程运行过程中只存在一个对象,就可以大大减少资源消耗。于是可以在BeanFactory中加入一个缓存区,并在getBean方法中设置缓存检查逻辑,如果缓存中存在指定对象,则直接返回;如果没有对象,则会先创建对象,并将创建好的对象放入缓存中,再返回。为了控制并发问题,需要引入双检锁保证对象只有一个(其思想参照懒汉单例模式),改造后的BeanFactory核心代码如代码清单2-15所示。

代码清单2-15 引入缓存区后的BeanFactory
public class BeanFactory {
    // 缓存区,保存已经创建好的对象
    private static Map<String, Object> beanMap = new HashMap<>();
    
    public static Object getBean(String beanName) {
        // 双检锁保证beanMap中确实没有beanName对应的对象
        if (!beanMap.containsKey(beanName)) {
            synchronized (BeanFactory.class) {
                if (!beanMap.containsKey(beanName)) {
                    // 过了双检锁,证明确实没有,可以执行反射创建
                    try {
                        Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
                        Object bean = beanClazz.getDeclaredConstructor().newInstance();
                        // 反射创建后放入缓存再返回
                        beanMap.put(beanName, bean);
                    } // catch throw ex ......
                }
            }
        }
        return beanMap.get(beanName);
    }
}

改造完成后,重启工程再次测试,观察这一次打印的结果,发现多次打印都指向同一个对象,说明改造已经完成,达到最终的目的。

com.linkedbear.spring00.e_cachedfactory.dao.impl.DemoDaoImpl@4a667700
com.linkedbear.spring00.e_cachedfactory.dao.impl.DemoDaoImpl@4a667700
com.linkedbear.spring00.e_cachedfactory.dao.impl.DemoDaoImpl@4a667700
......

2.1.6 IOC思想的引入

到此为止,整个场景的演绎结束,下面总结整个过程中出现的几个关键点:

静态工厂可将多处依赖抽取分离;

外部化配置文件+反射可解决配置的硬编码问题;

缓存机制可以控制对象的实例数。

对比演绎推理中出现的两种代码编写方式(如代码清单2-16所示),可以发现上面的是强依赖/紧耦合,在编译时就必须保证DemoDaoImpl存在;下面的是弱依赖/松耦合,只有在运行时通过反射创建过程才能得知DemoDaoImpl是否存在。

代码清单2-16 两种获取DemoDao的方式对比
private DemoDao dao = new DemoDaoImpl();
 
private DemoDao dao = (DemoDao) BeanFactory.getBean("demoDao");

再对比看,上面的写法是主动声明了DemoDao的实现类,只要代码可以编译通过,则运行正常;下面的写法没有指定实现类,而是由BeanFactory去帮我们查找一个名为demoDao的对象,倘若factory.properties文件中声明的全限定类名出现错误,则会抛出强制类型转换失败的异常ClassCastException。

仔细体会下面这种对象获取的方式,本来开发者可以使用上面的方式,主动声明实现类,但如果选择下面的方式,那就不再是开发者主动声明,而是 将获取对象的方式交给了 BeanFactory 。这种 将控制权交给别人 的思想就是所谓的 控制反转(Inverse of Control,IOC) 。而BeanFactory根据指定的beanName去获取和创建对象的过程,可以称作 依赖查找(Dependency Lookup,DL)

2.2 IOC的两种实现方式

了解IOC思想的由来之后,下面我们就要开始真正学习Spring Framework的IOC思想的具体实现。IOC的实现方式包含两种,分别是 依赖查找(Dependency Lookup)与依赖注入(Dependency Injection)

2.2.1 依赖查找

依赖查找的含义是,根据指定的对象名称或对象的所属类型,主动从IOC容器中获取对应的具体对象(后续将这种对象称为bean对象)。下面通过几个具体的示例来快速上手。

1.根据名称查找(byName)

(1)创建工程,引入依赖

首先我们创建第一个学习Spring Framework的工程 spring-01-ioc ,它基于Java 17编译,坐标依赖中只需要引入spring-context(另外同样需要引入编译插件,代码略),如代码清单2-17所示。笔者在编写本书时经历了Spring Framework的两个版本6.0.x和6.1.x,所以在本书中读者会看到两套不同的版本,对于绝大部分的功能它们是相同的,有新的变动或者特性出现时会相应地提醒和体现。

代码清单2-17 spring-01-ioc的坐标依赖
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.0.9</version>
    </dependency>
</dependencies>

(2)编写配置文件

接下来需要创建一个基于Spring Framework的配置文件。通过2.1节的推演,想必读者也能理解,外部化配置可以更灵活地修改容器中Bean的配置,Spring Framework使用XML配置文件的方式来描述类和对象的定义信息。在工程的src/main/resources目录下创建一个名为“basic_dl”的目录,并在其中创建quickstart-byname.xml文件,如图2-7所示。

图2-7 创建quickstart-byname.xml配置文件

XML配置文件的骨架内容由Spring Framework事先约定,从Spring Framework的官方文档中可以找到 XML 配置文件的骨架,如代码清单 2-18 所示,将这段代码粘贴到quickstart-byname.xml文件中即可。

代码清单2-18 Spring Framework中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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
 
</beans>

小提示:

细心的读者会注意到,使用IDEA开发时,在粘贴进配置文件的骨架后,IDEA会弹出一行提示,如图2-8所示。由此可见IDEA的强大之处在于,它可以监测并意识到我们要在工程中添加Spring Framework的配置文件,进而提醒我们是否要将这个配置文件配置到IDEA的项目工作环境中。在配置之后,IDEA可以自动提示我们编写的代码中注册了哪些Bean,以及这些Bean的信息等,我们可以选择配置也可以忽略,读者可以根据自己的喜好自行选择。

图2-8 粘贴XML头后IDEA弹出提示

(3)声明普通类并注册

本书中编写的示例代码尽可能按照包结构划分,本节的代码将统一创建在com.linkedbear.spring.basic_dl包中。我们可以创建一个普通的Person类,其内部不需要编写任何代码。随后在quickstart-byname.xml文件中使用Spring Framework的定义规则,将Person声明到配置文件中,如代码清单2-19所示。

代码清单2-19 声明的普通类Person与对应的XML
public class Person {
    
}
<?xml version="1.0" encoding="UTF-8"?>
<beans......>
 
    <bean id="person" class="com.linkedbear.spring.basic_dl.a_quickstart_byname.bean.Person"></bean>
</beans>

配置文件的编写方式非常简单,大体与 2.1 节中制作properties文件的方式类似,使用<bean>标签来声明一个注册到IOC容器中的Bean,它的id(名称)为“person”,对应的类型就是上面创建的Person类。

小提示:

按照代码清单2-19的方式编写完配置文件后,IDEA会提示XML标签体为空,这是因为XML中没有标签体的情况下是可以省略闭合标签的(读者如果了解最基础的HTML语法就应该知道)。

(4)创建启动类并测试

按照2.1节演绎推理的步骤,有了配置文件后下一步就是要读取和解析配置文件,我们创建一个QuickstartByNameApplication类,并声明main方法编写启动逻辑。读取quickstart-byname.xml文件的方法有很多,因为是第一个示例,所以我们选择一种相对简单的方法,如代码清单2-20所示。

代码清单2-20 QuickstartByNameApplication
public class QuickstartByNameApplication {
    
    public static void main(String[] args) throws Exception {
        BeanFactory factory = new ClassPathXmlApplicationContext("basic_dl/quickstart-byname.xml");
        Person person = (Person) factory.getBean("person");
        System.out.println(person);
    }
}

简单解释代码清单2-20的含义。要想读取quickstart-byname.xml配置文件,需要一个载体来加载,示例代码中选择使用ClassPathXmlApplicationContext来加载,可以从当前项目的类路径(classpath)下,找到basic_dl目录下的quickstart-byname.xml文件。加载完成后,我们使用BeanFactory接口来接收该对象(多态的思想)。之后从BeanFactory中调用getBean方法,从IOC容器中取出名为"person"的对象,并强制转换为Person类型,最后打印即可。

小提示:

笔者在编写测试main方法时,习惯在方法签名上声明throws Exception,如此编写后大部分场景下可以不用关心try-catch操作,读者在跟随本书练习时不必强行模仿笔者的风格,沿用自己的编码风格即可。

运行QuickstartByNameApplication的main方法,控制台可以成功打印出Person的全限定类名+内存地址,证明测试方法编写成功。

com.linkedbear.spring.basic_dl.a_quickstart_byname.bean.Person@6a4f787b
2.根据类型查找(byType)

从上面的示例中可以发现一个问题:通过名称获取的bean对象类型只能是Object,如果需要精准获取IOC容器中某一个类型的bean对象,则需要根据类型查找。为了与上面根据名称查找的内容做区分,我们不在原有代码的基础上修改,而是复制一份新的代码,并将配置文件更名为quickstart-bytype.xml。

为了演示基于类型查找,这次使用<bean>标签声明时,我们不再指定id属性,相应地,启动类QuickstartByTypeApplication中调用getBean方法时不再传入字符串变量,而是直接传入希望获取的Bean的class类型,而且接收对象不再需要强制类型转换,如代码清单2-21所示。

代码清单2-21 使用类型获取
<bean class="com.linkedbear.spring.basic_dl.b_bytype.bean.Person"></bean>
public class QuickstartByTypeApplication {
    
    public static void main(String[] args) {
        BeanFactory factory = new ClassPathXmlApplicationContext("basic_dl/quickstart-bytype.xml");
        Person person = factory.getBean(Person.class);
        System.out.println(person);
    }
}

要获得对应的测试效果,读者可以自行执行和验证,本书不再演示。

3.接口与实现类

基于类型查找时,还可以根据接口获取对应的实现类(当然这要求实现类已经注册到IOC容器中)。为了演示这一效果,我们将2.1节中的DemoDao与DemoDaoImpl复制到basic_dl. b_bytype目录中,并在quickstart-bytype.xml文件中加入DemoDaoImpl的声明定义。随后在启动类QuickstartByTypeApplication中使用BeanFactory取出DemoDao,并打印findAll方法的返回数据,如代码清单2-22所示。

代码清单2-22 根据接口获取对应实现类
<bean class="com.linkedbear.spring.basic_dl.b_bytype.dao.impl.DemoDaoImpl"/>
public class QuickstartByTypeApplication {
    
    public static void main(String[] args) throws Exception {
        BeanFactory factory = new ClassPathXmlApplicationContext("basic_dl/quickstart-bytype.xml");
        // ......
        DemoDao demoDao = factory.getBean(DemoDao.class);
        System.out.println(demoDao.findAll());
    }
}

运行main方法,控制台可以打印出 [aaa, bbb, ccc] ,证明DemoDaoImpl也成功注入,并且BeanFactory可以根据接口类型找到对应的实现类。

2.2.2 依赖注入

由上面的三个示例中可以发现一个问题:以上创建的bean对象都是不带属性值的!如果我们要创建的bean对象需要一些预设的属性,那么就要涉及IOC的另一种实现—— 依赖注入 。作为IOC思想的另一种实现,它的基本原则是一致的: 如果某个bean对象需要属性依赖,请不要自行声明/定义,而是将Bean定义至IOC容器,由IOC容器负责加载属性值 ,并设置到相应的属性上。

下面通过两个简单示例,快速体会依赖注入的使用和含义。

小提示:

本节的代码将统一创建在com.linkedbear.spring.basic_di包下。

1.简单属性值注入

为了与依赖查找的对象进行区分,我们新创建一个Person类,并声明两个属性name和age。随后编写XML配置文件inject-set.xml,将Person类注册到IOC容器中,如代码清单2-23所示。

代码清单2-23 声明带有属性的Person类和注册Bean
public class Person {
    private String name;
    private Integer age;
    // getter and setter toString ......
}
<?xml version="1.0" encoding="UTF-8"?>
<beans ......>
    <bean id="person" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Person"></bean>
</beans>

如果仅使用上述的代码驱动IOC容器初始化,那么从IOC容器中取出的Person对象中name与age的属性将全部为null(读者可自行测试效果)。为了能给Person对象的属性赋值,需要在<bean>标签的内部指定一些内容。借助IDE可以发现,<bean>标签的内部可以声明的标签如图2-9所示。

图2-9 <bean>标签内部可以声明的标签

这些标签不需要读者一开始就全部记住,学到哪个就熟悉和掌握哪个。如果需要给bean对象的属性赋值,使用的标签是<property>,这个标签有两个属性可以供我们使用,分别是name(属性名)和value(属性值)。对于Person对象的属性赋值,可以采用代码清单2-24中的方式。

代码清单2-24 使用<property>标签为bean对象的属性赋值
<bean id="person" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Person">
    <property name="name" value="test-person-byset"/>
    <property name="age" value="18"/>
</bean>

声明之后,可以编写测试类以验证对象的属性,如代码清单 2-25 所示。运行QuickstartInjectBySetXmlApplication类的main方法,控制台可以打印Person对象的属性值,说明依赖注入属性成功。

代码清单2-25 通过QuickstartInjectBySetXmlApplication测试依赖注入属性的效果
public class QuickstartInjectBySetXmlApplication {
    
    public static void main(String[] args) throws Exception {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("basic_di/inject-set.xml");
        Person person = beanFactory.getBean(Person.class);
        System.out.println(person);
    }
}
 
// Person{name='test-person-byset', age=18}
2.关联bean对象注入

对于2.1节中DemoService依赖DemoDao的场景,依赖注入同样可以实现。下面创建一个新的小猫类Cat,并声明name和master属性,分别指代小猫的名字和主人。随后在inject-set.xml配置文件中定义一个新的Bean,id为“cat”,并使用<property>标签的另一个属性ref引用IOC容器中的一个现有的bean对象,即person,如代码清单2-26所示。

代码清单2-26 新建Cat类并配置到XML中
public class Cat {
    private String name;
    private Person master;
    // getter and setter toString ......
}
<?xml version="1.0" encoding="UTF-8"?>
<beans......>
 
    <bean id="person" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Person">
        <property name="name" value="test-person-byset"/>
        <property name="age" value="18"/>
    </bean>
 
    <bean id="cat" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Cat">
        <property name="name" value="test-cat"/>
        <!-- ref引用上面的person对象 -->
        <property name="master" ref="person"/>
    </bean>
</beans>

编写完成后,在QuickstartInjectBySetXmlApplication类中获取Cat对象并打印,可以发现Cat对象中的master属性就是上面的Person对象,关联bean对象的依赖注入也实现完成(具体测试代码和结果略,读者可自行测试和验证)。

2.2.3 依赖查找与依赖注入的对比

根据本节内容进行简单总结和归纳,可以看出IOC的两种实现方式的区别。

(1)从作用目标来看:依赖注入的作用目标通常是类成员,依赖查找的作用目标可以是方法体内,也可以是方法体外。

(2)从实现方式来看:依赖注入通常借助一个上下文被动地接收,依赖查找通常主动使用上下文搜索。

2.3 BeanFactory与ApplicationContext

体会了IOC思想的两种具体实现方式后,下面需要解释几个概念并进行相关对比。

2.3.1 理解IOC容器

顾名思义,IOC容器即实现了IOC思想的容器。容器可以理解为“ 一个具备创建组件(对象)并管理组件能力的区域 ”,而引入IOC思想的容器,其内部应当具备IOC思想的两种具体实现方式。通过2.2节的几个示例,读者是否有一种感觉:我们将需要创建的对象的信息,以配置文件的方式提供给Spring Framework,在创建BeanFactory(或ApplicationContext)的时候它就会自动帮我们创建这些信息,并在我们使用依赖查找(调用getBean方法)时予以返回,这种从加载和解析配置文件到完成容器和内部组件(Bean)的过程,就是IOC容器的基本处理流程。

小提示:

当然,IOC容器的内部设计远比上面描述的复杂,在后续章节的不断学习中,读者可以对IOC容器的设计、思想、实现和原理有更深入的了解。

2.3.2 对比BeanFactory与ApplicationContext

Spring Framework中的BeanFactory就是一个基本的IOC容器的实现,而ApplicationContext是基于BeanFactory的扩展,它拥有BeanFactory的所有功能,并且还扩展了很多特性。从Spring Framework的官方文档中可以找到如下一段描述,解释这两个接口之间的关系。

这段描述的大体意思是,org.springframework.beans和org.springframework. context包是Spring Framework的IOC容器的基础。BeanFactory接口提供了一种高级配置机制,能够管理任何类型的对象。ApplicationContext是BeanFactory的子接口。它增加了:

与Spring Framework的AOP特性轻松集成;

消息资源处理(用于国际化);

事件发布;

应用层特定的上下文,例如Web应用程序中使用的WebApplicationContext。

此外,官方文档中还有一段内容,解释了开发者为什么要使用ApplicationContext而不是BeanFactory,内容如下。

You should use an ApplicationContext unless you have a good reason for not doing so, with GenericApplicationContext and its subclass AnnotationConfigApplicationContext as the common implementations for custom bootstrapping. These are the primary entry points to Spring’s core container for all common purposes:loading of configuration files, triggering a classpath scan, programmatically registering bean definitions and annotated classes, and (as of 5.0) registering functional bean definitions.

你应该使用ApplicationContext,除非有充分的理由不需要使用。在一般情况下,我们推荐将GenericApplicationContext及其子类AnnotationConfigApplicationContext作为自定义引导的常见实现。这些实现类是用于所有常见目的的Spring Framework核心容器的主要入口点:加载配置文件,触发类路径(classpath)扫描,编程式注册Bean定义和带注解的类,以及(从5.0版本开始)注册功能性Bean的定义。

这段话的下面还给了一张表,如表2-1所示,对比了BeanFactory与ApplicationContext的不同特性。

表2-1 BeanFactory与ApplicationContext的特性对比

基于上述文档和观点的总结,我们来概括BeanFactory与ApplicationContext的区别。

BeanFactory接口提供了一个 抽象的配置和对象的管理机制 ,ApplicationContext是BeanFactory的子接口,它简化了与AOP的整合、消息机制、事件机制,以及对Web环境的扩展(WebApplicationContext等),BeanFactory是没有这些扩展的。

ApplicationContext主要扩展了以下功能:

AOP的支持(AnnotationAwareAspectJAutoProxyCreator作用于bean对象的初始化之后);

配置元信息(BeanDefinition、Environment、注解等);

资源管理(Resource抽象);

事件驱动机制(ApplicationEvent、ApplicationListener);

消息与国际化(LocaleResolver);

Environment抽象(Spring Framework 3.1以后)。

2.3.3 理解Context与ApplicationContext

可能部分读者对Context这个概念比较模糊,抑或完全不理解Context这个概念和设计。Context意为“上下文”,简单地说,Context指的是 当程序运行到指定代码时,客观存在的额外数据和信息 。我们可以结合上学时的语文课来理解,在一篇文章中看到某个故事情节或者细节描述时,可能想要分析情节的作用或某个细节,这就需要 结合当前阅读的部分及其前后章节或段落来综合分析 ,这里的 “前后章节或段落” 上下文 ,也就是Context。比如著名作家朱自清的散文《背影》中有一个经典场面:“父亲往车外看了看说:‘我买几个橘子去。你就在此地,不要走动。’”如果仅将这一句话截取出来,我们完全无法体会父亲说这句话的用意和后续作者的内心波动,需要结合前后段落阅读才可以体会。

在软件开发中Context通常可以理解为我们编写的代码执行时可以获取的额外信息,包括全局变量、类中的静态成员、ThreadLocal中的值、配置属性等。在我们学习Spring Framework之后ApplicationContext也就成为运行Spring Framework应用时的上下文,我们可以借助ApplicationContext拿到IOC容器中的其他Bean,无论我们在代码中是否获取和使用,这些Bean都是客观存在的。

2.4 注解驱动的IOC

从Spring Boot发布以来,使用XML配置文件的场景越来越少,取而代之的是基于注解配置类来驱动IOC容器。从Spring Framework推出3.0版本后,支持的最低Java版本为Java 5,我们都知道Java 5最有特点的新特性之一就是引入了 注解 ,Spring Framework 3.0开始也通过引入大量注解代替XML的方式进行声明式开发。本节会简单介绍和演示基于注解驱动的IOC容器使用方式,以及注解配置类的编写。

小提示:

本节的代码将统一创建在com.linkedbear.spring.annotation包下。

2.4.1 注解驱动IOC的依赖查找

相较于XML配置文件的驱动方式,基于注解驱动的配置会全部编写在 配置类 中,一个配置类可以理解为一个XML配置文件。对配置类没有特殊的限制,只需要在类上标注一个@Configuration注解。

与XML配置文件使用<bean>标签的方式类比,通过注解配置类注册Bean所使用的是@Bean注解。代码清单2-27展示了一个注解配置类QuickstartConfiguration中注册Bean的方式,这段代码的含义是 QuickstartConfiguration 向IOC容器注册一个类型为 Person 、id为“ person ”的Bean,方法的返回值代表注册的类型,方法名代表Bean的id 。除了使用方法名作为id以外,也可以直接在@Bean注解中使用name属性显式地声明Bean的id。

代码清单2-27 使用注解配置类注册Person对象
@Configuration
public class QuickstartConfiguration {
    
    // 等同于 <bean id="person" class="c.l.s.b.a.bean.Person"/>
    @Bean(name = "person")
    public Person person() {
        return new Person();
    }
}

注解驱动的场景下,Spring Framework为我们提供的是 AnnotationConfigApp licationContext ,它可以用作最常用的注解驱动IOC容器。代码清单2-28展示了一个简单的注解驱动IOC容器,可以发现整体的编码方式非常简单,甚至与XML配置文件的方式没有什么区别。

代码清单2-28 使用AnnotationConfigApplicationContext驱动并获取Person对象
public class AnnotationConfigApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(QuickstartConfiguration.class);
        Person person = ctx.getBean(Person.class);
        System.out.println(person);
    }
}

2.4.2 注解驱动IOC的依赖注入

编写基于@Bean注解的依赖注入也非常简单,因为注册Bean时使用的是Java代码,所以注入哪些属性完全是在代码中编写的。代码清单2-29展示了注册Person和Cat对象的依赖注入方法,这种写法与2.2.2节中使用XML配置文件的方式完全等价。

代码清单2-29 编程式注入属性和依赖对象引用
@Configuration
public class AnnotationDIConfiguration {
    
    @Bean
    public Person person() {
        Person person = new Person();
        person.setName("person");
        person.setAge(123);
        return person;
    }
    
    @Bean
    public Cat cat() {
        Cat cat = new Cat();
        cat.setName("test-cat-anno");
        // 直接拿上面的person()方法作为返回值即可,相当于ref
        cat.setMaster(person());
        return cat;
    }
}

测试启动类的编写和运行与2.2.2节完全一致,不再展开,读者可自行验证。

2.4.3 组件注册与扫描机制

翻看AnnotationConfigApplicationContext的构造方法,可以发现它还有一个传入basePackage的构造方法,basePackage译为“根包”,要想理解这个概念,就需要了解注解驱动中一个非常重要的机制: 组件注册与扫描

1.组件注册的根源:@Component

Spring Framework规定,当一个类上标注了@Component注解(并被组件扫描)时,即认定这个类将在IOC容器初始化时被创建,并注册到IOC容器中成为一个Bean。代码清单2-30中的两种编写形式是等价的。

代码清单2-30 使用@Component注解与<bean>标签
@Component
public class Person {
    
}
<bean id="person" class="com.linkedbear.spring.basic_dl.a_quickstart_byname.bean.Person"/>

默认情况下,组件生成的beanName为 “首字母小写的类名” (例如Person的默认名称是person,DemoServiceImpl的默认名称是demoServiceImpl)。如果需要指定Bean的名称,就在@Component注解中声明value属性。

小提示:

注意一个细节,如果<bean>标签不声明id属性,默认生成的Bean的名称不是“首字母小写的类名”,这一点与注解扫描的规则不同!

2.组件扫描

如果只是将@Component注解标注在类上而不进行扫描,那么IOC容器将无法感知到组件注册,当没有扫描组件时强行获取Bean,一定会抛出NoSuchBeanDefinitionException异常,为此需要引入一个新的注解用于组件扫描:@ComponentScan。

@ComponentScan注解通常标注在配置类上。在代码清单 2-31 中,我们在配置类ComponentScanConfiguration上额外标注一个@ComponentScan注解,并指定要扫描的包路径,它就可以 扫描指定路径包及子包下的所有 @Component 组件 。如果不指定扫描路径,就 默认扫描本类所在包及子包下的所有 @Component 组件 。注意,basePackages是复数概念,这意味着它可以一次性声明多个扫描的包。

代码清单2-31 使用@ComponentScan注解
@Configuration
@ComponentScan("com.linkedbear.spring.annotation.c_scan.bean")
public class ComponentScanConfiguration {
    
}

声明了@ComponentScan之后,重新启动配置类,可以发现Person已经成功被注册,关于具体效果读者可自行测试验证。注意一点,如果Spring Framework的版本比较老,可能会看到如下的写法:@ComponentScan(basePackages="com.linkedbear.spring. annotation.c_scan.bean"),这两个写法实质上是一样的,写哪个都可以。

除了在配置类中使用@ComponentScan注解,Spring Framework还给我们提供了第二种组件扫描的使用方式。在AnnotationConfigApplicationContext的构造方法中有一个类型为String可变参数的构造方法,当我们传入需要扫描的包之后,也可以直接扫描到那些标注了@Component的Bean并注册到IOC容器中。

ApplicationContext ctx = new AnnotationConfigApplicationContext("com.linkedbear.spring. annotation.c_scan.bean");

组件扫描不是注解驱动IOC容器的专利,对于XML配置文件驱动的IOC容器同样可以启用组件扫描,只需要在XML配置文件中声明一个标签<context:component-scan>,之后使用ClassPathXmlApplicationContext驱动IOC容器,同样可以获取person对象。

<context:component-scan base-package="com.linkedbear.spring.annotation.c_scan.bean"/>
3.组件注册的其他注解

Spring Framework为了迎合Web应用开发时所采用的经典三层架构,额外提供了三个注解:@Controller、@Service、@Repository,分别对应三层架构中的表现层、业务层、持久层。这三个注解的作用与@Component完全一致,其实它们的底层也都是@Component,如代码清单2-32所示。

代码清单2-32 派生注解的底层仍然是@Component
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented 
@Component 
public @interface Controller { ... }

有了以上三个注解,在进行符合三层架构的应用开发时,对于那些业务逻辑层的类(如DemoServiceImpl),就可以直接标注@Service注解,而不用一个个地写<bean>标签或者借助@Bean注解进行组件注册。

小提示:

其实@Repository注解在Spring Framework 2.0中就已经存在,只是到Spring Framework 3.0才开始全面支持注解驱动开发。

4.@Configuration也是@Component

如果在驱动注解IOC容器时,直接扫描包括配置类在内的整个根包,则在运行main方法时会看到配置类ComponentScanConfiguration也被注册到IOC容器中,如代码清单2-33所示(上面的一组Spring内置的组件可忽略,只留意最后3行代码即可)。

代码清单2-33 扫描整个c_scan包
public static void main(String[] args) throws Exception {
    ApplicationContext ctx = new AnnotationConfigApplicationContext("com.linkedbear.spring. annotation.c_scan");
    String[] beanNames = ctx.getBeanDefinitionNames();
    Stream.of(beanNames).forEach(System.out::println);
}
 
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
componentScanConfiguration
cat
person

可能有读者会产生疑惑,为什么配置类不像配置文件那样仅作为一个配置的载体出现,而是连同自己一并注册到IOC容器中?其实原因很简单,翻看@Configuration注解的源码,可以发现它也被标注了@Component注解,说明被标注@Configuration注解的类对于IOC容器而言同样也是一个Bean。

代码清单2-34 @Configuration也是@Component
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented 
@Component 
public @interface Configuration { ... }

2.4.4 注解驱动与XML驱动互通

如果一个应用中既有注解驱动配置,又有XML配置文件,则可能出现由一方引入另一方配置的情况。下面分别演示互相引入的两种场景。

1.XML配置文件引入注解驱动

在XML中要引入注解驱动,需要开启注解配置,同时注册对应的配置类,如代码清单2-35所示。

代码清单2-35 XML配置文件中引入注解驱动
<?xml version="1.0" encoding="UTF-8"?>
<beans......>
    <!-- 开启注解配置 -->
    <context:annotation-config />
    <bean class="c.l.s.annotation.d_importxml.config.AnnotationConfigConfiguration"/>
</beans>
2.注解配置类引入XML配置文件

在注解配置类中引入XML配置文件,需要在配置类上标注@ImportResource注解,并声明配置文件的路径,如代码清单2-36所示。关于具体的组件注册和效果,读者可自行编写代码测试验证,本书不再赘述。

代码清单2-36 注解配置类引入XML配置文件
@Configuration
@ImportResource("classpath:annotation/beans.xml")
public class ImportXmlAnnotationConfiguration {
}

小提示:

由于目前主流的项目开发都基于Spring Boot,而Spring Boot推荐使用注解驱动配置,尽可能避免XML配置文件的引入,因此在没有特殊说明的前提下,本书后续的演示示例均基于注解驱动。 FAsgSjwTjdfst/eqe4yY6CzZXkGy9mRG0rN9XSBVMOrBcwvr7vq3MX3ZsfJwV71+

点击中间区域
呼出菜单
上一章
目录
下一章
×