Core Container(核心容器)
简介
本模块由 spring-core , spring-beans , spring-context , spring-context-support , and spring- expression (Spring Expression Language) 4 个模块组成。
我们学习 spring-core 和 spring-beans 模块,这两个模块提供了整个Spring框架最基础的设施:IoC (Inversion of Control,控制反转) 和 DI (Dependency Injection,依赖注入)。这两个概念也是面试中最经常考的那一部分.
这部分功能相当于所有Spring 框架运行的基础,以前我们操作对象都需要手动的 new 对象,由对象的作用域决定对象的生命周期。使用Spring后,由框架提供了统一的容器来实例化、管理这些对象,并自 动组织对象与对象间的关系。这种容器称为IoC容器,有些地方也叫Spring Bean容器、Spring容器。
对象之间的复杂关系(体现在代码中就是对象中成员变量,引用了另一个对象),也交给了容器来进行设置。
IoC/DI
背景
大多数应用程序都是由两个或是更多的类通过彼此的合作来实现企业逻辑,这使得每个对象都需要获取 与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么这将导致代 码高度耦合并且难以维护和调试。
什么叫IoC(IOC是一种思想)
IoC (Inversion of Control,控制反转) ,是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。只是因为该理论时间成熟相对较晚,并没有包含在GoF中。
系统中通过引入实现了IoC模式的IoC容器,即可由IoC容器来管理对象的生命周期、依赖关系等,从而使 得应用程序的配置和依赖性规范与实际的应用程序代码分离。
从使用上看,以前手动new对象,并设置对象中属性的方式,控制权是掌握在应用程序自身。现在则全 部转移到了容器,由容器来统一进行管理对象。因为控制权发生了扭转,所以叫控制反转。总的概括来说就是将对象创建和销毁的权利交给了IOC框架来进行管理.
关于IOC比较生动的一个例子:
可以看到我们Car类依赖于我们的FrameWork类,然后FrameWork类依赖于我们的Bottom类,Bottom类又依赖于我们的Tire类,此时可以看到Tire类中把我们车子的轮胎直接默认为了30的大小,但是我们并不想直接把车子的轮胎定义为30,那么我们此时就需要修改最后一个类,如上图所示,把Tire的无参构造方法转变为有参的构造方法,但是需要注意了,因为我们几个类之间是依赖的关系,所以当我们修改成有参的构造函数后,其他类都需要被修改,牵一发而动全身,这就是代码的耦合性,修改后的代码如下所示
此时为了降低耦合性,也就是我们所说的解耦,此时就需要我们将对象创建和销毁的权利交给了IOC框架来进行管理.也就是不再每个类都要修改一次,只需要修改我们的那一个公共类即可,此时我们来看修改过后的代码:
可以看到我们每个类传入的参数不再是自己new了,而是直接传入对象的引用,而把我们对象的创建交给了我们IOC容器,这样我们每次修改只需要去修改我们的Tire类即可,可以看到我们Tire类是以注入的方式到了我们的Bottom中,每个类之间的关系也都改为了注入的关系,并非依赖,这就是IOC思想
什么又是IoC容器
实现了IoC思想的容器就是IoC容器,比如:SpringFremework, Guice(Google开源的轻量级DI框架)
通过IOC容器可以实现对象生命周期管理,可以管理对象的依赖关系
什么又是DI(DI是对IOC思想的实现的具体技术)
DI (Dependency Injection,依赖注入) 是实现IoC的方法之一。所谓依赖注入,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。
所以,依赖注入(DI)和控制反转(IoC)是从不同的角度的描述的同一件事情,就是指通过引入 IoC 容器,利用依赖关系注入的方式,实现对象之间的解耦。
浅谈IOC–说清楚IOC是什么
Spring容器使用流程(重要)
Spring容器的API有 BeanFactory(现在基本已经不用) 和 ApplicationContext 两大类,他们都是顶级接口。其中ApplicationContext 是 BeanFactory 的子接口。对于两者的说明请参考面试题讲解Spring容器部分。我们主要使用 ApplicationContext 应用上下文接口。
下面是使用流程图,这个图要理解并会使用:
开发步骤
准备Maven项目及环境
首先创建一个Maven项目,名称为 springdemo1,以下是项目的maven配置文件 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>org.example</groupId> <artifactId>springdemo1</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>1.8</java.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-framework.version>5.2.10.RELEASE</spring-framework.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring-framework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring-framework.version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.16</version> </dependency> </dependencies> <build> <plugins> <!-- 明确指定一些插件的版本,以免受到 maven 版本的影响 --> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>3.1.0</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> </plugin> <plugin> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> <plugin> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> </plugin> <plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.1.0</version> </plugin> <plugin> <artifactId>maven-site-plugin</artifactId> <version>3.3</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> </plugins> </build> </project>
配置完成记得要刷新下maven面板哦
准备启动入口类
之后就可以使用Spring框架了,Spring提供了通过xml配置文件,来定义Bean,但是定义Bean的方式需 要通过包扫描的方式注册到容器中(其实还有其他方式,我们这里主要只掌握包扫描的方式)
写一个入口类:
package org.example; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author SongBiao * @Date 2021/1/18 */ public class App { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml"); //一定要记得释放资源 ((ClassPathXmlApplicationContext) applicationContext).close(); } }
准备spring配置文件
定义需要加载的Bean配置文件,在src/main/resource下,创建beans.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 https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="org.example"/> </beans>
这个配置文件中最主要的话就是定义我们spring扫描bean的根路径,然后spring会去这个包下搜索需要注入的bean
定义这个路径的意义:如果我们不定义这个路径,spring就会去全局扫描整个java项目,这样做有两个缺点:1:性能比较低 2:有些场景不需要类托管.
初始化/注册Bean
既然上面已经指定路径了,下面我们就要把类拖官到spring容器当中了,方式呢有以下两种:
方式1:类注解
在类上使用注解 @Controller , @Service , @Repository , @Component 。需要保证该类会被Spring 扫描到
也就是说在方式一中,只有在类上加上这四个注解才能代表当前这个类是需要被spring容器托管的类
@Controller:也即是我们的前端业务交互层
@Service:业务处理层
@Repository:数据交互层
@Component:注解层
注意:只有之前在bean.xml中设置的包扫描路径上的类,且是使用了spring的这四个注解的类才可以被注册到容器中
以下模拟一个登录的类,其路径为org.example.controller.LoginController,@Conreoller注解会注册名为loginController的对象到容器中
package org.example.controller; import org.springframework.stereotype.Controller; /** * @author SongBiao * @Date 2021/1/18 */ //加上controller注解后,spring就能扫描到啦. @Controller public class LoginController { //注意LoginController类在spring中的id是类名开头字母小写,为loginController }
定义好了Bean对象,注册到容器中以后,就可以获取Bean对象了,在入口类org.example.App中,
可以通过对象获取Bean,有两种方式获取:
通过类型获取:这种获取方式要求该类型的Bean只能有一个
通过名称获取:同一个类型的Bean可以有多个
下面来看代码:
package org.example; import org.example.controller.LoginController; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author SongBiao * @Date 2021/1/18 */ public class App { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml"); //之前我们都是new来获取对象,现在因为我们将LoginController注册到了spring容器当中 //所以直接使用getBean方法,里面传入LoginController类在spring容器中的id便可以获取到对象 //当然这是第一种获取对象的方式,但是不好在于假如我们的id写错了编译的时候并不会报错,但是运行后会报错 LoginController loginController = (LoginController) applicationContext.getBean("loginController"); //打印看是否获取成功,结果为org.example.controller.LoginController@543e710e System.out.println(loginController); //第二种方式是.class的方式,这种也是最常用的方式 LoginController loginController1 = applicationContext.getBean(LoginController.class); //打印看是否获取成功,结果为org.example.controller.LoginController@543e710e System.out.println(loginController1); //一定要记得释放资源 ((ClassPathXmlApplicationContext) applicationContext).close(); } }
此时我们再模拟一个URLController类,路径为org.example.controller.LoginController,但是这个类跟刚才有所不同的地方在于,@Conreoller注解会注册名为URLController的对象到容器中,并不是我们想象的第一个字母小写:例如uRLController这样的形式
package org.example.controller;
package org.example.controller; import org.springframework.stereotype.Controller; /** * @author SongBiao * @Date 2021/1/18 */ @Controller public class URLController { }
定义好了Bean对象,注册到容器中以后,就可以获取Bean对象了,在入口类org.example.App中,同样的我们获取的方式还是两种:
package org.example; import org.example.controller.URLController; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author SongBiao * @Date 2021/1/18 */ public class App { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml"); URLController urlController = (URLController) applicationContext.getBean("URLController"); //结果为org.example.controller.URLController@543e710e System.out.println(urlController); URLController urlController1 = (URLController) applicationContext.getBean(URLController.class); //结果为org.example.controller.URLController@543e710e System.out.println(urlController1); //一定要记得释放资源 ((ClassPathXmlApplicationContext) applicationContext).close(); } }
此时小伙伴们肯定会疑惑啦,我们spring到底是以怎样的一种方式来定义我们注册到spring容器中的id的呢?一会是开头首字母小写,一会又全是大写,那么到底是哪种定义方式呢?这个时候我们就需要去看我们的源码了:此时打开全局搜索后,搜索一个类叫做AnnotationBeanNameGenerator的类,这个类就是告诉我们spring到底是如何规范定义方式的
找到这个类中的一个方法名为generateBeanName
其返回值是一个buildDefaultBeanName方法,点到这个方法来看:
这个方法的返回值是buildDefaultBeanName这个方法
而这个方法最核心的语句就是下面这句话:程序把刚才的类名传到shortClassName参数这里生成id
return Introspector.decapitalize(shortClassName);
而decapitalize这个方法是属于jdk1.8的方法,点开进入到这个方法内部:
ok,我们此时在这个方法内部找到了端倪,也就是关于这个方法的注释告诉了我们命名的规则:大致意思如下:
假设第一个字母为大写,那么最后得到的第一个字母就是小写,假设是URL,最后得到的仍是URL
下面我们写一个测试类来测试一下:
package org.example.Test; import java.beans.Introspector; /** * @author SongBiao * @Date 2021/1/18 */ public class TestDemo { public static void main(String[] args) { String className = "URLController"; //结果为URLController System.out.println(Introspector.decapitalize(className)); String className1 = "LLoginController"; //结果为LLoginController System.out.println(Introspector.decapitalize(className1)); String className2 = "LoginController"; //结果为loginController System.out.println(Introspector.decapitalize(className2)); String className3 = "loginController"; //结果为loginController System.out.println(Introspector.decapitalize(className2)); } }
总结:
1:如果第一个字母为大写,第二个字母为小写,那么最终就是第一个字母为小写的值
2:如果第一个字母为大写,第二个字母为大写,那么最终就是第一个字母为大写的值
3:如果第一个字母和第二个字母都是小写,最后还是原样不变.
方式2:@Bean(方法注解)
当前类被 Spring 扫描到时,可以在方法上使用@Bean注解,通过方法返回类型,也可以定义、注册Bean对象,默认使用方法名为Bean的名称
先定义一个用户类 User,其路径为org.example.model.User ,注意这里没有使用任何Spring的注解,User类不会被扫描到,但是使用了lombok注解,使用了此注解后,我们就可以省略自己写getter,setter方法以及toString方法
package org.example.model; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter @ToString public class User { private String name; private String password; }
在此顺便我们来讲下lombok的原理,很多同学知道它,也会用,但是不清楚其中的原理:
lombok的实现原理:通过自定义的类注解,将自定义的类注解在程序编译的时候,转换成对应的JVM可以运行起来的代码,所以说我们的lombok只存在于编译时期,运行时期仍等价于下面的代码:小伙伴们可以build一下我们的项目,然后在target目录下的User.class文件查看,发现注解在编译时期就已经转换成了JVM可以运行起来的代码:
自定义的类注解:即就是lombok自己所提供的方法,例如@Getter,它就代表了两个含义:(1)代表类里面所有的属性都将被读取.(2)给每一个属性生成一个原生的Get方法
当然还有一个注解可以把刚才的@Getter,@Setter,@ToString这三个注解的作用全部给包括了,那就是@Data注解,这个注解也是非常的好用,写到实体类上也是可以的.
紧接着下一步,我们在已经注册到spring容器当中的LoginController中来完成剩余操作,此时我们在这个类中定义两个用户对象,并且这两个对象是在两个方法user1和user2内部的,来看代码:
package org.example.controller; import org.example.model.User; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Controller; //加上controller注解后,spring就能扫描到啦. @Controller public class LoginController { //对方法加上Bean注解,就可以注册到spring容器当中去啦 @Bean public User user1() { User user = new User(); user.setName("abc"); user.setPassword("123"); return user; } @Bean public User user2() { User user = new User(); user.setName("我不是汤神"); user.setPassword("tang"); return user; } }
定义好了Bean对象,注册到容器中以后,就可以获取Bean对象了,在入口类org.example.App中,同样的我们获取的方式还是两种:来看代码
package org.example; import org.example.model.User; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class App { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml"); User user1 = (User) applicationContext.getBean("user1"); //结果为User(name=abc, password=123) System.out.println(user1); //第一种方式获取user2对象 User user2 = (User) applicationContext.getBean("user2"); //输出结果为User(name=我不是汤神, password=tang) System.out.println(user2); //第二种方式获取user2对象 User user3 = applicationContext.getBean("user2", User.class); //输出结果为User(name=我不是汤神, password=tang) System.out.println(user3); //一定要记得释放资源 ((ClassPathXmlApplicationContext) applicationContext).close(); } }
方式3:@Configuration
在类被Spring扫描到时,使用**@Configuration注解,可以注册一个配置类到容器中。配置类一般用来自定义配置某些资源。之后会在SpringMVC中用到。
下面还是创建一个类名为MyAppConfig,这个类的路径为org.example.config.MyAppConfig**,在这个类上我们加入@Configuration注解
package org.example.controller.config; import org.springframework.context.annotation.Configuration; @Configuration public class MyAppConfig { }
定义好了Bean对象,注册到容器中以后,就可以获取Bean对象了,在入口类org.example.App中,同样的我们获取的方式还是两种:来看代码:
package org.example; import org.example.controller.config.MyAppConfig; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class App { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml"); //第一种方式 MyAppConfig myAppConfig = (MyAppConfig)applicationContext.getBean("myAppConfig"); //输出结果为org.example.controller.config.MyAppConfig$$EnhancerBySpringCGLIB$$b2652cee@1d76aeea System.out.println(myAppConfig); //第二种方式 MyAppConfig myAppConfig1 = applicationContext.getBean(MyAppConfig.class); //输出结果为org.example.controller.config.MyAppConfig$$EnhancerBySpringCGLIB$$b2652cee@1d76aeea System.out.println(myAppConfig1); //一定要记得释放资源 ((ClassPathXmlApplicationContext) applicationContext).close(); } }