Spring Boot+MVC实战指南

978-7-115-58061-0
作者: 高洪岩
译者:
编辑: 傅道坤

图书目录:

详情

《Spring Boot+MVC实战指南》主要讲解如何在Spring Boot框架中开发MVC应用,包括主流的JavaEE框架,如MyBatis、Spring、SpringMVC、FreeMarker和Thymeleaf等。读者可以进行“精要”式学习,正确地进行项目实战,同时汲取JavaEE的思想,并最终将这种思想灵活运用到实际工作中。 《Spring Boot+MVC实战指南》主要涉及MVC框架的原理实现、上传、下载、数据验证、国际化、多模块分组开发、转发/重定向、JSON的解析、将Ajax及JSON和MVC框架进行整合开发,以及MyBatis中映射文件的使用。本书还介绍了Spring中的核心技术(依赖注入与AOP),掌握这两项技术是学习Spring的重中之重。 《Spring Boot+MVC实战指南》语言简洁,示例丰富,以掌握实用技术为目的,帮助读者迅速掌握使用主流开源JavaEE框架进行开发所需的各种技能。本书适合已具有一定Java编程基础(具有Servlet编程经验)的读者阅读,也可供Java平台下进行各类软件开发的开发人员、测试人员参考。

图书摘要

版权信息

书名:Spring Boot+MVC实战指南

ISBN:978-7-115-58061-0

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


著    高洪岩

责任编辑 傅道坤

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


本书主要讲解如何在Spring Boot框架中开发MVC应用,包括主流的JavaEE框架,如MyBatis、Spring、SpringMVC、FreeMarker和Thymeleaf等。读者可以进行“精要”式学习,正确地进行项目实战,同时汲取JavaEE的思想,并最终将这种思想灵活运用到实际工作中。

本书内容主要涉及MVC框架的原理实现、上传、下载、数据验证、国际化、多模块分组开发、转发/重定向、JSON的解析、将Ajax及JSON和MVC框架进行整合开发,以及MyBatis中映射文件的使用。本书还介绍了Spring中的核心技术(依赖注入与AOP),掌握这两项技术是学习Spring的重中之重。

本书语言简洁,示例丰富,以掌握实用技术为目的,帮助读者迅速掌握使用主流开源JavaEE框架进行开发所需的各种技能。本书适合已具有一定Java编程基础(具有Servlet编程经验)的读者阅读,也可供Java平台下进行各类软件开发的开发人员、测试人员参考。


高洪岩,世界500强企业高级项目经理,具有10余年项目管理与开发经验,在多线程和并发、Android移动开发、智能报表和分布式处理等企业级架构技术领域深耕多年,深谙Java技术开发要点与难点,拥有良好的技术素养和丰富的实践经验,一直在持续关注架构的优化和重构领域,喜欢用技术与理论相结合的方式分享知识,以共同提高。著有《Java多线程编程核心技术》《Java并发编程:核心方法与框架》《Java EE核心框架实战》《NIO与Socket编程技术指南》《虚拟化高性能NoSQL存储案例精粹:Redis+Docker》《Java Web实操》等书籍。


在本书出版的过程中,我要感谢公司领导和同事的大力支持;感谢家人在我写作期间给予的理解与支持;感谢现年3岁多的儿子高晟京,你使我更有动力;最后感谢对本书耗费大量精力的编辑老师们,你们仔细谨慎的工作态度值得我学习。


用人单位对IT人才的要求越来越趋向于“实战性”,也就是要求员工进入软件公司后能立即投入开发的任务中,快速为公司创造巨大的经济价值。本书以此为出发点进行编写。

一本内容精炼而不失实用价值的主流JavaEE开源框架图书,应该包含主流框架中常用且重要的内容,这样读者就可以快速上手,根据这些内容探索出一些方向,并在工作和学习中不断拓展和深掘。这就是写作本书的主要目的。

JavaEE的世界非常庞大,以至于没有任何一本书能把它讲解得非常完整或详细。要想学好Java语言或JavaEE框架并掌握其中丰富的编码技巧、设计模式以及代码优化,并将它们熟练地综合应用在软件项目中,只有从零开始学习,没有捷径。

本书的章节编排不但涵盖了学习主流JavaEE框架所需掌握的核心技术,也涵盖了使用它们进行项目实战的必备知识,旨在希望读者尽快上手,掌握开源JavaEE框架的核心内容,正确进行项目实战,汲取JavaEE的思想,并最终将这种思想活用到实际工作中。

现在主流的JavaEE框架是SSM或Spring Boot,但Spring Boot框架仅仅是一个“盒子”,一个“封装器”,想要实现功能还需要整合其他第三方的框架,如MyBatis、Spring或SpringMVC等,所以在学习Spring Boot之前必须要有SSM框架的开发经验。能让一位初入JavaEE框架的学习者从零开始到最终掌握这几个框架,一直是我的写作目标。有些JavaEE的开源框架的确能非常有效地改善开发效率,但因为使用的人不多,所以导致覆盖面比较窄。而软件公司在招聘时的技术需求大多数情况下是“大众化”的,这就要求应聘者在面试前有主流JavaEE框架的学习或使用经验。如果读者找不到合适的教材,导致在学习某一项技术时根本摸不清哪些知识点是常用的,哪些是不常用的,就会极大地降低学习效率。对此,本书为读者提供了一条有效的学习路径。

首先,本书适合所有Java程序员。JavaEE作为Java开源世界的主流框架,Java程序员没有理由不学习它们。其次,本书适合希望学习使用这些框架进行编程的在校学生。由于学校的功课很多,一本大部头的框架书需要花费大量的时间研读,而本书可以带领读者快速进入JavaEE框架开发的殿堂,同时又不会遗漏应该掌握的核心技能。

第1章和第2章将介绍Spring中的IoC和AOP技术,包含注入、注入原理、动态代理的实现与AOP的原理。

第3章将学习最流行的SpringMVC框架,体会使用此框架开发一个经典登录功能时使用的技术点,还要掌握分组分模块开发的技术、重定向/转发的使用、JSON+AJAX+SpringMVC联合开发、上传/下载的实现、数据验证功能的使用、XML配置文件的处理、业务层Service的注入、Model和View对象的使用,以及HttpSession在SpringMVC中的使用等。

第4章将学习基于SQL映射的MyBatis框架,在本章中可以使用此框架操作主流的数据库,并学习MyBatis核心API的使用,采用自定义封装法简化MyBatis的操作代码,进而加快开发效率。

第5章主要讲解MyBatis映射相关的知识,包括<sql><resultMap><choose><set>和<foreach>等常用标签、DB连接信息存储到Properties文件的读取、使用JDBC数据源、别名typeAliases的配置、CLOB字段的读取以及分页等必备技术点。

第6章将介绍主流标签库FreeMarker和Thymeleaf的使用,以帮助读者实现视图层的静态化处理。

需要声明的是,本书不是Java Web的入门书。学习本书之前,读者要对Java Web中的JSP、Servlet等Web技术有所了解,并尽量能完整地使用JSP或Servlet开发一个小型项目。在此基础上再阅读本书,读者会发现代码的分层更加明确,结构更加清楚。

对于软件开发,实践才是硬道理,这就需要设计、排错,拥有更多的想法和经验,所以请拿起手中的键盘,练习一下吧!

尽量在自己的计算机中执行本书的所有代码,只读书不动手和自己动手存在天壤之别,两者结合起来才能更深刻地理解框架的各项功能。

由于JavaEE内容涵盖面广,涉及的知识点非常多,加之作者水平有限,错误之处在所难免,请各位读者赐教和斧正。读者可以通过邮箱279377921@qq.com与我联系。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供配套资源,要获得相关配套资源,请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

本书责任编辑的电子邮箱是fundaokun@ptpress.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书技术审校等工作,可以发邮件给本书的责任编辑。

如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社计算机图书出版的积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


本章目标

(1)反射技术的使用

(2)操作XML文件

(3)什么是IoC

(4)什么是IoC容器

(5)什么是依赖注入

(6)反射与依赖注入的关系

(7)实现装配JavaBean

本书针对Spring、Spring MVC、MyBatis三大框架的测试环境全部基于IntelliJ IDEA和Spring Boot。

框架就是软件功能的半成品。框架提供了一个软件项目中通用的功能,将大多数常见的功能进行封装,无须自己重复开发,增加了开发及运行效率。在软件公司中,大多数情况是使用框架开发软件项目。

Spring框架内部大量使用反射与操作XML技术,以至于MyBatis也高度依赖这两种技术。掌握这两种技术有助于高效理解与学习Java EE框架。

本书的全部案例均在IntelliJ IDEA开发工具中进行测试,项目类型为Maven。

本节介绍反射技术的基本使用,创建maven-archetype-quickstart类型的Maven项目reflectTest。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity;

public class Userinfo {

    private long id;
    private String username;
    private String password;

    public Userinfo() {
        System.out.println("public Userinfo()");
    }

    public Userinfo(long id) {
        super();
        this.id = id;
    }

    public Userinfo(long id, String username) {
        super();
        this.id = id;
        this.username = username;
    }

    public Userinfo(long id, String username, String password) {
        super();
        this.id = id;
        this.username = username;
        this.password = password;
        System.out.println("public Userinfo(long id, String username, String password)");
        System.out.println(id + " " + username + " " + password);

    }

    public long getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void test() {
        System.out.println("public void test1()");
    }

    public void test(String address) {
        System.out.println("public void test2(String address) address=" + address);
    }

    public String test(int age) {
        System.out.println("public String test3(int age) age=" + age);
        return "我是返回值";
    }
}

创建实体类Userinfo2,代码如下:

package com.ghy.www.entity;

public class Userinfo2 {

    private long id;
    private String username;
    private String password;

    private Userinfo2() {
        System.out.println("public Userinfo()");
    }

    public long getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

1.正常创建对象并调用方法

正常创建对象并调用方法的写法如下:

package com.ghy.www.test1;

import com.ghy.www.entity.Userinfo;

public class Test1 {
    public static void main(String[] args) {
        Userinfo userinfo = new Userinfo();
        userinfo.setId(100);
        userinfo.setUsername("中国");
        userinfo.setPassword("中国人");

        System.out.println(userinfo.getId());
        System.out.println(userinfo.getUsername());
        System.out.println(userinfo.getPassword());
    }
}

但在某些情况下,预创建对象的类名以字符串的形式存储在XML文件中,比如Servlet技术中的web.xml文件部分的示例代码如下:

<servlet>
    <servlet-name>InsertUserinfo</servlet-name>
    <servlet-class>controller.InsertUserinfo</servlet-class>
</servlet>

Servlet对象就是由Tomcat读取这段XML配置代码并解析出“controller.InsertUserinfo”字符串,然后使用反射技术动态创建出InsertUserinfo 类的对象。可见反射技术无处不在,是学习框架技术非常重要的知识点。

下面我们来模拟一下Tomcat处理的过程。

在resources文件夹中创建文件createClassName.txt,内容如下:

com.ghy.www.entity.Userinfo

创建运行类Test2,代码如下:

package com.ghy.www.test1;

import java.io.IOException;
import java.io.InputStream;

public class Test2 {
    public static void main(String[] args) throws IOException {
        byte[] byteArray = new byte[1000];
        InputStream inputStream = Test2.class.getResourceAsStream("/createClassName.txt");
        int readLength = inputStream.read(byteArray);
        String createClassName = new String(byteArray, 0, readLength);
        System.out.println(createClassName);
        inputStream.close();
    }
}

程序运行结果如下:

com.ghy.www.entity.Userinfo

变量createClassName存储的就是类名“com.ghy.www.entity.Userinfo”字符串,这时如果使用图1-1所示的代码就是错误的,会出现编译错误,Java编译器根本不允许使用这种写法创建com.ghy.www.entity.Userinfo类的对象,但需求是必须把类名“com.ghy.www.entity.Userinfo”对应的对象创建出来,对于这种情况可以使用反射技术来解决。反射是在运行时获得对象信息的一种技术。

图1-1 错误的代码

2.获得Class类的对象的方式

在使用反射技术之前,必须要获得某一个*.class类对应Class类的对象。

Class类封装了类的信息(属性、构造方法和方法等),而Class类的对象封装了具体的*.class类中的信息(属性、构造方法和方法等),有了这些信息,就相当于烹饪加工时有了制造食品的原料,就可以创建出类的对象。

有4种方式可以获得Class类的对象,代码如下:

package com.ghy.www.test1;

import com.ghy.www.entity.Userinfo;

public class Test4 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class class1 = Userinfo.class;
        Class class2 = new Userinfo().getClass();
        Class class3 = Userinfo.class.getClassLoader().loadClass("com.ghy.www.entity.Userinfo");
        Class class4 = Class.forName("com.ghy.www.entity.Userinfo");
        System.out.println(class1.hashCode());
        System.out.println(class2.hashCode());
        System.out.println(class3.hashCode());
        System.out.println(class4.hashCode());
    }
}

