6.Spring之DI高级
  Op9yysgqYUmV 2023年11月02日 81 0


前面已经说了DI的相关知识,本节再做一些补充

延迟初始化Bean

延迟初始化也叫做惰性初始化,指不提前初始化Bean,而是只有在真正使用时才创建及初始化Bean。配置方式很简单只需在<bean>标签上指定 “lazy-init” 属性值为“true”即可延迟初始化Bean。

Spring容器会在创建容器时提前初始化作用域为“singleton”的Bean(默认方式),“singleton”就是单例的意思即整个容器每个Bean只有一个实例,Spring容器预先初始化Bean通常能帮助我们提前发现配置错误,所以如果没有什么情况建议开启,除非有某个Bean可能需要加载很大资源,而且很可能在整个应用程序生命周期中很可能使用不到,可以设置为延迟初始化。

延迟初始化的Bean通常会在第一次使用时被初始化;或者在被非延迟初始化Bean作为依赖对象注入时被初始化,因为在这时使用了延迟初始化Bean。

容器管理初始化Bean消除了编程实现延迟初始化,完全由容器控制,只需在需要延迟初始化的Bean定义上配置即可,比编程方式更简单,而且是无侵入代码的。

使用depends-on

depends-on指定Bean初始化及销毁时的顺序,使用depends-on属性指定的Bean要先初始化完毕后才初始化当前Bean,由于只有“singleton”Bean能被Spring管理销毁,所以当指定的Bean都是“singleton”时,使用depends-on属性指定的Bean要在指定的Bean之后销毁。看一个例子:

<bean id="resourceBean" 
      class="com.spring.di.bean.ResourceBean" 
      init-method="init" destroy-method="destroy" lazy-init="true">
    <property name="file" value="D:/test.txt"/>      
</bean>
<bean id="dependentBean" 
      class="com.spring.di.bean.DependentBean" 
      init-method="init" destroy-method="destroy" depends-on="resourceBean">
    <property name="resourceBean" ref="resourceBean"/>
</bean>

init-method="init" :指定初始化方法,在构造器注入和setter注入完毕后执行。

destroy-method="destroy":指定销毁方法,只有“singleton”作用域能销毁,“prototype”作用域的一定

不能,其他作用域不一定能

“dependentBean”指定了“depends-on”属性为“resourceBean”,所以在“dependentBean”初始化之前要先初始化“resourceBean”,而在销毁“resourceBean”之前先要销毁“dependentBean”,depends-on属性可以指定多个Bean,若指定多个Bean可以用“;”、“,”、空格分割。

那“depends-on”有什么好处呢?主要是给出明确的初始化及销毁顺序,比如要初始化“dependentBean”时要确保“resourceBean”的资源准备好了,否则使用“dependentBean”时会得不到需要的资源;而在销毁时要先把对“resourceBean”资源的引用释放掉才能销毁“resourceBean”,否则可能销毁 “resourceBean”时而“dependentBean”还保持着资源访问,造成资源不能释放或释放错误。

