乐趣区

关于spring:强推Java大牛熬夜一周梳理的-Spring-IOC笔记收藏一波

Hello,明天给各位童鞋们分享 Spring IOC,连忙拿出小本子记下来吧!

1. IoC 原理

IoC 全称 Inversion of Control,直译为管制反转。

为什么要应用 IoC?

咱们假设一个在线书店,通过 BookService 获取书籍:

public class BookService {

private HikariConfig config = new HikariConfig();

private DataSource dataSource = new HikariDataSource(config);

public Book getBook(long bookId) {try (Connection conn = dataSource.getConnection()) {

        ...

        return book;

    }

}

}

为了从数据库查问书籍,BookService 持有一个 DataSource。为了实例

HikariDataSource,又不得不实例化一个 HikariConfig。当初,咱们持续编

UserService 获取用户:

public class UserService {

private HikariConfig config = new HikariConfig();

private DataSource dataSource = new HikariDataSource(config);

public User getUser(long userId) {try (Connection conn = dataSource.getConnection()) {

        ...

        return user;

    }

}

}

因为 UserService 也须要拜访数据库,因而,咱们不得不也实例化一个 HikariDataSource。

每一次调用办法,都须要实例化一个 HikariDataSource,容易造成资源节约。如果用某种办法实现了共享资源,那么怎么确保在所有性能残缺的状况下,销毁以开释资源呢?

因而,外围问题是:

谁负责创立组件?

谁负责依据依赖关系组装组件?

销毁时,如何按依赖程序正确销毁?

解决这一问题的外围计划就是 IoC。

传统的应用程序中, 控制权在程序自身, 程序的管制流程齐全由开发者管制,即在程序外部进行实例化类。

而在 IoC 模式下,控制权产生了反转,即从应用程序转移到了 IoC 容器,所有组件不再由应用程序本人创立和配置,而是由 IoC 容器负责,这样,应用程序只须要间接应用曾经创立好并且配置好的组件。

为了能让组件在 IoC 容器中被“拆卸”进去,须要某种“注入”机制,例如,BookService 本人并不会创立 DataSource,而是期待内部通过 setDataSource() 办法来注入一个 DataSource:

public class BookService {

private DataSource dataSource;

public void setDataSource(DataSource dataSource) {this.dataSource = dataSource;}

}

不间接 new 一个 DataSource,而是注入一个 DataSource,这个小小的改变尽管简略,却带来了一系列益处:

BookService 不再关怀如何创立 DataSource,因而,不用编写读取数据库配置之类的代码;

DataSource 实例被注入到 BookService,同样也能够注入到 UserService,因而,共享一个组件非常简单;

测试 BookService 更容易,因为注入的是 DataSource,能够应用内存数据库,而不是实在的 MySQL 配置。

因而,IoC 又称为依赖注入(DI:Dependency Injection),它解决了一个最次要的问题:将组件的创立 + 配置与组件的应用相拆散,并且,由 IoC 容器负责管理组件的生命周期。

因为 IoC 容器要负责实例化所有的组件,因而,有必要通知容器如何创立组件,以及各组件的依赖关系。一种最简略的配置是通过 XML 文件来实现,例如:

<beans>

<bean id="dataSource" class="HikariDataSource" />

<bean id="bookService" class="BookService">

    <property name="dataSource" ref="dataSource" />

</bean>

<bean id="userService" class="UserService">

    <property name="dataSource" ref="dataSource" />

</bean>

</beans>

上述 XML 配置文件批示 IoC 容器创立 3 个 JavaBean 组件,并把 id 为 dataSource 的组件通过属性 dataSource(即调用 setDataSource() 办法)注入到另外两个组件中。

在 Spring 的 IoC 容器中,咱们把所有组件统称为 JavaBean,即配置一个组件就是配置一个 Bean。

依赖注入(DI:Dependency Injection)形式

从下面的代码咱们能够得悉,依赖注入能够通过 set() 办法实现,但同时咱们也能够通过构造方法来实现:

//set() 办法

public class BookService {

private DataSource dataSource;

public void setDataSource(DataSource dataSource) {this.dataSource = dataSource;}

}

// 结构器办法

public class BookService {

private DataSource dataSource;

public BookService(DataSource dataSource) {this.dataSource = dataSource;}

}

无侵入容器

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

应用程序组件既能够在 Spring 的 IoC 容器中运行,也能够本人编写代码自行组装配置;

测试的时候并不依赖 Spring 容器,可独自进行测试,大大提高了开发效率。