程序运行结果如下:

public Userinfo()
460141958
460141958
460141958
460141958

从打印结果可以分析出,同一个*.class类对应的Class类的对象是同一个,单例的。

3.通过Class对象获得Field、Constructor和Method对象

一个*.class类包含Field、Constructor和Method信息,可以通过Class对象来获取这些信息。

示例代码如下:

package com.ghy.www.test1;

import com.ghy.www.entity.Userinfo;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Test5 {
    public static void main(String[] args) {
        Class userinfoClass = Userinfo.class;
        // 属性列表
 Field[] a = userinfoClass.getDeclaredFields();
        System.out.println(a.length);
        for (int i = 0; i < a.length; i++) {
            System.out.println(a[i].getName());
        }
        System.out.println();
        // 构造方法列表
 Constructor[] b = userinfoClass.getConstructors();
        System.out.println(b.length);
        System.out.println();
        // 方法列表
 Method[] c = userinfoClass.getDeclaredMethods();
        System.out.println(c.length);
        for (int i = 0; i < c.length; i++) {
            System.out.println(c[i].getName());
        }
    }
}

程序运行结果如下:

3
id
username
password

4

9
getId
test
test
test
getPassword
getUsername
setPassword
setId
setUsername

4.使用Class.newInstance()方法创建对象

方法Class.newInstance()调用的是无参构造方法。

示例代码如下:

package com.ghy.www.test1;

import com.ghy.www.entity.Userinfo;

public class Test6 {
    public static void main(String[] args)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        // 本实验不用new来创建对象,原理就是使用反射技术
 String newObjectName = "com.ghy.www.entity.Userinfo";
        Class class4 = Class.forName(newObjectName);
        // newInstance()开始创建对象
 Userinfo userinfo = (Userinfo) class4.newInstance();
        userinfo.setUsername("美国");
        System.out.println(userinfo.getUsername());
    }
}

程序运行结果如下:

public Userinfo()
美国

5.对Field进行赋值和取值

示例代码如下:

package com.ghy.www.test1;

import com.ghy.www.entity.Userinfo;

import java.lang.reflect.Field;

public class Test7 {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException, SecurityException {
        String newObjectName = "com.ghy.www.entity.Userinfo";
        Class class4 = Class.forName(newObjectName);
        Userinfo userinfo = (Userinfo) class4.newInstance();

        String fieldName = "username";
        String fieldValue = "法国";
        Field fieldObject = userinfo.getClass().getDeclaredField(fieldName);
        System.out.println(fieldObject.getName());
        fieldObject.setAccessible(true);
        fieldObject.set(userinfo, fieldValue);
        System.out.println(userinfo.getUsername());
        System.out.println(fieldObject.get(userinfo));
    }
}

程序运行结果如下:

public Userinfo()
username
法国
法国

6.获得构造方法对应的Constructor对象及调用无参构造方法

示例代码如下:

package com.ghy.www.test1;

import com.ghy.www.entity.Userinfo;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Test8 {
    public static void main(String[] args)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
        String newObjectName = "com.ghy.www.entity.Userinfo";
        Class class4 = Class.forName(newObjectName);
        Constructor constructor1 = class4.getDeclaredConstructor();
        Userinfo userinfo1 = (Userinfo) constructor1.newInstance();
        Userinfo userinfo2 = (Userinfo) constructor1.newInstance();
        Userinfo userinfo3 = (Userinfo) constructor1.newInstance();
        System.out.println(userinfo1.hashCode());
        System.out.println(userinfo2.hashCode());
        System.out.println(userinfo3.hashCode());
    }
}

程序运行结果如下:

public Userinfo()
public Userinfo()
public Userinfo()
460141958
1163157884
1956725890

7.通过Constructor对象调用有参构造方法

示例代码如下:

package com.ghy.www.test1;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Test9 {
    public static void main(String[] args)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
        String newObjectName = "com.ghy.www.entity.Userinfo";
        Class class4 = Class.forName(newObjectName);
        Constructor constructor = class4.getDeclaredConstructor(long.class, 
String.class, String.class);
        System.out.println(constructor.newInstance(11, "a1", "aa1").hashCode());
        System.out.println(constructor.newInstance(12, "a2", "aa2").hashCode());
        System.out.println(constructor.newInstance(13, "a3", "aa3").hashCode());
        System.out.println(constructor.newInstance(14, "a4", "aa4").hashCode());
    }
}

程序运行结果如下:

public Userinfo(long id, String username, String password)
11 a1 aa1
460141958
public Userinfo(long id, String username, String password)
12 a2 aa2
1163157884
public Userinfo(long id, String username, String password)
13 a3 aa3
1956725890
public Userinfo(long id, String username, String password)
14 a4 aa4
356573597

8.使用反射动态调用无参无返回值方法

示例代码如下:

package com.ghy.www.test1;

import com.ghy.www.entity.Userinfo;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test10 {
    public static void main(String[] args)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
        String newObjectName = "com.ghy.www.entity.Userinfo";
        Class class4 = Class.forName(newObjectName);
        Userinfo userinfo = (Userinfo) class4.newInstance();
        String methodName = "test";
        Method method = class4.getDeclaredMethod(methodName);
        System.out.println(method.getName());
        method.invoke(userinfo);
    }
}

程序运行结果如下:

public Userinfo()
test
public void test1()

9.使用反射动态调用有参无返回值方法

示例代码如下:

package com.ghy.www.test1;

import com.ghy.www.entity.Userinfo;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test11 {
    public static void main(String[] args)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
        String newObjectName = "com.ghy.www.entity.Userinfo";
        Class class4 = Class.forName(newObjectName);
        Userinfo userinfo = (Userinfo) class4.newInstance();
        String methodName = "test";
        Method method = class4.getDeclaredMethod(methodName, String.class);
        System.out.println(method.getName());
        method.invoke(userinfo, "我的地址在北京!");
    }
}

程序运行结果如下:

public Userinfo()
test
public void test2(String address) address=我的地址在北京!

10.使用反射动态调用有参有返回值方法

示例代码如下:

package com.ghy.www.test1;

import com.ghy.www.entity.Userinfo;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test12 {
    public static void main(String[] args)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
        String newObjectName = "com.ghy.www.entity.Userinfo";
        Class class4 = Class.forName(newObjectName);
        Userinfo userinfo = (Userinfo) class4.newInstance();
        String methodName = "test";
        Method method = class4.getDeclaredMethod(methodName, int.class);
        System.out.println(method.getName());
        Object returnValue = method.invoke(userinfo, 999999);
        System.out.println("returnValue=" + returnValue);
    }
}

程序运行结果如下:

public Userinfo()
test
public String test3(int age) age=999999
returnValue=我是返回值

11.反射破坏了OOP

创建单例模式,代码如下:

package com.ghy.www.objectfactory;

//单例模式
//保证当前进程中只有1个OneInstance类的对象
public class OneInstance {
    private static OneInstance oneInstance;

    private OneInstance() {
    }

    public static OneInstance getOneInstance() {
        if (oneInstance == null) {
            oneInstance = new OneInstance();
        }
        return oneInstance;
    }
}

由于构造方法是private(私有)的,因此不能new实例化对象,只能调用public static OneInstance getOneInstance()方法获得自身的对象,而且可以保证单例,测试代码如下:

package com.ghy.www.test1;

import com.ghy.www.objectfactory.OneInstance;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;

public class Test13 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
        OneInstance o1 = OneInstance.getOneInstance();
        OneInstance o2 = OneInstance.getOneInstance();
        OneInstance o3 = OneInstance.getOneInstance();
        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o3);
    }
}

程序运行结果如下:

com.ghy.www.objectfactory.OneInstance@1b6d3586
com.ghy.www.objectfactory.OneInstance@1b6d3586
com.ghy.www.objectfactory.OneInstance@1b6d3586

但是,使用反射技术会破坏面向对象编程(OOP),导致创建多个OneInstance类的对象。

示例代码如下:

package com.ghy.www.test1;

import com.ghy.www.objectfactory.OneInstance;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Test14 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
        Class classRef = Class.forName("com.ghy.www.objectfactory.OneInstance");
        Constructor c = classRef.getDeclaredConstructor();
        c.setAccessible(true);
        OneInstance one1 = (OneInstance) c.newInstance();
        OneInstance one2 = (OneInstance) c.newInstance();
        System.out.println(one1);
        System.out.println(one2);
    }
}

程序运行结果如下:

com.ghy.www.objectfactory.OneInstance@1b6d3586
com.ghy.www.objectfactory.OneInstance@4554617c

这里创建了两个OneInstance类的对象,没有保证单例性。虽然构造方法是private的,但依然借助反射技术创建了多个对象。

12.方法重载

使用反射可以调用重载方法。

示例代码如下:

package com.ghy.www.test1;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test15 {
    public void testMethod(String username) {
        System.out.println("public String testMethod(String username)");
        System.out.println(username);
    }

    public void testMethod(String username, String password) {
        System.out.println("public String testMethod(String username, String password)");
        System.out.println(username + " " + password);
    }

    public static void main(String[] args)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
        Class class15Class = Test15.class;
        Object object = class15Class.newInstance();
        Method method1 = class15Class.getDeclaredMethod("testMethod", String.class);
        Method method2 = class15Class.getDeclaredMethod("testMethod", String.class, String.class);
        method1.invoke(object, "法国");
        method2.invoke(object, "中国", "中国人");
    }
}

程序运行结果如下:

public String testMethod(String username)
法国
public String testMethod(String username, String password)
中国 中国人

提起标记语言,人们首先会想起HTML,HTML提供了很多标签来实现Web前端界面的设计,但HTML中的标签并不允许自定义,如果想定义一些自己独有的标签,HTML就不再可行了,这时可以使用XML来进行实现。

XML(eXtensible Markup Language,可扩展标记语言)可以自定义标记名称与内容,在灵活度上相比HTML改善很多,经常用在配置以及数据交互领域。

在开发软件项目时,经常会接触XML文件,比如web.xml文件中就有XML代码,而Spring框架也使用XML文件存储配置。XML代码的主要作用就是配置。

创建maven-archetype-quickstart类型的Maven项目xmlTest,添加如下依赖:

<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>

在resources文件夹中创建struts.xml配置文件,代码如下:

<mymvc>
    <actions>
        <action name="list" class="controller.List">
            <result name="toListJSP">
                /list.jsp
            </result>
            <result name="toShowUserinfoList" type="redirect">
                showUserinfoList.ghy
            </result>
        </action>

        <action name="showUserinfoList" class="controller.ShowUserinfoList">
            <result name="toShowUserinfoListJSP">
                /showUserinfoList.jsp
            </result>
        </action>
    </actions>
</mymvc>

1.解析XML文件

创建Reader类,代码如下:

package com.ghy.www.test1;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.util.List;

public class Reader {
    public static void main(String[] args) {
        try {
           SAXReader reader = new SAXReader();
           Document document = reader.read(reader.getClass()
                  .getResourceAsStream("/struts.xml"));

           Element mymvcElement = document.getRootElement();
           System.out.println(mymvcElement.getName());
           Element actionsElement = mymvcElement.element("actions");
           System.out.println(actionsElement.getName());
           System.out.println("");
           List<Element> actionList = actionsElement.elements("action");
           for (int i = 0; i < actionList.size(); i++) {
              Element actionElement = actionList.get(i);
              System.out.println(actionElement.getName());
              System.out.print("name="
                     + actionElement.attribute("name").getValue());
              System.out.println("action class="
                     + actionElement.attribute("class").getValue());

              List<Element> resultList = actionElement.elements("result");
              for (int j = 0; j < resultList.size(); j++) {
                 Element resultElement = resultList.get(j);
                 System.out.print("  result name="
                        + resultElement.attribute("name").getValue());
                 Attribute typeAttribute = resultElement.attribute("type");
                 if (typeAttribute != null) {
                    System.out.println(" type=" + typeAttribute.getValue());
                 } else {
                    System.out.println("");
                 }
                 System.out.println("   " + resultElement.getText().trim());
                 System.out.println("");
              }

              System.out.println("");
           }

        } catch (DocumentException e) {
           // 自动生成catch块
 e.printStackTrace();
        }
    }
}

程序运行结果如下:

mymvc
actions

action
name=listaction class=controller.List
  result name=toListJSP
   /list.jsp

  result name=toShowUserinfoList type=redirect
   showUserinfoList.ghy

action
name=showUserinfoListaction class=controller.ShowUserinfoList
  result name=toShowUserinfoListJSP
   /showUserinfoList.jsp