public class ResourceBean { 
    private FileOutputStream fos;    
    private File file;    
    //初始化方法
    public void init() {
        System.out.println("=========初始化ResourceBean");
        //加载资源
        System.out.println("=========加载ResourceBean的资源,执行一些预操作");
        try {
            this.fos = new FileOutputStream(file);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }    
    //销毁资源方法
    public void destroy() {
        System.out.println("=========销毁ResourceBean");
        //释放资源
        System.out.println("=========释放ResourceBean的资源,执行一些清理操作");
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }    
    public FileOutputStream getFos() {
        return fos;
    }    
    public void setFile(File file) {
        this.file = file;
    }
}

ResourceBean从配置文件中配置文件位置,然后定义初始化方法init打开指定的文件获取文件流;最后定义销毁方法destroy用于在应用程序关闭时调用该方法关闭掉文件流。

public class DependentBean {    
    ResourceBean resourceBean;    
    public void write(String ss) throws IOException {
        System.out.println("=========DependentBean写资源");
        resourceBean.getFos().write(ss.getBytes());
    }
    //初始化方法
    public void init() throws IOException {
        System.out.println("=========初始化DependentBean");
    }
    //销毁方法
    public void destroy() throws IOException {
        System.out.println("=========销毁DependentBean");
        //在销毁之前需要往文件中写销毁内容
        resourceBean.getFos().write("DependentBean:=========销毁=====".getBytes());
    }    
    public void setResourceBean(ResourceBean resourceBean) {
        this.resourceBean = resourceBean;
    }
}

DependentBean中会注入ResourceBean,并从ResourceBean中获取文件流写入内容;定义初始化方法init用来定义一些初始化操作并向文件中输出文件头信息;最后定义销毁方法用于在关闭应用程序时想文件中输出文件尾信息,测试一下:

public void testDependOn() throws IOException {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("di/depends-on.xml");
    context.registerShutdownHook();//一点要注册销毁回调,否则我们定义的销毁方法不执行
    DependentBean dependentBean = context.getBean("dependentBean", DependentBean.class);
    dependentBean.write("hello spring\n\t");
}

自动装配

自动装配就是指由Spring来自动地注入依赖对象,无需人工参与。常用的装配方式有“no”、“byName ”、“byType”、“constructor”四种,默认是“no”指不支持自动装配。自动装配的好处是减少构造器注入和setter注入配置,减少配置文件的长度。自动装配通过配置<bean>标签的“autowire”属性来改变自动装配方式。接下来让我们看下配置的含义

一、default:表示使用默认的自动装配,默认的自动装配需要在<beans>标签中使用default-autowire属性指定,其支持“no”、“byName ”、“byType”、“constructor”四种自动装配

二、no:意思是不支持自动装配,必须明确指定依赖。

三、byName:通过设置Bean定义属性autowire="byName",意思是根据名字进行自动装配,只能用于setter注入。比如我们的HelloApiDecorator有方法“setHelloApi”,则“byName”方式Spring容器将查找名字为helloApi的Bean并注入,如果找不到指定的Bean,将什么也不注入。

<bean id="helloApi" class="com.spring.di.HelloImpl"/>

<!-- 通过构造器注入 -->

<bean id="bean"

class="com.spring.di.bean.HelloApiDecorator"

autowire="byName"/>

可以看出,HelloApiDecorator不用配置<property>了,如果一个bean有很多setter注入,通过“byName”方式能减少很多<property>配置。需要注意的是在根据名字注入时,将把当前Bean自己排除在外,比如“hello”Bean类定义了“setHello”方法,则hello是不能注入到“setHello”的。

四、“byType”:通过设置Bean定义属性autowire="byType",意思是指根据类型注入,用于setter注入,比如如果指定自动装配方式为“byType”,而“setHelloApi”方法需要注入HelloApi类型数据,则Spring容器将查找HelloApi类型数据,如果找到一个则注入该Bean,如果找不到将什么也不注入,如果找到多个Bean将优先注入<bean>标签“primary”属性为true的Bean,否则抛出异常来表明有个多个Bean但不知道使用哪个。通常我们会遇到这几种情况

1)根据类型只找到一个Bean,此处注意了,在根据类型注入时,将把当前Bean自己排除在外,即如下配置中helloApi和bean都是HelloApi接口的实现,而“bean”通过类型进行注入“HelloApi”类型数据时自己是排除在外的,配置如下

<bean class="com.spring.di.HelloImpl"/>
<bean id="bean" 
      class="com.spring.di.bean.HelloApiDecorator"
      autowire="byType"/>

2)根据类型找到多个Bean时,对于集合类型(如List、Set)将注入所有匹配的候选者,而对于其他类型遇到这种情况可能需要使用“autowire-candidate”属性为false来让指定的Bean放弃作为自动装配的候选者,或使用“primary”属性为true来指定某个Bean为首选Bean:

2.1)通过设置Bean定义的“autowire-candidate”属性为false来把指定Bean后自动装配候选者中移除:

<bean class="com.spring.di.HelloImpl"/>
<!-- 从自动装配候选者中去除 -->
<bean class="com.spring.di.HelloImpl" autowire-candidate="false"/>
<bean id="bean" 
      class="com.spring.di.bean.HelloApiDecorator"
      autowire="byType"/>

2.2)通过设置Bean定义的“primary”属性为true来把指定自动装配时候选者中首选Bean:

<bean class="com.spring.di.HelloImpl"/>
<!-- 自动装配候选者中的首选Bean-->
<bean class="com.spring.di.HelloImpl" primary="true"/>
<bean id="bean" 
      class="com.spring.di.bean.HelloApiDecorator"
      autowire="byType"/>
<bean id="listBean" 
      class="com.spring.di.bean.ListBean" 
      autowire="byType"/>

五、“constructor”:通过设置Bean定义属性autowire="constructor",功能和“byType”功能一样,根据类型注入构造器参数,只能用于构造器注入方式

<bean class="com.spring.di.HelloImpl"/>
<!-- 自动装配候选者中的首选Bean-->
<bean class="com.spring.di.HelloImpl" primary="true"/>
<!-- 因为有空构造器将使用byType方式 -->
<bean id="bean" 
      class="com.spring.di.bean.HelloApiDecorator"
      autowire="constructor"/>

不是所有类型都能自动装配

• 不能自动装配的数据类型:Object、基本数据类型(Date、CharSequence、Number、URI、URL、Class、int)等;

