Spring 之IOC实现剖析


前言

在spring框架中,IOC容器是极为重要的核心概念。它可以管理所有轻量级的javaBean组件,提供众多底层服务:组件的声明周期管理、配置和组装服务、AOP支持及建立在AOP基础上的声明是事务服务等。

一、IOC原理

IOC,全称为Inversion of Control,即控制反转,IOC区别于传统组件创建实例化和自管理的方式,IOC容器将组件创建、依赖关系管理等托管负责,应用程序只需要直接使用已经创建好并配置好的组件即可。相当于外卖服务,人们不再通过自己亲自下厨的方式完成,只需手机点餐,然后等待外卖小哥送餐上门即可。

无侵入容器

在设计上,Spring的IoC容器是一个高度可扩展的无侵入容器。所谓无侵入,是指应用程序的组件无需实现Spring的特定接口,或者说,组件根本不知道自己在Spring的容器中运行。这种无侵入的设计有以下好处:

  1. 应用程序组件既可以在Spring的IoC容器中运行,也可以自己编写代码自行组装配置;
  2. 测试的时候并不依赖Spring容器,可单独进行测试,大大提高了开发效率。

二、实例IOC容器搭建仿用户登录、邮件通知(XML方式配置)

1.创建一个maven 工程

在这里插入图片描述

2.pom.xml文件中引入spring framework依赖包

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>spring-ioc-context</groupId>
    <artifactId>spring-ioc-context</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring.version>5.2.3.RELEASE</spring.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
    </dependencies>
</project>

3. 创建User实体

package entity;

/**
 * TODO:
 *
 * @Version 1.0
 * @Author HJL
 * @Date 2021/12/14 15:33
 */
public class User {

    private long id;
    private String email;
    private String name;
    private String password;

    public User(long id, String email, String password,String name) {
        this.id = id;
        this.email = email;
        this.name = name;
        this.password = password;
    }

    public long getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

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

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

4. 创建UserService服务类

package service;

import entity.User;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * TODO:
 *
 * @Version 1.0
 * @Author HJL
 * @Date 2021/12/14 15:25
 */
public class UserService {
    private MailService mailService;

    public void setMailService(MailService mailService) {
        this.mailService = mailService;
    }

    private List<User> users = new ArrayList<>(Arrays.asList( // users:
            new User(1, "zhangsan@example.com", "123456", "zhangsan"),
            new User(2, "lisi@example.com", "123456", "lisi"), 
            new User(3, "wangwu@example.com", "123456", "wangwu"))); 

    public User login(String email, String password) {
        System.out.println("登录邮箱:" + email);
        System.out.println("登录密码:" + password);
        for (User user : users) {
            if (user.getEmail().equalsIgnoreCase(email) && user.getPassword().equals(password)) {
                mailService.sendLoginMail(user);
                return user;
            }
        }
        throw new RuntimeException("登录失败.");
    }

}

5.创建MailService服务类

package service;

import entity.User;

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

/**
 * TODO:
 *
 * @Version 1.0
 * @Author HJL
 * @Date 2021/12/14 15:24
 */
public class MailService {
    private ZoneId zoneId = ZoneId.systemDefault();

    public void setZoneId(ZoneId zoneId) {
        this.zoneId = zoneId;
    }

    public String getTime() {
        return ZonedDateTime.now(this.zoneId).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }

    public void sendLoginMail(User user) {
        System.err.println(String.format("嗨, %s! 您在 %s 成功登入系统" , user.getName(), getTime()));
    }

    public void sendRegistrationMail(User user) {
        System.err.println(String.format("欢迎您, %s!", user.getName()));

    }

}

6.创建容器类运行测试

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import entity.User;
import service.UserService;

/**
 * TODO:
 *
 * @Version 1.0
 * @Author HJL
 * @Date 2021/12/14 15:43
 */
public class Application {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        UserService userService = context.getBean(UserService.class);
        User user = userService.login("zhangsan@example.com", "123456");
        System.out.println(user.getName());
    }
}

7.测试结果

在这里插入图片描述

8.XML配置实现方式总结

  1. Spring的IoC容器接口是ApplicationContext,并提供了多种实现类;
  2. 通过XML配置文件创建IoC容器时,使用ClassPathXmlApplicationContext;
  3. 持有IoC容器后,通过getBean()方法获取Bean的引用。

三.注解方式配置实现

1. 修改MailService添加一个@Component注解

@Component
public class MailService {
    ...
}

2. 修改UserService添加一个@Component注解和一个@Autowired注解

@Component
public class UserService {
    @Autowired
    MailService mailService;

