本系列文章:
Spring(一)控制反转、两种IOC容器、自动装配、作用域
Spring(二)延迟加载、生命周期、面向切面、事务
Spring(三)父子容器、国际化、异步调用、定时器、缓存
Spring(四)Spring MVC
Spring(五)Spring Boot
Spring(六)Spring Cloud
一、初识Spring
Spring是一个轻量级Java开发框架
,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的开源框架,为开发Java应用程序提供全面的基础架构支持。Spring负责基础架构,Java开发者可以专注于应用程序的开发。
Spring的功能底层都依赖于它的两个核心特性,也就是依赖注入
(DI)和面向切面编程
(AOP)。IOC用来管理对象以及他们之间的关系;AOP用来以动态非侵入的方式增强服务。
1.1 Spring的特点
- 1、轻量级
基本版本大约2MB。 - 2、控制反转
Spring通过控制反转(IOC)技术实现解耦。一个对象依赖的其他对象会通过被动的方式传递进来,而不需要对象自己创建或者查找依赖。 - 3、面向切面
支持切面(AOP)编程,并且把应用业务逻辑和系统服务区分开。 - 4、容器
Spring包含并管理应用对象的配置和生命周期。 - 5、MVC框架
Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。 - 6、事务管理
Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)。 - 7、异常处理
Spring提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked异常。
1.2 Spring的优缺点
优点:
- 1、方便解耦,简化开发(IOC)
Spring就是一个工厂,可以将所有对象的创建和依赖关系的维护,交给Spring管理,实现了松散耦合。 - 2、AOP
Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能,把应用业务逻辑和系统服务分开。 - 3、声明式事务的支持(@Transactional)
只需要通过配置就可以完成对事务的管理,而无需手动编程。 - 4、方便程序的测试
Spring对Junit4支持,可以通过注解方便的测试Spring程序。 - 5、方便集成各种优秀框架
Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持(如支持的ORM框架有:Hibernate、iBatis、JPA、JDO、OJB等)。
缺点:
- Spring依赖反射,反射影响性能
1.3 Spring的模块组成
Spring5的模块结构图:
Spring的核心模块:
- 1、Spring core(核心容器)
核心容器提供Spring框架的基本功能。Spring以bean的方式组织和管理Java应用中的各个组件及其关系。 - 2、Spring JDBC( Java数据库连接)
- 3、SpringAOP(面向切面编程)
是面向对象编程的有效补充和完善,Spring的AOP是基于动态代理实现的。 - 4、Spring Dao(JDBC和Dao模块)
JDBC、DAO的抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理,和不同数据库供应商所抛出的错误信息。异常层次结构简化了错误处理,并且极大的降低了需要编写的代码数量,比如打开和关闭链接。 - 5、Spring ORM(对象实体映射)
Spring框架插入了若干个ORM框架,从而提供了ORM对象的关系工具,其中包括了Hibernate、JDO和 IBatis SQL Map等,所有这些都遵从Spring的通用事物和DAO异常层次结构。 - 6、Spring Web(Web模块)
为创建Web应用程序提供支持。 - 7、Spring Test
提供了对JUnit测试的支持。
1.4 Spring的配置方式*
- 1、基于xml配置
bean所需的依赖项和服务在XML格式的配置文件中指定。这些配置文件通常包含许多bean定义和特定于应用程序的配置选项。它们通常以<bean>
标签开头。例如:
<bean id="studentbean" class="org.test.StudentBean">
<property name="name" value="zhangsan"></property>
</bean>
- 2、基于注解配置
可以通过在相关的类,方法或字段声明上使用注解,将bean配置为组件类本身,而不是使用XML来描述bean装配。这种方式使用起来比第一种更方便。
默认情况下,Spring容器中未打开注解装配。因此需要在使用它之前在Spring配置文件中启用它:
<beans>
<context:annotation-config/>
</beans>
- 3、基于Java API配置
Spring的Java配置是通过使用@Bean和@Configuration来实现。
@Configuration
public class StudentConfig {
@Bean
public StudentBean myStudent() {
return new StudentBean();
}
}
1.5 Spring的不同版本
Version | Feature |
---|---|
Spring2.5 | 发布于 2007 年。这是第一个支持注解的版本。 |
Spring3.0 | 发布于 2009 年。它完全利用了 Java5 中的改进,并为 JEE6 提供了支持。 |
Spring4.0 | 发布于 2013 年。这是第一个完全支持 JAVA8 的版本。 |
Spring5.0 | Spring Framework 5.0的最大特点之一是响应式编程(Reactive Programming)。 响应式编程核心功能和对响应式endpoints的支持可通过Spring Framework 5.0中获得。 |
- Spring5特点
整个 Spring5 框架的代码基于 Java8,运行时兼容 JDK9,许多不建议使用的类和方 法在代码库中删除。
1、日志。Spring5 已经移除 Log4jConfigListener,官方建议使用 Log4j2。
2、支持@Nullable注解。@Nullable 注解可以使用在方法上面,属性上面,参数上面,表示方法返回可以为空,属性值可以为空,参数值可以为空。
3、支持函数式风格GenericApplicationContext。
//函数式风格创建对象,交给spring进行管理
@Test
public void testGenericApplicationContext() {
//1 创建GenericApplicationContext对象
GenericApplicationContext context = new GenericApplicationContext();
//2 调用context的方法对象注册
context.refresh();
context.registerBean("user1",User.class,() -> new User());
//3 获取在spring注册的对象
// User user = (User)context.getBean("com.atguigu.spring5.test.User");
User user = (User)context.getBean("user1");
System.out.println(user);
}
4、新功能(Webflux)。webFlux是 Spring5 添加的新模块,用于 web 的开发,功能和 SpringMVC 类似的,Webflux 使用 当前一种比较流程响应式编程出现的框架。
在Spring5的jar包中和架构图中,都可以看见:
使用传统web框架,比如SpringMVC,这些基于 Servlet 容器,Webflux是一种异步非阻塞的框架,异步非阻塞的框架在Servlet3.X以后才支持,核心是基于 Reactor 的API 实现的。
Webflux特点:
特性一、 异步非阻塞
SpringMVC是同步阻塞的IO模型,资源浪费相对来说比较严重,当我们在处理一个比较耗时的任务时,例如:上传一个比较大的文件,首先,服务器的线程一直在等待接收文件,在这期间它就像个傻子一样等在那儿(放学别走),什么都干不了,好不容易等到文件来了并且接收完毕,我们又要将文件写入磁盘,在这写入的过程中,这根线程又要等到文件写完才能去干其它的事情。这一前一后的等待,浪费了大量的资源。
而Spring WebFlux就是来解决这个问题的,Spring WebFlux可以做到异步非阻塞。还是上面那上传文件的例子,Spring WebFlux是这样做的:线程发现文件还没准备好,就先去做其它事情,当文件准备好之后,通知这根线程来处理,当接收完毕写入磁盘的时候(根据具体情况选择是否做异步非阻塞),写入完毕后通知这根线程再来处理(异步非阻塞情况下)。
这个设计相对于SpringMVC而言,可以大大节省系统资源。
特性二、 响应式(reactive)函数编程。
Spring5 框架基于 java8,Webflux 使用 Java8 函数式编程方式实现路由请求,如果你觉得java8的lambda写起来很爽,那么,你会再次喜欢上Spring WebFlux,因为它支持函数式编程,得益于对于reactive-stream的支持(通过reactor框架来实现的)。
特性三、 不再拘束于Servlet容器。
以前,我们的应用都运行于Servlet容器之中,例如我们大家最为熟悉的Tomcat, Jetty…等等。而现在Spring WebFlux不仅能运行于传统的Servlet容器中(前提是容器要支持Servlet3.1,因为非阻塞IO是使用了Servlet3.1的特性),还能运行在支持NIO的Netty和Undertow中。
Webflux与SpringMVC的区别:
区别一: 两个框架都可以使用注解方式,都可以运行在Tomet 等容器中。
区别二: SpringMVC采用命令式编程(通常意味着你会在控制器中编写更多的逻辑来处理请求。这可以通过直接在控制器中编写服务层和数据访问层的代码来实现),Webflux采用异步响应式编程。
Webflux特点:
1.webflux是一个异步非阻塞的Web框架,它能够充分利用多核CPU的硬件资源去处理大量的并发请求。
2.内部使用的是响应式编程,以Reactor库为基础,基于异步和事件驱动,可以让我们在不扩充硬件资源的前提下,提升系统的吞吐量和伸缩性。
3.不能使接口的响应时间缩短,它仅仅能够提升吞吐量和伸缩性。
Webflux应用场景:特别适合在网络密集型的服务中,比如微服务网关。使用异步非阻塞式编程模型,能够显著地提升网关对下游服务转发的吞吐量。(WebFlux 主要还是应用在异步非阻塞编程模型,而 Spring MVC 是同步阻塞的)
二、控制反转
IoC (控制反转),由Spring容器管理bean的整个生命周期。通过反射实现对其他对象的控制,包括初始化、创建、销毁等,解放手动创建对象的过程,同时降低类之间的耦合度。
最直观的表达就是,IOC让对象的创建不用去new了,由Spring自动生产,使用Java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法。
- 为什么叫做控制反转
控制:指的是对象创建(实例化、管理)的权利。
反转:控制权交给外部环境了(Spring框架、IoC容器)。 - 控制反转的作用
1、管理对象的创建和依赖关系的维护;
2、解耦。 - IOC的优点
1、IOC和依赖注入降低了应用的代码量。
2、松耦合。
3、支持即时的实例化和延迟加载服务。
2.1 IoC和DI
IOC(控制反转),就是将原本在程序中手动创建对象的控制权,交由Spring框架管理
。简单说,就是创建对象的控制权被反转到了Spring框架。
DI(依赖注入),在Spring创建对象的过程中,把对象依赖的属性注入到对象中。依赖注入主要有两种方式:构造器注入和属性注入。
IOC 和 DI 描述的是同一件事情(对象实例化以及依赖关系的维护),只不过角度不同。
IoC控制反转,指将对象的创建权,反转到Spring容器;
DI依赖注入,指Spring创建对象的过程中,将对象依赖属性通过配置进行注入。
2.2 Spring IoC的实现原理*
Spring IoC的实现原理就是工厂模式加反射机制
。图示:
2.3 Spring IOC容器
Spring IOC容器负责创建对象,管理对象(通过依赖注入(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。
Spring IoC容器如何知道哪些是它管理的对象呢?这就需要配置文件,Spring IoC容器通过读取配置文件中的配置元数据,通过元数据对应用中的各个对象进行实例化及装配。由IoC容器管理的那些组成应用程序的对象我们就叫它Bean, Bean就是由Spring容器初始化、装配及管理的对象。
IOC容器的职责是:
- 实例化Bean;
- 把Bean关联在一起;
- 配置Bean;
- 管理Bean的整个生命周期。
Spring容器启动时读取应用程序提供的Bean配置信息,并在Spring容器中生成一份相应的Bean配置注册表,然后根据这张注册表实例化Bean,装配好Bean之间的依赖关系,为上层应用提供准备就绪的运行环境。其中Bean缓存池为HashMap实现。图示:
Spring中的IOC容器有:BeanFactory和ApplicationContext。
BeanFactory是Spring框架的基础设施,面向Spring本身。
2.3.1 BeanFactory
- BeanFactory
BeanFactory:是IOC容器的核心接口, 它定义了IOC的基本功能。从代码可以看到它主要定义了getBean方法,该方法的功能是返回特定名称对应的Bean。
BeanFactory只能管理单例Bean的生命周期
。它不能管理原型(非单例)Bean的生命周期。这是因为原型Bean实例被创建之后便被传给了客户端,容器失去了对它们的引用。
public interface BeanFactory {
//对FactoryBean的转义定义,因为如果使用bean的名字检索FactoryBean得到的对象是工厂生成的对象,
//如果需要得到工厂本身,需要转义
String FACTORY_BEAN_PREFIX = "&";
//根据bean的名字,获取在IOC容器中得到bean实例
Object getBean(String name) throws BeansException;
//根据bean的名字和Class类型来得到bean实例,增加了类型安全验证机制。
Object getBean(String name, Class requiredType) throws BeansException;
//提供对bean的检索,看看是否在IOC容器有这个名字的bean
boolean containsBean(String name);
//根据bean名字得到bean实例,并同时判断这个bean是不是单例
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
//得到bean实例的Class类型
Class getType(String name) throws NoSuchBeanDefinitionException;
//得到bean的别名,如果根据别名检索,那么其原名也会被检索出来
String[] getAliases(String name);
}
BeanFactory的继承关系:
- BeanFactory
顶层接口。位于类结构树的顶端 ,它最主要的方法就是getBean(String beanName),该方法从容器中返回特定名称的Bean,BeanFactory的功能通过其他的接口得到不断扩展。 - BeanDefinitionRegistry
注册表。Spring配置文件中每一个节点元素,在Spring容器里都通过一个BeanDefinition对象表示,它描述了Bean的配置信息
。而BeanDefinitionRegistry接口提供了向容器手工注册BeanDefinition对象的方法。 - ListableBeanFactory
该接口定义了访问容器中Bean基本信息的若干方法,如查看Bean的个数、获取某一类型Bean的配置名、查看容器中是否包括某一Bean等方法。 - HierarchicalBeanFactory
父子级联。通过HierarchicalBeanFactory接口, Spring的IoC容器可以建立父子层级关联的容器体系,子容器可以访问父容器中的Bean,但父容器不能访问子容器的Bean
。
Spring使用父子容器实现了很多功能,比如在Spring MVC中,展现层Bean位于一个子容器中,而业务层和持久层的Bean位于父容器中。这样,展现层Bean就可以引用业务层和持久层的Bean,而业务层和持久层的Bean则看不到展现层的Bean。
- ConfigurableBeanFactory
增强了IoC容器的可定制性,它定义了设置类装载器、属性编辑器、容器初始化后置处理器等方法。 - AutowireCapableBeanFactory
定义了将容器中的Bean按某种规则(如按名字匹配、按类型匹配等)进行自动装配的方法。 - SingletonBeanRegistry
运行期间注册单例Bean。定义了允许在运行期间向容器注册单例Bean的方法;对于单例Bean来说,BeanFactory会缓存Bean实例。所以第二次使用getBean()获取Bean时将直接从IoC容器的缓存中获取 Bean 实例。Spring在DefaultSingletonBeanRegistry类中提供了一个用于缓存单实例Bean的缓存器,它是一个用HashMap实现的缓存器,单例的Bean以beanName为键保存在这个HashMap中。
2.3.2 ApplicationContext*
BeanFactory是Spring框架中IoC容器的顶层接口
,它只是用来定义那些基础功能,。
ApplicationContext面向使用Spring框架的开发者,几乎所有的应用场合我们都直接使用ApplicationContext而非底层的BeanFactory。
通常,称BeanFactory为SpringIOC的基础容器,ApplicationContext是容器的高级接口。ApplicationContext比BeanFactory要拥有更多的功能,比如说国际化支持和资源访问(xml,Java配置类)等。
BeanFactory可以理解为就是个HashMap,Key是BeanName,Value是Bean实例。通常只提供注册、获取这两个功能。可以称之为 “低级容器”。
ApplicationContext 可以称之为 “高级容器”,因为比BeanFactory多了更多的功能。
ApplicationContext是BeanFactory的一个子接口,提供了更多面向实际应用的功能。在BeanFactory中,很多功能需要以编程的方式实现,而在ApplicationContext中则可以通过配置实现
。
BeanFactorty接口提供了配置框架及基本功能,但是无法支持spring的aop功能和web应用。ApplicationContext还在功能上做了扩展,具体为:
(1)MessageSource, 提供国际化的消息访问。
(2)资源访问,如URL和文件。
(3)事件传播特性,即支持AOP特性。
(4)载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
BeanFactory和ApplicationContext接口及其子类图:
可以看出,ApplicationContext继承BeanFactory接口(IOC基础功能)的同时, 也继承了容器的高级功能,如:MessageSource(国际化资源接口)、ResourceLoader(资源加载接口)、ApplicationEventPublisher(应用事件发布接口)等。
- ApplicationContext实现类
AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载上下文定义,适用于Java注解的方式(纯注解模式下启动Spring容器)。
ClassPathXmlApplicationContext:从类路径下的一个或多个xml配置文件中加载上下文定义,适用于xml配置的方式 ,需要正确设置classpath因为这个容器将在classpath里找bean配置。
FileSystemXmlApplicationContext:从文件系统下的一个或多个xml配置文件中加载上下文定义,也就是说系统盘符中加载xml配置文件,XML Bean 配置文件的全路径名必须提供给它的构造函数。
AnnotationConfigWebApplicationContext : 专门为web应用准备的,适用于注解方式。
XmlWebApplicationContext:从Web应用下的一个或多个xml配置文件加载上下文定义,适用于xml配置方式。 - ApplicationEventPublisher
让容器拥有发布应用上下文事件的功能,包括容器启动事件、关闭事件等。 - MessageSource
为应用提供国际化功能。 - ConfigurableApplicationContext
&emsp扩展于ApplicationContext,它新增加了两个主要的方法: refresh()和 close(),让ApplicationContext具有启动、刷新和关闭应用上下文的能力。在应用上下文关闭的情况下调用refresh()即可启动应用上下文,在已经启动的状态下,调用refresh()则清除缓存并重新装载配置信息,而调用close()则可关闭应用上下文。
2.3.3 BeanFactory和ApplicationContext的区别*
- 1、加载方式
BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化
。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
ApplicationContext,它是在容器启动时,一次性创建了所有的Bean
。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean,确保当你需要的时候,你就不用等待,因为它们已经创建好了。
相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。
Spring容器默认启动时就创建了所有的bean
。
通常情况下我们选择使用ApplicationContext。 - 2、依赖关系
BeanFactory是Spring里面最顶级的接口,包含了各种Bean的定义,读取Bean配置文档,管理Bean的加载、实例化,控制Bean的生命周期,维护Bean之间的依赖关系。
ApplicationContext接口作为BeanFactory的子接口,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:
- 继承MessageSource,因此
支持国际化
。统一的资源文件访问方式
。提供在监听器中注册bean的事件
。- 同时加载多个配置文件。
- 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
- 3、注册方式
BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。
Applicationcontext比BeanFactory加入了一些更好使用的功能。而且beanFactory的许多功能需要通过编程实现而Applicationcontext可以通过配置实现。比如后处理bean , Applicationcontext直接配置在配置文件即可,而BeanFactory要在代码中显示的写出来才可以被容器识别。 - 4、面向的对象
BeanFactory主要是面对与Spring框架的基础设施,面对Spring自己。而Applicationcontext主要面对使用Spring框架的开发者。
2.3.4 Spring容器的Bean什么时候被实例化
- 使用BeanFactory容器
如果使用BeanFactory作为Spring Bean的工厂类,则所有的Bean都是在第一次使用该Bean的时候实例化。 - 使用ApplicationContext容器*
如果你使用ApplicationContext作为Spring Bean的工厂类,则又分为以下几种情况:
1、如果Bean的scope是singleton的,并且lazy-init为false(默认是false,所以可以不用设置),则ApplicationContext启动的时候就实例化该Bean,并且将实例化的Bean放在一个map结构的缓存中,下次再使 用该 Bean的时候,直接从这个缓存中取。
2、如果bean的scope是singleton的,并且lazy-init为true,则该Bean的实例化是在第一次使用该Bean的时候进行实例化。
3、如果bean的scope是prototype的,则该Bean的实例化是在第一次使用该Bean的时候进行实例化。
三、通过XML方式创建Bean
3.1 通过构造器方式创建Bean
该方式对应的标签是<constructor-arg>
。
3.1.1 构造器实例化Bean
假设有一个Person类,有name、age两个属性,通过构造器的方法的配置方式示例:
<bean id="person" class="com.spring.demo.Person">
<constructor-arg name="name" value="等风的草"/>
<constructor-arg name="age" value="21"/>
</bean>
构造方法分有参和无参2种,因此通过构造方法来创建Bean自然也分为2种。
- 1、通过无参构造方法来创建Bean
实体类:
public class Hello {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void show(){
System.out.println("Hello,"+ name );
}
}
Spring的配置文件,在src目录下,命名为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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="hello" class="com.spring.test.Hello"></bean>
</beans>
测试类:
public class SpringTest {
public static void main(String[] args) {
test();
}
public static void test(){
//解析beans.xml文件 , 生成管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//getBean : 参数即为spring配置文件中bean的id
Hello hello = (Hello) context.getBean("hello");
//Hello,null
hello.show();
}
}
- 2、通过有参构造方法来创建Bean
在使用构造函数注入时,涉及的标签是<constructor-arg>
,该标签有如下属性:
name
:用于给构造函数中指定名称的参数赋值。index
:用于给构造函数中指定索引位置的参数赋值。value
:用于指定基本类型或者String类型的数据。ref
:用于指定其他Bean类型的数据。写的是其他bean的唯一标识。
通过有参构造方法来创建对象的方式有三种,示例:
<!-- 第一种根据index参数下标设置 -->
<bean id="user1" class="com.test.pojo.User">
<!-- index指构造方法 , 下标从0开始 -->
<constructor-arg index="0" value="zhangsan"/>
</bean>
<!-- 第二种根据参数名字设置 -->
<bean id="user2" class="com.test.pojo.User">
<!-- name指参数名 -->
<constructor-arg name="name" value="lisi"/>
</bean>
<!-- 第三种根据参数类型设置 -->
<bean id="user3" class="com.test.pojo.User">
<constructor-arg type="java.lang.String" value="wangwu"/>
</bean>
至于第4种标签ref,用于注入另一个Bean作为参数,被注入的Bean会在当前Bean实例化时自动注入到当前bean的构造函数中。示例:
<bean id="exampleBean2" class="com.example.ExampleBean2">
<constructor-arg ref="exampleBean"/>
</bean>
上面配置的含义是:将ExampleBean2的构造函数中的参数注入了ExampleBean。当ExampleBean2实例化时,ExampleBean会自动注入到ExampleBean2的构造函数中。
3.1.2 静态工厂实例化Bean
当采用静态工厂方法创建bean时,除了需要指定class属性外,还需要通过factory-method
属性来指定创建bean实例的工厂方法。
假设有这样的静态工厂PersonStaticFactory类:
public static Person createInstance() {
return new Person();
}
配置文件(class:指定静态工厂类;factory-method:指定哪个方法是工厂方法):
<bean id="person" class="com.spring.demo.PersonStaticFactory"
factory-method="createInstance">
<constructor-arg name="name" value="等风的草"/>
<constructor-arg name="age" value="21"/>
</bean>
这种方式获取Bean的方式也和通过构造器方法创建Bean的方式类似,示例:
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext(xmlPath);
System.out.println(applicationContext .getBean("person"));
3.1.3 实例工厂实例化Bean
示例:
public class InstanceFactory {
public Person createInstance() {
return new Person();
}
}
配置文件示例(factory-bean:指定使用哪个工厂实例。factory-method:指定使用哪个工厂实例的方法):
<bean id="instancefactory" class="com.spring.demo.InstanceFactory"/>
<bean id="personInstance" factory-bean="instancefactory"
factory-method="createInstance"/>
3.2 通过set注入属性的方法来创建Bean
在使用set方法注入时,需要使用<property>
标签,该标签属性:
name
:即Bean的某个属性名称。value
:指定注入的数据。它支持基本类型和String类型。ref
:指定注入的数据。它支持其他bean类型。写的是其他bean的唯一标识。
如果不想使用Bean构造方法来创建对象,可以使用set方法的注入方式,这种方式要求Bean有对应的setXxx方法。示例:
<bean id="hello" class="com.spring.test.Hello">
<property name="name" value="Spring"/>
</bean>
- 通过set方式注入集合数据
创建Bean时,可以通过set的方式注入最普通的字符串。其实,除了字符串,还可以通过set的方式注入其他复杂格式的数据。 - 1、注入数组
<bean id="student" class="com.test.pojo.Student">
<property name="name" value="小明"/>
<property name="address" ref="addr"/>
<property name="books">
<array>
<value>英雄志</value>
<value>沧浪之水</value>
</array>
</property>
</bean>
- 2、注入List
<!--注入字符串List-->
<property name="interest">
<list>
<value>听歌</value>
<value>看电影</value>
</list>
</property>
<!--注入对象List-->
<property name="empList">
<list>
<ref bean="emp1" />
<ref bean="emp2"/>
</list>
</property>
- 3、注入Map
<property name="story">
<!--注入字符串Map-->
<map>
<entry key="余华" value="活着"/>
<entry key="东野圭吾" value="白夜行"/>
</map>
</property>
<property name="empMap">
<!--注入对象Map-->
<map>
<entry key="1" value-ref="emp1" />
<entry key="2" value-ref="emp2" />
</map>
</property>
- 4、注入Set
<property name="movie">
<set>
<value>兹山鱼谱</value>
<value>浪客剑心</value>
</set>
</property>
<property name="empSets">
<set>
<ref bean="emp1" />
<ref bean="emp2"/>
</set>
</property>
- 5、注入null
在Spring中,不仅可以注入一个null值,也可以注入空字符串
。
<property name="wife">
<null/>
</property>
<property name="name">
<null></null>
</property>
- 6、注入属性
用于注入键值对,键和值都只能为String类型。
<property name="prop">
<props>
<prop key="driver">com.mysql.jdbc.Driver</prop>
<prop key="url">jdbc:mysql://localhost:3306/test</prop>
<prop key="username">root</prop>
<prop key="password">root</prop>
</props>
</property>
3.3 构造函数注入和setter注入的区别
通常,通过XML方式实现的依赖注入有3种方式:构造函数注入、setter注入和接口注入,前两种较常用。
构造函数注入 | setter注入 |
---|---|
没有部分注入 | 有部分注入 |
不会覆盖setter属性 | 会覆盖setter属性 |
任意修改都会创建一个新实例 | 任意修改都不会创建一个新实例 |
适用于设置很多属性 | 适用于设置少量属性 |
- 构造方法注入
1、构造注入需要该Bean包含带有这些属性的构造器
。
2、构造注入可以在构造器中决定依赖关系的注入顺序,优先依赖的优先注入。
3、对于依赖关系无需变化的Bean,构造注入更有用处。因为没有Setter方法,所有的依赖关系全部在构造器内设定。因此,无需担心后续的代码对依赖关系产生破坏。
4、依赖关系只能在构造器中设定,则只有组件的创建者才能改变组件的依赖关系。对组件的调用者而言,组件内部的依赖关系完全透明,更符合高内聚的原则。
5、参数位置的注入对参数顺序有很强的依赖性,若构造函数参数位置被人调整过,会导致注入出错。因此通常情况下,不建议去在代码中修改构造函数,如果需要新增参数的,可以新增一个构造函数来实现,这算是一种扩展,不会影响目前已有的功能。 - Setter方法注入
1、设值注入需要该Bean包含这些属性的setter方法
。
2、对于复杂的依赖关系,如果采用构造注入,会导致构造器过于臃肿。Spring在创建Bean实例时,需要同时实例化器依赖的全部实例,因而导致性能下降。而使用设值注入,则能避免这些问题。
3、尤其是在某些属性可选的情况下,多参数的构造器显得更加笨重。
4、setter注入相对于构造函数注入要灵活一些,构造函数需要指定对应构造函数中所有参数的值,而setter注入的方式没有这种限制,不需要对所有属性都进行注入,可以按需进行注入。
5、通常情况下,实体类都是标准的JavaBean,其特点:
- 属性都是private访问级别的。
- 属性通常情况下通过一组setter和getter方法来访问。
- setter方法,以set开头,后跟首字母大写的属性名,如:setUserName,简单属性一般只有一个方法参数,方法返回值通常为void。
getter方法,一般属性以get开头,对于boolean类型一般以is开头,后跟首字母大写的属性名
,如:getUserName,isOk。
在两种方式的选择上:最好的解决方案是用构造器参数实现强制依赖,setter方法实现可选依赖
。
3.4 通过XML方式实现自动装配
自动装配,是指:Spring自动的建立Bean对象和Bean对象之间的依赖关系,而不需要开发人员手动进行设置,这个过程就叫做自动装配。
进行依赖注入的时候,需要开发人员在XML配置文件里面,通过<property>
标签或者<constructor-arg>
标签,手动的进行属性赋值,可以想到,如果项目中有很多对象都需要赋值,那这样就需要编写许多的XML配置代码,这不利于XML配置文件的维护。而自动装配的目的就是让Spring自动替我们给属性赋值,从而简化XML配置。
先准备几个实体类:
public class Cat {
public void shout() {
System.out.println("喵~");
}
}
public class Dog {
public void shout() {
System.out.println("汪~");
}
}
@Data
public class User {
private Cat cat;
private Dog dog;
private String str;
}
3.4.1 no
默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean
。示例:
<bean id="dog" class="com.spring.test.Dog"/>
<bean id="cat" class="com.spring.test.Cat"/>
<bean id="user" class="com.spring.test.User">
<property name="cat" ref="cat"/>
<property name="dog" ref="dog"/>
<property name="str" value="test"/>
</bean>
测试:
public static void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) context.getBean("user");
user.getCat().shout(); //喵~
user.getDog().shout(); //汪~
}
3.6.2 byName
通过Bean的名称进行自动装配,如果一个Bean的property与另一个Bean的name相同,就进行自动装配
。
示例:
<bean id="user" class="com.spring.test.User" autowire="byName">
<property name="str" value="test"/>
</bean>
测试代码:
public static void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) context.getBean("user");
System.out.println(user.getStr()); //test
}
该模式表示根据Property的Name自动装配,如果一个Bean的name,和另一个Bean中的Property的name相同,则自动装配这个Bean到Property中。
也就是说,当一个Bean节点带有autowire="byName"
的属性时,将查找其类中所有的set方法名,获得将set去掉并且首字母小写的字符串,然后去spring容器中寻找是否有此字符串名称id的对象。如果有,就取出注入;如果没有,就报空指针异常
。
3.4.3 byType
通过参数的数据类型进行自动装配。这个过程需要借助setter注入来完成,因此必须存在set方法,否则注入失败。
使用autowire="byType"
时,首先需要保证:同一类型的对象,在Spring容器中唯一。如果不唯一,会报不唯一的异常(NoUniqueBeanDefinitionException)。
如果照下面的配置,就会报异常,因为有两个同类型的Bean:
<bean id="dog" class="com.spring.test.Dog"/>
<bean id="cat" class="com.spring.test.Cat"/>
<bean id="cat2" class="com.spring.test.Cat"/>
<bean id="user" class="com.spring.test.User" autowire="byType">
<property name="str" value="test"/>
</bean>
- 做法1
删掉id为cat或cat2的Bean,则代码可以正常运行,示例:
public static void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) context.getBean("user");
//test
System.out.println(user.getStr());
}
- 做法2
可以通过<bean>标签的autowire-candidate设置为false来过滤那些不需要注入的实例Bean。
autowire-candidate="false"表示进行注入时,不考虑该Bean。
<bean id="userDao"
class="com.test.dao.impl.UserDaoImpl" />
<!-- autowire-candidate="false" 过滤该类型 -->
<bean id="userDao2" autowire-candidate="false"
class="com.test.dao.impl.UserDaoImpl" />
3.4.4 constructor
利用构造函数进行装配,并且构造函数的参数通过byType进行装配。这个方式类似于byType,但是要提供给构造器参数,如果没有确定的带参数的构造器参数类型,将会抛出异常。
先在User实体类中加入一个构造方法:
public User(Cat cat) {
this.cat = cat;
}
然后配置文件改为:
<bean id="cat" class="com.spring.test.Cat"/>
<bean id="user" class="com.spring.test.User" autowire="constructor"></bean>
测试:
public static void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) context.getBean("user");
user.getCat().shout(); //喵~
}
3.6.5 autodetect
该模式自动探测使用构造器自动装配或者byType自动装配。首先尝试找合适的带参数的构造器,如果找到就是用构造器自动装配,如果在Bean内部没有找到相应的构造器或者是无参构造器,容器就会自动选择byTpe的自动装配方式。
3.5 Bean标签常用属性
在基于XML的IOC配置中,bean标签是最基础的标签。它表示了IOC容器中的1个对象。即如果1个对象想让spring管理,在XML的配置中都需要使用此标签配置。上面已经展示了一些标签的用法。
- id
用于给Bean提供1个唯一标识。在1个标签内部,标识必须唯一。 - class
用于指定创建Bean对象的全限定类名。 - name
用于给Bean提供1个或多个名称。多个名称用空格分隔。 - factory-bean
用于指定创建当前bean对象的工厂Bean的唯一标识。当指定了此属性之后,class属性失效。 - factory-method
用于指定创建当前bean对象的工厂方法,如配合factory-bean属性使用,则class属性失效。如配合class属性使用,则方法必须是static的。 - scope
用于指定bean对象的作用域。通常情况下就是singleton。当要用到多例模式时,可以配置为prototype。示例:
<bean name="user1" class="com.pojo.User" scope="singleton">
<property name="age" value="18"></property>
<property name="name" value="张三"></property>
</bean>
- init-method、destory-method
init-method用于指定Bean对象的初始化方法,此用法会在Bean对象装配后调用。必须是1个无参方法。
destory-method用于指定bean对象的销毁方法,此方法会在Bean对象销毁前执行。它只能为scope是singleton时起作用。
示例:
<bean id="testService" class="com.test.TestService"
init-method="myInit" destroy-method="myDestroy">
</bean>
- autowire
用于指定自动装配的方式。
四、通过注解方式创建Bean*
如果使用手动注入的方式管理应用中的Bean,会有以下缺点:
- 如果需要注入的对象比较多,比如A类中有几十个属性,那么上面的property属性需要写几十个,配置文件代码量暴增。
- 如果A类中新增或者删除了一些依赖,还需要手动去调整bean xml中的依赖配置信息,否则会报错。
使用xml的方式创建Bean,不利于维护和扩展。因此在实际开发中,更多的时候用的是注解(即自动注入)的方式。
使用注解创建Bean时,最基本的注解是@Component和@Value。
- @Component
@Component对应的xml方式的<Bean>
标签,注解加在类上。Bean的id属性内容直接配置在注解后,如果不配置,默认定义个这个bean的id为类的类名首字母小写。
另外,针对分层代码开发提供了@Componenet的三种别名@Controller、@Service、@Repository,分别用于控制层类、服务层类、dao层类的bean定义,这四个注解的用法完全一样,后三者只是为了更清晰的分层。
配置文件中需要添加context:component-scan
配置,示例:
<context:component-scan base-package="com.spring.test"/>
context:component-scan标签有如下两个作用:
- 扫描包内及其子包内的所有“类”(不包含接口),并为添加了@Component、@Service、@Controller、@Repository修饰的类创建对象并存入IOC容器。
- @Service、@Component、@Controller、@Repository修饰的类中含有@Autowired修饰的成员变量,则创建对象时会从IOC容器中取值为该成员变量赋值。
@Component的使用示例:
@Component("user")
//相当于配置文件中 <bean id="user" class="当前注解的类"/>
public class User {
String name;
}
- @Value
@Autowired和@Resource的自动装配的依赖注入并不适合简单值类型,如int、boolean、long、String以及Enum等。对于这些类型,Spring容器也提供了@Value注入的方式。一般情况下@Value会与properties文件或Apollo配置值结合使用,即该注解可以读取配置文件的数据,也可以读取Apollo上配置的数据。
示例:
@Component("user")
public class User {
@Value("自己")
// 相当于配置文件中 <property name="name" value="自己"/>
public String name;
}
4.1 通过注解的方式实现自动装配*
Jdk1.5开始支持注解,Spring2.5开始全面支持注解。
默认情况下,Spring容器中未打开注解装配。要使用注解,需要在配置文件里加上<context:annotation-config/>
,用来开启注解支持。
<context:annotation-config/>
此时就可以在代码中使用@Autowired注解自动获取到对应的对象了,这也是实际项目中常见的做法。示例:
public class User {
@Autowired
private Cat cat;
//...
}
4.2 自动装配的2种注解
4.2.1 @Autowired*
用来自动装配指定的bean。
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}
可以看出,@Autowired有一个required的属性,用来表示该属性是否必需,默认为true,表示必需。如果某个属性不是必需的,可以设置为@Autowired(required=false)
,这样即便在容器中没找到对应的对象时,也不会报错。
@Autowired注解原理:在启动spring IoC容器时,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowied、@Resource或@Inject时,就会在IoC容器自动查找需要的bean,并装配给该对象的属性。
@Autowired可用于:构造函数、成员变量(常用)、Setter方法。
假如在UserService类中需要自动装配一个UserDao类型的对象。
Spring容器会找到类型为UserDao的类,生成一个UserDao对象,然后将其注入进来。这样会产生一个问题,当一个类型有多个Bean值的时候,会造成无法选择具体注入哪一个的情况,Spring容器在启动时也会抛出BeanCreationException。这个时候可以配合@Qualifier使用,@Qualifier告诉spring具体去装配哪个对象(@Qualifier和@Autowired一块使用,在同一类型的bean有多个的情况下可以实现根据name注入的需求)。
当使用@AutoWired注解的时候,自动装配的时候是根据类型(在上面的例子中,即类型为UserDao的类)来装配的:
- 如果只找到一个UserDao类型的类,则直接进行赋值。如果没有找到UserDao类型的类,则直接抛出异常。
- 如果找到多个UserDao类型的类,那么会按照变量名(此处的变量名,为类名首字母小写)作为id继续匹配:
1)匹配上直接进行装配;
2)如果匹配不上则直接报异常。- 也可以可以不使用变量名,使用@Qualifier注解来指定id的名称,当使用@Qualifier注解的时候也会有两种情况:
1)找到,则直接装配;
2)找不到,就会报错。
@Autowired标注在字段上面:假定字段类型为一个自定义的普通的类型,候选者查找过程:
@Qualifier和@Autowired结合使用的示例:
public class UserService {
@Autowired
@Qualifier(name="userDao1")
private UserDao userDao;
}
如果不用@Qualifier指定一个具体类型的Bean,也可以使用@Primary来指定某个类型的Bean为首选者。
4.2.2 @Resource*
@Resource注解也可以完成自动注入,@Resource有两个中重要的属性:name和type。Spring容器对于@Resource注解的name属性解析为bean的名字,type属性则解析为bean的类型。因此使用name属性,则按byName模式的自动注入策略,如果使用type属性则按 byType模式自动注入策略。
如果既不指定name也不指定type属性,@Resource默认按byName模式注入
。
@Resource注解的自动注入规律:
- 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。
- 如果指定了name,则从上下文中查找名称匹配的bean进行装配,找不到则抛出异常。
- 如果指定了type,则从上下文中找到类似匹配的唯一Bean进行装配,找不到或是找到多个,都会抛出异常。
- 如果既没有指定name,也没有指定type,则自动按照byName方式进行装配。
@Resource的使用示例:
public class User {
//优先注入name为"cat2"的Bean
@Resource(name = "cat2")
private Cat cat;
//...
}
@Resource在Jdk11中已经移除,如果要使用,需要单独引入jar包。示例:
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
4.2.3 @Autowired和@Resource的区别*
- 1、@Autowired
@Autowired是Spring提供的注解。
@Autowired默认按类型装配,默认情况下必须要求依赖对象必须存在
。如果要允许null值,可以设置它的required属性为false。如果想使用按照名称来装配,可以结合@Qualifier注解一起使用。示例:
public class TestServiceImpl {
@Autowired
@Qualifier("userDao")
private UserDao userDao;
}
- 2、@Resource
@Resourc是jdk提供的注解。
@Resource默认按照ByName自动注入
。示例:
public class UserService {
@Resource
private UserDao userDao;
@Resource(name="studentDao")
private StudentDao studentDao;
@Resource(type="TeacherDao")
private TeacherDao teacherDao;
@Resource(name="manDao",type="ManDao")
private ManDao manDao;
}
- 3、总结:
@AutoWired | @Resource | |
---|---|---|
来源 | Spring | Jdk |
默认的Bean匹配方式 | 按类型 | 按名称 |
使用范围 | 只适合spring框架 | 扩展性更好 |
五、作用域*
当定义一个Bean在Spring里,我们可以给这个Bean声明一个作用域。Bean的作用域是指Bean在Spring整个框架中的某种行为模式
。Bean的作用域可以通过Bean定义中的scope属性来定义。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
@AliasFor("scopeName")
String value() default "";
@AliasFor("value")
String scopeName() default "";
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}
可以看出@Scope可以用在类上和方法上。常见2种用法:
- 和@Compontent一起使用在类上。
- 和@Bean一起标注在方法上。
Spring框架支持以下五种Bean的作用域:
类别 | 说明 | 注解 |
---|---|---|
singleton | 默认值 , 容器初始时创建Bean实例, 在整个容器的生命周期内只创建这一个Bean | @Scope(“singleton”) |
prototype | 容器初始化时不创建Bean实例,而在每次请求时都创建一个新的Bean的实例 | @Scope(“prototype”) |
request | 在每一次http请求时会创建一个实例,该实例仅在当前http request有效 | @Scope(“request”) |
session | 在每一次http请求时会创建一个实例,该实例仅在当前http session有效 | @Scope(“session”) |
globalSession | 全局Session,类似于标准的HTTP Session作用域,不过它仅仅在基于portlet的web应用中才有意义。 | @SessionScope |
仅当用户使用支持Web的ApplicationContext时,最后三个才可用。
Spring4.x的版本中包含两种作用域:request和session作用域,不过这两种作用域几乎不用,因此在Spring5版本的时候被淘汰了。
在这几类作用域中,最常用的是Singleton和Prototype。一般情况下,对有状态的bean使用prototype作用域,而对无状态的bean使用singleton作用域。
使用Prototype作用域时,频繁创建和销毁Bean会带来很大的性能开销。
Spring容器自带的有2种作用域,分别是singleton和prototype;还有3种分别是spring web容器环境中才支持的request、session、application。
5.1 Singleton*
当一个Bean的作用域为Singleton,那么Spring IoC容器中只会存在一个共享的Bean实例
。
该模式在多线程下是不安全的。
通常spring容器在启动的时候,会将scope为singleton的bean创建好放在容器中(有个特殊的情况,当bean的lazy被设置为true的时候,表示懒加载,那么使用的时候才会创建),用的时候直接返回。
当创建容器时,对象就被创建了。只要容器在,对象一直活着。当销毁容器时,对象就被销毁了。一句话总结:单例模式的Bean对象生命周期与容器相同
。
示例:
@Bean
@Scope(value = "singleton")
public Person person() {
return new Person();
}
5.2 Prototype*
当一个Bean的作用域为Prototype,表示一个Bean定义对应多个对象实例
。Prototype作用域的Bean会导致在每次对该Bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的Bean实例。
当使用对象时,创建新的对象实例。只要对象在使用中,就一直活着。当对象长时间不用时,被Java的垃圾回收器回收了。一句话总结:多例模式的bean对象,spring框架只负责创建,不负责销毁
。
一般来说,对有状态的bean使用prototype作用域,而对无状态的bean使用singleton作用域。
示例:
@Bean
@Scope(value = "prototype")
public Person person() {
return new Person();
}
多例Bean每次获取的时候都会重新创建,如果这个Bean比较复杂,创建时间比较长,会影响系统的性能,这个地方需要注意。
5.3 Request
当一个Bean的作用域为Request,表示在一次HTTP请求中,一个Bean定义对应一个实例
;即每个HTTP请求都会有各自的Bean实例,而且该bean仅在当前Http Request内有效,当前Http请求结束,该bean实例也将会被销毁。该作用域仅在基于Web的Spring ApplicationContext情形下有效。
示例:
<bean id="loginAction" class="cn.csdn.LoginAction" scope="request"/>
5.4 Session
当一个Bean的作用域为Session,表示在一个HTTP Session中,一个Bean定义对应一个实例
。每一次Session请求创建新的实例,而不同的实例之间不共享属性,且实例仅在自己的Session请求内有效,请求结束,则实例将被销毁。该作用域仅在基于Web的Spring ApplicationContext情形下有效。示例:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
5.5 Global Session
当一个bean的作用域为Global Session,表示在一个全局的HTTP Session中,一个bean定义对应一个实例。典型情况下,仅在使用portlet context的时候有效。该作用域仅在基于Web的Spring ApplicationContext情形下有效。
5.6 作用域的相关问题
5.6.1 Spring实现单例用到了什么集合对象
Spring对Bean实例的创建是采用单例注册表的方式进行实现的,而这个注册表的缓存是ConcurrentHashMap对象。
5.6.2 Controller方法中为什么不能定义局部变量*
因为Controller是默认单例模式,高并发下全局变量会出现线程安全问题。
- 解决方法
1、就将全局变量都变成局部变量,通过方法参数来传递。
2、使用ThreadLocal。
3、将Controller的作用域从单例改为原型,即scope=“prototype”,每次都创建新的controller。
六、创建Bean的注解
6.1 实体类常用的注解*
在实体类中主要用到的插件是Lombok,目的和作用就在于不用再去写Getter、Setter、Constructor等代码。常用的注解:
- @Data
使用这个注解,就不用再去手写Getter、Setter、equals、hasCode、toString等方法,注解后在编译时会自动加进去。 - @AllArgsConstructor
使用后添加一个构造函数,该构造函数含有所有已声明字段属性参数,构造方法中的参数顺序为实体类的属性顺序。 - @NoArgsConstructor
使用后创建一个无参构造函数。
@Data、@AllArgsConstructor、@NoArgsConstructor、@Builder使用示例:
@Data //生成getter、setter等函数
@AllArgsConstructor //生成全参数构造函数
@NoArgsConstructor //生成无参构造函数
public class test {
String name;
String age;
String sex;
}
- @Setter/@Getter
为相应的属性自动生成Getter/Setter方法。 - @Builder
和创建者模式类似,分模块地创建一个对象。
@Builder使用示例:
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private final Integer code = 200;
private String username;
private String password;
}
public class SpringTest {
public static void main(String[] args) {
System.out.println(User.builder().username("hecc").password("hecc").build());
}
}
- @ToString
生成一个toString()方法,默认情况下,会输出类名、所有属性(会按照属性定义顺序),用逗号来分割。 - @Required
@Required注解主要用在setter方法上,它表示该setter方法的属性必须要在配置时注入值。否则就会报BeanInitializationException异常。示例:
public class Student {
private String name;
private Integer age;
//表示该属性是必需的
@Required
public void setAge(Integer age) {
this.age = age;
}
//表示该属性是必需的
@Required
public void setName(String name) {
this.name = name;
}
}
6.2 @Bean*
该注释的属性的名称和语义类似于Spring XML模式中Bean的元素的名称和语义。@Bean指示方法产生一个由Spring容器管理的Bean。
默认使用方法名作为id,可以在后面定义id如:@Bean(“xx”)。
被@Configuration修饰的类,Spring容器中会通过cglib给这个类创建一个代理,代理会拦截所有被@Bean修饰的方法,默认情况(Bean为单例)下确保这些方法只被调用一次,从而确保这些Bean是同一个Bean,即单例的。
@Bean注解的使用场景常常为:某个方法返回一个特定的Bean。示例:
@Configuration
public class DataBaseConfig {
@Bean("dataSource")
public DataSource getDataSource(){
DataSource dataSource = new DataSource();
dataSource.setUserId("xxxx");
dataSource.setPassword("123456");
dataSource.setUrl("www");
return dataSource;
}
}
6.3 @DependsOn
Spring在创建Bean的时候,如果Bean之间没有依赖关系,那么Spring容器很难保证Bean实例创建的顺序,如果想确保容器在创建某些bean之前,需要先创建好一些其他的Bean,可以通过@DependsOn来实现。
@DependsOn可以指定当前Bean依赖的Bean,通过这个可以确保@DependsOn指定的Bean在当前bean创建之前先创建好。
常见2种用法:
- 和@Compontent一起使用在类上。
- 和@Bean一起标注在方法上。
示例:
@Component
public class Service3 {
public Service3() {
System.out.println("create Service3");
}
}
@Component
public class Service2 {
public Service2() {
System.out.println("create Service2");
}
}
@DependsOn({"service2", "service3"})
@Component
public class Service1 {
public Service1() {
System.out.println("create Service1");
}
}
Service1中使用了@DependsOn,指定了2个Bean:service2和service3,那么Spring容器在创建上面这个service1的时候会先将@DependsOn中指定的2个bean先创建好。
6.4 @Lazy*
@Lazy等效于xml配置方式中bean元素的lazy-init属性,可以实现bean的延迟初始化(使用到的时候才会去进行初始化)。
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR,
ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lazy {
boolean value() default true;
}
可以看出,用在任意类型、方法、构造器、参数、字段上面。参数value:boolean类型,用来配置是否应发生延迟初始化,默认为true。
常用方式:
- 和@Compontent一起标注在类上,可以是这个类延迟初始化。
- 和@Configuration一起标注在配置类中,可以让当前配置类中通过@Bean注册的bean延迟初始化。
6.5 创建Bean的相关问题
6.5.1 Bean注入容器有哪些方式*
将普通类交给Spring容器管理,通常有以下方法:
1、使用@Configuration与@Bean注解。
2、使用@Controller、@Service、@Repository、@Component注解标注该类,然后启用@ComponentScan自动扫描。
6.5.2 @Bean和@Component有什么区别*
都是使用注解定义Bean。@Bean是使用Java代码装配Bean,@Component 是自动装配Bean。
@Bean注解用在方法上,表示这个方法会返回一个Bean。@Bean需要在配置类中使用,即类上需要加上@Configuration注解。@Bean注解比@Component注解的自定义性更强,而且很多地方我们只能通过@Bean注解来注册bean。比如当我们引用第三方库中的类需要装配到 Spring 容器时,则只能通过@Bean来实现。
@Component可以用在任何类型上面,通常情况下将这个注解用在类上面,标注这个类为一个组件,默认情况下,被扫描的时候会被作为Bean注册到容器中。value参数:被注册为Bean的时候,用来指定Bean的名称。如果不指定,默认为类名首字母小写。如:类UserService对应的Bean名称为userService。
示例:
@Component
public class Student {
private String name = "lkm";
public String getName() {
return name;
}
}
@Configuration
public class WebSocketConfig {
@Bean
public Student student(){
return new Student();
}
}
6.5.3 @EqualsAndHashCode*
@Data注解会帮我们实现equals和hashCode方法,但是有继承关系时,Lombok自动生成的方法可能就不是我们期望的。
此时可以使用@EqualsAndHashCode注解。
@EqualsAndHashCode写在子类上:
- callSuper = true:根据子类自身的字段值和从父类继承的字段值来生成hashcode,当两个子类对象比较时,只有子类对象的本身的字段值和继承父类的字段值都相同,equals方法的返回值才是true。
- callSuper = false(默认值):根据子类自身的字段值来生成hashcode, 当两个子类对象比较时,只有子类对象的本身的字段值相同,父类字段值可以不同,equals方法的返回值是true。
示例:
@Data
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class Cat extends Animal{
private int weight;
private String color;
}
七、Spring中创建Bean的一些相关问题
7.1 Spring框架中的单例Bean是线程安全的吗*
Spring框架中的单例Bean不是线程安全的
。Spring中的Bean默认是单例模式,但并没有对单例Bean进行多线程的封装处理。
实际上,大部分的Spring Bean并没有可变的状态(比如Service类和DAO类),也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring MVC的Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身
。
如果某个Bean有多种状态的话,就需要自行保证线程安全。
在Spring中无状态的Bean适合用单例模式,这样可以共享实例提高性能。有状态的Bean在多线程环境下不安全,一般用Prototype模式或者使用ThreadLocal解决线程安全问题。
- 有状态对象
就是有实例变量的对象,可以保存数据,是非线程安全的。每个用户有自己特有的一个实例,在用户的生存期内,Bean保持了用户的信息,即“有状态”。一旦用户灭亡(调用结束或实例结束),Bean的生命期也告结束。 - 无状态对象
就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。
7.2 循环注入/循环依赖*
什么是循环注入?举个列子有一个类A,A有一个构造器里面的参数是类B;类B里面有个构造器参数是类C,类C里面有个构造器参数是类A。其实引用循环了A里面有B的引用,B里面有C的引用,C里面又有A的引用。
当用Spring加载A的时候Spring的流程是这样的:
- Spring创建类A的实例对象,首先去当前创建池中去查找当前类A的实例对象是否在创建,如果发明没有创建则准备其构造器需要的参数B,然后把创建A的标识放入当前创建池中。
- Spring创建B首先去当前创建池中去查找当前B是否在创建,如果发现没有创建则准备其构造器需要的参数C,然后把创建B的标识放入当前创建池中。
- Spring创建C首先去当前创建池中去查找当前C是否在创建,如果发现没有创建则准备其构造器需要的参数A,然后把创建C的标识放入当前创建池中。
- Spring创建C需要的A,这个时候会发现在当前创建池中已经有A的标识,A正在创建中则抛出BeanCurrentlyInCreationException。
单例模式下属性注入的循环依赖:通过三级缓存处理循环依赖。
非单例循环依赖:无法处理。
7.2.1 什么是循环依赖*
循环依赖其实就是循环引用,也就是两个或者两个以上的Bean互相持有对方,最终形成闭环
。比如A依赖于B,B依赖于C,C依赖于A。
Spring中循环依赖场景有:构造器的循环依赖(构造器注入)、Field属性的循环依赖(set注入)。
构造器的循环注入是没有办法解决的
,所以只能避免。因此,不要使用基于构造函数的依赖注入,可以通过以下方式解决
:
在字段上使用@Autowired注解
,让Spring决定在合适的时机注入;- 用基于setter方法的依赖注入。
在解决属性循环依赖时,Spring采用的是提前暴露对象的方法。
7.2.2 循环依赖处理机制*
只有单例的Bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的Bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的Bean是没有缓存的,不会将其放到三级缓存中。
- 1、单例Bean构造器参数循环依赖(无法解决)
- 2、原型Bean循环依赖(无法解决)
原因:
- 创建时机不同: 原型作用域的Bean是在每次请求时创建的,而
循环依赖的解决需要在对象创建过程中进行半成品对象的暴露和提前注入
。由于原型作用域的Bean在每次请求时都会创建新的实例,所以无法在对象创建过程中暴露半成品对象并注入依赖。- 容器控制能力有限: Spring IOC容器在单例作用域的Bean中通过三级缓存的方式实现了循环依赖的解决,但对于原型作用域的Bean,容器的控制能力有限。原型作用域的Bean是由应用程序主动请求创建的,容器无法提前探测到循环依赖并提供半成品对象。
原型作用域的Bean不能直接解决循环依赖问题,但可以考虑通过以下方式间接解决:
- 使用方法注入: 在原型作用域的Bean中,避免使用构造函数注入或字段注入,而使用方法注入。方法注入可以在每次请求时延迟注入依赖,从而避免在对象创建过程中的循环依赖问题。
- 使用代理或延迟初始化: 可以考虑使用代理或延迟初始化的方式创建原型作用域的Bean。通过代理或延迟初始化,可以在运行时动态地解决循环依赖问题,而不是依赖于容器的提前控制。
- 3、单例Bean通过setXxx或者@Autowired进行循环依赖
Spring的循环依赖的理论依据:基于Java的引用传递,当获得对象的引用时,对象的属性是可以延后设置的,但是构造器必须是在获取引用之前
。
Spring通过setXxx或者@Autowired方法解决循环依赖,其实是通过提前暴露1个ObjectFactory对象来完成的。
简单来说,就是在实例化过程中,提前把Bean暴露出来,虽然此时还是个半成品(属性未填充),但是允许先注入,这样确实能解决问题。梳理一下: 实例化A -> 暴露A对象(半成品)-> 属性填充注入B -> B还没有实例化,需要先进行实例化B(A等待) -> 实例化B -> 注入A(半成品) -> 实例化B成功 -> 实例化A成功。通过提前把半成品的对象暴露出来,支持别的Bean注入,确实可以解决循环依赖的问题。实际上,Spring也确实是这么做的。
Spring是通过三级缓存来解决循环依赖的:提前暴露的对象存放在三级缓存中,二级缓存存放过渡Bean,一级缓存存放最终形态的Bean。
/** 第一级缓存:单例bean的缓存 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 第二级缓存:早期暴露的bean的缓存 */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/** 第三级缓存:单例bean工厂的缓存 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
1)Spring容器初始化ClassA通过构造器初始化对象后提前暴露到Spring容器。
//AbstractAutowireCapableBeanFactory.doCreateBean方法中有如下代码
// 向容器中缓存单例模式的Bean对象,以防止循环引用
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
//将初始化后的对象提前以ObjectFactory对象,注入到容器中
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
2)ClassA调用setClassB方法,Spring先尝试从容器中获取ClassB,此时ClassB不存在Spring容器中。
3)Spring容器初始化ClassB,同时也会将ClassB提前暴露到Spring容器中。
4)ClassB调用setClassA方法,Spring从容器中获取ClassA ,因为第1步中已经提前暴露了ClassA,因此可以获取到ClassA实例。ClassA通过spring容器获取到ClassB,完成了对象初始化操作。
5)这样ClassA和ClassB都完成了对象初始化操作,解决了循环依赖问题。