• 通过“<beans>”标签default-autowire-candidates属性指定的匹配模式,不匹配的将不能作为自动装配的候选者,例如指定“*Service,*Dao”,将只把匹配这些模式的Bean作为候选者,而不匹配的不会作为候选者;

• 通过将“<bean>”标签的autowire-candidate属性可被设为false,从而该Bean将不会作为依赖注入的候选者。

数组、集合、字典类型的根据类型自动装配和普通类型的自动装配是有区别的

• 数组类型、集合(Set、Collection、List)接口类型:将根据泛型获取匹配的所有候选者并注入到数组或集合中,如“List<HelloApi> list”将选择所有的HelloApi类型Bean并注入到list中,而对于集合的具体类型将只选择一个候选者,“如 ArrayList<HelloApi> list”将选择一个类型为ArrayList的Bean注入,而不是选择所有的HelloApi类型Bean进行注入;

• 字典(Map)接口类型:同样根据泛型信息注入,键必须为String类型的Bean名字,值根据泛型信息获取,如“Map<String, HelloApi> map” 将选择所有的HelloApi类型Bean并注入到map中,而对于具体字典类型如“HashMap<String, HelloApi> map”将只选择类型为HashMap的Bean注入,而不是选择所有的HelloApi类型Bean进行注入

自动装配给我们带来很多好处。首先,自动装配确实减少了配置文件的量;其次,“byType”自动装配能在相应的Bean更改了字段类型时自动更新,即修改Bean类不需要修改配置,确实简单了。但自动装配也是有缺点的,最重要的缺点就是没有了配置,在查找注入错误时非常麻烦,还有比如基本类型没法完成自动装配,所以可能经常发生一些莫名其妙的错误,因此最好不要使用该方式,最好是指定明确的注入方式,或者采用注解注入方式。

自动装配注入方式能和配置注入方式一同工作吗?当然可以,只是配置注入的数据会覆盖自动装配注入的数据。

依赖检查

大家可能也注意到对于采用自动装配方式时如果没找到合适的的Bean时什么也不做,这样在程序中总会莫名其妙的发生一些空指针异常,而且是在程序运行期间才能发现,有没有办法能在提前发现这些错误呢?这就需要依赖检查来实现。但是现在已经不推荐配置的方式实现依赖检查了,我们使用@Required注解方式实现,这里不再详述。

方法注入

所谓方法注入其实就是通过配置方式覆盖或拦截指定的方法,通常通过代理模式实现。Spring提供两种方法注入:查找方法注入和方法替换注入。

因为Spring是通过CGLIB动态代理方式实现方法注入,也就是通过动态修改类的字节码来实现的,本质就是生成需方法注入的类的子类方式实现。

一、查找方法注入:又称为Lookup方法注入,用于注入方法返回结果,也就是说能通过配置方式替换方法返回结果。

使用<lookup-method name="方法名" bean="bean名字"/>配置;其中name属性指定方法名,bean属性指定方法需返回的Bean。

方法定义格式:访问级别必须是public或protected,保证能被子类重载,可以是抽象方法,必须有返回值,必须是无参数方法,查找方法的类和被重载的方法必须为非final:

<public|protected> [abstract] <return-type> theMethodName(no-arguments);

因为“singleton”Bean在容器中只有一个实例,而“prototype”Bean是每次获取容器都返回一个全新的实例,所以如果“singleton”Bean在使用“prototype” Bean情况时,那么“prototype”Bean由于是“singleton”Bean的一个字段属性,所以获取的这个“prototype”Bean就和它所在的“singleton”Bean具有同样的生命周期,所以不是我们所期待的结果。因此查找方法注入就是用于解决这个问题。

1) 首先定义我们需要的类,Printer类是一个有状态的类,counter字段记录访问次数

public class Printer {    
    private int counter = 0;    
    public void print(String type) {
        System.out.println(type + " printer: " + counter++);
    }    
}

HelloImpl5类用于打印欢迎信息,其中包括setter注入和方法注入,此处特别需要注意的是该类是抽象的,充分说明了需要容器对其进行子类化处理,还定义了一个抽象方法createPrototypePrinter用于创建“prototype”Bean,createSingletonPrinter方法用于创建“singleton”Bean,此处注意方法会被Spring拦截,不会执行方法体代码:

public abstract class HelloImpl5 implements HelloApi {    
    private Printer printer;    
    @Override
    public void sayHello() {
        printer.print("setter");
        createPrototypePrinter().print("prototype");
        createSingletonPrinter().print("singleton");
    }    
    public abstract Printer createPrototypePrinter();        
    public Printer createSingletonPrinter() {
        System.out.println("该方法不会被执行,如果输出就错了");
        return new Printer();
    }    
    public void setPrinter(Printer printer) {
        this.printer = printer;
    }
}

2) 配置文件xml文件,其中“prototypePrinter”是“prototype”Printer,