2. 拆卸 Bean 组件

咱们来看一个具体的用户注册登录的例子。整个工程的构造如下:

咱们先编写一个 MailService,用于在用户登录和注册胜利后发送邮件告诉:

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_ZONED_DATE_TIME);

}

public void sendLoginMail(User user) {System.err.println(String.format("Hi, %s! You are logged in at %s", user.getName(), getTime()));

}

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

}

}

再编写一个 UserService,实现用户注册和登录:

留神到 UserService 通过 setMailService() 注入了一个 MailService。而后,咱们须要编写一个特定的 application.xml 配置文件,通知 Spring 的 IoC 容器应该如何创立并组装 Bean:

<?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

    https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="userService" class="com.itranswarp.learnjava.service.UserService">

    <property name="mailService" ref="mailService" />

</bean>

<bean id="mailService" class="com.itranswarp.learnjava.service.MailService" />

</beans>

留神察看上述配置文件,其中与 XML Schema 相干的局部格局是固定的,咱们只关注两个 <bean …> 的配置:

每个 <bean …> 都有一个 id 标识,相当于 Bean 的惟一 ID;

在 userServiceBean 中,通过 <property name=”…” ref=”…” /> 注入了另一个 Bean;

Bean 的程序不重要,Spring 依据依赖关系会主动正确初始化。

最初一步,咱们须要创立一个 Spring 的 IoC 容器实例,而后加载配置文件,让 Spring 容器为咱们创立并拆卸好配置文件中指定的所有 Bean,这只须要一行代码:

ApplicationContextcontext=newClassPathXmlApplicationContext(“application.xml”);

接下来,咱们就能够从 Spring 容器中“取出”拆卸好的 Bean 而后应用它:

// 获取 Bean:

UserService userService = context.getBean(UserService.class);

// 失常调用:

User user = userService.login(“bob@example.com”, “password”);

创立 Spring IoC 容器

ClassPathXmlApplicationContext(罕用)

咱们从创立 Spring 容器的代码:

ApplicationContextcontext=newClassPathXmlApplicationContext(“application.xml”);

能够看到,Spring 容器就是 ApplicationContext,它是一个接口,有很多实现类,这里咱们抉择 ClassPathXmlApplicationContext,示意它会主动从 classpath 中查找指定的 XML 配置文件。

从 ApplicationContext 中咱们能够依据 Bean 的 ID 获取 Bean,但更多的时候咱们依据 Bean 的类型获取 Bean 的援用:

UserService userService = context.getBean(UserService.class);

其中,userService 为实例化的一个类,失去的 userService 能够调用类中的办法。

BeanFactory

Spring 还提供另一种 IoC 容器叫 BeanFactory,应用形式和 ApplicationContext

相似:

BeanFactoryfactory=newXmlBeanFactory(newClassPathResource(“application.xml”));

MailService mailService = factory.getBean(MailService.class);

BeanFactory 和 ApplicationContext 的区别在于,BeanFactory 的实现是按需创立,即第一次获取 Bean 时才创立这个 Bean,而 ApplicationContext 会一次性创立所有的 Bean。实际上,ApplicationContext 接口是从 BeanFactory 接口继承而来的,并且,ApplicationContext 提供了一些额定的性能,包含国际化反对、事件和告诉机制等。通常状况下,咱们总是应用 ApplicationContext,很少会思考应用 BeanFactory。

3. 应用 Annotation 进行简化配置

咱们能够应用 Annotation 配置,能够齐全不须要 XML,让 Spring 主动扫描 Bean 并组装它们。

先删除 XML 配置文件,而后,给 UserService 和 MailService 增加几个注解。

首先,咱们给 MailService 增加一个 @Component 注解:

@Component

public class MailService {

...

}

这个 @Component 注解就相当于定义了一个 Bean,它有一个可选的名称,默认是 mailService,即小写结尾的类名。

而后,咱们给 UserService 增加一个 @Component 注解和一个 @Autowired 注解:

@Component

public class UserService {

@Autowired

MailService mailService;

...

}

应用 @Autowired 就相当于把指定类型的 Bean 注入到指定的字段中。此外,还能够间接写在构造方法中:

最初,编写一个 AppConfig 类启动容器:

这里须要阐明的是,

应用的实现类是 AnnotationConfigApplicationContext,所以必须传入一个标注了 @Configuration 的类名。

AppConfig 还标注了 @ComponentScan,它通知容器,主动搜寻以后类所在的包以及子包,把所有标注为 @Component 的 Bean 主动创立进去,并依据 @Autowired 进行拆卸。

