1. Spring是什么?
Spring是一个开放源代码的设计层面框架,解决业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。
Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架。
2. Spring框架特点
- Spring是一个轻量级且非侵入式的开源框架 。
- Spring是包含并管理应用对象的配置和生命周期的一种容器。
- Spring是可以将简单的组件配置、组合成为复杂的应用的框架。
- Spring通过控制反转(IoC) 促进了低耦合。
- Spring提供了面向切面编程(AOP) 的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的开发。
3. Spring框架组成
Spring 框架是一个分层架构,由 7 个定义良好的模块组成。Spring 模块构建在核心容器之上,核心容器定义了创建、配置和管理 bean 的方式。
组成 Spring 框架的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现。每个模块的功能如下:
Spring Core(核心容器):核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转(IOC)模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。
Spring Context(上下文):Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI、EJB、电子邮件、国际化、校验和调度功能。
Spring AOP(面向切片):通过配置管理特性,Spring AOP 模块直接将面向切面的编程功能 , 集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理任何支持 AOP的对象。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖组件,就可以将声明性事务管理集成到应用程序中。
Spring DAO:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构。
Spring ORM:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构。
Spring Web 模块:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。
Spring MVC 框架:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。
重要拓展:Spring Boot与Spring Cloud
- Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务。
- Spring Cloud是基于Spring Boot实现的。
- Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架。
- Spring Boot使用了约束优于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置 , Spring Cloud很大的一部分是基于Spring Boot来实现,Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖的关系。
- SpringBoot在SpringClound中起到了承上启下的作用,如果你要学习SpringCloud必须要学习SpringBoot。
4. 控制反转(IOC)
控制反转IoC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现IoC的一种方法,也有人认为DI只是IoC的另一种说法。没有IoC的程序中 , 我们使用面向对象编程 , 对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转(IOC)后将对象的创建转移给第三方调用者,控制反转可认为是获得依赖对象的方式反转。
通俗来讲,以前所有东西都是由程序去进行控制创建(代码写死,直接实例化对象), 而现在是由调用者自行控制创建对象(由调用者按需求传入对象),把主动权交给了调用者,程序不用去管怎么创建,怎么实现了,它只负责提供一个接口。
4.1 Spring IOC容器
官方解释:
The org.springframework.context.ApplicationContext interface represents the Spring IoC container and is responsible for instantiating, configuring, and assembling the beans.
翻译:Spring IOC容器就是一个org.springframework.context.ApplicationContext的实例化对象,容器负责了实例化,配置以及装配一个bean
从代码层次来看:Spring IOC容器就是一个实现了ApplicationContext接口的对象,
从功能上来看: Spring 容器是 Spring 框架的核心,是用来管理对象的。容器将创建对象,把它们连接在一起,配置它们,并管理他们的整个生命周期从创建到销毁。
4.1.1 Spring IOC容器如何工作
IoC是Spring框架的核心内容,可以通过使用XML配置,也可以使用注解来实现了IoC,新版本的Spring也可以零配置实现IoC。
Spring容器在初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时再从Ioc容器中取出需要的对象。
采用XML方式配置Bean的时候,Bean的定义信息是和实现分离开的,而采用注解的方式可以把两者合为一体,Bean的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的。
4.2 如何实例化Bean
从官网上来看,主要有以下三种方法:
- 构造方法
- 通过静态工厂方法
- 通过实例工厂方法
简单分析下创建bean实例的方法
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
// 1.获取这个bean的class属性,确保beanDefinition中beanClass属性已经完成解析
// 我们通过xml从<bean>标签中解析出来的class属性在刚刚开始的时候必定是个字符串
Class<?> beanClass = resolveBeanClass(mbd, beanName);
// 省略异常判断代码.....
// 2.通过beanDefinition中的supplier实例化这个bean
Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
if (instanceSupplier != null) {
return obtainFromSupplier(instanceSupplier, beanName);
}
// 3.通过FactoryMethod实例化这个bean
if (mbd.getFactoryMethodName() != null) {
return instantiateUsingFactoryMethod(beanName, mbd, args);
}
// 4.下面这段代码都是在通过构造函数实例化这个Bean,分两种情况,一种是通过默认的无参构造,一种是通过推断出来的构造函数
boolean resolved = false;
boolean autowireNecessary = false;
if (args == null) {
synchronized (mbd.constructorArgumentLock) {
if (mbd.resolvedConstructorOrFactoryMethod != null) {
resolved = true;
autowireNecessary = mbd.constructorArgumentsResolved;
}
}
}
if (resolved) {
if (autowireNecessary) {
return autowireConstructor(beanName, mbd, null, null);
}
else {
return instantiateBean(beanName, mbd);
}
}
// Candidate constructors for autowiring?
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
return autowireConstructor(beanName, mbd, ctors, args);
}
// Preferred constructors for default construction?
ctors = mbd.getPreferredConstructors();
if (ctors != null) {
return autowireConstructor(beanName, mbd, ctors, null);
}
// No special handling: simply use no-arg constructor.
return instantiateBean(beanName, mbd);
}
通过上述代码可以发现实例化bean的方法主要分为三类:
1. 通过instanceSupplier获取,这个方法一般不常用,平常我们也使用不到,就不做过多探究,笔者认为,这应该是Spring提供的一种方便外部扩展的手段,让开发者能够更加灵活的实例化一个bean。
2. FactoryMethod,通过断点调试可以发现@compent、@Service、普通XML的方式(同@compent注解)、@Configuration等方式创建bean都是走instantiateUsingFactoryMethod(beanName, mbd, args)方法,即通过工厂方法实例化bean(包括静态工厂方法和实例工厂方法)。
public static void main(String[] args) { ClassPathXmlApplicationContext cc = new ClassPathXmlApplicationContext("application.xml"); System.out.println(cc.getBean("service")); }
<?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="myServiceImpl" class="com.dmz.official.service.Service"/>--> <!-- the factory bean, which contains a method called get() --> <bean id="myFactoryBean" class="com.dmz.official.service.MyFactoryBean"> <!-- inject any dependencies required by this locator bean --> </bean> <!-- 测试实例工厂方法创建对象--> <bean id="clientService" factory-bean="myFactoryBean" factory-method="get"/> <!--测试静态工厂方法创建对象--> <bean id="service" class="com.dmz.official.service.MyFactoryBean" factory-method="staticGet"/> </beans>
通过静态工厂方法这种方式特殊之处在于,包含这个静态方法的类,不需要实例化,不需要被Spring管理。Spring的调用逻辑大概是:
- 通过<bean>标签中的class属性得到一个Class对象
- 通过Class对象获取到对应的方法名称的Method对象
- 最后反射调用Method.invoke(null,args)获取bean
3. 构造方法。
4.3 实例化总结
1. 对象实例化,只是得到一个对象,还不是一个完全的Spring中的Bean,我们实例化后的这个对象还没有完成依赖注入,没有走完一系列的声明周期,这里需要大家注意。
2. Spring官网上指明了,在Spring中实例化一个对象有三种方式:
- 构造函数
- 实例工厂方法
- 静态工厂方法
3. 流程图
5. 依赖注入(DI)
5.1 依赖注入概念
依赖注入(Dependency Injection,DI):
- 依赖 : 指Bean对象的创建依赖于容器。
- 注入 : 指Bean对象所依赖的资源 , 由容器来设置和装配。
5.2 依赖注入方式
依赖注入主要分为两种方式:
官方解释:
- 构造函数注入
- Setter方法注入
5.3 代码测试
public class Main02 {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new
// config类主要完成对类的扫描
AnnotationConfigApplicationContext(Config.class);
Service service = (Service) ac.getBean("service");
service.test();
}
}
// setter方法注入
@Component
public class Service {
private LuBanService luBanService;
public Service() {
System.out.println("service create");
}
public void test(){
System.out.println(luBanService);
}
// 通过autowired指定使用set方法完成注入
@Autowired
public void setLuBanService(LuBanService luBanService) {
System.out.println("注入luBanService by setter");
this.luBanService = luBanService;
}
}
// 构造方法注入
@Component
public class Service {
private LuBanService luBanService;
public Service() {
System.out.println("service create by no args constructor");
}
// 通过Autowired指定使用这个构造函数,否则默认会使用无参
@Autowired
public Service(LuBanService luBanService) {
System.out.println("注入luBanService by constructor with arg");
this.luBanService = luBanService;
System.out.println("service create by constructor with arg");
}
public void test(){
System.out.println(luBanService);
}
}
@Component
public class LuBanService {
LuBanService(){
System.out.println("luBan create ");
}
}
上述代码中,@Autowired 注解加在setter 方法和属性字段上的区别:
1. 直接添加@Autowired注解到字段上,不需要提供setter方法也能完成注入。以上面的例子来说,Spring会通过反射获取到Service中luBanService这个字段,然后通过反射包的方法,Filed.set(Service,luBanService)这种方式来完成注入。
2. 将@Autowired添加到setter方法时,我们可以通过断点看一下方法的调用栈,如下:
对于这种方式来说,最终是通过Method.invoke(object,args)的方式来完成注入的,这里的method对象就是我们的setter方法。
同时采用构造注入加属性注入会怎么样呢?
Spring虽然能在构造函数里完成属性注入,但是这属于实例化对象阶段做的事情,那么在后面真正进行属性注入的时候,属性注入会将构造函数注入的属性覆盖。
总结:构造函数注入和属性注入使用官方推荐:
1. 构造函数注入跟setter方法注入可以混用,对于一些强制的依赖,我们最好使用构造函数注入,对于一些可选依赖我们可以采用setter方法注入。
2. Spring团队推荐使用构造函数的方式完成注入。但是对于一些参数过长的构造函数,Spring是不推荐的。
5.3 方法注入
5.3.1 Bean的作用域
类别 | 说明 |
Singleton | 在Spring IoC容器中仅存在一个Bean实例,Bean以单例方式存在,Spring IoC容器中所有对象默认是单例的 |
Prototype | 原型模式,每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行new XxxBean() |
Request | 每次HTTP请求都会创建一个新的Bean,该作用域仅适用于WebApplicationContext环境 |
Session | 同一个Http Session 共享一个Bean,不同的Session 使用不用Bean,仅适用于WebApplicationContext环境 |
5.3.2 方法注入作用
有了上面Bean的作用域基础知识之后,就能理解方法注入的作用。
@Component
public class MyService {
@Autowired
private LuBanService luBanService;
public void test(int a){
luBanService.addAndPrint(a);
}
}
@Component
// 原型对象
@Scope("prototype")
public class LuBanService {
int i;
LuBanService() {
System.out.println("luBan create ");
}
// 每次将当前对象的属性i+a然后打印
public void addAndPrint(int a) {
i+=a;
System.out.println(i);
}
}
public class Main02 {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
MyService service = (MyService) ac.getBean("myService");
service.test(1);
service.test(2);
service.test(3);
}
}
如上述代码,我们目的是每次都获取到不同的 LuBanService 对象,然而实际是每次调用到的LuBanService是同一个对象。当然,这也很好理解,因为在依赖注入阶段我们就完成了LuBanService的注入,之后我们在调用测试方法时,不会再去进行注入,所以我们一直使用的是同一个对象。
方法注入的作用就是解决此问题,每次使用Bean时都重新去获取。
实现方式:
1. 通过注入上下文(applicationContext对象)获取Bean
// 实现org.springframework.context.ApplicationContextAware接口
@Component
public class MyService implements ApplicationContextAware {
private ApplicationContext applicationContext;
public void test(int a) {
LuBanService luBanService = ((LuBanService) applicationContext.getBean("luBanService"));
luBanService.addAndPrint(a);
}
@Override
public void setApplicationContext(@Nullable ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
// 直接注入上下文
@Component
public class MyService{
@Autowired
private ApplicationContext applicationContext;
public void test(int a) {
LuBanService luBanService = ((LuBanService) applicationContext.getBean("luBanService"));
luBanService.addAndPrint(a);
}
}
2. 通过@LookUp的方式(也分为注解跟XML两种方式,这里只演示注解的)
@Component
public class MyService{
public void test(int a) {
LuBanService luBanService = lookUp();
luBanService.addAndPrint(a);
}
@Lookup
public LuBanService lookUp(){
return null;
}
}
3. 方法注入 之 replace-method(使用很少)
5.4 依赖注入总结
Spring中的依赖注入就是属性注入。
- 我们知道一个对象由两部分组成:属性+行为(方法),可以说Spring通过属性注入+方法注入的方式掌控的整个bean。
- 属性注入跟方法注入都是Spring提供给我们用来处理Bean之间协作关系的手段。
- 属性注入有两种方式:构造函数,Setter方法。
- 方法注入(LookUp Method跟Replace Method)需要依赖动态代理完成。
- 方法注入对属性注入进行了一定程度上的补充,因为属性注入的情况下,原型对象可能会失去原型的意义。
6. 自动注入
官方说明:
翻译:
Spring可以自动注入互相协作的bean之间的依赖。自动注入有以下两个好处:
- 自动注入能显著的减少我们指定属性或构造参数的必要,即可以不用依赖注入的方式为类注入相关依赖对象。
- 自动装配可以随着对象的演化更新配置。例如,如果需要向类添加依赖项,则可以自动满足该依赖项,而不需要修改配置。因此,自动装配在开发过程中特别有用,但是当我们的代码库变的稳定时,自动装配也不会影响我们将装配方式切换到精确注入。
自动注入四种模型:
模型 | 解释 |
no | 这是目前Spring默认的注入模型,也可以说默认情况下Spring是关闭自动注入,必须要我们通过setter方法或者构造函数完成依赖注入,并且Spring也不推荐修改默认配置。建议我们使用精确的方式注入依赖。 |
byName | 这种方式,我们为了让Spring完成自动注入需要提供两个条件:
在找不到对应名称的bean的情况下,Spring也不会报错,只是不会给我们完成注入。 |
byType | 测试代码跟之前唯一不同的就是修改配置autowire="byType",这里我们测试以下三种异常情况:
另外需要说明的是,我在测试的过程,将set方法仅仅命名为set,像这样public void set(DmzService dmzService),这种情况下Spring也不会进行注入。 我们可以发现,对于这两种注入模型都是依赖setter方法完成注入的,并且对setter方法命名有一定要求(只要我们平常遵从代码书写规范,一般也不会踩到这些坑)。第一,不能有多个参数;第二,不能仅仅命名为set |
constructor | 当我们使用这种注入模型时,Spring会根据构造函数查找有没有对应参数名称的bean,有的话完成注入(跟前文的byName差不多),如果根据名称没找到,那么它会再根据类型进行查找,如果根据类型还是没找到,就会报错。 |
自动注入的缺陷:
1. 精确注入会覆盖自动注入。并且我们不能注入基本数据类型,字符串,Class类型(这些数据的数组也不行)。而且这是Spring故意这样设计的;
2. 自动注入不如精确注入准确。而且我们在使用自动注入时,对象之间的依赖关系不明确;
3. 对于一些为Spring容器生成文档的工具,无法获取依赖关系;
4. 容器中的多个bean定义可能会与自动注入的setter方法或构造函数参数指定的类型匹配。对于数组、集合或映射实例,这可能不会产生什么问题。但是,对于期望单个值的依赖项,我们无法随意确定到底有谁进行注入。如果没有唯一的bean定义可用,则会抛出异常。
补充一些注解的知识:
@Autowired 与 @Qualifier
1. @Autowired(required=false) 说明:false,对象可以为null;true,对象必须存对象,不能为null。
2. @Autowired是根据类型自动装配的,加上@Qualifier则可以根据byName的方式自动装配。
3. @Qualifier不能单独使用。
@Resource
1. @Resource如有指定的name属性,先按该属性指定的name进行byName方式查找装配;其次再进行默认的byName方式进行装配。
2. 如果以上都不成功,则按byType的方式自动装配。
3. 都不成功,则报异常。@Autowired与@Resource异同:
1. @Autowired与@Resource都可以用来装配bean。都可以写在属性上,或写在setter方法上。
2. @Autowired 默认按类型装配(属于spring规范),默认情况下必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用。
3. @Resource(属于J2EE复返),默认按照名称进行装配,名称可以通过name属性进行指定。如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
4. 它们的作用相同都是用注解方式注入对象,但执行顺序不同。@Autowired先byType,@Resource先byName。
总结:
此篇文章只是简单介绍了Spring重要的知识点,很多地方并没有深挖,对Spring感兴趣的同学可以参考Spring官网阅读 | 总结篇_程序员DMZ的博客-CSDN博客,阅读Spring官方文档。