2.创建XML文件

创建Writer类,代码如下:

package com.ghy.www.test1;

import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;

import java.io.FileWriter;
import java.io.IOException;

public class Writer {

    public static void main(String[] args) {
        try {
           Document document = DocumentHelper.createDocument();
           Element mymvcElement = document.addElement("mymvc");
           Element actionsElement = mymvcElement.addElement("actions");
           //
 Element listActionElement = actionsElement.addElement("action");
           listActionElement.addAttribute("name", "list");
           listActionElement.addAttribute("class", "controller.List");

           Element toListJSPResultElement = listActionElement
                  .addElement("result");
           toListJSPResultElement.addAttribute("name", "toListJSP");
           toListJSPResultElement.setText("/list.jsp");

           Element toShowUserinfoListResultElement = listActionElement
                  .addElement("result");
           toShowUserinfoListResultElement.addAttribute("name",
                  "toShowUserinfoList");
           toShowUserinfoListResultElement.addAttribute("type", "redirect");
           toShowUserinfoListResultElement.setText("showUserinfoList.ghy");
           //

 Element showUserinfoListActionElement = actionsElement
                  .addElement("action");
           showUserinfoListActionElement.addAttribute("name",
                  "showUserinfoList");
           showUserinfoListActionElement.addAttribute("class",
                  "controller.ShowUserinfoList");

           Element toShowUserinfoListJSPResultElement = showUserinfoListActionElement
                  .addElement("result");
           toShowUserinfoListJSPResultElement.addAttribute("name",
                  "toShowUserinfoListJSP");
           toShowUserinfoListResultElement.setText("/showUserinfoList.jsp");
           //

 OutputFormat format = OutputFormat.createPrettyPrint();
           XMLWriter writer = new XMLWriter(new FileWriter("ghy.xml"), format);
           writer.write(document);
           writer.close();

        } catch (IOException e) {
           e.printStackTrace();
        }

    }

}

程序运行后在项目中创建ghy.xml文件,如图1-2所示。

图1-2 创建的ghy.xml文件

3.修改XML文件

创建Update类,代码如下:

package com.ghy.www.test1;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

import java.io.FileWriter;
import java.io.IOException;
import java.util.List;

public class Update {

    public static void main(String[] args) throws IOException {
        try {
           SAXReader reader = new SAXReader();
           Document document = reader.read(reader.getClass().getResourceAsStream("/struts.xml"));
           Element mymvcElement = document.getRootElement();
           Element actionsElement = mymvcElement.element("actions");
           List<Element> actionList = actionsElement.elements("action");
           for (int i = 0; i < actionList.size(); i++) {
              Element actionElement = actionList.get(i);
              List<Element> resultList = actionElement.elements("result");
              for (int j = 0; j < resultList.size(); j++) {
                 Element resultElement = resultList.get(j);
                 String resultName = resultElement.attribute("name").getValue();
                 if (resultName.equals("toShowUserinfoList")) {
                    Attribute typeAttribute = resultElement.attribute("type");
                    if (typeAttribute != null) {
                       typeAttribute.setValue("zzzzzzzzzzzzzzzzzzzzzz");
                       resultElement.setText("xxxxxxxxxxxxxxxxxxx");
                    }
                 }
              }
           }
           OutputFormat format = OutputFormat.createPrettyPrint();
           XMLWriter writer = new XMLWriter(new FileWriter("src\\ghy.xml"), format);
           writer.write(document);
           writer.close();
        } catch (DocumentException e) {
           e.printStackTrace();
        }
    }
}

程序运行后产生的ghy.xml文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<mymvc>
    <actions>
        <action name="list" class="controller.List">
            <result name="toListJSP">/list.jsp</result>
            <result name="toShowUserinfoList" type="zzzzzzzzzzzzzzzzzzzzzz">xxxxxxxxxxxxxxxxxxx</result>
        </action>
        <action name="showUserinfoList" class="controller.ShowUserinfoList">
            <result name="toShowUserinfoListJSP">/showUserinfoList.jsp</result>
        </action>
    </actions>
</mymvc>

成功更改XML文件中的属性值与文本内容。

4.删除节点

创建Delete类,代码如下:

package com.ghy.www.test1;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

import java.io.FileWriter;
import java.io.IOException;
import java.util.List;

public class Delete {

    public static void main(String[] args) throws IOException {
        try {
           SAXReader reader = new SAXReader();
           Document document = reader.read(reader.getClass().getResourceAsStream("/struts.xml"));
           Element mymvcElement = document.getRootElement();
           Element actionsElement = mymvcElement.element("actions");
           List<Element> actionList = actionsElement.elements("action");
           for (int i = 0; i < actionList.size(); i++) {
              Element actionElement = actionList.get(i);
              List<Element> resultList = actionElement.elements("result");
              Element resultElement = null;
              boolean isFindNode = false;
              for (int j = 0; j < resultList.size(); j++) {
                 resultElement = resultList.get(j);
                 String resultName = resultElement.attribute("name").getValue();
                 if (resultName.equals("toShowUserinfoList")) {
                    isFindNode = true;
                    break;
                 }
              }
              if (isFindNode == true) {
                 actionElement.remove(resultElement);
              }
           }
           OutputFormat format = OutputFormat.createPrettyPrint();
           XMLWriter writer = new XMLWriter(new FileWriter("src\\ghy.xml"), format);
           writer.write(document);
           writer.close();
        } catch (DocumentException e) {
           e.printStackTrace();
        }
    }

}

程序运行后产生的ghy.xml文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>

<mymvc>
    <actions>
        <action name="list" class="controller.List">
            <result name="toListJSP">/list.jsp</result>
        </action>
        <action name="showUserinfoList" class="controller.ShowUserinfoList">
            <result name="toShowUserinfoListJSP">/showUserinfoList.jsp</result>
        </action>
    </actions>
</mymvc>

节点被成功删除。

5.删除属性

创建DeleteAttr类,代码如下:

package com.ghy.www.test1;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

import java.io.FileWriter;
import java.io.IOException;
import java.util.List;

public class DeleteAttr {

    public static void main(String[] args) throws IOException {
        try {
           SAXReader reader = new SAXReader();
           Document document = reader.read(reader.getClass().getResourceAsStream("/struts.xml"));
           Element mymvcElement = document.getRootElement();
           Element actionsElement = mymvcElement.element("actions");
           List<Element> actionList = actionsElement.elements("action");
           for (int i = 0; i < actionList.size(); i++) {
              Element actionElement = actionList.get(i);
              List<Element> resultList = actionElement.elements("result");
              for (int j = 0; j < resultList.size(); j++) {
                 Element resultElement = resultList.get(j);
                 String resultName = resultElement.attribute("name").getValue();
                 if (resultName.equals("toShowUserinfoList")) {
                    Attribute typeAttribute = resultElement.attribute("type");
                    if (typeAttribute != null) {
                       resultElement.remove(typeAttribute);
                    }
                 }
              }
           }
           OutputFormat format = OutputFormat.createPrettyPrint();
           XMLWriter writer = new XMLWriter(new FileWriter("src\\ghy.xml"), format);
           writer.write(document);
           writer.close();
        } catch (DocumentException e) {
           e.printStackTrace();
        }
    }
}

程序运行后产生的ghy.xml文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>

<mymvc>
    <actions>
        <action name="list" class="controller.List">
            <result name="toListJSP">/list.jsp</result>
            <result name="toShowUserinfoList">showUserinfoList.ghy</result>
        </action>
        <action name="showUserinfoList" class="controller.ShowUserinfoList">
            <result name="toShowUserinfoListJSP">/showUserinfoList.jsp</result>
        </action>
    </actions>
</mymvc>

XML文件中的type属性成功被删除。

Spring框架简化了Java EE开发的流程,它是为了应对企业级应用开发的复杂性而创建的,其强大之处在于对Java EE开发进行全方位的简化,对大部分常用的功能进行了封装,比如管理JavaBean,包含创建及销毁,还提供了基于Web的MVC分层框架,支持数据库操作、安全验证等功能,但这些功能的实现却要依赖两个技术原理:控制反转(Inversion of Control,IoC)和面向切面编程(Aspect Oriented Programming,AOP)。本书的目的就是使读者学习并掌握Spring中的这两个核心技术,以在实际的软件开发中得以运用。

Spring是一个开放源代码的Java EE框架,使用Spring简化了Java EE开发,提升了Java EE软件项目的开发效率,提高开发效率的办法就是使用模块架构,每个模块处理一个功能或者业务。模块架构允许程序员选择使用哪一个模块参与开发,同时为Java EE应用程序开发提供集成的容器。在Spring框架中提供了1个JavaBean容器(可以暂时将容器理解为1个List),在该容器中存储不同数据类型的JavaBean对象。在容器中,可以将很多种不同功能的JavaBean进行整合和集成,以达到多个技术综合应用的目的。

Spring框架发展多年,现在已经是一个初具规模的Java EE开发平台,在Spring 5中的主要模块如下。

(1)Core(核心)模块:依赖注入(dependency injection)、事件处理(events)、资源访问(resources)、国际化(i18n)、验证(validation)、数据绑定(data binding)、数据类型转换(type conversion)、表达式语言(SpEL)、面向切面编程(AOP)。

(2)Testing(测试)模块:模拟对象(mock objects)、TestContext框架、Spring MVC Test、WebTestClient测试框架。

(3)Data Access(数据访问)模块:事务处理(transactions)、数据访问对象(DAO)支持、Java数据库连接(JDBC)、对象关系映射(ORM)、处理XML(Marshalling XML)。

(4)Spring MVC和Spring WebFlux Web框架模块。

(5)Integration(集成)模块:远程访问(remoting)、Java消息服务(JMS)、Java加密体系结构(JCA)、Java管理扩展(JMX)、邮件处理(email)、任务(task)、执行计划(scheduling)、缓存(cache)。

(6)Languages(语言)模块:支持使用Kotlin、Groovy、动态语言等进行开发。

Spring框架的功能可以用在任何Java EE服务器中,其核心要点是,保证相同代码在不同Java EE容器的可移植性。

在没有Spring框架的时候,如果在A类中使用B类,则必须在A类中new实例化出B类的对象,这就造成了A类和B类的紧耦合,A类完全依赖B类的功能实现,这样的情况就属于典型的“侵入式开发”。随着软件业务的复杂度提升,当原有的B类不能满足A类的功能实现时,就需要创建更为高级的BExt类,结果就是把所有实例化B类的代码替换成new BExt()代码,这就产生了源代码的改动,不利于软件运行的稳定性,并不符合商业软件的开发与维护流程。IoC技术就可以解决这样的问题,其办法就是使用“反射”技术,动态地对一个类中的属性进行反射赋值,对于这样的功能,Spring形成了一个模块,模块的功能非常强大,并且Spring对这种机制进行了命名,叫作控制反转(Inversion of Control,IoC)。

IoC要达到的目的就是将调用者与被调用者进行分离,让类与类之间的关系进行解耦,这是一种设计思想。解耦的原理如图1-3所示。

图1-3 使用IoC实现解耦

Spring框架中的IoC技术可以实现A类和B类的解耦,在A类中不再出现new B()的情况,实例化new B()类的任务由Spring框架来进行处理,Spring框架再使用反射的机制将B类的对象赋值给A类中的B b变量。原来A类是主动实例化B类的对象,控制方是A类,而现在以被动的方式由Spring框架来进行实现,控制方现在变成了Spring框架,实现了反转,所以此种技术称为“控制反转”。

IoC是一个理念,也是一种设计思想。A类中的B b对象的值是需要被赋值的,实现的方式是依赖注入(Dependency Injection,DI)。依赖注入是IoC思想的实现,侧重于实现,A类依赖B类,B类的对象由容器进行创建,容器再对A类中的B b对象进行值的注入。依赖注入在Java中的底层技术原理就是“反射”,使用反射技术对某一个类中的属性进行动态赋值,以达到A模块和B模块之间的解耦。

在Spring中,官方将管理JavaBean的容器定义为IoC容器(IoC container),而在Spring中对IoC的主要实现方式就是依赖注入,依赖注入的称呼更容易被人所接受。

什么是IoC容器?前面介绍过,Spring的依赖注入其实就是对JavaBean的属性使用反射技术进行赋值,当有很多的JavaBean需要这样的操作时,这些JavaBean的管理就成了问题,因为某些JavaBean之间需要关联,而某些JavaBean之间并不需要关联,而且所有这些JavaBean的创建和销毁都要统一的调度,由Spring框架管理它们的生命周期,为了方便这种管理,Spring框架提供了IoC容器,对JavaBean进行统一组织,便于后期代码的维护。曾经在任意的位置进行new实例化任何类对象的情况不复存在了,所有new实例化类的对象的任务都要交给IoC容器实现,这时,对JavaBean的管理就更加规范了。

