Spring5框架入门学习之IOC依赖注入(二)

1. 注入方式

IOC的核心就是DI,Spring提供简单的机制来管理组件的依赖项,并在组件整个生命期都能进行管理,Spring主要提供了两种注入方式。

1.1 基于构造函数注入

当在组件的构造函数中提供依赖项时,就发生了构造函数注入。这与调用具有特有参数的静态工厂方法创建Bean的实例一致(可参考上一篇文章内容)。代码如下所示:
在这里插入图片描述

1.2 setter方法注入

IOC 容器可通过JavaBean的setter方法注入组件的依赖,下面演示使用setter方法注入依赖。
在这里插入图片描述

1.3 案例实战

下面演示一个典型的构造器注入案例。

  1. 定义消息接口
/**
 * 消息服务接口
 */
public interface MessageService {

    String getMessage();
}
  1. 定义消息实现类
/**
 * 消息服务实现
 */
public class MessageServiceImpl implements MessageService {

    private String name;

    private int age;

    public MessageServiceImpl(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String getMessage() {
        return "this is a message comes from a "+age+" year's boy whose name is "+name;
    }
}
  1. 定义消息发送器,用来打印消息
public class MessageSender {

    private MessageService messageService;

    /**
     * 构造器注入
     * @param messageService messageService
     */
    public MessageSender(MessageService messageService) {
        this.messageService = messageService;
    }

    public void printMessage() {
        System.out.println(messageService.getMessage());
    }
}
  1. 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="messageService" class="com.codegeek.day2.MessageServiceImpl">
        <constructor-arg name="name" value="张三"/>
        <constructor-arg name="age" value="23"/>
    </bean>

    <!--构造器注入-->
    <bean id="messageSender" class="com.codegeek.day2.MessageSender">
        <constructor-arg name="messageService" ref="messageService"/>
    </bean>
</beans>
  1. 应用测试类
public class SpringTest {
    
    public static void main(String[] args) {
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("day2/applicationContext.xml");
        MessageSender messageSender = classPathXmlApplicationContext.getBean("messageSender", MessageSender.class);
        messageSender.printMessage();
    }
}

运行结果如下:
在这里插入图片描述

2. 依赖注入配置详解

2.1 直接配置

下面以开发中常常遇见的数据库数据源配置为例,如下图所示一个常见的数据源配置
在这里插入图片描述
其中property属性就是类的属性名称,而value就是为其进行赋值,可以看到PropertyPlaceholderConfigurer 其继承树图父类 PropertiesLoaderSupport
在这里插入图片描述
在这个类中有PropertiesLoaderSupport 中 setLocation方法。
在这里插入图片描述
如果Bean之间有协作方式则可以引用Bean,ref 元素可以是constructor-arg标签也可以是Property。
在这里插入图片描述

2.2 集合配置

Java集合类型中如List、Set、Map、Properties 可以使用提供的<list>、<set>、<map>、<props> 进行属性参数配置。下面在MessageSender类添加如下四种类型数据进行配置如下:
在这里插入图片描述

  • list

XML中元数据配置信息如下所示:

    <bean id="messageSender1" class="com.codegeek.day2.MessageSender">
      <property name="list">
         <list>
            <value>我是messageSender的list数据</value>
         </list>
      </property>
    </bean>

测试类代码如下:

    @Test
    public void test() {
        MessageSender messageSender = applicationContext.getBean("messageSender1", MessageSender.class);
        System.out.println(messageSender.getList());
    }

运行结果:
在这里插入图片描述

  • set

XML中元数据配置信息如下所示:

    <bean id="messageSender2" class="com.codegeek.day2.MessageSender">
        <property name="set">
            <set>
                <value>我是messageSender的set数据1</value>
                <value>我是messageSender的set数据2</value>
            </set>
        </property>
    </bean>

测试类代码如下:

    @Test
    public void testSet() {
        MessageSender messageSender = applicationContext.getBean("messageSender2", MessageSender.class);
        System.out.println(messageSender.getSet());
    }

运行结果:
在这里插入图片描述

  • map

XML中元数据配置信息如下所示:

    <bean id="student" class="com.codegeek.day1.Student">
        <property name="name" value="张三"/>
        <property name="age" value="23"/>
        <property name="gender" value=""/>
        <property name="address" value="中国"/>
    </bean>

    <!--静态工厂方法参数赋值-->
    <bean id="iphone11" class="com.codegeek.day1.Phone" factory-method="instance">
        <constructor-arg name="brandName" value="iphone 11"/>
        <constructor-arg name="price" value="5999"/>
        <constructor-arg name="producePlace" value="china"/>
    </bean>