应用 @ComponentScan 十分不便,然而,咱们也要特地留神包的层次结构。通常来说,启动配置 AppConfig 位于自定义的顶层包,其余 Bean 按类别放入子包。

4. 定制 Bean 组件

Scope(@Scope(“prototype”))

Bean 只须要一个实例:

对于 Spring 容器来说,当咱们把一个 Bean 标记为 @Component 后,它就会主动为咱们创立一个单例(Singleton),即容器初始化时创立 Bean,容器敞开前销毁 Bean。在容器运行期间,咱们调用 getBean(Class) 获取到的 Bean 总是同一个实例。

须要不同实例:

还有一种 Bean,咱们每次调用 getBean(Class),容器都返回一个新的实例,这种 Bean 称为 Prototype(原型),它的生命周期显然和 Singleton 不同。申明一个 Prototype 的 Bean 时,须要增加一个额定的 @Scope 注解:

@Component

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope(“prototype”)

public class MailSession {

...

}

注入 List

有些时候,咱们会有一系列接口雷同,不同实现类的 Bean。例如,注册用户时,咱们要对 email、password 和 name 这 3 个变量进行验证。为了便于扩大,咱们先定义验证接口:

public interface Validator {

// 定义方法

void validate(String email, String password, String name);

}

而后,别离应用 3 个 Validator 对用户参数进行验证:

最初,咱们通过一个 Validators 作为入口进行验证:

留神到 Validators 被注入了一个 List<Validator>,Spring 会主动把所有类型为 Validator 的 Bean 拆卸为一个 List 注入进来,这样一来,咱们每新增一个 Validator 类型,就主动被 Spring 拆卸到 Validators 中了,十分不便。

可选注入(无指定 Bean)

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

能够给 @Autowired 减少一个 required = false 的参数:

@Component

public class MailService {

@Autowired(required = false)

ZoneId zoneId = ZoneId.systemDefault();

...

}

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

这种形式非常适合有定义就应用定义,没有就应用默认值的状况。

创立第三方 Bean(不在包中的 Bean)

如果一个 Bean 不在咱们本人的 package 治理之内,例如 ZoneId,如何创立它?

答案是咱们本人在 @Configuration 类中编写一个 Java 办法创立并返回它,留神给办法标记一个 @Bean 注解:

@Configuration

@ComponentScan

public class AppConfig {

// 创立一个 Bean:

@Bean

ZoneId createZoneId() {return ZoneId.of("Z");

}

}

Spring 对标记为 @Bean 的办法只调用一次,因而返回的 Bean 依然是单例。(屡次则 @Bean(prototype))

初始化和销毁

有些时候,一个 Bean 在注入必要的依赖后,须要进行初始化(监听音讯等)。在容器敞开时,有时候还须要清理资源(敞开连接池等)。

在此之前,须要引入 JSR-250 定义的 Annotation:

<dependency>

<groupId>javax.annotation</groupId>

<artifactId>javax.annotation-api</artifactId>

<version>1.3.2</version>

</dependency>

在 Bean 的初始化和清理办法上标记 @PostConstruct 和 @PreDestroy:

Spring 容器会对上述 Bean 做如下初始化流程:

调用构造方法创立 MailService 实例;

依据 @Autowired 进行注入;

调用标记有 @PostConstruct 的 init() 办法进行初始化。

而销毁时,容器会首先调用标记有 @PreDestroy 的 shutdown() 办法。

Spring 只依据 Annotation 查找无参数办法,对办法名不作要求。

应用别名

当咱们须要创立多个同类型的 Bean 时,咱们就会用到别名:

能够用 @Bean(“name”) 指定别名,也能够用 @Bean+@Qualifier(“name”) 指定别名。

指定了别名后,注入时就须要指定 Bean 的名称,不然会报错:

@Component

public class MailService {

@Autowired(required = false)

@Qualifier(“z”) // 指定注入名称为 ”z” 的 ZoneId

ZoneId zoneId = ZoneId.systemDefault();

...

}

或者指定默认 Bean,当注入时没有指定 Bean 的名字,则默认注入标记有 @Primary 的 Bean:

请点击输出图片形容(最多 18 字)

应用 FactoryBean(工厂模式)

用工厂模式创立 Bean 须要实现

FactoryBean 接口。咱们察看上面的代码:

当一个 Bean 实现了 FactoryBean 接口后,Spring 会先实例化这个工厂,而后调用 getObject() 创立真正的 Bean。getObjectType() 能够指定创立的 Bean 的类型,因为指定类型不肯定与理论类型统一,能够是接口或抽象类。