另外,Spring的IoC容器完全脱离了平台,可以在任何支持Java语言的环境中运行,具有极好的可移植性。IoC容器就是管理JavaBean并创建JavaBean的一个内存区。

IoC/依赖注入从编程技术上来讲就是将接口和实现相分离,然后使用反射技术对类中的属性进行动态赋值。IoC容器的职责是管理JavaBean的生命周期,处理多个JavaBean之间的注入关系。

通过前面的解释,我们已经大概了解IoC与IoC容器,以及依赖注入的作用与使用场景了。程序员在任何位置创建任何类的对象在Spring框架中是不规范的,Spring框架对JavaBean的管理更加具有规划性,比如创建、销毁,还可以动态地对一个属性注入值,通过使用Spring的IoC容器,使软件项目对JavaBean的管理更加统一。

什么是AOP呢?AOP是面向切面编程(Aspect Oriented Programming)。在没有AOP技术时,如果我们想对软件项目记录日志,则必须要在关键的业务点写上记录日志的程序代码,日志的信息包含“开始时间”“结束时间”“执行人”以及“角色”等信息,随着软件项目越来越稳定,曾经的日志代码需要删除,因为输出日志会影响程序运行的效率,这时就要在Java源代码中删除记录日志的代码,造成代码的改动。另外,在未来我们有可能还需要记录日志,到时还要重新加入日志的代码,造成源代码的反复更改,不利于软件运行的稳定性。使用Spring的AOP技术就可以解决这些具有“通用性”的问题,Spring的AOP功能模块具有可插拔性,所以几乎不需要大幅更改代码即可完成前面想要实现的功能。

依赖注入使用的技术原理是“反射”,AOP使用的技术原理是“动态代理”。代理设计模式是23个标准设计模式中的一个。代理设计模式分为静态代理与动态代理。动态代理是在不改变原有代码的基础上,对原有的模块进行功能上的扩展,使原有的模块与扩展后的模块充分解耦,利于软件项目的模块化设计。应用AOP的场景可以在不改变Controller(控制层)代码的基础上加入日志的功能,在不改变Controller代码的基础上加入数据库事务的功能等。所以AOP主要实现对功能模块进行扩展与模块间的解耦合。

AOP的详细内容将在第2章介绍。

前面我们讨论了使用IoC可以实现解耦,依赖注入是IoC思想的实现,那么如何在Spring框架中实现解耦呢?在介绍如何实现解耦之前,我们先看一下使用传统的方法实现一个数据保存功能的弊端。

创建测试项目firstSaveTest。

创建业务类SaveDBService,代码如下:

package com.ghy.www.service;

public class SaveDBService {
    public void saveMethod() {
        System.out.println("将数据保存到数据库");
    }
}

创建运行类Test,代码如下:

package com.ghy.www.test1;

import com.ghy.www.service.SaveDBService;

public class Test {
    private SaveDBService service = new SaveDBService();

    public SaveDBService getService() {
        return service;
    }

    public void setService(SaveDBService service) {
        this.service = service;
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.getService().saveMethod();
    }
}

程序运行结果如下:

将数据保存到数据库

控制台输出正确的结果,这样的代码结构在大多数的软件项目中被使用,而且有些已经应用到商业项目中,虽然输出的结果是正确的,但从项目的整体设计结构上来看,明显是不合理的,比如以下几点。

(1)源代码反复被修改。本项目将数据保存到数据库,如果换成保存到XML文件中,就不得不更改程序,将SaveDBService类中的代码改成保存到XML文件,或者创建新的SaveXMLService类,然后更改代码,变成实例化对象(private SaveXMLService service = new SaveXMLService();),这就造成了程序的改动,不利于项目运行的稳定性。

(2)出现紧耦合。Test类和SaveDBService类产生了紧耦合,Test类负责创建SaveDBService类的对象,不利于软件功能的扩展、测试与复用。

(3)无法保证单例性。SaveDBService类无法在单实例的情况下被复用,因为它的声明是在Test类中,也就是在Test类中可以随意地创建出很多SaveDBService类的实例,无法保证该类实例的单例性。

(4)无法保证资源被正确释放。如果从SaveDBService类中获取一些资源,比如数据库的连接(Connection),输入输出流(Stream)等,那么Test类不得不维护这些资源的开启(open)和关闭(close)。如果出现忘记关闭的情况,则资源不能有效释放,造成资源占用,影响项目运行的稳定性。

以上4个缺点造成了这个设计是失败的,那么,根据面向对象三大特性中的“多态”技术,可以把一个SaveDBService类改成接口与实现类的模式吗?也就是在Test类中声明接口,然后实例化这个接口的实现类(测试代码在firstSaveTestInterface项目中)。这样的设计的确比上面的示例要灵活一些,也符合面向接口编程的方式,但这仅仅是将接口和实现进行分离,并没有完全解决业务变更后源代码还要被更改的问题。那么,如果在项目中经常有这样耦合的结构,该如何解决这样的问题?Spring是如何解决的呢?

在IoC容器的帮助下,可以实现松耦合,并且模块之间是分离的,彼此可以互相访问。使用依赖注入技术加上IoC容器之后,创建对象的操作是由IoC容器来进行控制的,并且也完全基于接口(interface)和实现类(imple)的分离开发。这样将SaveDBService类的控制权由原来在Test类中转变成在IoC容器中,接口的实现类是依赖IoC容器进行注入赋值的。下面就使用Spring框架来解决这两个类之间的紧耦合问题,把这个问题划上一个句号。

创建测试项目spring-first。

添加pom.xml配置,代码如下:

<parent>
    <!-- 从SpringBoot继承默认的配置 -->
 <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

创建一个保存数据的接口,代码如下:

package com.ghy.www.service;

public interface ISaveService {
    public void saveMethod();
}

创建一个将数据保存到数据库的实现类,代码如下:

package com.ghy.www.service;

public class SaveDBService implements ISaveService {
    public SaveDBService() {
        System.out.println("Spring通过反射机制来实例化SaveDBService类的对象 " + this);
    }

    @Override
    public void saveMethod() {
        System.out.println("将数据保存到数据库");
    }
}

创建一个将数据保存到XML文件的实现类,代码如下:

package com.ghy.www.service;

public class SaveXMLService implements ISaveService {
    public SaveXMLService() {
        System.out.println("Spring通过反射机制来实例化SaveXMLService类的对象 " + this);
    }

    @Override
    public void saveMethod() {
        System.out.println("将数据保存到XML文件");
    }
}

创建运行类Test,代码如下:

package com.ghy.www.test1;

import com.ghy.www.service.ISaveService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
    private ISaveService service;
    private String username;

    public ISaveService getService() {
        return service;
    }

    public void setService(ISaveService service) {
        this.service = service;
        System.out.println("setService(ISaveService service) service=" + service);
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Test test = context.getBean(Test.class);
        test.getService().saveMethod();
        System.out.println(test.getUsername());
    }
}

使用接口ApplicationContext可以从IoC容器中获得JavaBean对象。

创建IoC容器可以使用ClassPathXmlApplicationContext类加载applicationContext.xml配置文件来实现,然后使用ApplicationContext.getBean()方法从容器中获得Java对象。

在resources文件夹中创建配置文件applicationContext.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">

    <bean id="saveDBService" class="com.ghy.www.service.SaveDBService"></bean>
    <bean id="saveXMLService" class="com.ghy.www.service.SaveXMLService"></bean>
    <bean id="test" class="com.ghy.www.test1.Test">
        <property name="service" ref="saveDBService"></property>
        <property name="username" value="我是username的值"></property>
    </bean>

</beans>

配置代码如下:

<bean id="saveDBService" class="com.ghy.www.service.SaveDBService"></bean>

它的作用是通过反射机制在IoC容器中创建com.ghy.www.service.SaveDBService类的对象,对象的别名就是id属性值"saveDBService"。

配置代码如下:

<bean id="test" class="com.ghy.www.test1.Test">
   <property name="service" ref="saveDBService"></property>
   <property name="username" value="我是username的值"></property>
</bean>

它的作用是创建com.ghy.www.test1包中Test类的对象,并且对Test类对象中的service属性进行反射赋值,也就是依赖注入,赋值的方式是通过反射调用public void setService (ISaveService service)方法。赋予的值来自ref="saveDBService"配置代码。ref属性名代表反射赋予的值是复杂对象,是引用数据类型,ref属性值"saveDBService"是配置代码的id值:

<bean id="saveDBService" class="com.ghy.www.service.SaveDBService"></bean>

配置代码如下:

<property name="username" value="我是username的值"></property>

它的作用是对username属性注入值。在IoC容器中,如果注入的数据类型是简单数据类型或String数据类型,则要使用value属性,ref属性注入的是引用数据类型,这是value属性和ref属性的区别。

注入关系如图1-4所示。

图1-4 注入关系

图1-4要说明的是对com.ghy.www.test1包中的Test类中的service属性注入别名为saveDBService的对象,对username属性注入字符串"我是username的值"。

程序运行结果如下:

Spring通过反射机制来实例化SaveDBService类的对象 com.ghy.www.service.SaveDBService@56235b8e
Spring通过反射机制来实例化SaveXMLService类的对象 com.ghy.www.service.SaveXMLService@cd2dae5
setService(ISaveService service) service=com.ghy.www.service.SaveDBService@56235b8e
将数据保存到数据库
我是username的值

接口ISaveService的实现类不再由Test类创建,而是由Spring的IoC容器进行创建与管理。接口ISaveService的实现类与Test类完全解耦。

通过使用Spring框架,如果业务变更,在需要用XML文件保存数据的情况下,只需要创建一个将数据保存到XML文件的实现类,并且对service属性进行注入,这样就可以非常灵活地将数据保存到数据库或XML文件中了,符合“开闭原则”的设计思想,即对扩展开放,对修改关闭。

另外,通过使用IoC容器来对创建的JavaBean对象进行管理,完全可以非常独立地对各个模块进行依赖注入,这样使用所带来的结果就是软件的JavaBean完全复用了,因此使用Spring的IoC容器技术,可以使复杂的业务逻辑、多变的模块关系、烦琐的软件后期维护及扩展等问题得以解决,这也是Spring的“哲学思想”,即用最简单的技术原理(反射)解决复杂的问题。

使用传统方式保存数据时存在前面总结的4个问题,但通过使用Spring框架,这4个问题都一一被解决。

(1)源代码反复被修改。依赖关系不需要更改*.java文件,而只需要更改applicationContext.xml配置文件。

(2)出现紧耦合。A类和B类彻底解耦,B类由Spring框架创建,再向A类进行注入。

(3)无法保证单例性。IoC容器可以保证JavaBean的单例性,后面会验证。

(4)无法保证资源被正确释放。结合Spring框架中的AOP技术,会自动执行close()方法,资源永远会被自动释放。

Spring框架的IoC容器技术中大量使用了反射。我们可以查看一下堆栈信息,证明使用了反射技术创建对象,如图1-5所示。

图1-5 使用反射技术创建对象

我们继续证明使用反射技术实现依赖注入,如图1-6所示。

图1-6 使用反射技术实现依赖注入

下面就对依赖注入的细节进行进一步的介绍,深入理解IoC容器控制反转的理念与实现方式。

在Spring中创建JavaBean的实例时并不使用传统的实例化的方式new Object(),而是使用其他多种途径来创建出类的对象。如果使用new Object()方式创建对象,那么创建类的对象的目的是达成了,但对对象的管理却非常不方便,以致工程化程度不高,因此,在Spring框架中将创建的JavaBean对象放入IoC容器中,在容器中统一管理这些对象。

在Spring框架中使用两种方式创建对象,分别是XML声明法以及注解法。但使用XML声明法创建对象时容易造成applicationContext.xml文件中<bean>声明的配置代码过多而出现“声明爆炸”,对代码的后期维护非常不利,使用注解法可以解决这个问题。注解法也是Spring Boot默认使用的方式。

配置代码<context:component-scan base-package="">的作用是在指定的包中扫描符合创建对象的类,如果某些类需要被Spring实例化,则类的上方必须使用相关的注解(annotation)。

创建测试项目annotationCreateBean。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity;

import org.springframework.stereotype.Component;

@Component
public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this.hashCode());
    }
}

创建配置文件applicationContext.xml,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.ghy.www.entity"></context:component-scan>
</beans>

注解@Component的作用就是标识Userinfo类是IoC容器中的一个组件,能被<context: component-scan base-package="com.ghy.www.entity"></context:component-scan>扫描器所识别并进行自动实例化,最后将Userinfo类的对象放入IoC容器中。另外,我们也可以不使用注解@Component,转而使用注解@Repository来进行声明,程序运行结果是没有变化的。两个注解有下列区别。