    <bean id="messageSender3" class="com.codegeek.day2.MessageSender">
        <property name="map">
            <map>
                <entry key="小学生" value="小明"/>
                <entry key="china">
                    <map>
                        <entry key="hubei" value="wuhan~加油"/>
                    </map>
                </entry>
                <entry key="student" value-ref="iphone11"/>
            </map>
        </property>
    </bean>

测试类代码如下:

    @Test
    public void testMap() {
        MessageSender messageSender = applicationContext.getBean("messageSender3", MessageSender.class);
        System.out.println(messageSender.getMap());
    }

运行结果:
在这里插入图片描述

  • properties

XML中元数据配置信息如下所示:

    <bean id="messageSender4" class="com.codegeek.day2.MessageSender">
       <property name="properties">
           <props>
               <prop key="driver">com.mysql.jdbc.Driver</prop>
           </props>
       </property>
    </bean>

测试类代码如下:

    @Test
    public void testProps() {
        MessageSender messageSender = applicationContext.getBean("messageSender4", MessageSender.class);
        System.out.println(messageSender.getProperties());
    }

运行结果:
在这里插入图片描述
还有一种特色的集合类型设置即util名称创建空间集合,如使用util:list 如下所示:

    <bean id="messageSender1" class="com.codegeek.day2.MessageSender">
        <property name="list" ref="list1">
        </property>
    </bean>

    <util:list id="list1">
        <value>123</value>
        <value>456</value>
    </util:list>

再次运行testList方法运行结果如下:
在这里插入图片描述
其他类型集合与上面类似,这里就不在一一演示,其常见简单配置如下所示:
在这里插入图片描述

3. XML命名空间

  • p命名空间

开发中可以使用P命名空间简化类property标签赋值,XML配置如下所示:
在这里插入图片描述
需要注意的是需要导入P命名空间,利用Idea智能提示可以自动添加P命名空间。
在这里插入图片描述

  • c命名空间

与P命名空间类似,开发可以使用C标签替换constructor-arg,xml配置信息如下所示:
在这里插入图片描述

4. 继承Bean

开发中常常会复用父类的属性或方法,此时如果希望子类继承父类的一些配置数据就可以在子Bean中定义parent属性,该属性指向父Bean,Xml配置如下所示:
在这里插入图片描述
声明一个抽象父类Person,然后User继承自Person

@Data
@AllArgsConstructor
@NoArgsConstructor
public abstract class Person {

    private String userName;

    private String email;

    private Integer age;
    
}


@NoArgsConstructor
@Getter
@Setter
public class User extends Person {

    public User(String userName, String email, Integer age) {
       this.setUserName(userName);
       this.setEmail(email);
       this.setAge(age);
    }


    @Override
    public String toString() {
        return new StringJoiner(", ", User.class.getSimpleName() + "[", "]")
                .add("userName='" + this.getUserName() + "'")
                .add("email='" + this.getEmail() + "'")
                .add("age=" + this.getAge())
                .toString();
    }
}

测试类如下:
在这里插入图片描述
运行结果可以看出,User2对象继承了来自Person的属性。
在这里插入图片描述

5. Bean的scope

默认情况下位于IOC容器的Bean都是单例的,我们可以简单测试证明一下。
在这里插入图片描述
Bean的scope属性可用值如下所示:

范围 说明
singleton 默认值,IOC容器只会有一个实例
prototype IOC容器会重复创建该类型Bean实例
request web环境一次请求对应一个实例
session web环境下一次会话对应一个实例
application 定义IOC容器一个实例
websocket web环境下一次webSocket对应一个实例

下面在演示一下prototype的实例,其他属性值设置不常见就不在演示了。
将user2的scope设置为prototype如下所示:
在这里插入图片描述
再次运行测试类代码发现结果为false。
在这里插入图片描述

6. 延迟加载Bean

一般情况下当IOC容器启动时就会自动实例化所有的单例对象,如果想取消IOC容器这个行为可以设置懒加载解决:即当要使用这个类在进行初始化。
在这里插入图片描述
如果上面的Bean设置了懒加载那么启动容器将不会创建该Bean,新增LazyBean对象:

@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class LazyBean {

    private Boolean flag;

    public void setFlag(Boolean flag) {
        System.out.println("设置了flag:"+flag);
        this.flag = flag;
    }
}

启动IOC容器如下所示:
在这里插入图片描述
需要注意的是当延迟加载Bean作为非延迟加载对象的引用时,延迟加载的对象会跟随IOC容器启动而创建,不再进行懒加载。
在这里插入图片描述
新增非延迟加载对象ReferenceBean
在这里插入图片描述
测试代码运行如下:
在这里插入图片描述

6. Bean的生命周期

开发过程中可以使用三种方法来完成自定义Bean生命周期,可以使用 Spring提供的接口 InitializingBeanDisposableBean 或者使用JSR-250提供的两个注解 @PostConstruct@preDestroy ,或者还可以使用Spring的 init-methoddestroy-method

6.1 使用 Spring接口InitializingBeanDisposableBean

Spring的两个接口 InitializingBeanDisposableBean 实现了容器Bean的创建与销毁。开发中若实现了这两个接口方法,那么容器将会自动调用 afterPropertiesSet()destroy()以管理我们Bean的生命周期,实现如下所示:

@Data
public class Account {

