反向控制(Ioc)以及装配JavaBean方法的变革_Beer Bear~的博客-CSDN博客

反向控制(Ioc)以及装配JavaBean方法的变革

关于学习,为什么很多时候总是感觉付出了时间而回过头来又觉得什么都没有留下。我想这是无论是我写本文前还是各位看本文前都应该思考的问题。而解决这个问题的答案要从另一个问题的答案中去寻找——怎样才算是把学习过程中的某个问题彻底搞懂了?我是这么看待的,如果试着去探究而不是逃避学习过程中的问题的时候,那么这个问题已经解决了50%了,剩下的50%只是时间问题;而当经过一段时间的学习,问题的答案差不多能在心里形成个大概轮廓的时候了,那么这个问题的解决程度也才完成了百分之70%;如果问题的答案能口头表达出来或者书面语言写出来的时候,问题的解决程度差不多就有了90%了;这时候你想象一下,假设回到几天前或者你刚遇到这个问题的时候,你是否能给当时的你用最系统、快速、准确且简单的方法讲述出来,最重要的是对方能听懂且信服,如果是是的话,那么这个问题的完成度可以是99%了;最后的1%又在哪里了,我想对于程序员来说,最好的品德就是不断学习与开源的分享,对于问题,始终保持谦虚的态度,在计算机的世界中“唯一不变的就是变化”,问题亦是如此。

我想以上便是我写此文最大的动力,本文也尽量从最基本的概念开始,通过生动想象的例子来阐述问题的答案。希望与大家共同学习进度,如有有失偏颇之处还望不吝赐教。接下来就让我们带着问题来一探究竟吧!

何为反向控制

初学Spring的小伙伴,一定会有如下经历:常常会在一些文章或者书籍上与控制反转、依赖注入等的字眼不期而遇。也肯定会有这种疑惑到底何为控制反转(即本文所称的反向控制,该词摘自《Java Web编程实战宝典一书》)。或清晰或迷惑或一知半解,不求甚解又或刨根问底。无论从前如何,希望再看下文时能对这个概念加深印象或者有新的感悟。

控制

什么叫控制?大千时间控制无处不在,人创造了机器使用机器来生产这叫控制、父母不让自己的孩子玩游戏、好好睡觉也是一种控制,控制有好也有坏,程度有强有弱。但一旦建立了控制的关系,一定是一个对象充当控制者,另一方充当被控制者的角色。

在Java的世界中,解释控制实在太简单不过,因为Java是一种面向对象的语言,很多事物都能抽象成Java的类。比如现在我们就有一个Java类Worker,它是工厂工人这一客体的抽象模拟,那么相应的,工厂中受工人操作的机器就可以抽象成Machine类。当考虑的生产这个过程时,工人有控制机器的动作,机器有生产产品的动作,这两个动作就分别抽象成了两个类中的方法。

考虑到单一职责原则,这两个类分别在两个Java文件下。那么这个两个类的代码应该如下:

public class Worker {
    private String name;

    public void work(){
        System.out.println("开启机器!");
        Machine machine = new Machine();
        machine.produce();
        System.out.println("机器完成生产任务!");
    }

    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.work();
    }
}
public class Machine {
    public void produce(){
        System.out.println("机器正在生产!");
        System.out.println("--------\n--------\n--------\n--------");
        System.out.println("机器生产完成!");
    }
}

现在开始做生产任务,执行Worker下的main方法,结果如下:

image-20201206162818254

通过Worker类中的work方法中代码可知道,工人对象控制机器的方法就是去new一个机器的对象,然后调用机器中的方法,这是初学Java中最常用的方法。这是最简单的控制的实现方法。

稍作改进

在讲反向控制之前,我们可以试着把上面的代码优化一下。

  • Worker中的main方法实现的功能应该抽离出来,正常来说工人开启生产工作的指令是由老板发起来的,不能自己实例化自己。
  • 一个工厂中的机器是多种多样的,但他们都有一个相同的生产动作。

解决上面两个问题,那么就建立新的老板类、并将机器类抽象成接口。并对机器接口添加两个实现类。代码如下:

Boss.java:

public class Boss {
    public static void main(String[] args) {
        Worker worker = new Worker();
        System.out.println("boss:快去干活吧worker!");
        worker.work();
    }
}

Worker.java:

public class Worker {
    private String name;

    public void work(){
        System.out.println("worker:开启机器!");
        Machine machine = new ComputerMachine();
        //生产计算机
        machine.produce();
        System.out.println("worker:机器完成生产任务!");
    }
}

Machine.java:

public interface Machine {
    public void produce();
}

PhoneMachine.java:生产手机机器的类

public class PhoneMachine implements Machine{
    @Override
    public void produce(){
        System.out.println("机器正在生产手机机!");
        System.out.println("--------\n--------\n--------\n--------");
        System.out.println("机器生产手机完成!");
    }
}

ComputerMachine.java:生产计算机的机器类

public class ComputerMachine implements Machine{
    @Override
    public void produce(){
        System.out.println("机器正在生产计算机!");
        System.out.println("--------\n--------\n--------\n--------");
        System.out.println("机器生产计算机完成!");
    }
}

执行Boss中的main方法:

image-20201206184443997

好像一切都很完美了,满足接口编程原则、单一职责原则。

但这时候老板说,你接下来生产手机吧——业务发生改变了!Worker类中的work方法要发生改变了——这不满足开闭原则了,因为Worker类要发生改变了。

public class Worker {
    private String name;

    public void work(){
        System.out.println("worker:开启机器!");
        Machine machine = new PhoneMachine();
         //生产手机
        machine.produce();
        System.out.println("worker:机器完成生产任务!");
    }
}

难道老板每次改命令,Worker都要改代码么?显然这是不可能的,这严重违背了开闭原则(对新增开放,对修改关闭)。

进一步思考代码缺陷

很明显上面的代码是由Worker类来控制Machine类来实现一些功能的,由此引发了一些不希望看到的结果。如果要认真去分析的话,主要存在以下几点缺陷:

  • Worker类和Machine类的耦合性极高。Worker需要使用另外一个机器的时候,必须通过修改自身代码实现。如果在一个系统中存在大量这种情况,就会牵一发而动全身,耦合度很高的情况下修改代码后出现BUG的几率也会大大增加。
  • Worker类的使用过于局限,换个机器必须要更改代码,不具有动态性。

那是不是这样就好了,其他地方调用worker对象中的work方法时,加一个参数,在work中通过if判断看到底应该实例化哪个机器类。

public class Worker {
    private String name;

    public void work(String machineType){
        if (machineType.equals("phone")){
            System.out.println("worker:开启机器!");
            Machine machine = new PhoneMachine();
            machine.produce();
            System.out.println("worker:机器完成生产任务!");
        }
        else if(machineType.equals("computer")){
            System.out.println("worker:开启机器!");
            Machine machine = new ComputerMachine();
            machine.produce();
            System.out.println("worker:机器完成生产任务!");
        }
        else {
            System.out.println("没有该类型的机器");
        }
    }
}

好像还真的满足了动态性了。最起码试着去解决问题了。但但但是当新增机器时,又要去Worker新增if语句么?

  • 这样做还是不满足开闭原则,我希望的是,即使新增了Machine类,也不要来改Worker了。
  • 太多的if语句并且其中的代码都是重复的,一是不够优雅,二是通过来实现不同逻辑,应该是由策略模式来实现的,而不是用过多的if。
  • 传参数用于If判断,Boss和Worker的耦合性还是太高了,因为传参数必须有严格的对应才行,Worker的行为必须对Boss完全透明,否则Boss根本不知道该怎么用Worker啊。

所以这种方式还是不太行。

控制反转

某些事物从本质上就带有的缺陷,是想破脑袋都无法解决的。就像是资产阶级一生下来就带有剥削性,这是无法改变的事情,伪装改进的再好这种本质上的特点也无法消弭。

同样的,这样通过一个类来创建另一个类并使用它的这种主动的工作方式带来的弊端也是无法解决的。这时候控制反转就浮出水面了——将创建对象的工作交给外部的协调者(比如Spring容器)来完成,就能解决上面的问题了,下面介绍的方式就是通过IOC的思想来降低耦合度。

我们在Worker类的外部来创建Machine类。既然是老板决定工人来使用什么机器,那就在Boss中实现Machine类,通过setter方法向Worker对象传递Machine对象。

Boss.java

