spring容器原理
spring家族体系庞大,功能强大,但就其最根本的本质来说,spring就是一个IoC容器,通常也称为spring容器。什么是IoC容器呢?所谓容器,顾名思义,就是可以装很多东西的一个器物。spring容器里装的是什么呢?是一个个对象,也就是说,spring容器是一个可以装很多对象的容器。
在spring以前,我们在开发程序时,所有的对象都是要自己一个个new出来的,或者通过工厂模式使用对象工厂一个个创建出来的。比如我们有这么几个类,一个是UserController,另一个是UserService接口,第三个是实现了UserService接口的类UserServiceImpl,UserController里包含了一个实现了UserService接口的成员对象:
public class UserController {
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
public String sayHello(String to) {
return userService.sayHello(to);
}
}
public interface UserService {
String sayHello(String to);
}
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String to) {
return "hello " + to;
}
}
在main函数里,我们要自己new出来UserController对象和UserServieImpl对象,并将UserServieImpl对象设置到UserController对象中,代码如下:
public class Test{
public static void main(String[] args) {
UserController uerController = new UserController();
UserService userService = new UserServiceImpl();
userController.setUserService(userService);
System.out.println(userController.sayHello("tom"));
}
}
显然,自己将对象一个个new出来,再将相关的对象组装起来(如上例中将userSerice对象设置到userController对象中,称之为注入)是很麻烦的,另外,上例中的UserController的成员userService本来只依赖于UserService接口,但我们却在main函数里直接new了一个实现类UserServiceImpl对象,将来如果我们要更换UserService的实现类,那我们也要改main函数里的代码,这就导致了UserController类和实现类UserServiceImpl耦合在了一起,违背了接口和实现分离的原则。
如果有这么一个框架,在程序启动时就自动将对象创建好,并将创建好的对象放入到一个容器中,并将容器中的对象都组装好,然后我们在用到某个对象时,直接从这个容器中获取,那该有多方便,用伪代码表示如下:
public class Test{
public static void main(String[] args) {
Container container = Container.scanAndCreateContainer();
UserController uerController = container.getObject(UserController.class);
System.out.println(userController.sayHello("tom"));
}
}
伪代码Container.scanAndCreateContainer()会扫描相关类,创建好这些类的对象并放入容器中,再将对象组装好(比如将一个UserServiceImpl对象注入到UserController对象中),最后返回这个容器,然后我们直接从容器中获取UserController对象。可以看出,这样可以给我们带来几个好处,一是我们不必再关心对象的创建工作,二是解除了对象之间的依赖关系,在这个例子中,UserController对象依赖于UserService接口,容器将实现类UserServiceImpl对象注入到UserController对象中,将来如果我们要更换UserService的实现类,容器就会将更换后的实现类创建对象并注入到UserController对象中,我们main函数里的代码也无需更改,这样就实现了接口和实现的分离。
综上,使用容器可以带来以下好处:
1.不用关心对象的创建工作;
2.解除了对象之间的依赖关系。
显然,容器需要做这么几件事:
1.扫描相关类,为每个类创建对象,并放入容器中;
2.将容器中的对象装配好。
从根本上来说,spring容器也就是做了这么两件事。现在有两个问题,
一是spring容器怎么知道要创建哪些对象?
二是spring容器是怎么创建并装配这些对象的?
要解决这两个问题,得从java的运行机制说起。我们知道,java程序都是在java虚拟机(jvm)中运行的,每个java类都会被编译成一个字节码文件(即.class文件),java虚拟机通过类加载器(ClassLoader)将字节码文件加载到内存中并运行。jvm将字节码文件加载到内存中并解析后,显然就掌握了一个类的所有细节,包括类的名称,类有哪些成员变量和成员方法等等。既然jvm掌握了一个类的所有细节,那么显然,无需你手动new,jvm自己就能创建一个类的对象,并为它设置好各个成员变量的值,这就是java的反射机制。第一个问题,我们可以通过某种方式(比如配置文件)来告诉spring容器我们希望它为创建哪些类对象,spring容器就会按照配置文件的指示,去扫描并加载相关的类;第二个问题,spring就是通过java的反射机制来创建并装配好相关的对象的。这就是spring的运行原理。我们把存放在spring容器的对象称之为bean(豌豆)。
spring容器的配置方式
有3种方式来配置spring容器:
1.基于xml文件的配置方式;
2.基于注解的配置方式;
3.基于java类的配置方式。
即spring容器可以通过这三种方式来获知需要创建哪些类对象。
基于xml文件的配置方式
通过xml文件来给spring容器提供需要创建对象的类信息。
先创建一个空的maven工程,工程名为spring-demo-xml,空的工程如下:
在pom.xml文件中添加如下依赖:
<?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>com.mystudy</groupId>
<artifactId>spring-demo-xml</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.10.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
再编写UserController、UserService和UserServiceImpl三个类(接口),内容与上面一样:
再在resources目录下添加名为spring.xml的配置文件:
在spring.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-3.1.xsd">
<bean id = "userService" class="com.mystudy.service.impl.UserServiceImpl"/>
<bean id = "userController" class="com.mystudy.controller.UserController">
<property name="userService">
<ref bean="userService"></ref>
</property>
</bean>
</beans>
spring.xml中先定义了一个名为userService的bean(即对象),class指出了这个类的全限定名。再定义了一个名为userController的bean,它的成员变量userService引用userService这个bean。通过这个配置文件,spring容器会分别创建一个UserServiceImpl对象和UserController对象,并将UserServiceImpl对象注入到UserController对象中。接下来添加main函数:
main函数的代码如下:
package com.mystudy;
import com.mystudy.controller.UserController;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring.xml"); // 1
UserController userController = (UserController) context.getBean("userController"); //2
System.out.println(userController.sayHello("tom")); //3
}
}
语句1通过ClassPathXmlApplicationContext这个类创建了一个spring容器,顾名思义,这个类是通过xml配置文件来配置spring容器的,传入的参数就是配置文件的地址。spring容器会根据配置文件的bean定义,创建好对象并将对象装配好。语句2直接从spring容器中获取想要的对象。语句3直接使用从spring容器中获取到的对象。执行main函数,输出为:
hello tom
基于注解的配置
用xml配置文件的方式,每个要创建的对象我们都要定义一个bean,这样还是比较麻烦的,并且如果要创建的对象比较多的话,这个工作量还是很大的。有没有更简单的方式呢?最好是能够让spring容器自动去扫描某个包下的类文件,自动去发现哪些类是需要创建对象的,并且能够自动完成对象的装配。幸好,spring提供的基于注解的配置方式实现了这一功能。我们只要将类标记上相关的注解,spring就知道需要为其创建对象,并能自动完成对象的装配工作。下面我们将上面的例子改成基于注解的配置方式。
首先修改UserController类,添加@Controller注解,在成员变量userService上添加@Autowired注解。@Controller注解表示希望spring创建这个类的对象,@Autowired注解表示希望spring将一个实现了UserService接口的对象注入进来:
package com.mystudy.controller;
import com.mystudy.service.UserService;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
@Autowired
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
public String sayHello(String to) {
return userService.sayHello(to);
}
}
接下来修改UserServiceImpl类,添加@Service注解,@Service注解也表示希望spring创建这个类的对象:
package com.mystudy.service.impl;
import com.mystudy.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService{
@Override
public String sayHello(String to) {
return "hello " + to;
}
}
除了@Controller、@Service注解外,还有@Bean、@Component等注解,这些注解都表示希望spring创建对象,之所以名称不同,只是为了区分一个工程中不同类型的组件。
接下来修改spring.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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.mystudy"/>
</beans>
可以看出,配置文件里只有一句话,那就是component-scan,component-scan告诉spring容器去扫描哪些包。这里我们让spring容器去扫描"com.mystudy"这个包下的所有类,spring会为该包下所有标记了@Controller、@Service、@Component、@Bean等注解的类创建对象,并为所有标注了@Autowired的成员注入合适的对象。
main函数不用修改,执行main函数,输出为:
hello tom
基于java类的配置
使用基于注解的配置加上自动扫描,已经非常方便了。不过spring还提供了基于java类的配置方式,这种方式的优点一是可以用代码控制对象的创建逻辑,二是提供了类型安全。下面我们将上面的例子改成基于Java类的配置方式。
首先将UserController和UserServiceImpl上的所有注解全部删除,然后将spring.xml文件删除。
然后在com.mystudy下创建一个名为configure的包,在该包下创建一个用来配置spring容器的类SpringConfig:
SpringConfig类的内容如下:
package com.mystudy.configure;
import com.mystudy.controller.UserController;
import com.mystudy.service.UserService;
import com.mystudy.service.impl.UserServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
@Bean
public UserController userController() {
UserController userController = new UserController();
userController.setUserService(userService());
return userController;
}
@Bean
public UserService userService() {
return new UserServiceImpl();
}
}
SpringConfig类上添加了@Configuration注解,表示这是一个配置类。这个类里有两个方法,一个是userContoller()方法,返回类型为UserController,该方法被标记了@Bean注解,这告诉Spring,用这个方法来创建UserController类的对象。另一个userSerice方法同理。
接下来修改main函数为:
package com.mystudy;
import com.mystudy.configure.SpringConfig;
import com.mystudy.controller.UserController;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MyTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); // 1
UserController userController = (UserController) context.getBean("userController");
System.out.println(userController.sayHello("tom"));
}
}
注意语句1,这里和前面不同,前面是用ClassPathXmlApplicationContext类来创建spring容器的,而这里用的是AnnotationConfigApplicationContext类来创建spring容器,顾名思义,它是通过Java类来配置spring容器的。语句1传入的参数是SpringConfig.class,表示让spring用SpringConfig这个类来配置spring容器。执行main函数,输出:
hello tom
推荐的配置方式
多种配置方式中,spring官方推荐使用java类的配置方式。在上面基于java类的配置方式中,我们把UserController和UserServiceImpl上的注解全都删除了,并且只有在SpringConfig类中定义了的Bean,spring容器才会去创建对象。基于java类的配置方式,再加上自动扫描自动装配,这就完美了。下面我们将工程改造成基于java类配置+自动扫描自动装配的方式。
首先恢复UserController和UserServiceImpl这两个类上删除的注解(即@Controller, @Autowired和@Serivice,然后将SpringConfig类修改为:
package com.mystudy.configure;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(value = {"com.mystudy"})
public class SpringConfig {
}
只在SpringConfig类上添加了一个@ComponentScan注解,告诉spring容器去扫描"com.mystudy"这个包下的类。
接下来执行main函数,仍然输出:
hello tom