    private String userId;
    private String accountId;
    private BigDecimal accountBalance;

    private static final BigDecimal BIG_DECIMAL = new BigDecimal(10);

    public void addBalance(BigDecimal balance) {
        if (this.accountBalance.compareTo(new BigDecimal(10000)) < -1) {
            this.accountBalance = this.accountBalance.add(balance);
            System.out.println("用户:" + userId + " 账号:" + accountId + "入账" + balance + "元");
        }
    }

    public void reduceBalance(BigDecimal balance) {
        if (this.accountBalance.compareTo(new BigDecimal(10000)) > -1) {
            this.accountBalance = this.accountBalance.subtract(balance);
            System.out.println("用户:" + userId + " 账号:" + accountId + "扣款" + balance + "元");
        }
    }
}


public class BeanLife implements InitializingBean, DisposableBean {

    @Autowired
    private Account account;

    /**
     * bean销毁的时候可执行的操作
     *
     * @throws Exception
     */
    @Override
    public void destroy() throws Exception {
        System.out.println("销毁的操作 .. 执行扣钱的操作");
        this.execute(account, "reduce");
    }

    /***
     * 初始化对象可执行的操作
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("初始化对象的操作.....执行账号加钱操作");
        this.execute(account, "add");
    }

    private void execute(Account account, String method) {
        System.out.println("-----执行excute--------"+method);
        Assert.state(account != null, "account must not empty!");
        if ("add".equals(method)) {
            account.addBalance(new BigDecimal(50001));
        } else if ("reduce".equals(method)) {
            account.reduceBalance(new BigDecimal(49999));
        }
    }
}
  • xml配置如下:
    在这里插入图片描述
  • 测试类代码如下:
    在这里插入图片描述

6.2 使用 JSR-250 @PostConstruct@preDestroy 注解

使用注解的方式要比直接实现Bean接口方式要好,在一个类中标注 @PostConstruct@preDestroy两个注解,下面是这种方式的演示:

  • 新增PostConstructDemo
public class PostConstructDemo {

    @PostConstruct
    public void init() {
        System.out.println("---我是postConstruct注解的init方法");
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println("---我是preDestroy注解的destroy方法");
    }
}
  • xml 配置如下:
    在这里插入图片描述
  • 运行测试类如下:
    在这里插入图片描述

6.3 使用Spring提供的 init-methoddestroy-method 方法

可以在XML元数据配置文件中使用Spring提供的 init-methoddestroy-method 方法完成Bean创建与销毁的一些工作。

  • 新增 SpringInitDemo
public class SpringInitDemo {

    public void SpringInit() {
        System.out.println("---我是spring调用的init方法");
    }

    public void SpringDestroy() {
        System.out.println("---我是spring调用的destroy方法");
    }
}
  • xml元数据配置:
    在这里插入图片描述
    运行结果如下:
    在这里插入图片描述

6.4 三种方法执行优先级

将上面三种控制Bean生命周期方法一起如下所示:

  • 改造 BeanLife
public class BeanLife implements InitializingBean, DisposableBean {

    @Autowired
    private Account account;

    /**
     * bean销毁的时候可执行的操作
     *
     * @throws Exception
     */
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean.destroy() 销毁的操作 .. 执行扣钱的操作");
        this.execute(account, "reduce");
    }