public class Boss {
    public static void main(String[] args) {
        Machine machine = new PhoneMachine();
        Worker worker = new Worker();
        worker.setMachine(machine);
        System.out.println("boss:快去干活吧worker!");
        worker.work();
    }
}

Worker.java

public class Worker {
    private String name;

    private Machine machine;

    public void setMachine(Machine machine){
        this.machine = machine;
    }

    public void work(){
        System.out.println("worker:开启机器!");
        machine.produce();
        System.out.println("worker:机器完成生产任务!");
    }
}

通过setMachine方法来完成machine对象的注入。下次老板再想让工人操作不同机器(或者加新机器了)完成不同生产任务的时候,只需要修改Boss中的一行代码就能完成。Worker代码不用动了。

Machine machine = new AnyMachine();

这次好像无懈可击了!至少目前来说已经是最好的方法了。这就是通过依赖注入的方式来实现了控制反转。原本Machine的创建权从Worker手上转移到了Boss手上,使得Machine与Worker的耦合性大大降低。

改变前后,上述代码完成生产功能时有什么不同。如下图所示:

image-20201206195846273

疑惑

我们最后使用IOC的时候,说不用对Worker代码进行更改了,把创建机器的权力交给Boss了,但要使用不同机器的时候还是要更改Boss中的代码。这不是还是要更改么。其实在JavaWeb的项目中,Boss、Worker、Machine所代表的层级是不同的。Boss是用户层(可以当作是前端页面),Worker是Service层,Machine是持久层。

它们之间的关系应该是这样的:

image-20201206200316802

服务层的代码应该是极具动态性的,用一套代码就能为不同的用户服务。所以该层的代码是不能轻易更改的,否则将会出现很多问题。而Boss层的内容随着用户意愿而更改是很正常的现象。

至此便解释清楚了何为反向控制了,不知道我讲的大家是否能明白,又或者有什么新的认识。

JavaBean与Ioc的使用

都说装配JavaBean,大家有去想过JavaBean是什么,为什么需要装配JavaBean吗?

JavaBean是什么

我自己对JavaBean的定义是:Java工程中能在业务上提供具体作用且能被其他类使用的Java类。

如果非要有一些详细的定义:

  • 用作JavaBean的类必须有一个公共的、无参数的构造方法。
  • 这个类中的属性必须能被其他类访问和修改,并且使用getter、setter方法实现。——比如上文中的Worker类中的Machine属性就能由Boss类通过setter方法注入。

JSP时代使用JavaBean

Web 技术的历史

要谈JSP与JavaBean就不得不扯点历史。浏览器在几十年前绝对在Web技术应用上占绝对地位,HTML语言的发明,使得人们在浏览器上可以做很多事情。最初的动态Web技术(实现客户端与服务端的动态交互)是由CGI(通用网关接口)实现的,交互的内容是静态的HTML文件,其动态性主要体现在可以与数据库交互了。1998年,Servlet出现了——一种由Java语言实现的技术。为了更好的使用Servlet,Sun公司发明了JSP语言,并未JSP的使用提供了两种模型分别是Model1和Model2,其中Model2就是基于MVC的,也是目前很多Web MVC框架的前身了。

JSP

JSP技术可以将静态内容(如HTML,CSS,JavaScript)和动态内容(Java)代码混合在一个文件(.jsp)中。

其中Java代码放在<%= %>或<% %>中,在此之外的就是静态内容了。jsp文件会由JSP引擎去解析,将解析出来的Java代码交由Servlet引擎处理,从而使得Java代码能够运行。在一个Web项目中,一定也有Java文件,jsp文件中的Java代码想使用这些.java文件中的内容,就得去实例化这个类——创建Java类对象。

方法

使用<jsp:useBean>标签,该标签主要用于在不使用Java代码的前提下创建类的实例对象。好好体会这句话,是不是有一点前文中提到的“在外部创建类的实例对象”的意味了。原来控制反转早就在远古的JSP时代运用了(这句话可能并不是很正确,但也绝不会跑很偏)。

<jsp:useBean id="MyDate" class="className" scope="application">

这句话就相当于

<%
    java.util.Date myDate = new java.util.Date();
%>

当然相当于的意思是指,是对于这个jsp文件而言的。

