简介
IOC
IOC(Inversion of Control),即控制反转。
这不是一项技术,而是一种思想。
其根本就是对象创建的控制权由使用它的对象转变为第三方的容器,即控制权的反转。
DI
DI(Dependency Injection),即依赖注入,IOC 的实现方式。
所谓依赖注入,就是由 IOC 容器在运行期间,动态地将某种依赖关系注入到对象之中。
优点
对象之间的解耦
对象之间不会相互影响,减少程序出错的可能性,提高了代码的灵活性和可维护性。
缺点
生成对象的步骤变得更为复杂
会增加团队成员学习和认识的培训成本。
运行效率有一定的损耗
由于IOC容器生成对象是通过反射方式,相较于传统的创建对象方式效率是要低一些的。
需要大量的配置工作
由于生成对象交给了第三方容器,对第三方容器的配置是较为繁琐的。
体验
创建一个普通的 Maven 项目,引入 spring-context 依赖。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
</dependencies>
在 resources 目录下创建一个 spring 的配置文件。
官方推荐命名为 applicationContext.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">
</beans>
配置需要注册到 Spring 容器的 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 http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.javaboy.Book" id="book"/>
</beans>
class 表示需要注册的 bean 的全路径。
id 表示 bean 的唯一标记。
也可以用 name 属性作为 bean 的标记,它们之间有一定的区别,后面会介绍。
加载配置文件
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
从容器中去获取对象
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Book book = ctx.getBean("book", Book.class);
ClassPathXmlApplicationContext 是去 classpath 下查找配置文件的加载方式。
也可以使用 FileSystemXmlApplicationContext ,它会从操作系统路径下去寻找配置文件。
FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext("D:\\Work\\Project\\training\\src\\main\\resources");
Book book = (Book) ctx.getBean("book");
这种方式要写较长的系统路径,且如果项目路径变更就需要修改,不推荐使用。
通过 Class 获取 Bean
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Book book = ctx.getBean(Book.class);
这种方式有一个很大的弊端:如果存在多个实例,这种方式就不可用。
例如,xml 文件中存在两个同 class 的 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 http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.javaboy.Book" id="book"/>
<bean class="org.javaboy.Book" id="book2"/>
</beans>
此时,如果通过 Class 去查找 Bean,会报如下错误:
所以,一般建议使用 name 或者 id 去获取 Bean 的实例。
属性注入
构造方法
通过 Bean 的构造方法给 Bean 的属性注入值
给 Bean 添加对应的构造方法:
public class Book implements Serializable {
private static final long serialVersionUID = 5492270562431552420L;
private Integer id;
private String name;
private Double price;
public Book() {
}
public Book(Integer id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
}
在 xml 文件中注入 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
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--构造方法注入:不能清晰看到定义的哪个属性,不推荐使用-->
<bean id="book" class="cn.sail.training.spring.ioc.Book">
<constructor-arg index="0" value="1"/>
<constructor-arg index="1" value="红楼梦"/>
<constructor-arg index="2" value="33"/>
</bean>
</beans>
constructor-arg 中的 index 和 Book 中的构造方法参数一一对应。
写的顺序可以颠倒,但是 index 的值和 value 要一一对应。
另一种构造方法中的属性注入,则是通过直接指定参数名来注入
<?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="book1" class="cn.sail.training.spring.ioc.Book">
<constructor-arg name="id" value="2"/>
<constructor-arg name="name" value="西游记"/>
<constructor-arg name="price" value="40"/>
</bean>
</beans>
如果有多个构造方法,则会根据给出参数个数以及参数类型,自动匹配到对应的构造方法上,进而初始化一个对象。
set 方法
<?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">
<!--set方法注入:可以较直观的看到定义的属性,推荐使用-->
<bean id="user" class="cn.sail.training.spring.ioc.User">
<property name="id" value="1"/>
<property name="age" value="27"/>
<property name="name" value="哈哈"/>
</bean>
</beans>
set 方法注入,不是根据属性名对应的值,而是根据 get/set 方法分析出来的属性名。
如果改变set方法的名称,就算属性名不变,也会找不到值而报错。
p 名称空间
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="book2" class="cn.sail.training.spring.ioc.Book" p:id="2" p:name="西游记" p:price="33"/>
</beans>
这种方式本质上也是调用了set方法,但层次结构不清晰,且需要额外引进标签值,不推荐使用。
静态工厂
提供一个 OkHttpClient 的静态工厂
import okhttp3.OkHttpClient;
public class OkHttpUtils {
private static OkHttpClient okHttpClient;
public static OkHttpClient getInstance() {
if (okHttpClient == null) {
okHttpClient = new OkHttpClient.Builder().build();
}
return okHttpClient;
}
}
在 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"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--静态工厂注入-->
<bean id="okHttpUtils" class="cn.sail.training.spring.ioc.OkHttpUtils" factory-method="getInstance"/>
</beans>
这个配置表示 OkHttpUtils 类中的 getInstance 是我们需要的实例,实例的名字就叫 okHttpClient。
然后,在 Java 代码中,获取到这个实例,就可以直接使用了。
实例工厂
实例工厂就是工厂方法是一个实例方法,这样,工厂类必须实例化之后才可以调用工厂方法。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--实例工厂注入-->
<bean id="okHttpUtils" class="cn.sail.training.spring.ioc.OkHttpUtils" factory-method="getInstance"/>
<bean id="okHttpClient1" class="okhttp3.OkHttpClient" factory-bean="okHttpUtils"/>
</beans>
复杂属性注入
对象
可以通过 xml 注入对象,通过 ref 来引用一个对象。
<bean id="user1" class="cn.sail.training.spring.ioc.User">
<property name="cat" ref="cat"/>
</bean>
<bean id="cat" class="cn.sail.training.spring.ioc.Cat">
<property name="name" value="小白"/>
<property name="color" value="白色"/>
</bean>
数组
<bean id="user2" class="cn.sail.training.spring.ioc.User">
<property name="favorites">
<array>
<value>足球</value>
<value>篮球</value>
<value>乒乓球</value>
</array>
</property>
</bean>
集合
即可以通过 ref 使用外部定义好的 Bean,也可以直接在 list 或者 array 节点中定义 bean。
<bean id="user3" class="cn.sail.training.spring.ioc.User">
<property name="cats">
<list>
<ref bean="cat"/>
<bean id="cat2" class="cn.sail.training.spring.ioc.Cat">
<property name="name" value="小黑"/>
<property name="color" value="黑色"/>
</bean>
</list>
</property>
</bean>
Map
<bean id="user4" class="cn.sail.training.spring.ioc.User">
<property name="map">
<map>
<entry key="name" value="sail"/>
<entry key="age" value="27"/>
</map>
</property>
</bean>
Properties
<bean id="user5" class="cn.sail.training.spring.ioc.User">
<property name="info">
<props>
<prop key="name">sail</prop>
<prop key="age">27</prop>
</props>
</property>
</bean>
Java 配置
在 Spring 中,想要将一个 Bean 注册到 Spring 容器中,整体上来说,有三种不同的方式
- XML 注入
- Java 配置(通过 Java 代码将 Bean 注册到 Spring 容器中)
- 自动化扫描
Java 配置的方式注册有以下步骤:
配置类上加 @Configuration 注解
表示这个类是一个配置类,它的作用相当于 applicationContext.xml。
定义方法,方法返回对象,方法上添加 @Bean 注解
表示将这个方法的返回值注入的Spring容器中去。
也就是说,@Bean 所对应的方法,就相当于 applicationContext.xml 中的 bean 节点。
@Configuration
public class Config {
@Bean
User user() {
return new User();
}
}
配置加载
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
User user = ctx.getBean(User.class);
Bean 的默认名称是方法名。如果想自定义方法名,直接在 @Bean 中进行配置。
如下配置表示修改 Bean 的名字为 sail。
@Configuration
public class Config {
@Bean("sail")
User user() {
return new User();
}
}
配置加载
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
User user = ctx.getBean("sail", User.class);
配置类需要在项目启动时加载才生效。
自动化配置
自动化配置既可以通过 Java 配置来实现,也可以通过 xml 配置来实现。
类自动化扫描
类自动化扫描的注解一共有四个
- @Component
- @Repository
- @Service
- @Controller
这四个中,另外三个都是基于 @Component 做出来的,从目前的源码来看,功能也是一致的,使用另外三个的目的主要是区分业务。
- 在 Service 层上,添加注解时,使用 @Service。
- 在 Dao 层,添加注解时,使用 @Repository。
- 在 Controller 层,添加注解时,使用 @Controller。
- 在其他组件上添加注解时,使用 @Component。
比如:
@Service
public class UserService {
}
添加完成后,自动化扫描有两种方式
通过 Java 代码配置自动化扫描。
通过 xml 文件来配置自动化扫描。
Java代码配置自动扫描
@Configuration
@ComponentScan(basePackages = "cn.sail.training.spring.ioc.service")
public class JavaConfig {
}
然后,在项目启动中加载配置类,在配置类中,通过 @ComponentScan 注解指定要扫描的包,然后就可以获取 UserService 的实例了。
如果不指定,默认情况下扫描的是配置类所在的包下面的 Bean 以及配置类所在的包下的子包下的类。
Bean 的名字叫什么
默认情况下,Bean 的名字是类名首字母小写。例如上面的 UserService,它的实例名,默认就是 userService。如果开发者想要自定义名字,就直接在 @Service 注解中添加即可。
有几种扫描方式
上面的配置,我们是按照包的位置来扫描的。也就是说,Bean 必须放在指定的扫描位置,否则,即使你有 @Service 注解,也扫描不到。
除了按照包的位置来扫描,还有另外一种方式,就是根据注解来扫描。例如如下配置:
@Configuration
@ComponentScan(basePackages = "cn.sail.training.spring.ioc", useDefaultFilters = true, excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)})
public class JavaConfig {
}
这个配置表示扫描指定包下的所有 Bean,但是除了 Controller。
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"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/spring-context.xsd
">
<!--配置自动化扫描-->
<context:component-scan base-package="cn.sail.training.spring.ioc.service"/>
</beans>
这行配置表示扫描指定包下的所有 Bean。也可以按照类来扫描。
也可以在 XML 配置中按照注解的类型进行扫描:
<!--按照注解的类型进行扫描-->
<context:component-scan base-package="cn.sail.training.spring.ioc" use-default-filters="true">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
对象注入
自动扫描时的对象注入有三种方式:
-
@Autowired
-
@Resource
-
@Injected
@Autowired 是根据类型去查找,然后赋值,这就有一个要求,这个类型只可以有一个对象,否则就会报错。
@Resources 是根据名称去查找,默认情况下,定义的变量名,就是查找的名称,也可以在注解中手动指定。如果名称找不到,会再根据类型去找,类型也找不到才会报错。
所以,如果一个类存在多个实例,那么就应该使用 @Resources 去注入。
如果非要使用 @Autowired,需要配合另外一个注解 @Qualifier。
在 @Qualifier 中可以指定变量名,两个一起用就可以实现通过变量名查找到变量。
@Service
public class UserService {
@Autowired
private User user;
public String hello() {
return user.getName();
}
}
条件注解
条件注解就是在满足某一个条件的情况下,生效的配置。
获取 Windows 和 Linux 下的不同文件目录查看命令
定义一个显示文件夹目录的接口。
public interface ShowCmd {
String showCmd();
}
分别实现 Windows 下的实例和 Linux 下的实例。
public class WinShowCmd implements ShowCmd{
@Override
public String showCmd() {
return "dir";
}
}
public class LinuxShowCmd implements ShowCmd{
@Override
public String showCmd() {
return "ls";
}
}
定义两个条件,一个是 Windows 下的条件,另一个是 Linux 下的条件。
public class WindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return conditionContext.getEnvironment().getProperty("os.name").toLowerCase().contains("windows");
}
}
public class LinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return conditionContext.getEnvironment().getProperty("os.name").toLowerCase().matches("linux");
}
}
在定义 Bean 的时候,就可以去配置条件注解了。
这里一定要给两个 Bean 取相同的名字,这样在调用时,才可以自动匹配。
给每一个 Bean 加上条件注解,当条件中的 matches 方法返回 true 的时候,这个 Bean 的定义就会生效。
@Bean("showCmd")
@Conditional(WindowsCondition.class)
ShowCmd winCmd() {
return new WinShowCmd();
}
@Bean("showCmd")
@Conditional(LinuxCondition.class)
ShowCmd linuxCmd() {
return new LinuxShowCmd();
}
多环境切换
研发中,如何在开发、生产、测试环境之间进行快速切换是很重要的。
Spring 中提供了 Profile 来解决这个问题。
Profile 的底层就是条件注解。
这个从 @Profile 注解的定义就可以看出来。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({ProfileCondition.class})
public @interface Profile {
String[] value();
}
其中的 ProfileCondition 就通过实现 Condition 和重写 matches 来进行条件判断:
class ProfileCondition implements Condition {
ProfileCondition() {
}
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
Iterator var4 = ((List)attrs.get("value")).iterator();
Object value;
do {
if (!var4.hasNext()) {
return false;
}
value = var4.next();
} while(!context.getEnvironment().acceptsProfiles(Profiles.of((String[])((String[])value))));
return true;
} else {
return true;
}
}
}
定义一个 DataSource
public class DataSource implements Serializable {
private static final long serialVersionUID = 3802892946419732473L;
private String url;
private String username;
private String password;
@Override
public String toString() {
return "DataSource{" +
"url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
在配置 Bean 时,通过 @Profile 注解指定不同的环境
@Bean("ds")
@Profile("dev")
DataSource devDataSource() {
DataSource dataSource = new DataSource();
dataSource.setUrl("devUrl");
dataSource.setUsername("devUserName");
dataSource.setPassword("devPassWord");
return dataSource;
}
@Bean("ds")
@Profile("prod")
DataSource prodDataSource() {
DataSource dataSource = new DataSource();
dataSource.setUrl("prodUrl");
dataSource.setUsername("prodUserName");
dataSource.setPassword("prodPassWord");
return dataSource;
}
加载配置类
需要先设置当前环境,然后再去加载配置类。
AnnotationConfigApplicationContext不能先加载配置类,否则refresh方法会报错。
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.register(JavaConfig.class);
ctx.refresh();
DataSource dataSource = ctx.getBean("ds", DataSource.class);
System.out.println(dataSource);
环境的切换,也可以在 XML 文件中配置。
如下配置在 XML 文件中,必须放在其他节点后面,否则会层级混乱而报错。
<beans profile="dev">
<bean id="dataSource" class="cn.sail.training.spring.ioc.conditionAnnotation.DataSource">
<property name="url" value="devUrl"/>
<property name="username" value="devUserName"/>
<property name="password" value="devPassWord"/>
</bean>
</beans>
<beans profile="prod">
<bean id="dataSource" class="cn.sail.training.spring.ioc.conditionAnnotation.DataSource">
<property name="url" value="prodUrl"/>
<property name="username" value="prodUserName"/>
<property name="password" value="prodPassWord"/>
</bean>
</beans>
启动类中设置当前环境并加载配置
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
ctx.getEnvironment().setActiveProfiles("prod");
ctx.setConfigLocation("applicationContext.xml");
ctx.refresh();
DataSource dataSource = ctx.getBean("dataSource", DataSource.class);
System.out.println(dataSource);
Bean 的作用域
从 Spring 容器中多次获取同一个 Bean,默认情况下,获取到的实际上是同一个实例。
XML 配置
通过设置 scope 属性,我们可以调整默认的实例个数。
<bean id="book" class="cn.sail.training.spring.ioc.Book" scope="prototype">
<constructor-arg index="0" value="1"/>
<constructor-arg index="1" value="红楼梦"/>
<constructor-arg index="2" value="33"/>
</bean>
scope 的值为 singleton(默认),表示这个 Bean 在 Spring 容器中,是以单例的形式存在。
如果值为 prototype,表示这个 Bean 在 Spring 容器中不是单例,多次获取将拿到多个不同的实例。
还有两个取值,request 和 session,这两个取值在 web 环境下有效。
Java配置
@Bean("sail")
@Scope("prototype")
User user() {
return new User();
}
在 Java 代码中,我们可以通过 @Scope 注解指定 Bean 的作用域。
在自动扫描配置中,也可以指定 Bean 的作用域。
id 和 name 的区别
name 支持取多个值
多个 name 之间,用 , 隔开。
<bean name="book3,book4,book5" class="cn.sail.training.spring.ioc.Book" scope="prototype">
<constructor-arg index="0" value="1"/>
<constructor-arg index="1" value="红楼梦"/>
<constructor-arg index="2" value="33"/>
</bean>
此时,通过 book3、book4、book5 都可以获取到当前对象。
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Book book3 = ctx.getBean("book3", Book.class);
Book book4 = ctx.getBean("book4", Book.class);
Book book5 = ctx.getBean("book5", Book.class);
id 不支持有多个值
如果强行用 , 隔开,它还是一个值。
<bean id="user6,user7,user8" class="cn.sail.training.spring.ioc.User">
<property name="id" value="1"/>
<property name="age" value="27"/>
<property name="name" value="廖航"/>
</bean>
这个配置表示 Bean 的名字为 user6,user7,user8,具体调用如下。
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
User user6 = ctx.getBean("user6,user7,user8", User.class);
混合配置
混合配置就是 Java 配置 + XML 配置。
混用的话,可以在 Java 配置中引入 XML 配置。
@Configuration
@ImportResource("classpath:applicationContext.xml")
public class JavaConfig {
}