因而,如果定义了一个 FactoryBean,要留神 Spring 创立的 Bean 实际上是这个 FactoryBean 的 getObject() 办法返回的 Bean。为了和一般 Bean 辨别,咱们通常都以 XxxFactoryBean 命名。

5. 应用 Resource 读取文件

在 Java 程序中,咱们常常会读取配置文件、资源文件等。应用 Spring 容器时,咱们也能够把“文件”注入进来,不便程序读取。

上图是工程的构造,咱们须要读取 logo.txt 文件,通常状况下,咱们须要写很多繁琐的代码,次要是为了定位文件,关上 InputStream。Spring 则提供了一个 org.springframework.core.io.Resource,能够间接注入:

也能够间接指定文件的门路,例如:

@Value(“file:/path/to/logo.txt”)

private Resource resource;

6. 注入配置(读取配置文件)

@PropertySource 注入配置

除了像 Resource 读取文件那样,Spring 容器提供了一个更简略的 @PropertySource

来主动读取配置文件。咱们只须要在 @Configuration 配置类上再增加一个注解:

Spring 容器看到 @PropertySource(“app.properties”) 注解后,主动读取这个配置文件,而后,咱们应用 @Value 失常注入:

@Value(“${app.zone:Z}”)

String zoneId;

留神注入的字符串语法,它的格局如下:

“${app.zone}” 示意读取 key 为 app.zone 的 value,如果 key 不存在,

启动将报错;

“${app.zone:Z}” 示意读取 key 为 app.zone 的 value,但如果 key 不存在,就应用默认值 Z

还能够把注入的注解写到办法参数中:

@BeanZoneId

createZoneId(@Value(“${app.zone:Z}”) String zoneId) {

return ZoneId.of(zoneId);

}

Bean 中标记,须要注入的中央再标记

另一种注入配置的形式是先通过一个简略的 JavaBean 持有所有的配置,例如,一个

SmtpConfig:

而后,在须要读取的中央,应用 #{smtpConfig.host} 注入:

“#{smtpConfig.host}” 的意思是,从名称为 smtpConfig 的 Bean 读取 host 属性,即调用 getHost() 办法。

应用一个独立的 JavaBean 持有所有属性,而后在其余 Bean 中以 #{bean.property} 注入的益处是,多个 Bean 都能够援用同一个 Bean 的某个属性。例如,如果 SmtpConfig 决定从数据库中读取相干配置项,那么 MailService 注入的 @Value(“#{smtpConfig.host}”) 依然能够不批改失常运行。

7. 应用条件拆卸

定义不同环境

Spring 为应用程序筹备了 Profile 这一概念,用来示意不同的环境。例如,咱们别离定义开发、测试和生产这 3 个环境:

native

test

production

创立某个 Bean 时,Spring 容器能够依据注解 @Profile 来决定是否创立。例如,以下配置:

如果以后的 Profile 设置为 test,则 Spring 容器会调用 createZoneIdForTest() 创立 ZoneId,否则,调用 createZoneId() 创立 ZoneId。留神到 @Profile(“!test”) 示意非 test 环境。

在运行程序时,加上 JVM 参数 -Dspring.profiles.active=test 就能够指定以 test 环境启动。

实际上,Spring 容许指定多个 Profile,例如:

-Dspring.profiles.active=test,master

能够示意 test 环境,并应用 master 分支代码。

要满足多个 Profile 条件,能够这样写:

@Bean

@Profile({“test”, “master”}) // 同时满足 test 和 master

ZoneId createZoneId() {

...

}

应用 Conditional(条件注解)决定是否创立 Bean

除了依据 @Profile 条件来决定是否创立某个 Bean 外,Spring 还能够依据

@Conditional 决定是否创立某个 Bean。

例如,咱们对 SmtpMailService 增加如下注解:

@Component

@Conditional(OnSmtpEnvCondition.class)

public class SmtpMailService implements MailService {

...

}

它的意思是,如果满足 OnSmtpEnvCondition 的条件,才会创立 SmtpMailService 这个 Bean。

public class OnSmtpEnvCondition implements Condition {

public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {return "true".equalsIgnoreCase(System.getenv("smtp"));

}

}

Spring 只提供了 @Conditional 注解,具体判断逻辑还须要咱们本人实现。

好啦,明天的文章就到这里,心愿能帮忙到屏幕前迷茫的你们!

退出移动版