不同在于<jsp:useBean>中还有一个很重要的参数 scope。scope表示对象实例的有效范围。可选的范围有4个,从小到大依次是page、request、session和application。默认值为page,如将scope设为session,就意味者所有属于同一个session的JSP页面和Servlet都可以访问这个对象实例了。通过scope就能控制实例对象的作用域,这一种由外部创建实例对象的感觉是不是已经愈发强烈了。

JSP中的useBean标签还有其他参数,在此不是重点,就不介绍了。这一小节主要是为了让大家明确,反转控制与JavaBean的注入绝不是Spring的专属,早就从早期的JSP中就有了。本节内容很多是参考了《JavaWeb 编程实战宝典》一书,2014年出版的。大学就买了这本书,但翻开还是在几年后的今天(2020年12月初),书中的Web开发方式早已过时,但却可以用来回顾历史——真是讽刺。

更讽刺的是,大学课程中除了JSP外没有学习任何Web开发框架,等工作了要学习Spring,需要再回顾JSP和Servlet的时候还全忘了。

Struts2部署JavaBean

Struts2使用xml文件,并用标签将组件(比如JavaBean)来部署到Struts的Ioc容器中。下面是一个示例:

<struts>
    <bean class="com.yaodao.Deomo" scope="default" option="true"/>
</struts>

本人也对Struts2框架也只是浅尝辄止的状态,在此不过多解释一些用法了,仅仅是为了展示Ioc容器在web框架中历史的传承。

Spring与Bean

装配Bean

说明:Bean其实就是容器中放置的一个个工程中可复用的组件,下文中的组件即为Bean。

时间如果倒退到好几年前(本文写于2020-12),Spring装配JavaBean的含义可能是这样的:

装配Bean实际上就是在XML中配置文件JavaBean的相关信息,然后由Spring框架读取该配置文件,并创建相应的JavaBean对象实例的过程。

但时代变了,历史上,指导Spring应用上下文将bean装配在一起的方式是使用一个或者多个XML文件(用于描述各个组件以及它们与其他组件的关联关系)。但是u现在的Spring更推荐基于Java的配置——通过构造一个配置类并用注解的方式来配置Bean。而随着Spring带来了自动配置的功能——由自动装配与组件扫描技术所支持,XML与Java配置的手动配置又显得鸡肋了,在下一节中将会讲解SpringBoot是如何进行自动装配的。

XML方式装配Bean

这种方式的装配与JSP时代、Struts2框架没有多大的区别(仅指使用方式,而非深层实现方式)。在xml文件中配置了Bean只是第一步,回到上文中那段话:“装配Bean实际上就是在XML中配置文件JavaBean的相关信息,然后由Spring框架读取该配置文件,并创建相应的JavaBean对象实例的过程。”,后面还有两步即读取配置文件以及创建对象实例。

在Spring中,提供了两种方式来读取配置文件并创建JavaBean的对象实例。

  • Bean工厂(BeanFactory)
  • 应用上下文(ApplicationContext)

下面我们分别来介绍两种方法的使用方式。在此之前,我们先在IDEA中创建一个简单的Spring的项目用于代码展示。先准备一个类作为JavaBean。在此就用一个上文中写过的Worker类,但先不在此类中使用机器类(删除了部分代码)。

public class Worker {
    private String name;

    public void work(){
        System.out.println("worker:开启机器!");
        System.out.println("worker:机器完成生产任务!");
    }
}

并创建XML文件配置该JavaBean

<?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-2.5.xsd">

    <bean id = "worker" class = "Worker"/>
</beans>

使用BeanFactory接口

被抛弃的XmlBeanFactory

在很久(Spring3.1)之前,使用BeanFactory是这样使用的:

public class Test {
    public static void main(String[] args) {

        BeanFactory factory = new XmlBeanFactory(new FileSystemResource("src\\SpringBean.xml"));
        Worker worker = (Worker)factory.getBean("worker");
        worker.work();
    }
}

将配置文件通过FileSystemResource对象传入XmlBeanFactory类(是BeanFactory类的实现类)的构造方法。

image-20201213130329707

我使用的Spring是5.0的,在Idea中使用XmlBeanFactory时会有删除的横线,提醒该实现类已被抛弃,运行的时候会报错的。

新的方法
public class Test {
    public static void main(String[] args) {

        Resource resource = new ClassPathResource("SpringBean.xml");
        BeanFactory beanFactory = new DefaultListableBeanFactory();
        BeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader((BeanDefinitionRegistry) beanFactory);
        beanDefinitionReader.loadBeanDefinitions(resource);

        Worker worker = (Worker)beanFactory.getBean("worker");
        worker.work();
    }
}

