有人说,“Java程序员都是Spring程序员”,本人不太赞成这个观点,但是这也可以看出Spring在Java世界里举足轻重的作用。
Spring Logo
一句话概括:Spring 是一个轻量级、非入侵式的控制反转 (IoC) 和面向切面 (AOP) 的框架。
2003年,一个音乐家Rod Johnson决定发展一个轻量级的Java开发框架,Spring
作为Java战场的龙骑兵渐渐崛起,并淘汰了EJB
这个传统的重装骑兵。
Spring重要版本
到了现在,企业级开发的标配基本就是 Spring5 + Spring Boot 2 + JDK 8
Spring有哪些特性呢?
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模块划分
最主要的七大模块:
Spring有很多模块,甚至广义的SpringBoot、SpringCloud也算是Spring的一部分,我们来分模块,按功能来看一下一些常用的注解:
Spring常用注解
Web:
容器:
AOP:
@After
:在方法执行之后执行(方法上)。
@Before
:在方法执行之前执行(方法上)。
@Around
:在方法执行之前与之后执行(方法上)。
@PointCut
:声明切点 在java配置类中使用@EnableAspectJAutoProxy注解开启Spring对AspectJ代理的支持(类上)。
事务:
Spring 框架中广泛使用了不同类型的设计模式,下面我们来看看到底有哪些设计模式?
Spring中用到的设计模式
Java 是面向对象的编程语言,一个个实例对象相互合作组成了业务逻辑,原来,我们都是在代码里创建对象和对象的依赖。
所谓的IOC(控制反转):就是由容器来负责控制对象的生命周期和对象间的关系。以前是我们想要什么,就自己创建什么,现在是我们需要什么,容器就给我们送来什么。
引入IOC之前和引入IOC之后
也就是说,控制对象生命周期的不再是引用它的对象,而是容器。对具体对象,以前是它控制其它对象,现在所有对象都被容器控制,所以这就叫控制反转。
控制反转示意图
DI(依赖注入):指的是容器在实例化对象的时候把它依赖的类注入给它。有的说法IOC和DI是一回事,有的说法是IOC是思想,DI是IOC的实现。
为什么要使用IOC呢?
最主要的是两个字解耦,硬编码会造成对象间的过度耦合,使用IOC之后,我们可以不用关心对象间的依赖,专心开发应用就行。
PS:这道题老三在面试中被问到过,问法是“你有自己实现过简单的Spring吗?”
Spring的IOC本质就是一个大工厂,我们想想一个工厂是怎么运行的呢?
工厂运行
我们简单地实现一个mini版的Spring IOC:
mini版本Spring IOC
Bean定义:
Bean通过一个配置文件定义,把它解析成一个类型。
beans.properties
偷懒,这里直接用了最方便解析的properties,这里直接用一个<key,value>类型的配置来代表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
BeanFactory
对象工厂,我们最核心的一个类,在它初始化的时候,创建了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 接口
BeanFactory是类的通用工厂,可以创建并管理各种类的对象。
Spring为BeanFactory提供了很多种实现,最常用的是XmlBeanFactory,但在Spring 3.2中已被废弃,建议使用XmlBeanDefinitionReader、DefaultListableBeanFactory。
Spring5 BeanFactory继承体系
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 接口
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定义中。
容器启动和Bean实例化阶段
容器启动开始,首先会通过某种途径加载Congiguration MetaData,在大部分情况下,容器需要依赖某些工具类(BeanDefinitionReader)对加载的Congiguration MetaData进行解析和分析,并将分析后的信息组为相应的BeanDefinition。
xml配置信息映射注册过程
最后把这些保存了Bean定义必要信息的BeanDefinition,注册到相应的BeanDefinitionRegistry,这样容器启动就完成了。
在Spring中,基本容器BeanFactory和扩展容器ApplicationContext的实例化时机不太一样,BeanFactory采用的是延迟初始化的方式,也就是只有在第一次getBean()的时候,才会实例化Bean;ApplicationContext启动之后会实例化所有的Bean定义。
Spring IOC 中Bean的生命周期大致分为四个阶段:实例化(Instantiation)、属性赋值(Populate)、初始化(Initialization)、销毁(Destruction)。
Bean生命周期四个阶段
我们再来看一个稍微详细一些的过程:
简单总结一下,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()
。
Bean生命周期源码追踪
有三种方式:直接编码方式、配置文件方式、注解方式。
Bean依赖配置方式
Spring支持构造方法注入、属性注入、工厂方法注入,其中工厂方法注入,又可以分为静态工厂方法注入和非静态工厂方法注入。
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四种自动装配类型
Spring的Bean主要支持五种作用域:
Spring Bean支持作用域
以下三个作用域于只在Web应用中适用:
首先结论在这:Spring中的单例Bean不是线程安全的。
因为单例Bean,是全局只有一个Bean,所有线程共享。如果说单例Bean,是一个无状态的,也就是线程中的操作不会对Bean中的成员变量执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。
假如这个Bean是有状态的,也就是会对Bean中的成员变量进行写操作,那么可能就存在线程安全的问题。
Spring单例Bean线程安全问题
单例Bean线程安全问题怎么解决呢?
常见的有这么些解决办法:
什么是循环依赖?
Spring循环依赖
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初始化完成,要经历三步:
Bean初始化步骤
注入就发生在第二步,属性赋值,结合这个过程,Spring 通过三级缓存解决了循环依赖:
三级缓存
我们来看一下三级缓存解决循环依赖的过程:
当 A、B 两个类发生循环依赖时:
A实例的初始化过程:
1
2
5
所以,我们就知道为什么Spring能解决setter注入的循环依赖了,因为实例化和属性赋值是分开的,所以里面有操作的空间。如果都是构造器注入的化,那么都得在实例化这一步完成注入,所以自然是无法支持了。
不行,主要是为了⽣成代理对象。如果是没有代理的情况下,使用二级缓存解决循环依赖也是OK的。但是如果存在代理,三级没有问题,二级就不行了。
因为三级缓存中放的是⽣成具体对象的匿名内部类,获取Object的时候,它可以⽣成代理对象,也可以返回普通对象。使⽤三级缓存主要是为了保证不管什么时候使⽤的都是⼀个对象。
假设只有⼆级缓存的情况,往⼆级缓存中放的显示⼀个普通的Bean对象,Bean初始化过程中,通过 BeanPostProcessor 去⽣成代理对象之后,覆盖掉⼆级缓存中的普通Bean对象,那么可能就导致取到的Bean对象不一致了。
二级缓存不行的原因
实现@Autowired的关键是:AutowiredAnnotationBeanPostProcessor
在Bean的初始化阶段,会通过Bean后置处理器来进行一些前置和后置的处理。
实现@Autowired的功能,也是通过后置处理器来完成的。这个后置处理器就是AutowiredAnnotationBeanPostProcessor。
Spring在创建bean的过程中,最终会调用到doCreateBean()方法,在doCreateBean()方法中会调用populateBean()方法,来为bean进行属性填充,完成自动装配等工作。
在populateBean()方法中一共调用了两次后置处理器,第一次是为了判断是否需要属性填充,如果不需要进行属性填充,那么就会直接进行return,如果需要进行属性填充,那么方法就会继续向下执行,后面会进行第二次后置处理器的调用,这个时候,就会调用到AutowiredAnnotationBeanPostProcessor的postProcessPropertyValues()方法,在该方法中就会进行@Autowired注解的解析,然后实现自动装配。
/**
* 属性赋值
**/
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;
}
}
}
}
//…………
}
postProcessorPropertyValues()方法的源码如下,在该方法中,会先调用findAutowiringMetadata()方法解析出bean中带有@Autowired注解、@Inject和@Value注解的属性和方法。然后调用metadata.inject()方法,进行属性填充。
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 可以将遍布应用各处的功能分离出来形成可重用的组件。在编译期间、装载期间或运行期间实现在不修改源代码的情况下给程序动态添加功能。从而实现对业务逻辑的隔离,提高代码的模块化能力。
Java语言执行过程
AOP 的核心其实就是动态代理,如果是实现了接口的话就会使用 JDK 动态代理,否则使用 CGLIB 代理,主要应用于处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等。
AOP有哪些核心概念?
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是通过动态代理来实现的,动态代理主要有两种方式JDK动态代理和Cglib动态代理,这两种动态代理的使用和原理有些不同。
JDK 动态代理
CgLib 动态代理
我们来看一个常见的小场景,客服中转,解决用户问题:
用户向客服提问题
JDK动态代理实现:
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动态代理实现:
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 AOP和AspectJ对比
Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring 是无法提供事务功能的。Spring 只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过数据库自己的事务机制实现。
Spring 支持编程式事务
管理和声明式
事务管理两种方式:
Spring事务分类
编程式事务管理使用 TransactionTemplate,需要显式执行事务。
Spring的接口TransactionDefinition中定义了表示隔离级别的常量,当然其实主要还是对应数据库的事务隔离级别:
Spring 事务的传播机制说的是,当多个事务同时存在的时候——一般指的是多个事务方法相互调用时,Spring 如何处理这些事务的行为。
事务传播机制是使用简单的 ThreadLocal 实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。
7种事务传播机制
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才回滚事务,其他异常不会触发回滚事务。
Spring默认支持的异常回滚
// 希望自定义的异常可以进行回滚
@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的工作流程
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请求响应示意图
Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。
SpringBoot图标
Spring Boot 以约定大于配置
核心思想开展工作,相比Spring具有如下优势:
SpringBoot开启自动配置的注解是@EnableAutoConfiguration
,启动类上的注解@SpringBootApplication
是一个复合注解,包含了@EnableAutoConfiguration:
SpringBoot自动配置原理
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
测试
至此,随手写的一个自定义SpringBoot-Starter就完成了,虽然比较简单,但是完成了主要的自动装配的能力。
创建一个工程,引入自定义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());
}
}
运行结果
运行结果
SpringApplication 这个类主要做了以下四件事情:
SpringBoot 启动大致流程如下 :
SpringBoot 启动大致流程-图片来源网络
SpringCloud是Spring官方推出的微服务治理框架。
Spring Cloud Netfilx核心组件-来源参考[2]
什么是微服务?
微服务架构主要要解决哪些问题?
有哪些主流微服务框架?
SpringCloud有哪些核心组件?
SpringCloud
PS:微服务后面有机会再扩展,其实面试一般都是结合项目去问。