    ...
}

3.启动类中配置@Configuration及@ComponentScan注解

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.support.ClassPathXmlApplicationContext;
import entity.User;
import service.UserService;

/**
 * TODO:
 *  * @Version 1.0
 * @Author HJL
 * @Date 2021/12/14 15:43
 */
@Configuration
@ComponentScan("service")
public class Application {

    public static void main(String[] args) {
//        ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        ApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
        UserService userService = context.getBean(UserService.class);
        User user = userService.login("zhangsan@example.com", "123456");
        System.out.println(user.getName());
    }
}

4.注解说明

  • @Component:相当于定义一个Bean;

  • @Autowired:就相当于把指定类型的Bean注入到指定的字段中。该注解可以写在属性上,也可以写在set()方法上,甚至可以写在构造方法的参数中;如:

@Component
public class UserService {
    MailService mailService;

    public UserService(@Autowired MailService mailService) {
        this.mailService = mailService;
    }
    ...
}
  • @Configuration:标明类是一个配置类。
  • @ComponentScan:它告诉容器,自动搜索当前类所在的包以及子包,把所有标注为@Component的Bean自动创建出来,并根据@Autowired进行装配。

四、定制Bean

1.Scope

在Spring 容器中存在两种bean创建机制:其一为单例(Singleton),即在@Component注解标记下的形式,该模式下容器创建时初始化,容器关闭前销毁;其二为Prototype(原型),每次通过调用getBean(Class),容器都返回一个新的实例。配置方式如下:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
public class MailSession {
    ...
}

2.注入List

当我们有一系列接口需要同时实现多个Bean时,可以使用List方式;
示例为用户注册验证:

  1. 声明一个公共的验证接口
 public interface Validator {
    void validate(String email, String password, String name);
}
  1. 实现邮箱号验证
 @Component
public class EmailValidator implements Validator {
    public void validate(String email, String password, String name) {
        if (!email.matches("^[a-z0-9]+\\@[a-z0-9]+\\.[a-z]{2,10}$")) {
            throw new IllegalArgumentException("invalid email: " + email);
        }
        System.out.println("邮箱验证通过!");
    }
}
  1. 实现用户名验证
 @Component
public class NameValidator implements Validator {
    public void validate(String email, String password, String name) {
        if (name == null || name.isEmpty() || name.length() > 20) {
            throw new IllegalArgumentException("无效用户名: " + name);
        }
        System.out.println("用户名:" + name + " 验证通过!");
    }
}
  1. 实现密码验证
 @Component
public class PasswordValidator implements Validator {
    public void validate(String email, String password, String name) {
        if (!password.matches("^.{6,20}$")) {
            throw new IllegalArgumentException("无效密码");
        }
        System.out.println("密码验证通过!" );
    }
}
  1. 配置最终提供用户注册验证的接口
 @Component
public class Validators {
    @Autowired
    List<Validator> validators;

    public void validate(String email, String password, String name) {
        for (Validator validator : this.validators) {
            validator.validate(email, password, name);
        }
    }
}
  1. 模拟用户注册验证
 @Configuration
@ComponentScan({"service","validator"})
public class Application {

    public static void main(String[] args) {
//        ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        ApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
        UserService userService = context.getBean(UserService.class);
//        User user = userService.login("zhangsan@example.com", "123456","zhangsan");
        userService.register("huangmou@qq.com","123456","黄某");
//        System.out.println(user.getName());
    }
}
  1. 测试结果
    在这里插入图片描述
    注:因为Spring是通过扫描classpath获取到所有的Bean,而List是有序的,要指定List中Bean的顺序,可以加上@Order注解:
@Component
@Order(1)
public class EmailValidator implements Validator {
    ...
}
@Component
@Order(2)
public class PasswordValidator implements Validator {
    ...
}
@Component
@Order(3)
public class NameValidator implements Validator {
    ...
}

3.可选注入

默认情况下,当我们标记了一个@Autowired后,Spring如果没有找到对应类型的Bean,它会抛出NoSuchBeanDefinitionException异常。

可以给@Autowired增加一个required = false的参数:

@Component
public class MailService {
    @Autowired(required = false)
    ZoneId zoneId = ZoneId.systemDefault();
    ...
}

这个参数告诉Spring容器,如果找到一个类型为ZoneId的Bean,就注入,如果找不到,就忽略。

这种方式非常适合有定义就使用定义,没有就使用默认值的情况。

4.创建第三方Bean

只需在@Configuration类中编写一个Java方法创建并返回它:
如对数据源的配置,或其它引入第三方工具组件、缓存redis等都是开发中常用的手段。