运行成功:

image-20201213125311326

关于为什么要抛弃之前的实现类我还没有搞明白,由于时间关系和自身能力限制,只能等以后搞明白啦,到时候来补充上!

使用ApplicationContext接口

使用ApplicationContext接口可以实现bean的装配,主要有两种实现类可以使用。
使用FileSystemXmlApplicationContext实现类
通过绝对或相对路径指定XML配置文件
public class Test2 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new FileSystemXmlApplicationContext("SpringBean.xml");
        Worker worker = (Worker)applicationContext.getBean("worker");
        worker.work();
    }
}
使用ClasspathXmlApplicationContext实现类
从类路径中搜索XML配置文件
public class Test2 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("SpringBean.xml");
        Worker worker = (Worker)applicationContext.getBean("worker");
        worker.work();
    }
}

image-20201213140318251

总结

但从装配Bean上看,ApplicationContext和BeanFactory类似,但ApplicationContext比BeanFactory提供了更多的功能,比如国际化,装载文件资源、向监听器Bean发送事件等。因此当用XML来配置Bean的时候,用ApplicationContext来装配Bean是比较好的方式。

但!XML已经不是Spring推荐的配置方式了。

使用Java装配JavaBean

“在最近的Spring版本中,基于Java的配置更为常见。”

创建如下一个配置类:

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

@Configuration
public class BeanConfig {
    @Bean
    public Worker worker(){
        return new Worker();
    }
}

它的作用和下面的XML的作用是一样的。

<bean id = "worker" class = "Worker"/>

@Configuration注解会告知Spring这是一个配置类,会为Spring应用上下文提供Bean。这个配置类的方法使用@Bean注解进行了标注,表明这些方法所返回的对象会以bean的形式添加到Spring的应用上下文中(默认情况下,这些bean所对应的bean ID与定义它们的方法名称是相同的)。

如何使用?如下:

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

public class Test3 {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanConfig.class);
        Worker worker = ctx.getBean(Worker.class);
        worker.work();
    }
}

使用了一个ApplicationContext(应用上下文)接口的一个实现类:AnnotationConfigApplicationContext,和XML的装配没有什么不同。

但为啥Spring推荐使用Java的配置方法呢?

在《Spring实战》一书中,这么写道:“相对于基于XML的配置方式,基于Java的配置会带来更多项额外的收益,包括更强的类型安全性以及更好的重构能力。”

Spring Boot的魔法

自动装配

Spring Boot为Spring家族带来的最大改变之一就是自动配置(autoconfiguration)。Spring Boot能够基于类路径中的条目、环境变量和其他因素合理猜测需要配置的组件并将它们装配在一起。

创建SpringBoot工程

首先创建一个SpringBoot工程,结构目录:

image-20201213223806058

新建一个Worker类,并实现自动注入。

package com.example.demo;

import org.springframework.context.annotation.Configuration;

@Configuration
public class Worker {
    private String name;

    public void work(){
        System.out.println("worker:开启机器!");
        System.out.println("worker:机器完成生产任务!");
    }
}

仅仅加上一个Configuration的注解即可。

用起来也是十分的方便,在Spring Boot自动提供的测试类中编写代码:

package com.example.demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    private Worker worker;
    @Test
    void contextLoads() {
        worker.work();
    }
}

只需要声明一个私有变量,并且用Autowired注解标注,在测试代码中就能直接用worker的对象了。

image-20201213224448988

没用一点点配置文件,这就是Spring Boot自动装配的魅力所在。而关于Spring Boot的自动配置又完全可以另起一篇文章了。【评论留言,我就更新Spring Boot自动装配的文章】

关于Ioc与装配Bean的文章到此就结束啦。不知道小伙伴们是否哪怕一丝丝的收获,写这么多字不易,希望大家能帮忙点点赞!

关注俺的公众号,方便我们能够互相交流学习!

大家也可以来我的个人博客:妖刀的个人博客 评论留言。


原网址: 访问
创建于: 2021-08-27 09:45:43
目录: default
标签: 无

请先后发表评论
  • 最新评论
  • 总共0条评论