一句话概括:Spring 是一个轻量级、非入侵式的控制反转 (IoC) 和面向切面 (AOP) 的框架。
2003 年,一个音乐家 Rod Johnson 决定发展一个轻量级的 Java 开发框架,Spring
作为 Java 战场的龙骑兵渐渐崛起,并淘汰了EJB
这个传统的重装骑兵。
到了现在,企业级开发的标配基本就是 Spring5 + Spring Boot 2 + JDK 8
Spring 有哪些特性呢?
Spring 有很多优点:
Spring 的核心就是一个大的工厂容器,可以维护所有对象的创建和依赖关系,Spring 工厂用于生成 Bean,并且管理 Bean 的生命周期,实现高内聚低耦合的设计理念。
Spring 提供了面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等切面功能。
支持通过配置就来完成对事务的管理,而不需要通过硬编码的方式,以前重复的一些事务提交、回滚的 JDBC 代码,都可以不用自己写了。
Spring 对 Junit 提供支持,可以通过注解快捷地测试 Spring 程序。
方便集成各种优秀框架,Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如:Struts、Hibernate、MyBatis、Quartz 等)的直接支持。
Spring 对 JavaEE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等)都提供了模板化的封装,这些封装 API 的提供使得应用难度大大降低。
Spring 框架是分模块存在,除了最核心的Spring Core Container
是必要模块之外,其他模块都是可选
,大约有 20 多个模块。
最主要的七大模块:
Spring 有很多模块,甚至广义的 SpringBoot、SpringCloud 也算是 Spring 的一部分,我们来分模块,按功能来看一下一些常用的注解:
Web:
@RequestMapping(“/hello/{name}”)
申明的路径,将注解放在参数中前,即可获取该值,通常作为 Restful 的接口实现方法。容器:
#{} 跟 \${}
两个方式。一般将 SpringbBoot 中的 application.properties 配置的属性值赋值给变量。@Bean(initMethod=”init”,destroyMethod=”destroy”)
定义,在构造之后执行 init,在销毁之前执行 destroy。AOP:
@After
:在方法执行之后执行(方法上)。@Before
: 在方法执行之前执行(方法上)。@Around
: 在方法执行之前与之后执行(方法上)。@PointCut
: 声明切点 在 java 配置类中使用@EnableAspectJAutoProxy 注解开启 Spring 对 AspectJ 代理的支持(类上)。事务:
Spring 框架中广泛使用了不同类型的设计模式,下面我们来看看到底有哪些设计模式?
Java 是面向对象的编程语言,一个个实例对象相互合作组成了业务逻辑,原来,我们都是在代码里创建对象和对象的依赖。
所谓的IOC(控制反转):就是由容器来负责控制对象的生命周期和对象间的关系。以前是我们想要什么,就自己创建什么,现在是我们需要什么,容器就给我们送来什么。
也就是说,控制对象生命周期的不再是引用它的对象,而是容器。对具体对象,以前是它控制其它对象,现在所有对象都被容器控制,所以这就叫控制反转。
DI(依赖注入):指的是容器在实例化对象的时候把它依赖的类注入给它。有的说法 IOC 和 DI 是一回事,有的说法是 IOC 是思想,DI 是 IOC 的实现。
为什么要使用 IOC 呢?
最主要的是两个字解耦,硬编码会造成对象间的过度耦合,使用 IOC 之后,我们可以不用关心对象间的依赖,专心开发应用就行。
PS:这道题老三在面试中被问到过,问法是“你有自己实现过简单的 Spring 吗?”
Spring 的 IOC 本质就是一个大工厂,我们想想一个工厂是怎么运行的呢?
生产产品:一个工厂最核心的功能就是生产产品。在 Spring 里,不用 Bean 自己来实例化,而是交给 Spring,应该怎么实现呢?——答案毫无疑问,反射。
那么这个厂子的生产管理是怎么做的?你应该也知道——工厂模式。
库存产品:工厂一般都是有库房的,用来库存产品,毕竟生产的产品不能立马就拉走。Spring 我们都知道是一个容器,这个容器里存的就是对象,不能每次来取对象,都得现场来反射创建对象,得把创建出的对象存起来。
订单处理:还有最重要的一点,工厂根据什么来提供产品呢?订单。这些订单可能五花八门,有线上签签的、有到工厂签的、还有工厂销售上门签的……最后经过处理,指导工厂的出货。
在 Spring 里,也有这样的订单,它就是我们 bean 的定义和依赖关系,可以是 xml 形式,也可以是我们最熟悉的注解形式。
我们简单地实现一个 mini 版的 Spring IOC:
Bean 定义:
Bean 通过一个配置文件定义,把它解析成一个类型。
beans.properties
偷懒,这里直接用了最方便解析的 properties,这里直接用一个
类型的配置来代表 Bean 的定义,其中 key 是 beanName,value 是 class
userDao:cn.fighter3.bean.UserDao
BeanDefinition.java
bean 定义类,配置文件中 bean 定义对应的实体
public class BeanDefinition {
private String beanName;
private Class beanClass;
//省略getter、setter
}
ResourceLoader.java
资源加载器,用来完成配置文件中配置的加载
public class ResourceLoader {
public static Map<String, BeanDefinition> getResource() {
Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>(16);
Properties properties = new Properties();
try {
InputStream inputStream = ResourceLoader.class.getResourceAsStream("/beans.properties");
properties.load(inputStream);
Iterator<String> it = properties.stringPropertyNames().iterator();
while (it.hasNext()) {
String key = it.next();
String className = properties.getProperty(key);
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setBeanName(key);
Class clazz = Class.forName(className);
beanDefinition.setBeanClass(clazz);
beanDefinitionMap.put(key, beanDefinition);
}
inputStream.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return beanDefinitionMap;
}
}
BeanRegister.java
对象注册器,这里用于单例 bean 的缓存,我们大幅简化,默认所有 bean 都是单例的。可以看到所谓单例注册,也很简单,不过是往 HashMap 里存对象。
public class BeanRegister {
//单例Bean缓存
private Map<String, Object> singletonMap = new HashMap<>(32);
/**
* 获取单例Bean
*
* @param beanName bean名称
* @return
*/
public Object getSingletonBean(String beanName) {
return singletonMap.get(beanName);
}
/**
* 注册单例bean
*
* @param beanName
* @param bean
*/
public void registerSingletonBean(String beanName, Object bean) {
if (singletonMap.containsKey(beanName)) {
return;
}
singletonMap.put(beanName, bean);
}
}
BeanFactory.java
对象工厂,我们最核心的一个类,在它初始化的时候,创建了 bean 注册器,完成了资源的加载。
获取 bean 的时候,先从单例缓存中取,如果没有取到,就创建并注册一个 bean
public class BeanFactory {
private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
private BeanRegister beanRegister;
public BeanFactory() {
//创建bean注册器
beanRegister = new BeanRegister();
//加载资源
this.beanDefinitionMap = new ResourceLoader().getResource();
}
/**
* 获取bean
*
* @param beanName bean名称
* @return
*/
public Object getBean(String beanName) {
//从bean缓存中取
Object bean = beanRegister.getSingletonBean(beanName);
if (bean != null) {
return bean;
}
//根据bean定义,创建bean
return createBean(beanDefinitionMap.get(beanName));
}
/**
* 创建Bean
*
* @param beanDefinition bean定义
* @return
*/
private Object createBean(BeanDefinition beanDefinition) {
try {
Object bean = beanDefinition.getBeanClass().newInstance();
//缓存bean
beanRegister.registerSingletonBean(beanDefinition.getBeanName(), bean);
return bean;
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
测试
UserDao.java
我们的 Bean 类,很简单
public class UserDao {
public void queryUserInfo(){
System.out.println("A good man.");
}
}
单元测试
public class ApiTest {
@Test
public void test_BeanFactory() {
//1.创建bean工厂(同时完成了加载资源、创建注册单例bean注册器的操作)
BeanFactory beanFactory = new BeanFactory();
//2.第一次获取bean(通过反射创建bean,缓存bean)
UserDao userDao1 = (UserDao) beanFactory.getBean("userDao");
userDao1.queryUserInfo();
//3.第二次获取bean(从缓存中获取bean)
UserDao userDao2 = (UserDao) beanFactory.getBean("userDao");
userDao2.queryUserInfo();
}
}
运行结果
A good man.
A good man.
至此,我们一个乞丐+破船版的 Spring 就完成了,代码也比较完整,有条件的可以跑一下。
PS:因为时间+篇幅的限制,这个 demo 比较简陋,没有面向接口、没有解耦、边界检查、异常处理……健壮性、扩展性都有很大的不足,感兴趣可以学习参考[15]。
可以这么形容,BeanFactory 是 Spring 的“心脏”,ApplicantContext 是完整的“身躯”。
BeanFactory和ApplicantContext的比喻
BeanFactory 是类的通用工厂,可以创建并管理各种类的对象。
Spring 为 BeanFactory 提供了很多种实现,最常用的是 XmlBeanFactory,但在 Spring 3.2 中已被废弃,建议使用 XmlBeanDefinitionReader、DefaultListableBeanFactory。
BeanFactory 接口位于类结构树的顶端,它最主要的方法就是 getBean(String var1),这个方法从容器中返回特定名称的 Bean。
BeanFactory 的功能通过其它的接口得到了不断的扩展,比如 AbstractAutowireCapableBeanFactory 定义了将容器中的 Bean 按照某种规则(比如按名字匹配、按类型匹配等)进行自动装配的方法。
这里看一个 XMLBeanFactory(已过期) 获取 bean 的例子:
public class HelloWorldApp{
public static void main(String[] args) {
BeanFactory factory = new XmlBeanFactory (new ClassPathResource("beans.xml"));
HelloWorld obj = (HelloWorld) factory.getBean("helloWorld");
obj.getMessage();
}
}
ApplicationContext 由 BeanFactory 派生而来,提供了更多面向实际应用的功能。可以这么说,使用 BeanFactory 就是手动档,使用 ApplicationContext 就是自动档。
Spring5 ApplicationContext部分体系类图
ApplicationContext 继承了 HierachicalBeanFactory 和 ListableBeanFactory 接口,在此基础上,还通过其他的接口扩展了 BeanFactory 的功能,包括:
这是 ApplicationContext 的使用例子:
public class HelloWorldApp{
public static void main(String[] args) {
ApplicationContext context=new ClassPathXmlApplicationContext("beans.xml");
HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
obj.getMessage();
}
}
ApplicationContext 包含 BeanFactory 的所有特性,通常推荐使用前者。
PS:这道题老三面试被问到过
Spring 的 IOC 容器工作的过程,其实可以划分为两个阶段:容器启动阶段和Bean 实例化阶段。
其中容器启动阶段主要做的工作是加载和解析配置文件,保存到对应的 Bean 定义中。
容器启动开始,首先会通过某种途径加载 Congiguration MetaData,在大部分情况下,容器需要依赖某些工具类(BeanDefinitionReader)对加载的 Congiguration MetaData 进行解析和分析,并将分析后的信息组为相应的 BeanDefinition。
最后把这些保存了 Bean 定义必要信息的 BeanDefinition,注册到相应的 BeanDefinitionRegistry,这样容器启动就完成了。
可以看看:Spring Bean 生命周期,好像人的一生。。open in new window
在 Spring 中,基本容器 BeanFactory 和扩展容器 ApplicationContext 的实例化时机不太一样,BeanFactory 采用的是延迟初始化的方式,也就是只有在第一次 getBean()的时候,才会实例化 Bean;ApplicationContext 启动之后会实例化所有的 Bean 定义。
Spring IOC 中 Bean 的生命周期大致分为四个阶段:实例化(Instantiation)、属性赋值(Populate)、初始化(Initialization)、销毁(Destruction)。
我们再来看一个稍微详细一些的过程:
简单总结一下,Bean 生命周期里初始化的过程相对步骤会多一些,比如前置、后置的处理。
PersonBean
类,实现DisposableBean
,InitializingBean
, BeanFactoryAware
, BeanNameAware
这 4 个接口,同时还有自定义的init-method
和destroy-method
。public class PersonBean implements InitializingBean, BeanFactoryAware, BeanNameAware, DisposableBean {
/**
* 身份证号
*/
private Integer no;
/**
* 姓名
*/
private String name;
public PersonBean() {
System.out.println("1.调用构造方法:我出生了!");
}
public Integer getNo() {
return no;
}
public void setNo(Integer no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
System.out.println("2.设置属性:我的名字叫"+name);
}
@Override
public void setBeanName(String s) {
System.out.println("3.调用BeanNameAware#setBeanName方法:我要上学了,起了个学名");
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("4.调用BeanFactoryAware#setBeanFactory方法:选好学校了");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("6.InitializingBean#afterPropertiesSet方法:入学登记");
}
public void init() {
System.out.println("7.自定义init方法:努力上学ing");
}
@Override
public void destroy() throws Exception {
System.out.println("9.DisposableBean#destroy方法:平淡的一生落幕了");
}
public void destroyMethod() {
System.out.println("10.自定义destroy方法:睡了,别想叫醒我");
}
public void work(){
System.out.println("Bean使用中:工作,只有对社会没有用的人才放假。。");
}
}
MyBeanPostProcessor
实现BeanPostProcessor
接口。public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("5.BeanPostProcessor.postProcessBeforeInitialization方法:到学校报名啦");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("8.BeanPostProcessor#postProcessAfterInitialization方法:终于毕业,拿到毕业证啦!");
return bean;
}
}
init-method
和destroy-method
属性<?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 name="myBeanPostProcessor" class="cn.fighter3.spring.life.MyBeanPostProcessor" />
<bean name="personBean" class="cn.fighter3.spring.life.PersonBean"
init-method="init" destroy-method="destroyMethod">
<property name="idNo" value= "80669865"/>
<property name="name" value="张铁钢" />
</bean>
</beans>
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
PersonBean personBean = (PersonBean) context.getBean("personBean");
personBean.work();
((ClassPathXmlApplicationContext) context).destroy();
}
}
1.调用构造方法:我出生了!
2.设置属性:我的名字叫张铁钢
3.调用BeanNameAware#setBeanName方法:我要上学了,起了个学名
4.调用BeanFactoryAware#setBeanFactory方法:选好学校了
5.BeanPostProcessor#postProcessBeforeInitialization方法:到学校报名啦
6.InitializingBean#afterPropertiesSet方法:入学登记
7.自定义init方法:努力上学ing
8.BeanPostProcessor#postProcessAfterInitialization方法:终于毕业,拿到毕业证啦!
Bean使用中:工作,只有对社会没有用的人才放假。。
9.DisposableBean#destroy方法:平淡的一生落幕了
10.自定义destroy方法:睡了,别想叫醒我
关于源码,Bean 创建过程可以查看AbstractBeanFactory#doGetBean
方法,在这个方法里可以看到 Bean 的实例化,赋值、初始化的过程,至于最终的销毁,可以看看ConfigurableApplicationContext#close()
。
有三种方式:直接编码方式、配置文件方式、注解方式。
Spring 支持构造方法注入、属性注入、工厂方法注入,其中工厂方法注入,又可以分为静态工厂方法注入和非静态工厂方法注入。
构造方法注入
通过调用类的构造方法,将接口实现类通过构造方法变量传入
public CatDaoImpl(String message){
this. message = message;
}
<bean id="CatDaoImpl" class="com.CatDaoImpl">
<constructor-arg value=" message "></constructor-arg>
</bean>
属性注入
通过 Setter 方法完成调用类所需依赖的注入
public class Id {
private int id;
public int getId() { return id; }
public void setId(int id) { this.id = id; }
}
<bean id="id" class="com.id ">
<property name="id" value="123"></property>
</bean>
工厂方法注入
静态工厂注入
静态工厂顾名思义,就是通过调用静态工厂的方法来获取自己需要的对象,为了让 Spring 管理所有对象,我们不能直接通过"工程类.静态方法()"来获取对象,而是依然通过 Spring 注入的形式获取:
public class DaoFactory { //静态工厂
public static final FactoryDao getStaticFactoryDaoImpl(){
return new StaticFacotryDaoImpl();
}
}
public class SpringAction {
//注入对象
private FactoryDao staticFactoryDao;
//注入对象的 set 方法
public void setStaticFactoryDao(FactoryDao staticFactoryDao) {
this.staticFactoryDao = staticFactoryDao;
}
}
//factory-method="getStaticFactoryDaoImpl"指定调用哪个工厂方法
<bean name="springAction" class=" SpringAction" >
<!--使用静态工厂的方法注入对象,对应下面的配置文件-->
<property name="staticFactoryDao" ref="staticFactoryDao"></property>
</bean>
<!--此处获取对象的方式是从工厂类中获取静态方法-->
<bean name="staticFactoryDao" class="DaoFactory"
factory-method="getStaticFactoryDaoImpl"></bean>
非静态工厂注入
非静态工厂,也叫实例工厂,意思是工厂方法不是静态的,所以我们需要首先 new 一个工厂实例,再调用普通的实例方法。
//非静态工厂
public class DaoFactory {
public FactoryDao getFactoryDaoImpl(){
return new FactoryDaoImpl();
}
}
public class SpringAction {
//注入对象
private FactoryDao factoryDao;
public void setFactoryDao(FactoryDao factoryDao) {
this.factoryDao = factoryDao;
}
}
<bean name="springAction" class="SpringAction">
<!--使用非静态工厂的方法注入对象,对应下面的配置文件-->
<property name="factoryDao" ref="factoryDao"></property>
</bean>
<!--此处获取对象的方式是从工厂类中获取实例方法-->
<bean name="daoFactory" class="com.DaoFactory"></bean>
<bean name="factoryDao" factory-bean="daoFactory" factory-method="getFactoryDaoImpl"></bean>
什么是自动装配?
Spring IOC 容器知道所有 Bean 的配置信息,此外,通过 Java 反射机制还可以获知实现类的结构信息,如构造方法的结构、属性等信息。掌握所有 Bean 的这些信息后,Spring IOC 容器就可以按照某种规则对容器中的 Bean 进行自动装配,而无须通过显式的方式进行依赖配置。
Spring 提供的这种方式,可以按照某些规则进行 Bean 的自动装配,
元素提供了一个指定自动装配类型的属性:autowire="<自动装配类型>"
Spring 提供了哪几种自动装配类型?
Spring 提供了 4 种自动装配类型:
Spring 的 Bean 主要支持五种作用域:
以下三个作用域于只在 Web 应用中适用:
首先结论在这:Spring 中的单例 Bean不是线程安全的。
因为单例 Bean,是全局只有一个 Bean,所有线程共享。如果说单例 Bean,是一个无状态的,也就是线程中的操作不会对 Bean 中的成员变量执行查询以外的操作,那么这个单例 Bean 是线程安全的。比如 Spring mvc 的 Controller、Service、Dao 等,这些 Bean 大多是无状态的,只关注于方法本身。
假如这个 Bean 是有状态的,也就是会对 Bean 中的成员变量进行写操作,那么可能就存在线程安全的问题。
单例 Bean 线程安全问题怎么解决呢?
常见的有这么些解决办法:
将 Bean 定义为多例
这样每一个线程请求过来都会创建一个新的 Bean,但是这样容器就不好管理 Bean,不能这么办。
在 Bean 对象中尽量避免定义可变的成员变量
削足适履了属于是,也不能这么干。
将 Bean 中的成员变量保存在 ThreadLocal 中 ⭐
我们知道 ThredLoca 能保证多线程下变量的隔离,可以在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 里,这是推荐的一种方式。
什么是循环依赖?
Spring 循环依赖:简单说就是自己依赖自己,或者和别的 Bean 相互依赖。
只有单例的 Bean 才存在循环依赖的情况,原型(Prototype)情况下,Spring 会直接抛出异常。原因很简单,AB 循环依赖,A 实例化的时候,发现依赖 B,创建 B 实例,创建 B 的时候发现需要 A,创建 A1 实例……无限套娃,直接把系统干垮。
Spring 可以解决哪些情况的循环依赖?
Spring 不支持基于构造器注入的循环依赖,但是假如 AB 循环依赖,如果一个是构造器注入,一个是 setter 注入呢?
看看几种情形:
第四种可以而第五种不可以的原因是 Spring 在创建 Bean 时默认会根据自然排序进行创建,所以 A 会先于 B 进行创建。
所以简单总结,当循环依赖的实例都采用 setter 方法注入的时候,Spring 可以支持,都采用构造器注入的时候,不支持,构造器注入和 setter 注入同时存在的时候,看天。
PS:其实正确答案是开发人员做好设计,别让 Bean 循环依赖,但是没办法,面试官不想听这个。
我们都知道,单例 Bean 初始化完成,要经历三步:
注入就发生在第二步,属性赋值,结合这个过程,Spring 通过三级缓存解决了循环依赖:
Map
singletonObjects,单例池,用于保存实例化、属性赋值(注入)、初始化完成的 bean 实例Map
earlySingletonObjects,早期曝光对象,用于保存实例化完成的 bean 实例Map>
singletonFactories,早期曝光对象工厂,用于保存 bean 创建工厂,以便于后面扩展有机会创建代理对象。我们来看一下三级缓存解决循环依赖的过程:
A 实例的初始化过程:
所以,我们就知道为什么 Spring 能解决 setter 注入的循环依赖了,因为实例化和属性赋值是分开的,所以里面有操作的空间。如果都是构造器注入的化,那么都得在实例化这一步完成注入,所以自然是无法支持了。
不行,主要是为了⽣成代理对象。如果是没有代理的情况下,使用二级缓存解决循环依赖也是 OK 的。但是如果存在代理,三级没有问题,二级就不行了。
因为三级缓存中放的是⽣成具体对象的匿名内部类,获取 Object 的时候,它可以⽣成代理对象,也可以返回普通对象。使⽤三级缓存主要是为了保证不管什么时候使⽤的都是⼀个对象。
假设只有⼆级缓存的情况,往⼆级缓存中放的显示⼀个普通的 Bean 对象,Bean 初始化过程中,通过 BeanPostProcessor 去⽣成代理对象之后,覆盖掉⼆级缓存中的普通 Bean 对象,那么可能就导致取到的 Bean 对象不一致了。
实现@Autowired 的关键是:AutowiredAnnotationBeanPostProcessor
在 Bean 的初始化阶段,会通过 Bean 后置处理器来进行一些前置和后置的处理。
实现@Autowired 的功能,也是通过后置处理器来完成的。这个后置处理器就是 AutowiredAnnotationBeanPostProcessor。
/**
* 属性赋值
**/
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
//…………
if (hasInstAwareBpps) {
if (pvs == null) {
pvs = mbd.getPropertyValues();
}
PropertyValues pvsToUse;
for(Iterator var9 = this.getBeanPostProcessorCache().instantiationAware.iterator(); var9.hasNext(); pvs = pvsToUse) {
InstantiationAwareBeanPostProcessor bp = (InstantiationAwareBeanPostProcessor)var9.next();
pvsToUse = bp.postProcessProperties((PropertyValues)pvs, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
if (filteredPds == null) {
filteredPds = this.filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
//执行后处理器,填充属性,完成自动装配
//调用InstantiationAwareBeanPostProcessor的postProcessPropertyValues()方法
pvsToUse = bp.postProcessPropertyValues((PropertyValues)pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
return;
}
}
}
}
//…………
}
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
//@Autowired注解、@Inject和@Value注解的属性和方法
InjectionMetadata metadata = this.findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
//属性填充
metadata.inject(bean, beanName, pvs);
return pvs;
} catch (BeanCreationException var6) {
throw var6;
} catch (Throwable var7) {
throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", var7);
}
}
AOP:面向切面编程。简单说,就是把一些业务逻辑中的相同的代码抽取到一个独立的模块中,让业务逻辑更加清爽。
具体来说,假如我现在要 crud 写一堆业务,可是如何业务代码前后前后进行打印日志和参数的校验呢?
我们可以把日志记录
和数据校验
可重用的功能模块分离出来,然后在程序的执行的合适的地方动态地植入这些代码并执行。这样就简化了代码的书写。
业务逻辑代码中没有参和通用逻辑的代码,业务模块更简洁,只包含核心业务代码。实现了业务逻辑和通用逻辑的代码分离,便于维护和升级,降低了业务逻辑和通用逻辑的耦合性。
AOP 可以将遍布应用各处的功能分离出来形成可重用的组件。在编译期间、装载期间或运行期间实现在不修改源代码的情况下给程序动态添加功能。从而实现对业务逻辑的隔离,提高代码的模块化能力。
AOP 的核心其实就是动态代理,如果是实现了接口的话就会使用 JDK 动态代理,否则使用 CGLIB 代理,主要应用于处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等。
AOP 有哪些核心概念?
切面(Aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象
连接点(Joinpoint):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
切点(Pointcut):对连接点进行拦截的定位
通知(Advice):所谓通知指的就是指拦截到连接点之后要执行的代码,也可以称作增强
目标对象 (Target):代理的目标对象
织入(Weabing):织入是将增强添加到目标类的具体连接点上的过程。
编译期织入:切面在目标类编译时被织入
类加载期织入:切面在目标类加载到 JVM 时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。
运行期织入:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。SpringAOP 就是以这种方式织入切面。
Spring 采用运行期织入,而 AspectJ 采用编译期织入和类加载器织入。
引介(introduction):引介是一种特殊的增强,可以动态地为类添加一些属性和方法
AOP 有哪些环绕方式?
AOP 一般有 5 种环绕方式:
多个切面的情况下,可以通过 @Order 指定先后顺序,数字越小,优先级越高。
PS:这道题老三的同事面试候选人的时候问到了,候选人说了一堆 AOP 原理,同事就势来一句,你能现场写一下 AOP 的应用吗?结果——场面一度很尴尬。虽然我对面试写这种百度就能出来的东西持保留意见,但是还是加上了这一问,毕竟招人最后都是要撸代码的。
这里给出一个小例子,SpringBoot 项目中,利用 AOP 打印接口的入参和出参日志,以及执行时间,还是比较快捷的。
引入依赖:引入 AOP 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
自定义注解:自定义一个注解作为切点
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface WebLog {
}
配置 AOP 切面:
@Aspect
@Component
public class WebLogAspect {
private final static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
/**
* 以自定义 @WebLog 注解为切点
**/
@Pointcut("@annotation(cn.fighter3.spring.aop_demo.WebLog)")
public void webLog() {}
/**
* 在切点之前织入
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 打印请求相关参数
logger.info("========================================== Start ==========================================");
// 打印请求 url
logger.info("URL : {}", request.getRequestURL().toString());
// 打印 Http method
logger.info("HTTP Method : {}", request.getMethod());
// 打印调用 controller 的全路径以及执行方法
logger.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
// 打印请求的 IP
logger.info("IP : {}", request.getRemoteAddr());
// 打印请求入参
logger.info("Request Args : {}",new ObjectMapper().writeValueAsString(joinPoint.getArgs()));
}
/**
* 在切点之后织入
* @throws Throwable
*/
@After("webLog()")
public void doAfter() throws Throwable {
// 结束后打个分隔线,方便查看
logger.info("=========================================== End ===========================================");
}
/**
* 环绕
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//开始时间
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 打印出参
logger.info("Response Args : {}", new ObjectMapper().writeValueAsString(result));
// 执行耗时
logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
return result;
}
}
使用:只需要在接口上加上自定义注解
@GetMapping("/hello")
@WebLog(desc = "这是一个欢迎接口")
public String hello(String name){
return "Hello "+name;
}
Spring 的 AOP 是通过动态代理open in new window来实现的,动态代理主要有两种方式 JDK 动态代理和 Cglib 动态代理,这两种动态代理的使用和原理有些不同。
JDK 动态代理
CgLib 动态代理
我们来看一个常见的小场景,客服中转,解决用户问题:
JDK 动态代理实现:
接口
public interface ISolver {
void solve();
}
目标类:需要实现对应接口
public class Solver implements ISolver {
@Override
public void solve() {
System.out.println("疯狂掉头发解决问题……");
}
}
态代理工厂:ProxyFactory,直接用反射方式生成一个目标对象的代理对象,这里用了一个匿名内部类方式重写 InvocationHandler 方法,实现接口重写也差不多
public class ProxyFactory {
// 维护一个目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
// 为目标对象生成代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("请问有什么可以帮到您?");
// 调用目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("问题已经解决啦!");
return null;
}
});
}
}
客户端:Client,生成一个代理对象实例,通过代理对象调用目标对象方法
public class Client {
public static void main(String[] args) {
//目标对象:程序员
ISolver developer = new Solver();
//代理:客服小姐姐
ISolver csProxy = (ISolver) new ProxyFactory(developer).getProxyInstance();
//目标方法:解决问题
csProxy.solve();
}
}
Cglib 动态代理实现:
目标类:Solver,这里目标类不用再实现接口。
public class Solver {
public void solve() {
System.out.println("疯狂掉头发解决问题……");
}
}
动态代理工厂:
public class ProxyFactory implements MethodInterceptor {
//维护一个目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
//为目标对象生成代理对象
public Object getProxyInstance() {
//工具类
Enhancer en = new Enhancer();
//设置父类
en.setSuperclass(target.getClass());
//设置回调函数
en.setCallback(this);
//创建子类对象代理
return en.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("请问有什么可以帮到您?");
// 执行目标对象的方法
Object returnValue = method.invoke(target, args);
System.out.println("问题已经解决啦!");
return null;
}
}
客户端:Client
public class Client {
public static void main(String[] args) {
//目标对象:程序员
Solver developer = new Solver();
//代理:客服小姐姐
Solver csProxy = (Solver) new ProxyFactory(developer).getProxyInstance();
//目标方法:解决问题
csProxy.solve();
}
}
Spring AOP
Spring AOP 属于运行时增强
,主要具有如下特点:
AspectJ
AspectJ 是一个易用的功能强大的 AOP 框架,属于编译时增强
, 可以单独使用,也可以整合到其它框架中,是 AOP 编程的完全解决方案。AspectJ 需要用到单独的编译器 ajc。
AspectJ 属于静态织入,通过修改代码来实现,在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的,一般有如下几个织入的时机:
整体对比如下:
Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring 是无法提供事务功能的。Spring 只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过数据库自己的事务机制实现。
Spring 支持编程式事务
管理和声明式
事务管理两种方式:
编程式事务管理使用 TransactionTemplate,需要显式执行事务。
Spring 的接口 TransactionDefinition 中定义了表示隔离级别的常量,当然其实主要还是对应数据库的事务隔离级别:
Spring 事务的传播机制说的是,当多个事务同时存在的时候——一般指的是多个事务方法相互调用时,Spring 如何处理这些事务的行为。
事务传播机制是使用简单的 ThreadLocal 实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。
Spring 默认的事务传播行为是 PROPAFATION_REQUIRED,它适合绝大多数情况,如果多个 ServiceX#methodX()都工作在事务环境下(均被 Spring 事务增强),且程序中存在调用链 Service1#method1()->Service2#method2()->Service3#method3()
,那么这 3 个服务类的三个方法通过 Spring 的事务传播机制都工作在同一个事务中。
就是通过 AOP/动态代理。
MethodInterceptor
,事务增强对该接口的实现为TransactionInterceptor
,类图如下:事务拦截器TransactionInterceptor
在invoke
方法中,通过调用父类TransactionAspectSupport
的invokeWithinTransaction
方法进行事务处理,包括开启事务、事务提交、异常回滚。
1、@Transactional 应用在非 public 修饰的方法上
如果 Transactional 注解应用在非 public 修饰的方法上,Transactional 将会失效。
是因为在 Spring AOP 代理时,TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法 或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource 的 computeTransactionAttribute方法,获取 Transactional 注解的事务配置信息。
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
此方法会检查目标方法的修饰符是否为 public,不是 public 则不会获取@Transactional 的属性配置信息。
2、@Transactional 注解属性 propagation 设置错误
3、@Transactional 注解属性 rollbackFor 设置错误
rollbackFor 可以指定能够触发事务回滚的异常类型。Spring 默认抛出了未检查 unchecked 异常(继承自 RuntimeException 的异常)或者 Error 才回滚事务,其他异常不会触发回滚事务。
// 希望自定义的异常可以进行回滚
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。
4、同一个类中方法调用,导致@Transactional 失效
开发中避免不了会对同一个类里面的方法调用,比如有一个类 Test,它的一个方法 A,A 再调用本类的方法 B(不论方法 B 是用 public 还是 private 修饰),但方法 A 没有声明注解事务,而 B 方法有。则外部调用方法 A 之后,方法 B 的事务是不会起作用的。这也是经常犯错误的一个地方。
那为啥会出现这种情况?其实这还是由于使用 Spring AOP 代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由 Spring 生成的代理对象来管理。
//@Transactional
@GetMapping("/test")
private Integer A() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
/**
* B 插入字段为 3的数据
*/
this.insertB();
/**
* A 插入字段为 2的数据
*/
int insert = cityInfoDictMapper.insert(cityInfoDict);
return insert;
}
@Transactional()
public Integer insertB() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("3");
cityInfoDict.setParentCityId(3);
return cityInfoDictMapper.insert(cityInfoDict);
}
这种情况是最常见的一种@Transactional 注解失效场景
@Transactional
private Integer A() throws Exception {
int insert = 0;
try {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
cityInfoDict.setParentCityId(2);
/**
* A 插入字段为 2的数据
*/
insert = cityInfoDictMapper.insert(cityInfoDict);
/**
* B 插入字段为 3的数据
*/
b.insertB();
} catch (Exception e) {
e.printStackTrace();
}
}
如果 B 方法内部抛了异常,而 A 方法此时 try catch 了 B 方法的异常,那这个事务就不能正常回滚了,会抛出异常:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
Spring MVC 虽然整体流程复杂,但是实际开发中很简单,大部分的组件不需要开发人员创建和管理,只需要通过配置文件的方式完成配置即可,真正需要开发人员进行处理的只有 Handler(Controller) 、View 、Model。
当然我们现在大部分的开发都是前后端分离,Restful 风格接口,后端只需要返回 Json 数据就行了。
PS:这是一道全新的八股,毕竟 ModelAndView 这种方式应该没人用了吧?现在都是前后端分离接口,八股也该更新换代了。
我们都知道 Restful 接口,响应格式是 json,这就用到了一个常用注解:@ResponseBody
@GetMapping("/user")
@ResponseBody
public User user(){
return new User(1,"张三");
}
加入了这个注解后,整体的流程上和使用 ModelAndView 大体上相同,但是细节上有一些不同:
Spring MVC Restful请求响应示意图
客户端向服务端发送一次请求,这个请求会先到前端控制器 DispatcherServlet
DispatcherServlet 接收到请求后会调用 HandlerMapping 处理器映射器。由此得知,该请求该由哪个 Controller 来处理
DispatcherServlet 调用 HandlerAdapter 处理器适配器,告诉处理器适配器应该要去执行哪个 Controller
Controller 被封装成了 ServletInvocableHandlerMethod,HandlerAdapter 处理器适配器去执行 invokeAndHandle 方法,完成对 Controller 的请求处理
HandlerAdapter 执行完对 Controller 的请求,会调用 HandlerMethodReturnValueHandler 去处理返回值,主要的过程:
5.1. 调用 RequestResponseBodyMethodProcessor,创建 ServletServerHttpResponse(Spring 对原生 ServerHttpResponse 的封装)实例
5.2.使用 HttpMessageConverter 的 write 方法,将返回值写入 ServletServerHttpResponse 的 OutputStream 输出流中
5.3.在写入的过程中,会使用 JsonGenerator(默认使用 Jackson 框架)对返回值进行 Json 序列化
执行完请求后,返回的 ModealAndView 为 null,ServletServerHttpResponse 里也已经写入了响应,所以不用关心 View 的处理
Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。
Spring Boot 以约定大于配置
核心思想开展工作,相比 Spring 具有如下优势:
SpringBoot 开启自动配置的注解是@EnableAutoConfiguration
,启动类上的注解@SpringBootApplication
是一个复合注解,包含了@EnableAutoConfiguration:
EnableAutoConfiguration
只是一个简单的注解,自动装配核心功能的实现实际是通过 AutoConfigurationImportSelector
类
@AutoConfigurationPackage //将main同级的包下的所有组件注册到容器中
@Import({AutoConfigurationImportSelector.class}) //加载自动装配类 xxxAutoconfiguration
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
AutoConfigurationImportSelector
实现了ImportSelector
接口,这个接口的作用就是收集需要导入的配置类,配合@Import()
就可以将相应的类导入到 Spring 容器中
获取注入类的方法是 selectImports(),它实际调用的是getAutoConfigurationEntry
,这个方法是获取自动装配类的关键,主要流程可以分为这么几步:
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
//1.获取到注解的属性
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
//2.获取需要自动装配的所有配置类,读取META-INF/spring.factories,获取自动配置类路径
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//3.1.移除重复的配置
configurations = this.removeDuplicates(configurations);
//3.2.处理需要排除的配置
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
知道了自动配置原理,创建一个自定义 SpringBoot Starter 也很简单。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
编写配置文件
这里定义了属性配置的前缀
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {
private String name;
//省略getter、setter
}
自动装配
创建自动配置类 HelloPropertiesConfigure
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloPropertiesConfigure {
}
配置自动类
在/resources/META-INF/spring.factories
文件中添加自动配置类路径
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.fighter3.demo.starter.configure.HelloPropertiesConfigure
测试
创建一个工程,引入自定义 starter 依赖
<dependency>
<groupId>cn.fighter3</groupId>
<artifactId>demo-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
在配置文件里添加配置
hello.name=张三
测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloTest {
@Autowired
HelloProperties helloProperties;
@Test
public void hello(){
System.out.println("你好,"+helloProperties.getName());
}
}
至此,随手写的一个自定义 SpringBoot-Starter 就完成了,虽然比较简单,但是完成了主要的自动装配的能力。
SpringApplication 这个类主要做了以下四件事情:
SpringBoot 启动大致流程如下 :
SpringCloud 是 Spring 官方推出的微服务治理框架。
Spring Cloud Netfilx核心组件-来源参考[2]
什么是微服务?
微服务架构主要要解决哪些问题?
有哪些主流微服务框架?
SpringCloud 有哪些核心组件?