    /***
     * 初始化对象可执行的操作
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean.afterPropertiesSet() 初始化对象的操作.....执行账号加钱操作");
        this.execute(account, "add");
    }

    @PostConstruct
    public void init() {
        System.out.println("---我是postConstruct注解的init方法");
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println("---我是preDestroy注解的destroy方法");
    }


    public void SpringInit() {
        System.out.println("---我是spring调用的init方法");
    }


    public void SpringDestroy() {
        System.out.println("---我是spring调用的destroy方法");
    }


    private void execute(Account account, String method) {
       // System.out.println("-----执行excute--------"+method);
        Assert.state(account != null, "account must not empty!");
        if ("add".equals(method)) {
            account.addBalance(new BigDecimal(50001));
        } else if ("reduce".equals(method)) {
            account.reduceBalance(new BigDecimal(49999));
        }
    }
}

运行测试结果如下:
BeanLife
由上面运行结果可知:一个Bean如果配置了多个生命周期的方法,其执行顺序如下:

  1. 包含由@PostConstruct注解方法。
  2. 实现了InitializingBean接口的afterPropertiesSet()方法。
  3. 指定Spring XML的init-method属性的自定义方法。
  4. 包含由 @PreDestroy注解方法。
  5. 实现了DisposableBean接口的destroy()方法。
  6. 指定Spring XML的destroy-method属性的自定义方法。

7. 获取 ApplicationContext

开发中有时候需要在未被Spring管理Bean对象去获取Spring管理的JavaBean对象,这个功能很有用,比如调用Spring管理JavaBean对象逻辑层进行数据库的读写。而这只需我们定义一个类去实现 ApplicationContextAware 接口然后设置Spring的上下文对象 ApplicationContext 即可,下面演示是项目常用的工具类如下:

public class SpringUtils implements ApplicationContextAware {

    public static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtils.applicationContext == null) {
            SpringUtils.applicationContext = applicationContext;
        }
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }


    //通过name获取 Bean.
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }

    //通过class获取Bean.
    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }

    //通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }
}

8. 使用 BeanPostProcessor 处理器

最新版本的Spring中的 BeanPostProcessor 提供的两个接口都为默认方法:
在这里插入图片描述
BeanPostProcessor 方法定义了回调方法,可以结合项目实现自己对Bean初始化前或销毁前所做的一些逻辑。

  • 新增JavaBean对象
@Getter
@Setter
@AllArgsConstructor
public class Order {

    private String orderName;

    private Double price;
    
    public Order() {
        System.out.println("order被创建了");
    }
    
    public void  init() {
        System.out.println("order的init方法");
    }

    public void destroy() {
        System.out.println("order的destroy方法");
    }
}
  • 自定义实现接口
public class BeanPostProcessorDemo implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("postProcessBeforeInitialization1: "+beanName+"开始执行");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("postProcessAfterInitialization1: "+beanName+"开始执行");
        return bean;
    }
}
  • xml元数据配置如下:
    在这里插入图片描述
  • 测试运行
    在这里插入图片描述
    由此我们可知后置处理器在容器初始化方法之前和容器销毁方法之前进行业务逻辑的设定。

9. 使用 PropertyPlaceholderConfigurer

最新版本该类标记的过时,不过这个类还是比较实用的。
在这里插入图片描述
在这里插入图片描述
开发中可使用该类具体化值,也就是我们将常见数据库连接的用户名和密码进行加密这样别人看到我们的数据库连接信息是密文状态的,然后当IOC容器启动时再将其进行解密成明文然后连接数据库。

  • 定义加密类
public class DESUtil {
    private static Key key;
    private static String KEY_STR = "myKey";
    private static String CHAR_SET_NAME = "UTF-8";
    private static String ALGORITHM = "DES";

    static {
        try {
            KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM);
            SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
            secureRandom.setSeed(KEY_STR.getBytes());
            generator.init(secureRandom);
            key = generator.generateKey();
            generator = null;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static String getEncryptString(String str) {
        Base64.Encoder encoder = Base64.getEncoder();
        try {
            byte[] bytes = str.getBytes(CHAR_SET_NAME);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] doFinal = cipher.doFinal(bytes);
            return encoder.encodeToString(doFinal);
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    public static String getDecryptString(String str) {
        Base64.Decoder decoder = Base64.getDecoder();
        try {
            byte[] bytes = decoder.decode(str);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] doFinal = cipher.doFinal(bytes);
            return new String(doFinal, CHAR_SET_NAME);
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    public static void main(String[] args) {
        System.out.println(getEncryptString("123456")); //QAHlVoUc49w=
        System.out.println(getEncryptString("root")); // WnplV/ietfQ=
    }
}
  • 新增数据库配置文件在这里插入图片描述
  • xml元数据配置
    在这里插入图片描述
  • PropertyPlaceholderConfigurer 继承类
public class EncryptPropertySourcesPlaceholderConfigurer extends
        PropertyPlaceholderConfigurer {

    private String[] encryptPropNames = {"jdbc.username", "jdbc.password"};

    @Override
    protected String convertProperty(String propertyName, String propertyValue) {
        if (isEncryptProp(propertyName)) {
            String decryptValue = DESUtil.getDecryptString(propertyValue);
            System.out.println("解密后的值:"+decryptValue);
            return decryptValue;
        } else {
            return propertyValue;
        }
    }

    private boolean isEncryptProp(String propertyName) {
        for (String encryptPropertyName : encryptPropNames) {
            if (encryptPropertyName.equals(propertyName)) {
                return true;
            }
        }
        return false;
    }
}
  • 测试类运行如下:
    在这里插入图片描述
    可以看到我们的properties配置的 jdbc.usernamejdbc.password两个属性的值设置的是加密后的,当重写 PropertyPlaceholderConfigurerconvertProperty方法然后在对其进行解密后即可。

源码

以上代码均可在 codegeekgao.git 下载查看,下一章将温习Spring基于注解配置相关知识的学习。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章