“singletonPrinter”是“singleton”Printer,“helloApi1”是“singleton”Bean,而“helloApi2”注入了“prototype”Bean:

<!-- prototype -->
<bean id="prototypePrinter" class="com.spring.di.bean.Printer" scope="prototype"/>
<!-- singleton -->
<bean id="singletonPrinter" class="com.spring.di.bean.Printer" scope="singleton"/>
<bean id="helloApi1" class="com.spring.di.HelloImpl5" scope="singleton">
    <property name="printer" ref="prototypePrinter"/>
    <lookup-method name="createPrototypePrinter" bean="prototypePrinter"/>
    <lookup-method name="createSingletonPrinter" bean="singletonPrinter"/>
</bean>           
<bean id="helloApi2" class="com.spring.di.HelloImpl5" scope="prototype">
    <property name="printer" ref="prototypePrinter"/>
    <lookup-method name="createPrototypePrinter" bean="prototypePrinter"/>
    <lookup-method name="createSingletonPrinter" bean="singletonPrinter"/>
</bean>

3)测试代码如下:

public void testLookup() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("di/lookupMethodInject.xml");
    System.out.println("=======singleton sayHello======");
    HelloApi helloApi1 = context.getBean("helloApi1", HelloApi.class);
    helloApi1.sayHello();
    helloApi1 = context.getBean("helloApi1", HelloApi.class);
    helloApi1.sayHello();
    System.out.println("=======prototype sayHello======");
    HelloApi helloApi2 = context.getBean("helloApi2", HelloApi.class);
    helloApi2.sayHello();
    helloApi2 = context.getBean("helloApi2", HelloApi.class);
    helloApi2.sayHello();
}

其中“helloApi1”测试输出结果如下:

=======prototype sayHello======  

setter printer: 0  

prototype printer: 0  

singleton printer: 2  

setter printer: 0  

prototype printer: 0  

singleton printer: 3 

首先“helloApi1”是“singleton”,通过setter注入的“printer”是“prototypePrinter”,所以它应该输出

“setter printer:0”和“setter printer:1”;而“createPrototypePrinter”方法注入了“prototypePrinter”,所以

应该输出两次“prototype printer:0”;而“createSingletonPrinter”注入了“singletonPrinter”,所以应该输出“singleton printer:0”和“singleton printer:1”。

而“helloApi2”测试输出结果如下:

=======prototype sayHello======

setter printer: 0

prototype printer: 0

singleton printer: 2

setter printer: 0

prototype printer: 0

singleton printer: 3

首先“helloApi2”是“prototype”,通过setter注入的“printer”是“prototypePrinter”,所以它应该输出两

次“setter printer:0”;而“createPrototypePrinter”方法注入了“prototypePrinter”,所以应该输出两次prototype printer:0”;而“createSingletonPrinter”注入了“singletonPrinter”,所以应该输出“singleton

printer:2”和“singleton printer:3”。

 

二、替换方法注入:也叫“MethodReplacer”注入,和查找注入方法不一样的是,他主要用来替换方法体。首先定义一个MethodReplacer接口实现,然后如下配置来实现:

<replaced-method name="方法名" replacer="MethodReplacer实现">
<arg-type>参数类型</arg-type>
</replaced-method>

1)首先定义MethodReplacer实现,完全替换掉被替换方法的方法体及返回值,其中reimplement方法重定义方法功能,参数obj为被替换方法的对象,method为被替换方法,args为方法参数;最需要注意的是不能再 通过“method.invoke(obj, new String[]{"hehe"});” 反射形式再去调用原来方法,这样会产生循环调用;如果返回值类型为Void,请在实现中返回null:

public class PrinterReplacer implements MethodReplacer {

    @Override
    public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
        System.out.println("Print Replacer");
        //注意此处不能再通过反射调用了,否则会产生循环调用,知道内存溢出
        //method.invoke(obj, new String[]{"hehe"});
        return null;
    }
}

2)配置如下,首先定义MethodReplacer实现,使用< replaced-method >标签来指定要进行替换方法,属性name指定替换的方法名字,replacer指定该方法的重新实现者,子标签< arg-type >用来指定原来方法参数的类型,必须指定否则找不到原方法:

<bean id="replacer" class="com.spring.di.bean.PrinterReplacer"/>
<bean id="printer" class="com.spring.di.bean.Printer">
    <replaced-method name="print" replacer="replacer">
        <arg-type>java.lang.String</arg-type>
    </replaced-method>
</bean>

3)测试代码将输出“Print Replacer ”,说明方法体确实被替换了:

public void testMethodReplacer() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("di/methodReplacerInject.xml");
    Printer printer = context.getBean("printer", Printer.class);
    printer.print("我将被替换");
}


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
Op9yysgqYUmV