(1)@Repository主要用来声明DAO层。

(2)@Component主要用来声明一些通用性的组件。

创建运行类Test1,代码如下:

package com.ghy.www.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    }
}

程序运行结果如下:

public Userinfo() 1059063940

我们使用注解法成功进行了Userinfo类的实例化。

使用<context:component-scan base-package="">创建对象后,获取对象时使用getBean()方法,示例代码如下:

package com.ghy.www.test;

import com.ghy.www.entity.Userinfo;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test2 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        System.out.println(context.getBean(Userinfo.class).hashCode());
        System.out.println(context.getBean("userinfo").hashCode());
    }
}

程序运行结果如下:

public Userinfo() 1059063940
1059063940
1059063940

在本节中我们将使用“全注解”的方式创建对象,而不再使用applicationContext.xml配置文件。“全注解”法也称为JavaConfig法。

配置文件applicationContext.xml的根节点是<beans>。<beans>起到全局配置定义的作用,它的子节点<bean>包含了创建JavaBean时的细节信息。

在使用“全注解”法创建对象时,与<beans>标记具有相同作用的注解是@Configuration,与<bean>标记具有相同功能的注解是@Bean。

创建测试项目allAnnotationCreateObject1。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity;

import org.springframework.stereotype.Component;

@Component
public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this.hashCode());
    }
}

创建配置类SpringConfig,代码如下:

package com.ghy.www.javaconfig;

import com.ghy.www.entity.Userinfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {
    @Bean(name = "u1")
    public Userinfo createUserinfo1() {
        Userinfo userinfo1 = new Userinfo();
        System.out.println("userinfo1 " + userinfo1.hashCode());
        return userinfo1;
    }

    @Bean(name = "u2")
    public Userinfo xxxxxxxxxxxxxxx() {
        Userinfo userinfo2 = new Userinfo();
        System.out.println("userinfo2 " + userinfo2.hashCode());
        return userinfo2;
    }
}

使用@Bean注解声明工厂方法的名称可以是任意的,但必须有返回值,不然会出现没有声明返回值的异常。

创建运行类Test1,代码如下:

package com.ghy.www.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www.javaconfig");
    }
}

实例化类(new AnnotationConfigApplicationContext())时传入参数"com.ghy.www.javaconfig"代表在"com.ghy.www.javaconfig"包中寻找哪个类带有注解@Configuration,再根据该类中的信息创建JavaBean对象。

程序运行结果如下:

public Userinfo() 1914301543
userinfo1 1914301543
public Userinfo() 1157726741
userinfo2 1157726741

我们成功使用“全注解”法创建了两个Userinfo类的对象。

注意:使用new AnnotationConfigApplicationContext("com.ghy.www.javaconfig");扫描配置类时,"com.ghy.www. javaconfig"包中可以有多个配置类。

在使用“全注解”法获取对象时,如果获取相同类型的对象,就会出现NoUniqueBeanDefinition Exception异常。

创建运行类Test2,代码如下:

package com.ghy.www.test;

import com.ghy.www.entity.Userinfo;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test2 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www.javaconfig");
        context.getBean(Userinfo.class);
    }
}

程序运行后出现如下异常:

Exception in thread "main" org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.ghy.www.entity.Userinfo' available: expected single matching bean but found 2: u1,u2

这说明有多个对象的数据类型是相同的,解决这个问题的办法是使用beanId。

创建运行类Test3,代码如下:

package com.ghy.www.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test3 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www.javaconfig");
        System.out.println();
        System.out.println(context.getBean("u2").hashCode());
    }
}

对注解@Bean中的name属性赋值,设置每个Userinfo类的对象拥有不同的id值,并且还要结合getBean(String)方法。

程序运行结果如下:

public Userinfo() 1914301543
userinfo1 1914301543
public Userinfo() 1157726741
userinfo2 1157726741

1157726741

与XML配置代码<context:component-scan base-package="packageName"></context:component- scan>作用相同的注解是@ComponentScan(basePackages = "packageName"),该注解也可以进行扫描并实例化。

创建测试项目ComponentScanTest。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity;

import org.springframework.stereotype.Component;

@Component
public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this.hashCode());
    }
}

创建工厂类SpringConfig,代码如下:

package com.ghy.www.javaconfig;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Date;

@Configuration
public class SpringConfig {
    @Bean
    public Date createDate() {
        Date nowDate = new Date();
        System.out.println("createDate " + nowDate.hashCode());
        return nowDate;
    }
}

创建运行类Test1,代码如下:

package com.ghy.www.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www.javaconfig");
    }
}

程序运行结果如下:

createDate -1505740341

从控制台输出的信息来看,只将Date类的对象进行了实例化,并没有创建Userinfo类的对象。如果我们想将其他包中的类进行实例化,就需要使用注解:

@ComponentScan(basePackages = "com.ghy.www.entity")

更改工厂类SpringConfig,代码如下:

package com.ghy.www.javaconfig;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import java.util.Date;

@Configuration
@ComponentScan(basePackages = "com.ghy.www.entity")
public class SpringConfig {
    @Bean
    public Date createDate() {
        Date nowDate = new Date();
        System.out.println("createDate " + nowDate.hashCode());
        return nowDate;
    }
}

程序运行结果如下:

public Userinfo() 1625082366
createDate -1505655400

注意:使用@ComponentScan注解可以扫描其他包中的类,那些类包含Spring Config配置类。

注解@ComponentScan支持对多个包进行扫描。

创建测试项目ComponentScanMorePackage。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity1;

import org.springframework.stereotype.Component;

@Component
public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this.hashCode());
    }
}

创建实体类Bookinfo,代码如下:

package com.ghy.www.entity2;

import org.springframework.stereotype.Component;

@Component
public class Bookinfo {
    public Bookinfo() {
        System.out.println("public Bookinfo() " + this.hashCode());
    }
}

创建工厂类SpringConfig,代码如下:

package com.ghy.www.javaconfig;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import java.util.Date;

@Configuration
@ComponentScan(basePackages = {"com.ghy.www.entity1", "com.ghy.www.entity2"})
public class SpringConfig {
    @Bean
    public Date createDate() {
        Date nowDate = new Date();
        System.out.println("createDate " + nowDate.hashCode());
        return nowDate;
    }
}

注解还可以使用如下写法:

@ComponentScan(basePackages = "com.ghy.www.entity1")
@ComponentScan(basePackages = "com.ghy.www.entity2")

其作用是相同的。

创建运行类Test1,代码如下:

package com.ghy.www.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www.javaconfig");
    }
}

程序运行结果如下:

public Userinfo() 384294141
public Bookinfo() 1024597427
createDate -1505250055

注解@ComponentScan的basePackageClasses属性的作用是扫描指定*.class文件所在的包路径,然后创建该包下以及其子孙包下类的对象。

创建测试项目basePackageClassesTest。创建的包结构如图1-7所示。

图1-7 项目结构

在com.ghy.www.a包中有A类,在com.ghy.www.a.b包中有B1类和B2类,在com.ghy.www.a.b.c包中有C类。

下面我们要使用basePackageClasses属性扫描B1类所在的包及其子孙包中所有的组件。

创建工厂类SpringConfig,代码如下:

package com.ghy.www.javaconfig;

import com.ghy.www.a.b.B1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import java.util.Date;

@Configuration
@ComponentScan(basePackageClasses = {B1.class})
public class SpringConfig {
    @Bean
    public Date createDate() {
        Date nowDate = new Date();
        System.out.println("createDate " + nowDate.hashCode());
        return nowDate;
    }
}

创建运行类Test1,代码如下:

package com.ghy.www.test;

import com.ghy.www.javaconfig.SpringConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
    }
}

程序运行结果如下:

public B1()
public B2()
public C()
createDate -1504539077

类A并没有被实例化,属性basePackageClasses只是将类Date、B1、B2和C进行了实例化,说明该属性实例化的对象为同级及子孙级包。

注解@ComponentScan还可以指定多个class文件所在的包路径并进行扫描,示例代码如下:

@ComponentScan(basePackageClasses = {X.class, Y.class})

注解@ComponentScan允许将上面这行代码拆分成两行,下面的写法是有效的:

@ComponentScan(basePackageClasses = X.class)
@ComponentScan(basePackageClasses = Y.class)

如果在使用注解@ComponentScan时不使用任何属性,那么注解@ComponentScan默认扫描的是使用注解@Configuration的配置类所在的包路径下的所有组件,包含子孙包中的组件。

创建测试项目ComponentScanTest2,该项目结构如图1-8所示。

图1-8 项目结构

创建工厂类SpringConfig,代码如下:

package com.ghy.www.a.b;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import java.util.Date;

@Configuration
@ComponentScan
public class SpringConfig {
    @Bean
    public Date createDate() {
        Date nowDate = new Date();
        System.out.println("createDate " + nowDate.hashCode());
        return nowDate;
    }
}

创建运行类Test1,代码如下:

package com.ghy.www.test;

import com.ghy.www.a.b.SpringConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
    }
}

程序运行后控制台输出如下:

public B()
public C()
createDate -1503759984

在不同的包中如果有相同的类名,则在扫描时会发生异常:

org.springframework.context.annotation.ConflictingBeanDefinitionException

创建测试项目packageDiffClassSame。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity1;

import org.springframework.stereotype.Component;

@Component
public class Userinfo {
    public Userinfo() {
        System.out.println("public com.ghy.www.entity1.Userinfo() " + this.hashCode());
    }
}

创建实体类Userinfo,代码如下:

package com.ghy.www.entity2;

import org.springframework.stereotype.Component;

@Component
public class Userinfo {
    public Userinfo() {
        System.out.println("public com.ghy.www.entity2.Userinfo() " + this.hashCode());
    }
}

创建工厂类SpringConfig,代码如下:

package com.ghy.www.javaconfig;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "com.ghy.www.entity1")
@ComponentScan(basePackages = "com.ghy.www.entity2")
public class SpringConfig {
}

创建运行类Test1,代码如下:

package com.ghy.www.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www.javaconfig");
    }
}

程序运行后控制台输出如下异常信息:

Exception in thread "main" org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [com.ghy.www.javaconfig.SpringConfig]; nested exception is org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'userinfo' for bean class [com.ghy.www.entity2.Userinfo] conflicts with existing, non-compatible bean definition of same name and class [com.ghy.www.entity1.Userinfo]

处理异常的方法有以下两种。

(1)将类名设置为不相同。

(2)为组件定义一个别名。

下面我们实现第2种方法——更改两个实体类,代码如下:

@Component(value = "userinfo1")
public class Userinfo {

@Component(value = "userinfo2")
public class Userinfo {

然后,创建新的运行类,代码如下:

package com.ghy.www.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test2 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www.javaconfig");
        System.out.println(context.getBean("userinfo1").hashCode());
        System.out.println(context.getBean("userinfo2").hashCode());
    }
}

再次运行程序,输出结果如下:

public com.ghy.www.entity1.Userinfo() 1344199921
public com.ghy.www.entity2.Userinfo() 2025269734
1344199921
2025269734

这样,不同包中有相同类名时出现异常的问题就被解决了。

在使用Spring注解进行扫描包操作时,好的实践方式是在包的root位置启用扫描来对子孙包中的类进行搜索。

创建测试用的项目codeStruts。项目结构如图1-9所示。

图1-9 推荐使用的项目结构

类Application的主要作用是在root位置启用扫描搜索,使用代码“new AnnotationConfig ApplicationContext("com.ghy.www");”针对root位置和所有子孙包中的组件进行实例化,这样就不需要再使用如下注解对指定的包进行扫描了:

@ComponentScan(value="packageName")

创建运行类Application,代码如下:

package com.ghy.www;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www");
    }
}

控制台输出结果如下:

public Userinfo1()
public Userinfo2()

创建测试项目annotationLazyTest。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity;

import org.springframework.stereotype.Component;

@Component
public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this);
    }
}

创建工厂类SpringConfig,代码如下:

package com.ghy.www.javaconfig;

import com.ghy.www.entity.Userinfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

@Configuration
public class SpringConfig {
    @Bean(name = "userinfo1")
    public Userinfo getUserinfo1() {
        Userinfo userinfo = new Userinfo();
        System.out.println("getUserinfo1 " + userinfo);
        return userinfo;
    }

    @Bean(name = "userinfo2")
    @Lazy(value = true)
    public Userinfo getUserinfo2() {
        Userinfo userinfo = new Userinfo();
        System.out.println("getUserinfo2 " + userinfo);
        return userinfo;
    }
}

注解@Lazy和@Component可以联合使用,代码如下:

@Component
@Lazy(value = true)

创建运行类Test1,代码如下:

package com.ghy.www.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www.javaconfig");
    }
}

程序运行结果如下:

public Userinfo() com.ghy.www.entity.Userinfo@65d6b83b
getUserinfo1 com.ghy.www.entity.Userinfo@65d6b83b

上述结果表明程序并没有执行getUserinfo2()方法来创建Userinfo类的对象,说明使用@Lazy(value = true)实现的效果是延迟加载。

创建运行类Test2,代码如下:

package com.ghy.www.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test2 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www.javaconfig");
        context.getBean("userinfo2");
    }
}

程序运行结果如下:

public Userinfo() com.ghy.www.entity.Userinfo@65d6b83b
getUserinfo1 com.ghy.www.entity.Userinfo@65d6b83b
public Userinfo() com.ghy.www.entity.Userinfo@1f3f4916
getUserinfo2 com.ghy.www.entity.Userinfo@1f3f4916

上述结果表明程序执行了 getUserinfo2()方法来创建 Userinfo 类的对象,说明执行代码context.getBean("userinfo2")才会创建Userinfo类的对象,属于延迟创建。

延迟加载是指当调用getBean()方法时JavaBean才会创建对象,不调用getBean()方法时JavaBean不创建对象。

使用“全注解”法会出现bean覆盖的情况。

创建测试项目beanOverriding。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity;

import org.springframework.stereotype.Component;

@Component
public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this);
    }
}

创建工厂类SpringConfig,代码如下:

package com.ghy.www.javaconfig;

import com.ghy.www.entity.Userinfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {
    @Bean(name = "userinfo")
    public Userinfo getUserinfo() {
        System.out.println("@Bean creator getUser");
        return new Userinfo();
    }
}

创建运行类Test1,代码如下:

package com.ghy.www.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www");
    }
}

程序运行后,控制台输出结果如下:

21:01:22.512 [main] DEBUG org.springframework.beans.factory.support.DefaultListable BeanFactory - Overriding bean definition for bean 'userinfo' with a different definition: replacing [Generic bean: class [com.ghy.www.entity.Userinfo]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in file [C:\Users\Administrator\Desktop\ssm\第1章\beanOverriding\target\classes\ com\ghy\www\entity\Userinfo.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=springConfig; factoryMethodName=getUserinfo; initMethodName=null; destroy MethodName=(inferred); defined in com.ghy.www.javaconfig.SpringConfig]
@Bean creator getUser
public Userinfo() com.ghy.www.entity.Userinfo@7b227d8d

控制台输出提示“Overriding bean definition”,而且并不是异常信息,这就会造成出现隐式的问题,本来想创建两个Userinfo类的对象,现在却只有一个。

出现“Overriding bean definition”的原因是在工厂方法中使用注解@Bean(name = "userinfo")创建Userinfo的别名是userinfo,而通过扫描创建Userinfo的别名也是userinfo,这就出现了JavaBean覆盖的情况,解决的办法就是将@Bean(name = "xxxxxxxxxx")的id设置为不同的值。解决此问题的项目是beanOverridingOK,项目beanOverridingOK的输出结果如下:

public Userinfo() com.ghy.www.entity.Userinfo@197d671
@Bean creator getUser
public Userinfo() com.ghy.www.entity.Userinfo@7219ec67

IoC容器对JavaBean的管理存在作用域,在Spring中一共有7种作用域:singleton、prototype、request、session、globalSession、application和websocket。

Singleton作用域代表在IoC容器中只有一个JavaBean的对象,而prototype代表当使用getBean()方法取得一个JavaBean时,IoC容器新建一个指定JavaBean的实例并且返回给程序员,每调用一次getBean()方法相当于执行一次new操作,它们的区别仅仅在于singleton永远是一个实例,而prototype是多实例。

Spring默认使用singleton(单例)。

在使用singleton时一定要注意JavaBean的线程安全问题。非线程安全是指多个线程访问同一个对象的同一个实例变量时,此变量值有可能被覆盖。

我们通过一个实例来具体看一下这两种作用域的区别!

创建测试项目scopeTest。

单例的示例代码如下:

package com.ghy.www.entity;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope(scopeName = "singleton")
public class Userinfo1 {
    public Userinfo1() {
        System.out.println("public Userinfo1() " + this);
    }
}

多例的示例代码如下:

package com.ghy.www.entity;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope(scopeName = "prototype")
public class Userinfo2 {
    public Userinfo2() {
        System.out.println("public Userinfo2() " + this);
    }
}

创建运行类Test,代码如下:

package com.ghy.www.test;

import com.ghy.www.entity.Userinfo1;
import com.ghy.www.entity.Userinfo2;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www");
        System.out.println(context.getBean(Userinfo1.class).hashCode());
        System.out.println(context.getBean(Userinfo1.class).hashCode());
        System.out.println(context.getBean(Userinfo1.class).hashCode());
        System.out.println();
        System.out.println();
        System.out.println(context.getBean(Userinfo2.class).hashCode());
        System.out.println(context.getBean(Userinfo2.class).hashCode());
        System.out.println(context.getBean(Userinfo2.class).hashCode());
    }
}

程序运行结果如下:

public Userinfo1() com.ghy.www.entity.Userinfo1@5e955596
1586845078
1586845078
1586845078

public Userinfo2() com.ghy.www.entity.Userinfo2@641147d0
1678854096
public Userinfo2() com.ghy.www.entity.Userinfo2@6e38921c
1849201180
public Userinfo2() com.ghy.www.entity.Userinfo2@64d7f7e0
1691875296

当有多个线程对同一个对象的同一个实例变量进行写操作时,要避免出现“非线程安全”问题,所以要使用prototype(多例),比如Struts 2的Action必须使用多例,因为Action中存在接收前端传入参数的实例变量。而Spring MVC中的Controller可以使用单例,前端传入的参数是通过方法的参数进行传入的,而不是实例变量。

当没有出现多个线程对同一个对象的同一个实例变量进行写操作时,为了减少内存的使用率,可以使用singleton(单例)模式。在开发中Service类和DAO类都使用单例模式,因为这两个类中的实例变量在大多数情况下是只读的。

依赖注入会产生类与类之间的关联,产生类之间关联的行为叫作装配(wiring)。本节将使用多种方式来装配JavaBean之间的关联。

在Spring框架中,一个JavaBean不需要创建另一个JavaBean,JavaBean之间的关系全部由IoC容器进行管理,也就是在A类中虽然声明了B类的对象,但是不需要实例化B类,B类的对象由容器进行创建,容器还能对A类中的B类的属性进行赋值,这一切都是由Spring框架的IoC容器来完成的。

把原来由A类实例化B类的方式改为由IoC容器来管理,这样就使A类和B类产生了松耦合,有利于代码的可扩展性与后期维护。

Spring默认按类型(byType)进行匹配,然后实施注入。

创建测试项目diTest。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity;

import org.springframework.stereotype.Component;

@Component
public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this.hashCode());
    }
}

创建运行类Test,代码如下:

package com.ghy.www.test;

import com.ghy.www.entity.Userinfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class Test {
    public Test() {
        System.out.println("public Test() " + this.hashCode());
    }

    @Autowired
    private Userinfo userinfo;

    public Userinfo getUserinfo() {
        return userinfo;
    }

    public void setUserinfo(Userinfo userinfo) {
        this.userinfo = userinfo;
        System.out.println("public void setUserinfo(Userinfo userinfo) userinfo=" + userinfo);
    }

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www");
        Test test = (Test) context.getBean(Test.class);
        System.out.println("main test " + test.hashCode());
        System.out.println(test.getUserinfo().hashCode());
    }
}

程序运行结果如下:

public Userinfo() 1295226194
public Test() 1931444790
main test 1931444790
1295226194

方法setUserinfo()并没有被执行,因为使用反射技术直接对userinfo变量进行字段赋值。如果将@Autowired注解放在public void setUserinfo(Userinfo userinfo)方法之上,则setUserinfo()方法才会被调用。

如果对接口类型的变量进行注入,当IoC容器发现有多个实现类时,Spring并不知道应该把哪个实现类对接口进行注入,从而出现NoUniqueBeanDefinitionException异常。

创建测试项目diTestNO。

创建接口IUserinfoService,代码如下:

package com.ghy.www.service;

public interface IUserinfoService {
    public void save();
}

创建实现类UserinfoServiceA,代码如下:

package com.ghy.www.service;

import org.springframework.stereotype.Service;

@Service
public class UserinfoServiceA implements IUserinfoService {
    public UserinfoServiceA() {
        System.out.println("public Userinfo() " + this.hashCode());
    }

    public void save() {
        System.out.println("将数据保存到A数据库中");
    }
}

创建实现类UserinfoServiceB,代码如下:

package com.ghy.www.service;

import org.springframework.stereotype.Service;

@Service
public class UserinfoServiceB implements IUserinfoService {
    public UserinfoServiceB() {
        System.out.println("public Userinfo() " + this.hashCode());
    }

    public void save() {
        System.out.println("将数据保存到B数据库中");
    }
}

创建运行类Test,代码如下:

package com.ghy.www.test;

import com.ghy.www.service.IUserinfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class Test {
    public Test() {
        System.out.println("public Test() " + this.hashCode());
    }

    @Autowired
    private IUserinfoService userinfoService;

    public IUserinfoService getUserinfoService() {
        return userinfoService;
    }

    public void setUserinfoService(IUserinfoService userinfoService) {
        this.userinfoService = userinfoService;
    }

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www");
        Test test = (Test) context.getBean(Test.class);
        test.getUserinfoService().save();
    }
}

程序运行后出现如下异常信息:

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.ghy.www.service.IUserinfoService' available: expected single matching bean but found 2: userinfoServiceA,userinfoServiceB

上面的结果表明出现了NoUniqueBeanDefinitionException异常。因为Spring框架不能确定注入哪个实现类对象,所以出现异常,解决的方法有以下3种。

(1)使用@Primary注解。

(2)使用@Autowired注解结合@Qualifier注解。

(3)使用@Resource注解。

1.使用@Primary注解

创建测试项目diTestOK_1。

更改实现类UserinfoServiceB,代码如下:

@Service
@Primary
public class UserinfoServiceB implements IUserinfoService {

注解@Primary表示在遇到相同类型的注入时,当前的组件具有高优先级。

2.使用@Autowired注解结合@Qualifier注解

不使用按类型方式,而是使用JavaBean的别名进行注入。

对@Service组件设置一个别名,使用@Qualifier注解注入指定别名的组件。

创建测试项目diTestOK_2。

更改实现类UserinfoServiceA和实现类UserinfoServiceB,代码如下:

@Service(value = "userinfoServiceA")
public class UserinfoServiceA implements IUserinfoService {

``` @Service(value = "userinfoServiceB") public class UserinfoServiceB implements IUserinfoService {

对JavaBean设置了别名。

注入的代码如下:

@Autowired
@Qualifier(value = "userinfoServiceB")
private IUserinfoService userinfoService;

注意:@Qualifier比@Primary优先级高。

3.使用@Resource注解

javax.annotation.Resource注解是由Java EE规范提供的,而不是Spring框架。

创建测试项目diTestOK_3。

更改实现类UserinfoServiceA和实现类UserinfoServiceB,代码如下:

@Service(value = "userinfoServiceA")
public class UserinfoServiceA implements IUserinfoService {

@Service(value = "userinfoServiceB")
public class UserinfoServiceB implements IUserinfoService {

运行类代码更改如下:

@Resource(name = "userinfoServiceB")
private IUserinfoService userinfoService;

注解@Resource使用name属性时,以byName的方式进行注入。

注解@Resource不使用name属性时,以byType的方式进行注入,示例代码如下:

@Resource
private IUserinfoService userinfoService;

上面代码的执行结果与下列代码一致,注解@Autowired就是以byType的方式进行注入的:

@Autowired
private IUserinfoService userinfoService;

创建测试项目autowired_constructor。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity;

import org.springframework.stereotype.Component;

@Component
public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this.hashCode());
    }
}

创建实体类Bookinfo,代码如下:

package com.ghy.www.entity;

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

@Component
public class Bookinfo {
    @Autowired
    public Bookinfo(Userinfo userinfo) {
        System.out.println("public Bookinfo(Userinfo userinfo) userinfo=" + userinfo.hashCode());
    }
}

创建配置类SpringConfig,代码如下:

package com.ghy.www.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "com.ghy.www.entity")
public class SpringConfig {
}

创建运行类Test,代码如下:

package com.ghy.www.test;

import com.ghy.www.config.SpringConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test {
    public static void main(String[] args) {
        new AnnotationConfigApplicationContext(SpringConfig.class);
    }
}

程序运行结果如下:

public Userinfo() 366590980
public Bookinfo(Userinfo userinfo) userinfo=366590980

如果类仅有一个有参构造方法,则可以省略@Autowired注解,因为Spring会自动进行注入。

我们可以使用@Autowired注解向setter方法的参数进行注入。

创建测试项目autowired_setter。

创建实体类Bookinfo,代码如下:

package com.ghy.www.entity;

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

@Component
public class Bookinfo {
    @Autowired
    public void setUserinfo(Userinfo userinfo) {
        System.out.println("public void setUserinfo(Userinfo userinfo) userinfo=" + userinfo.hashCode());
    }
}

创建测试项目autowired_field。

创建实体类Bookinfo,代码如下:

package com.ghy.www.entity;

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

@Component
public class Bookinfo {
    @Autowired
    private Userinfo userinfo;

    public Userinfo getUserinfo() {
        return userinfo;
    }

    public void setUserinfo(Userinfo userinfo) {
        this.userinfo = userinfo;
    }
}

创建运行类Test,代码如下:

package com.ghy.www.test;

import com.ghy.www.config.SpringConfig;
import com.ghy.www.entity.Bookinfo;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        Bookinfo bookinfo = (Bookinfo) context.getBean(Bookinfo.class);
        System.out.println(bookinfo.getUserinfo().hashCode());
    }
}

程序运行结果如下:

public Userinfo() 6320204
6320204

通过前面的测试可知,使用注解@Autowired可以向字段、方法的参数以及构造方法的参数进行注入。

@Inject注解可以实现注入,但需要javaee-api-8.0.jar文件,因为@Inject注解是Oracle提供的注解,而@Autowired是Spring官方提供的注解。

使用@Inject注解向构造方法注入的测试项目为inject_test1,代码如下:

package com.ghy.www.entity;

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

import javax.inject.Inject;

@Component
public class Bookinfo {
    @Inject
    public Bookinfo(Userinfo userinfo) {
        System.out.println("public Bookinfo(Userinfo userinfo) userinfo=" + userinfo.hashCode());
    }
}

使用@Inject注解向方法注入的测试项目为inject_test2,核心代码如下:

package com.ghy.www.entity;

import org.springframework.stereotype.Component;

import javax.inject.Inject;

@Component
public class Bookinfo {
    @Inject
    public void setUserinfo(Userinfo userinfo) {
        System.out.println("public void setUserinfo(Userinfo userinfo) userinfo=" + userinfo.hashCode());
    }
}

使用@Inject注解向字段注入的测试项目为inject_test3,核心代码如下:

package com.ghy.www.entity;

import org.springframework.stereotype.Component;

import javax.inject.Inject;

@Component
public class Bookinfo {
    @Inject
    private Userinfo userinfo;

    public Userinfo getUserinfo() {
        return userinfo;
    }

    public void setUserinfo(Userinfo userinfo) {
        this.userinfo = userinfo;
    }
}

向接口进行注入:

@Inject
private IUserinfoService userinfoService;

在有多个实现类时,也会出现NoUniqueBeanDefinitionException异常:

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.ghy.www.service.IUserinfoService' available: expected single matching bean but found 2: userinfoServiceA,userinfoServiceB

以上的异常结果所涉及的测试代码在项目inject_test4中。

为了处理此异常,我们可以使用如下示例代码注入指定别名的JavaBean对象:

@Inject
@Qualifier(value = "userinfoServiceB")
private IUserinfoService userinfoService;

以上测试代码在项目inject_test5中。

创建测试项目di-factorymethod。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity;

import org.springframework.stereotype.Component;

@Component
public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this.hashCode());
    }
}

创建实体类Bookinfo,代码如下:

package com.ghy.www.entity;

public class Bookinfo {
    public Bookinfo(Userinfo userinfo) {
        System.out.println("public Bookinfo(Userinfo userinfo) userinfo=" + userinfo.hashCode());
    }
}

创建配置类SpringConfig,代码如下:

package com.ghy.www.config;

import com.ghy.www.entity.Bookinfo;
import com.ghy.www.entity.Userinfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "com.ghy.www.entity")
public class SpringConfig {
    @Bean
    public Bookinfo getBookinfo(Userinfo userinfo) {
        System.out.println("public Bookinfo getBookinfo(Userinfo userinfo) userinfo=" + userinfo.hashCode());
        return new Bookinfo(userinfo);
    }
}

创建运行类Test,代码如下:

package com.ghy.www.test;

import com.ghy.www.config.SpringConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test {
    public static void main(String[] args) {
        new AnnotationConfigApplicationContext(SpringConfig.class);
    }
}

程序运行结果如下:

public Userinfo() 1571967156
public Bookinfo getBookinfo(Userinfo userinfo) userinfo=1571967156
public Bookinfo(Userinfo userinfo) userinfo=1571967156

在注入时,如果找不到符合条件的JavaBean对象,控制台会提示异常:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.ghy.www.entity.Userinfo' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation. Autowired(required=true)}

创建测试项目diTest-required1。

创建配置类SpringConfig,代码如下:

package com.ghy.www.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.ghy.www.test")
public class SpringConfig {
}

创建实体类Userinfo,代码如下:

package com.ghy.www.entity;

import org.springframework.stereotype.Component;

@Component
public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this.hashCode());
    }
}

创建运行类Test,代码如下:

package com.ghy.www.test;

import com.ghy.www.config.SpringConfig;
import com.ghy.www.entity.Userinfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class Test {
    @Autowired
    private Userinfo userinfo;

    public Userinfo getUserinfo() {
        return userinfo;
    }

    public void setUserinfo(Userinfo userinfo) {
        this.userinfo = userinfo;
    }

    public static void main(String[] args) {
        new AnnotationConfigApplicationContext(SpringConfig.class);
    }
}

程序运行后出现如下异常信息:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.ghy.www.entity.Userinfo' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

该异常信息提示我们没有找到符合条件的JavaBean对象。为了避免出现异常,我们可以加入required = false属性,代表注入的userinfo对象并不是必需的,如果没有对象就不进行注入。创建测试项目diTest-required2,更改后的代码如下:

@Autowired(required = false)
private Userinfo userinfo;

程序运行后并没有出现异常。由于没有找到符合条件的记录,因此userinfo对象的值为null。

以下两种写法使用@Autowired(required = false)是无效的,依然会出现NoSuchBeanDefinition Exception异常,说明注入的对象必须存在。

写法1代码如下:

package com.ghy.www.entity;

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

@Component
public class Bookinfo {
    @Autowired(required = false)
    public Bookinfo(Userinfo userinfo) {
        System.out.println("public Bookinfo(Userinfo userinfo) userinfo=" + userinfo.hashCode());
    }
}

测试示例在项目diTest-required3中。

写法2代码如下:

package com.ghy.www.config;

import com.ghy.www.entity.Bookinfo;
import com.ghy.www.entity.Userinfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "com.ghy.www.entity")
public class SpringConfig {
    @Bean
    @Autowired(required = false)
    public Bookinfo getBookinfo(Userinfo userinfo) {
        System.out.println("public Bookinfo getBookinfo(Userinfo userinfo) userinfo=" + userinfo.hashCode());
        return new Bookinfo(userinfo);
    }
}

测试示例在项目diTest-required4中。

默认时,使用@Bean创建的JavaBean的id就是方法的名称,实际上id也可以自定义。

创建测试项目di-id-is-methodname。

示例代码如下:

package com.ghy.www.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import java.util.Date;

@Configuration
@ComponentScan(basePackages = "com.ghy.www.test")
public class SpringConfig {
    @Bean(name = "xxxxxxxxxxxxxxxxxxxxxxx")
    public Date createDate() {
        Date date = new Date();
        System.out.println("public Date createDate() xxxxxxxxxxxxxxxxxxxxxxx=" + date.hashCode());
        return date;
    }

    @Bean
    public Date getDate() {
        Date date = new Date();
        System.out.println("public Date getDate() zzzzzzzzzzzzzzzzzzzzz=" + date.hashCode());
        return date;
    }
}

创建运行类Test,代码如下:

package com.ghy.www.test;

import com.ghy.www.config.SpringConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Date;

@Component
public class Test {
    @Resource(name = "xxxxxxxxxxxxxxxxxxxxxxx")
    private Date date1;
    @Resource(name = "getDate")
    private Date date2;

    public Date getDate1() {
        return date1;
    }

    public void setDate1(Date date1) {
        this.date1 = date1;
    }

    public Date getDate2() {
        return date2;
    }

    public void setDate2(Date date2) {
        this.date2 = date2;
    }

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        Test test1 = (Test) context.getBean(Test.class);
        System.out.println("main date1=" + test1.getDate1().hashCode());
        System.out.println("main date2=" + test1.getDate2().hashCode());
    }
}

程序运行结果如下:

public Date createDate() xxxxxxxxxxxxxxxxxxxxxxx=-885661621
public Date getDate() zzzzzzzzzzzzzzzzzzzzz=-885661642
main date1=-885661621
main date2=-885661642

Spring的上下文可以理解为Spring运行的环境,可以创建多个Spring上下文。在默认的情况下,不同上下文中的JavaBean对象不可以共享。

1.创建多个Spring上下文

创建多个Spring上下文就是创建多个ApplicationContext对象。

创建测试项目context-test1。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity;

public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this.hashCode());
    }
}

创建实体类Bookinfo,代码如下:

package com.ghy.www.entity;

public class Bookinfo {
    public Bookinfo() {
        System.out.println("public Bookinfo() " + this.hashCode());
    }
}

创建配置类SpringConfig1,代码如下:

package com.ghy.www.config;

import com.ghy.www.entity.Bookinfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig1 {
    @Bean
    public Bookinfo getBookinfo() {
        Bookinfo bookinfo = new Bookinfo();
        System.out.println("getBookinfo bookinfo=" + bookinfo.hashCode());
        return bookinfo;
    }
}

创建配置类SpringConfig2,代码如下:

package com.ghy.www.config;

import com.ghy.www.entity.Userinfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig2 {
    @Bean
    public Userinfo getUserinfo() {
        Userinfo userinfo = new Userinfo();
        System.out.println("getUserinfo userinfo=" + userinfo.hashCode());
        return userinfo;
    }
}

创建运行类Test1,代码如下:

package com.ghy.www.test;

import com.ghy.www.config.SpringConfig1;
import com.ghy.www.config.SpringConfig2;
import com.ghy.www.entity.Bookinfo;
import com.ghy.www.entity.Userinfo;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        ApplicationContext context1 = new AnnotationConfigApplicationContext(SpringConfig1.class);
        ApplicationContext context2 = new AnnotationConfigApplicationContext(SpringConfig2.class);

        System.out.println("context1=" + context1.getBean(Bookinfo.class).hashCode());
        System.out.println("context2=" + context2.getBean(Userinfo.class).hashCode());
    }
}

程序运行结果如下:

public Bookinfo() 1131040331
getBookinfo bookinfo=1131040331
public Userinfo() 932285561
getUserinfo userinfo=932285561
context1=1131040331
context2=932285561

通过上面的程序,我们成功创建了不同的Spring上下文,在自己的上下文中可以获取自己的JavaBean。

2.不同Spring上下文中的JavaBean对象是不共享的

创建运行类Test2,代码如下:

package com.ghy.www.test;

import com.ghy.www.config.SpringConfig1;
import com.ghy.www.config.SpringConfig2;
import com.ghy.www.entity.Bookinfo;
import com.ghy.www.entity.Userinfo;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test2 {
    public static void main(String[] args) {
        ApplicationContext context1 = new AnnotationConfigApplicationContext(SpringConfig1.class);
        ApplicationContext context2 = new AnnotationConfigApplicationContext(SpringConfig2.class);

        System.out.println("context1=" + context1.getBean(Bookinfo.class).hashCode());
        context1.getBean(Userinfo.class);
    }
}

程序运行后出现如下异常信息:

public Bookinfo() 1131040331
getBookinfo bookinfo=1131040331
public Userinfo() 932285561
getUserinfo userinfo=932285561
context1=1131040331
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.ghy.www.entity.Userinfo' available
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:351)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1127)
    at com.ghy.www.test.Test2.main(Test2.java:16)

该异常信息提示Userinfo对象没有被找到。

3.让多个配置类互相通信

那么,如何使不同上下文中的JavaBean可以互相共享呢?

创建测试项目context-test2。

创建配置类SpringConfig1,代码如下:

package com.ghy.www.config;

import com.ghy.www.entity.Bookinfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(SpringConfig2.class)
public class SpringConfig1 {
    @Bean
    public Bookinfo getBookinfo() {
        Bookinfo bookinfo = new Bookinfo();
        System.out.println("getBookinfo bookinfo=" + bookinfo.hashCode());
        return bookinfo;
    }
}

我们可以使用如下写法的注解导入其他配置类:

@Import(SpringConfig2.class)

还可以使用如下写法的注解一次性导入多个配置类,代码如下:

@Import({SpringConfig100.class, SpringConfig200.class})

创建运行类Test1,代码如下:

package com.ghy.www.test;

import com.ghy.www.config.SpringConfig1;
import com.ghy.www.entity.Bookinfo;
import com.ghy.www.entity.Userinfo;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        ApplicationContext context1 = new AnnotationConfigApplicationContext(SpringConfig1.class);
        System.out.println("context1=" + context1.getBean(Bookinfo.class).hashCode());
        System.out.println("context2=" + context1.getBean(Userinfo.class).hashCode());
    }
}

程序运行结果如下:

public Userinfo() 1043351526
getUserinfo userinfo=1043351526
public Bookinfo() 937773018
getBookinfo bookinfo=937773018
context1=937773018
context2=1043351526

4.JavaBean的id值相同时出现ConflictingBeanDefinitionException异常

如果多个上下文中的JavaBean的id值相同,则会出现ConflictingBeanDefinitionException异常。

创建测试项目context-test3。

创建工厂类SpringConfig1,代码如下:

package com.ghy.www.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@ComponentScan("com.ghy.www.entity1")
@Import(SpringConfig2.class)
public class SpringConfig1 {
}

创建工厂类SpringConfig2,代码如下:

package com.ghy.www.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.ghy.www.entity2")
public class SpringConfig2 {
}

创建实体类Userinfo,代码如下:

package com.ghy.www.entity1;

import org.springframework.stereotype.Component;

@Component("mybean")
public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this.hashCode());
    }
}

创建实体类Bookinfo,代码如下:

package com.ghy.www.entity2;

import org.springframework.stereotype.Component;

@Component("mybean")
public class Bookinfo {
    public Bookinfo() {
        System.out.println("public Bookinfo() " + this.hashCode());
    }
}

创建运行类Test1,代码如下:

package com.ghy.www.test;

import com.ghy.www.config.SpringConfig1;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        new AnnotationConfigApplicationContext(SpringConfig1.class);
    }
}

程序运行结果如下:

Exception in thread "main" org.springframework.beans.factory.BeanDefinitionStoreException: Failed to process import candidates for configuration class [com.ghy.www.config.SpringConfig1]; nested exception is org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'mybean' for bean class [com.ghy.www.entity2.Bookinfo] conflicts with existing, non-compatible bean definition of same name and class [com.ghy.www.entity1.Userinfo]

5.不同上下文中的工厂方法的名称不可以相同

注意:在不同配置类中创建JavaBean工厂方法时的名称不能一样,否则会出现“Overriding bean definition”问题。原因是在默认情况下,JavaBean的id和方法名称一样,如果多个工厂方法的名称一样,则不会创建其他的JavaBean,导致出现找不到对象的诡异问题。

创建测试项目context-test4。

创建工厂类SpringConfig1,代码如下:

package com.ghy.www.config;

import com.ghy.www.entity.Bookinfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(SpringConfig2.class)
public class SpringConfig1 {
    @Bean
    public Bookinfo beanIdSame() {
        Bookinfo bookinfo = new Bookinfo();
        System.out.println("getBookinfo bookinfo=" + bookinfo.hashCode());
        return bookinfo;
    }
}

创建工厂类SpringConfig2,代码如下:

package com.ghy.www.config;

import com.ghy.www.entity.Userinfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig2 {
    @Bean
    public Userinfo beanIdSame() {
        Userinfo userinfo = new Userinfo();
        System.out.println("getUserinfo userinfo=" + userinfo.hashCode());
        return userinfo;
    }
}

创建实体类Userinfo,代码如下:

package com.ghy.www.entity1;

import org.springframework.stereotype.Component;

public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this.hashCode());
    }
}

创建实体类Bookinfo,代码如下:

package com.ghy.www.entity2;

import org.springframework.stereotype.Component;

public class Bookinfo {
    public Bookinfo() {
        System.out.println("public Bookinfo() " + this.hashCode());
    }
}

创建运行类Test1,代码如下:

package com.ghy.www.test;

import com.ghy.www.config.SpringConfig1;
import com.ghy.www.entity.Bookinfo;
import com.ghy.www.entity.Userinfo;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test1 {
    public static void main(String[] args) {
        ApplicationContext context1 = new AnnotationConfigApplicationContext(SpringConfig1.class);
        System.out.println("context1=" + context1.getBean(Bookinfo.class).hashCode());
        System.out.println("context2=" + context1.getBean(Userinfo.class).hashCode());
    }
}

程序运行结果如下:

[main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Overriding bean definition for bean 'beanIdSame' with a different definition: replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=com.ghy.www.config.SpringConfig2; factoryMethodName=beanIdSame; initMethodName=null; destroyMethodName=(inferred); defined in com.ghy.www.config.SpringConfig2] with [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=springConfig1; factoryMethodName=beanIdSame; initMethodName=null; destroy MethodName=(inferred); defined in com.ghy.www.config.SpringConfig1]
public Bookinfo() 330084561
getBookinfo bookinfo=330084561
context1=330084561
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.ghy.www.entity.Userinfo' available
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean (DefaultListableBeanFactory.java:351)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean (DefaultListableBeanFactory.java:342)
    at org.springframework.context.support.AbstractApplicationContext.getBean(Abstract ApplicationContext.java:1127)
    at com.ghy.www.test.Test1.main(Test1.java:13)

处理这种异常的方法是设置工厂方法的名称时不要重名。

创建测试项目context-test5。

更改配置类SpringConfig1,代码如下:

@Configuration
@Import(SpringConfig2.class)
public class SpringConfig1 {
    @Bean
    public Bookinfo createBookinfo() {

更改配置类SpringConfig2,代码如下:

@Configuration
public class SpringConfig2 {
    @Bean
    public Userinfo createUserinfo() {

6.创建AllConfig全局配置类

对于多个Spring上下文共享JavaBean的写法,比较好的代码组织方式是创建一个全局配置类,然后在类的上方使用注解@Import导入其他配置类。

创建测试项目context-test6。

创建全局配置类AllConfig,代码如下:

package com.ghy.www.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(value = { SpringConfig1.class, SpringConfig2.class })
public class AllConfig {
}

包org.springframework.beans和org.springframework.context是Spring框架IoC容器的基础。

BeanFactory接口提供了一种能够管理任何类型对象的高级配置机制,ApplicationContext 是BeanFactory的一个子接口,它使得与Spring的AOP功能更容易集成,支持消息资源处理(用于国际化)、事件发布和应用程序层特定的上下文。

其实Spring的IoC容器就是一个实现了BeanFactory接口的实现类,因为ApplicationContext接口继承自BeanFactory接口。通过工厂模式取得JavaBean对象。

下面我们来看一下Spring的API DOC,如图1-10所示。

图1-10 BeanFactory接口结构

从图1-10中可以发现,ApplicationContext是BeanFactory的子接口,BeanFactory接口提供了最基本的对象管理功能,而子接口ApplicationContext提供了更多附加的功能,如与Web整合、支持国际化、事件发布和通知等。

如果把其他对象中的属性值作为注入的来源,就使用#{}。如果把*.properties属性文件中key对应的value作为注入的来源,就使用${}。

创建测试项目valueTest。

创建实体类Userinfo,代码如下:

package com.ghy.www.entity;

public class Userinfo {
    public Userinfo() {
        System.out.println("public Userinfo() " + this.hashCode());
    }
}

创建属性文件db.properties,代码如下:

dbdriver=oracleDriver
dburl=oracleURL
dbusername=oracleUsername
dbpassword=oraclePassword

创建业务类UserinfoService,代码如下:

package com.ghy.www.service;

import com.ghy.www.entity.Userinfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service(value = "userinfoService")
public class UserinfoService {

    @Value("UserinfoService常<量>值&")
    private String username;

    private Userinfo userinfo = new Userinfo();

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Userinfo getUserinfo() {
        return userinfo;
    }

    public void setUserinfo(Userinfo userinfo) {
        this.userinfo = userinfo;
    }

}

创建运行类Test,代码如下:

package com.ghy.www.test;

import com.ghy.www.entity.Userinfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@ComponentScan(basePackages = {"com.ghy.www.test", "com.ghy.www.service"})
@PropertySource(value = {"classpath:db.properties"})
@Configuration
public class Test {

    @Value("#{userinfoService.userinfo}")
    public Userinfo userinfo;

    @Value("#{userinfoService.username}")
    public String injectStringValue;

    @Value("${dbdriver}")
    public String a;
    @Value("${dburl}")
    public String b;
    @Value("${dbusername}")
    public String c;
    @Value("${dbpassword}")
    public String d;

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(Test.class);
        Test test = context.getBean(Test.class);
        System.out.println("userinfo=" + test.userinfo.hashCode());
        System.out.println("injectStringValue=" + test.injectStringValue);
        System.out.println("a=" + test.a);
        System.out.println("b=" + test.b);
        System.out.println("c=" + test.c);
        System.out.println("d=" + test.d);
    }
}

程序运行结果如下:

public Userinfo() 370440646
userinfo=370440646
injectStringValue=UserinfoService常<量>值&
a=oracleDriver
b=oracleURL
c=oracleUsername
d=oraclePassword

当A类依赖B类,而B类又依赖A类时,就会有在Spring中被称为“循环依赖”的情况,循环依赖会产生BeanCurrentlyInCreationException异常。

创建测试项目bean-currently-in-creation-exception。

创建类A,代码如下:

package com.ghy.www.entity;

public class A {

    private B b;

    public A() {
    }

    public A(B b) {
        super();
        this.b = b;
    }

    public B getB() {
        return b;
    }

    public void setB(B b) {
        this.b = b;
    }

}

创建类B,代码如下:

package com.ghy.www.entity;

public class B {
    private A a;

    public B() {
    }

    public B(A a) {
        super();
        this.a = a;
    }

    public A getA() {
        return a;
    }

    public void setA(A a) {
        this.a = a;
    }

}

创建配置类SpringConfig,代码如下:

package com.ghy.www.config;

import com.ghy.www.entity.A;
import com.ghy.www.entity.B;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {
    @Bean
    public A getA(B b) {
        return new A(b);
    }

    @Bean
    public B getB(A a) {
        return new B(a);
    }
}

创建运行类Test,代码如下:

package com.ghy.www.test;

import com.ghy.www.config.SpringConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test {
    public static void main(String[] args) {
        new AnnotationConfigApplicationContext(SpringConfig.class);
    }
}

程序运行后出现异常信息如下:

Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'getA' defined in com.ghy.www.config.SpringConfig: Unsatisfied dependency expressed through method 'getA' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'getB' defined in com.ghy.www.config.SpringConfig: Unsatisfied dependency expressed through method 'getB' parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'getA': Requested bean is currently in creation: Is there an unresolvable circular reference?

循环依赖产生的原因是使用了有参工厂方法:

@Bean
public A getA(B b) {
    return new A(b);
}

@Bean
public B getB(A a) {
    return new B(a);
}

改用setter方法实现注入就可以解决循环依赖的问题。

创建bean-currently-in-creation-exception-ok项目。

创建类A,代码如下:

package com.ghy.www.entity;

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

@Component
public class A {

    private B b;

    public B getB() {
        return b;
    }

    @Autowired
    public void setB(B b) {
        this.b = b;
    }

}

创建类B,代码如下:

package com.ghy.www.entity;

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

@Component
public class B {
    private A a;

    public A getA() {
        return a;
    }

    @Autowired
    public void setA(A a) {
        this.a = a;
    }

}

创建运行类Test,代码如下:

package com.ghy.www.test;

import com.ghy.www.entity.A;
import com.ghy.www.entity.B;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.ghy.www");
        System.out.println(context.getBean(A.class));
        System.out.println(context.getBean(B.class));
    }
}

程序运行结果如下:

com.ghy.www.entity.A@77e4c80f
com.ghy.www.entity.B@35fc6dc4

这样,循环依赖问题就解决了。


相关图书

深入浅出Spring Boot 3.x
深入浅出Spring Boot 3.x
云原生Spring实战Spring Boot与?Kubernetes实践
云原生Spring实战Spring Boot与?Kubernetes实践
Spring实战(第6版)
Spring实战(第6版)
Java研发自测入门与进阶
Java研发自测入门与进阶
Spring核心技术和案例实战
Spring核心技术和案例实战
Java EE企业级应用开发实战(Spring Boot+Vue+Element)
Java EE企业级应用开发实战(Spring Boot+Vue+Element)

相关文章

相关课程