前言
在spring框架中,IOC容器是极为重要的核心概念。它可以管理所有轻量级的javaBean组件,提供众多底层服务:组件的声明周期管理、配置和组装服务、AOP支持及建立在AOP基础上的声明是事务服务等。
一、IOC原理
IOC,全称为Inversion of Control,即控制反转,IOC区别于传统组件创建实例化和自管理的方式,IOC容器将组件创建、依赖关系管理等托管负责,应用程序只需要直接使用已经创建好并配置好的组件即可。相当于外卖服务,人们不再通过自己亲自下厨的方式完成,只需手机点餐,然后等待外卖小哥送餐上门即可。
无侵入容器
在设计上,Spring的IoC容器是一个高度可扩展的无侵入容器。所谓无侵入,是指应用程序的组件无需实现Spring的特定接口,或者说,组件根本不知道自己在Spring的容器中运行。这种无侵入的设计有以下好处:
- 应用程序组件既可以在Spring的IoC容器中运行,也可以自己编写代码自行组装配置;
- 测试的时候并不依赖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配置实现方式总结
- Spring的IoC容器接口是ApplicationContext,并提供了多种实现类;
- 通过XML配置文件创建IoC容器时,使用ClassPathXmlApplicationContext;
- 持有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方式;
示例为用户注册验证:
- 声明一个公共的验证接口
public interface Validator {
void validate(String email, String password, String name);
}
- 实现邮箱号验证
@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("邮箱验证通过!");
}
}
- 实现用户名验证
@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 + " 验证通过!");
}
}
- 实现密码验证
@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("密码验证通过!" );
}
}
- 配置最终提供用户注册验证的接口
@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);
}
}
}
- 模拟用户注册验证
@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());
}
}
- 测试结果
注:因为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.注解配置方式
- 通过@PropertySource注解配置来读取配置文件信息;
- 通过@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持有配置信息的方式
- 持有配置
@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;
}
}
- 读取配置
@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的指定属性值注入。