@Configuration
public class MyDataSourceConfig implements WebMvcConfigurer {
    /**
     * 当向容器中添加了 Druid 数据源
     * 使用 @ConfigurationProperties 将配置文件中 spring.datasource 开头的配置与数据源中的属性进行绑定
     * @return
     */
    @ConfigurationProperties("spring.datasource")
    @Bean
    public DataSource dataSource() throws SQLException {
        DruidDataSource druidDataSource = new DruidDataSource();
        //同时开启 sql 监控(stat) 和防火墙(wall),中间用逗号隔开。
        //开启防火墙能够防御 SQL 注入攻击
        druidDataSource.setFilters("stat,wall");
        return druidDataSource;
    }

    /**
     * 开启 Druid 数据源内置监控页面
     *
     * @return
     */
    @Bean
    public ServletRegistrationBean statViewServlet() {
        StatViewServlet statViewServlet = new StatViewServlet();
        //向容器中注入 StatViewServlet,并将其路径映射设置为 /druid/*
        ServletRegistrationBean servletRegistrationBean =
                new ServletRegistrationBean(statViewServlet, "/druid/*");
        //配置监控页面访问的账号和密码(选配)
        servletRegistrationBean.addInitParameter("loginUsername", "admin");
        servletRegistrationBean.addInitParameter("loginPassword", "123456");
        return servletRegistrationBean;
    }

    /**
     * 向容器中添加 WebStatFilter
     * 开启内置监控中的 Web-jdbc 关联监控的数据
     * @return
     */
    @Bean
    public FilterRegistrationBean druidWebStatFilter() {
        WebStatFilter webStatFilter = new WebStatFilter();
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(webStatFilter);
        // 监控所有的访问
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
        // 监控访问不包括以下路径
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return filterRegistrationBean;
    }
}

5.Bean的初始化和销毁监听

  • 调用标记有@PostConstruct的init()方法进行初始化。
  • 销毁时,容器会首先调用标记有@PreDestroy的shutdown()方法。
@Component
public class MailService {
    @Autowired(required = false)
    ZoneId zoneId = ZoneId.systemDefault();

    @PostConstruct
    public void init() {
        System.out.println("Init mail service with zoneId = " + this.zoneId);
    }

    @PreDestroy
    public void shutdown() {
        System.out.println("Shutdown mail service");
    }
}

6.使用别名

在有多个bean的相同接口实现类时,通过别名指定需要注入的bean;示例:

@Configuration
@ComponentScan
public class AppConfig {
    @Bean("z")
    ZoneId createZoneOfZ() {
        return ZoneId.of("Z");
    }

    @Bean
    @Qualifier("utc8")
    ZoneId createZoneOfUTC8() {
        return ZoneId.of("UTC+08:00");
    }
}

或者把其中某个Bean指定为@Primary,这样在注入时,如果没有指出Bean的名字,Spring会注入标记有@Primary的Bean。

@Configuration
@ComponentScan
public class AppConfig {
    @Bean
    @Primary
    DataSource createMasterDataSource() {
        ...
    }

    @Bean
    @Qualifier("slave")
    DataSource createSlaveDataSource() {
        ...
    }
}

五.注入配置文件

1.注解配置方式

  1. 通过@PropertySource注解配置来读取配置文件信息;
  2. 通过@Value注解标注在属性上完成配置文件信息的注入。
    示例:
 @Configuration
@ComponentScan
@PropertySource("app.properties") // 表示读取classpath的app.properties
public class AppConfig {
    @Value("${app.zone:Z}")
    String zoneId;

    @Bean
    ZoneId createZoneId() {
        return ZoneId.of(zoneId);
    }
}

2.JavaBean持有配置信息的方式

  1. 持有配置
@Component
public class SmtpConfig {
    @Value("${smtp.host}")
    private String host;

    @Value("${smtp.port:25}")
    private int port;

    public String getHost() {
        return host;
    }

    public int getPort() {
        return port;
    }
}
  1. 读取配置
@Component
public class MailService {
    @Value("#{smtpConfig.host}")
    private String smtpHost;

    @Value("#{smtpConfig.port}")
    private int smtpPort;
}
  • Spring容器可以通过@PropertySource自动读取配置,并以@Value("${key}")的形式注入;
  • 可以通过${key:defaultValue}指定默认值;
  • 以#{bean.property}形式注入时,Spring容器自动把指定Bean的指定属性值注入。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

today_code_logs

做最明亮的自己,然后照亮他人

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值