Spring

190 阅读13分钟

Spring简介

Spring是一个轻量级,非入侵式的控制反转IOC和面向切面AOP框架。Spring包含7大模块,分别是:

  • Spring Core:Spring核心,提供IOC和DI功能
  • Spring Context:上下文容器,是BeanFactory功能加强的一个子接口
  • Spring Web:提供对Web应用的支持
  • Spring MVC:提供对MVC架构的支持
  • Spring DAO:提供对JDBC的支持
  • Spring ORM:用于整合ORM框架
  • Spring AOP:提供面向切面功能

Spring AOP

AOP即面向切面编程,在核心业务代码中以切面的形式加上非核心的或公共的业务逻辑(如日志,权限等),相当于对原代码进行统一的管理或装饰,这样既不会影响原业务代码,又可以加入需要的功能。AOP应用场景如日志,权限,事务。

AOP核心概念

  • 横切关注点:抽取出来的同一类非核心业务逻辑,如日志
  • 切面:对横切关注点进行封装的类,如封装了日志切面
  • 通知/增强:切面要执行的增强代码(方法)
    • 通知方式有五种:前置,后置,环绕(前置+后置),异常,最终(返回后执行)。
    • 如日志使用环绕通知计算方法执行时长
  • 连接点:通知应用的时机,例如方法被调用就是日志切面的连接点
  • 切点:通知应用的范围,例如日志的应用范围可能是所有controller接口
  • 织入:将增强添加到切点上的过程,分为编译期织入和运行期织入
    • Spring AOP属于运行时增强,基于动态代理实现,性能较差
    • AspectJ AOP属于编译时增强,基于修改代码实现,性能较好

AOP底层原理

AOP底层原理是动态代理。代理即创建一个代理类去控制调用目标类(被代理类)中的方法,并可加入增强逻辑。

  • 静态代理是有一个目标类就要手动创建一个代理类
  • 动态代理是程序执行过程中利用反射机制(或cglib)动态创建代理类并指定目标类。
  • 动态代理又分为JDK动态代理和CGLIB动态代理。
    • 目标类实现了接口时使用JDK动态代理
      • 创建实现接口的目标类
      • 创建实现InvocationHandler接口的代理类,在invoke方法中定义代理逻辑
      • 通过Proxy类的newProxyInstance方法创建代理对象(传入目标类的类加载器,接口和代理类对象)
    • 目标类没有实现接口时使用CGLIB动态代理(CGLIB通过字节码生成类)
      • 先创建目标类
      • 创建继承MethodInterceptor类的类,在intercept方法中定义代理逻辑
      • 使用Enhancer类创建目标类的子类作为代理对象(故目标类不能为final)

Spring IOC

IOC控制反转,即把对象创建销毁和调用过程都交给Spring进行管理,避免让我们自己管理对象,具体来说是Spring的容器负责管理Bean的生命周期和依赖关系

Spring容器

Spring容器主要有两个类

  • BeanFactory:Spring的核心类,提供基本的Bean创建和依赖注入功能。BeanFactory是懒加载的,在启动应用时不创建对象而是使用时才创建,启动速度快,占用空间少,但是运行速度慢。
  • ApplicationContext:BeanFactory的子接口,做了功能扩展(AOP,事务等),Spring默认使用。ApplicationContext在启动时创建所有单例bean,故启动慢,占用空间多,但是运行速度快。SpringBoot启动时加载的IOC容器就是ApplicationContext。

Spring容器工作流程

  • 容器启动加载配置信息,扫描指定的包找到需要创建的类,根据类的配置信息利用反射机制生成Bean,把实例化的Bean缓存在容器中并注入到需要的类。

依赖管理

当某个组件需要依赖其他组件时,就需要为其注入对应的组件,即依赖注入。依赖注入的方式有

  • 构造函数注入:在对象创建时注入依赖(完整性)
  • setter方法注入:允许对象创建后修改依赖(灵活性)
  • 接口注入(不使用)

Spring提供自动装配机制,即无需显式定义依赖关系就能进行依赖注入。自动装配方式有

  • 根据名称:@Qualifier
  • 根据类型:@Autowired(原理是Spring创建bean时会调用后置处理器判断是否需要属性填充,如果需要就会解析@Autowired注解,找到对应的bean并装配到当前bean中)
  • 先根据类型,再根据名称:@Resource

Bean管理

Bean实例化方式

  • 无参构造函数,Spring默认使用
  • 静态工厂方法,在Bean定义中指定工厂方法名称
<bean id="myBean" class="com.example.MyClass" factory-method="createInstance"/>
  • 实例工厂方法,在实例化对象上调用非静态方法来创建Bean
<bean id="myFactory" class="com.example.FactoryClass"/>
<bean id="myBean" factory-bean="myFactory" factory-method="createInstance"/>
  • 在@Configuration类中使用@Bean注解的方法自定义Bean的创建逻辑
@Bean
public MyBean myBean(DependencyBean dependency) {
    MyBean myBean = new MyBean();
    myBean.setDependency(dependency);
    return myBean;
}

Bean的作用域

  • singleton:容器中只存在唯一bean实例,默认使用
  • prototype:每次从容器调用bean时都创建一个新实例
  • request:每次http请求创建一个新的bean实例
  • session:同一个Http Session共享一个Bean实例
  • globalSession:同一个全局Session共享一个Bean实例(跨Portlet作用域)
  • 注意后三个仅在Web应用中使用

Bean生命周期

  • 实例化:Spring通过构造器创建bean实例,为其分配空间
  • 设置属性:Spring容器为bean注入属性(通过反射机制)
  • 初始化:
    • 检查Aware相关接口
    • 调用前置处理器
    • 调用bean初始化方法
    • 调用后置处理器
  • 使用中:此时就可以使用bean了
  • 销毁:容器关闭时调用自定义或默认的销毁方法

Bean循环依赖问题

  • 指Spring创建Bean实例时两个Bean互相依赖导致两个Bean都无法完成创建。
  • 使用构造函数注入的循环依赖问题无法处理,抛出异常,因为Bean的实例化与依赖注入需要同时完成,而使用setter函数注入时Bean的实例化和依赖注入是分开的,使用三级缓存处理。
    • 三级缓存
      • 第一级缓存:存放完全初始化好的bean
      • 第二级缓存:存放实例化完成但未初始化的bean
      • 第三级缓存:存放bean工厂
    • 若对象AB相互依赖。
      • Bean A先开始创建,将其bean工厂放到第三级缓存,然后尝试注入Bean B
      • Bean B开始创建,同样放入第三级缓存,尝试注入Bean A,从三级缓存中获取到Bean A
      • Bean B属性注入完毕放到第一级缓存,然后Spring容器再调用Bean A初始化方法完成创建

Bean的并发安全问题

  • Bean默认作用域是单例,即只存在一个Bean实例并被多个线程共享。如果单例Bean是无状态的(没有成员变量),那么这个Bean是线程安全的,例如Controller,Service等
  • 如果Bean的内部状态是可变的(成员变量可以被修改),就可能出现并发安全问题。解决方案包括
    1. 线程使用自己的局部变量,而不是使用Bean中的共享变量(即尽量使用无状态的Bean)
    2. 如果Bean确实需要保存可变状态,通过Synchronized关键字或Lock接口或ThreadLocal保证线程安全
    3. 将Bean设置为原型作用域(prototype),每个请求都会创建新实例,不存在线程安全问题

Spring拦截器与JavaWeb三大组件

  • Spring拦截器必须实现HandlerInterceptor接口,这个接口定义了三种方法,重写相关方法并在配置类中通过实现WebMvcConfigurer接口的addInterceptors方法来注册。
    • preHandle(请求处理之前被调用,可用于权限检查)
    • postHandle(请求处理之后,视图渲染之前被调用,可修改Controller返回的ModelAndView对象)
    • afterCompletion(请求完全处理完毕后调用,可用于资源清理工作)
  • JavaWeb三大组件
    • Servlet:Servlet用于处理客户端请求并生成响应,是JavaWeb应用程序的基础,可以扩展HttpServlet来处理Http请求。
    • Filter:Filter用于在请求到达Servlet之前或响应发送给客户端之后对请求和响应进行预处理和后处理。故在Filter层可以获取用户身份,做参数校验,鉴权,链路追踪,限流等工作。
    • Listener:Listener组件用于监听和响应Web应用程序中的特定事件。
    • 三种组件的使用方式都是继承对应的接口,重写对应的方法并向SpringBoot注册
  • Spring拦截器Interceptor与JavaWeb过滤器Filter有什么区别?
    • 触发时机不一致,顺序依次是请求进入tomcat容器—>filter—>Servlet—>interceptor—>controller
    • 过滤器是JavaWeb的组件,需要在servlet容器中实现,拦截器是Spring的组件,依赖于spring,可以使用spring容器中各种bean

Spring事务管理

Spring事务本质是数据库对事务的支持,Spring只提供统一事务管理接口,具体实现依赖具体的数据库。

编程式事务

使用TransactionTemplate模板通过编程的方式实现事务。虽然编程麻烦一些,但是可以精确控制事务范围,即想在方法中哪里开启、提交、回滚事务都可以直接操作。

声明式事务

声明式事务基于AOP。只需要在对应方法或类上加@Transactional注解即可,但是只能在方法级别上使用,粒度较大。本质是目标方法开始前启动事务,目标方法执行结束后根据执行情况提交或回滚事务(方法抛出异常就会回滚)

  • 使用isolation属性声明事务的隔离级别(读未提交,读已提交,可重复读,串行化,数据库默认)
  • 使用propagation属性声明事务的传播行为(方法被另一个方法调用时事务的覆盖范围,同一个线程内)
    • required:加入当前事务,若没有事务就新建一个。默认
    • supports:加入当前事务,没有事务就以非事务方式执行
    • mandatory:加入当前事务,没有事务抛出异常
    • requires_new:新建事务,若当前有事务将其挂起
    • not_supported:以非事务方式执行,若有事务将其挂起
    • never:以非事务方式执行,若有事务抛出异常
    • nested:若有事务则加入并嵌套事务处理,若没有则新建
  • 注意事务传播机制是使用ThreadLocal实现的,所以若在新线程中调用方法则事务传播失效,此时可能需要其他机制,例如分布式事务或消息队列。
  • @Transactional失效的情况
    • 类内部访问(事务是基于AOP的,AOP基于代理对象,只有方法被当前类以外的类调用时才会生成代理对象。解决方法是改为动态代理对象调用)
    • @Transactional应用于私有方法上(外部无法访问,无法生成代理对象)
    • @Transactional的传播行为设置错误(例如使用了noSupport导致没有使用事务)
    • @Transactional的rollbackFor设置错误(即指定了只有某些异常才触发事务回滚,Spring是默认error或运行时异常都回滚)
    • 异常被catch了导致没有抛出
    • 多线程,若在主线程(方法)中开启事务,但是其中开启了子线程,子线程抛出异常主线程无法捕获,故事务不生效。同样的,若子线程正常执行完毕,但是主线程抛出异常,子线程也无法回滚
    • 没有被Spring管理的类也不会生成代理对象,事务也会失效
  • 若A方法加事务注解,B方法不加事务注解,A调用B,AB是在同一个事务中,因为实际执行的是代理类中的方法,其中包含了AB两个方法。

Spring缓存机制

Spring缓存机制允许程序员通过简单的注解来实现对方法或类级别的缓存,还能以统一的方式使用不同的缓存实现,如Guava,Caffeine,Redis。主要注解包括:

  • @Cacheable:用于标注需要缓存的方法,当方法被调用时Spring先检查缓存中是否存在对应的数据,若存在则直接返回,若不存在则将执行结果存入缓存,还可以通过condition参数设置条件缓存。
  • @CachePut:用于标注需要更新缓存的方法,方法调用时即使缓存存在也会执行该方法并更新缓存。
  • @CacheEvict:用于标注需要清除缓存的方法。
  • @Caching:复合注解,包括上面三个注解的功能。

更换缓存源

  • 引入对应缓存源的依赖,在配置文件设置spring-cache-type以及对应缓存源的配置文件位置,在配置类中创建并配置CacheManager类,缓存注解无需修改。

实现多级缓存

  • 可以创建复合的CacheManager类,并在其中实现多级缓存获取的方法,先去本地缓存中拿数据,若失败再去分布式缓存中拿数据,最后将分布式缓存中的数据同步到本地缓存中。

Spring异步机制

Spring的异步机制允许程序员通过注解开启方法的异步执行,主要注解包括:

  • @Async:标注开启方法的异步执行,需要指定一个Executor作为异步任务的执行器,Spring会将方法交给线程池中的线程处理,执行完成后,方法的返回值通过Future或CompletableFuture进行封装返回。

异步机制的实现原理

  • Bean的初始化后置处理器AsyncAnnotationBeanPostProcessor,负责解析带有@Async注解的方法,将其封装为一个代理对象,并注册为一个可执行的异步任务。当这个任务被调用时,代理对象将方法转移到线程池中处理
  • 任务执行器AsyncTaskExecutor:可以通过配置来指定具体的线程池或任务调度器,还可以调整线程池大小,类型等。

异步方法与事务

  • 默认情况下,在标注了@Async的方法上标注@Transactional注解,事务将会失效,因为方法并不在当前线程中运行。
  • 如果希望异步方法参与事务,可以将事务的传播行为设置为Requires_New,这样异步方法将在一个新的事务中执行,与原方法的事务隔离,两者的提交回滚不会相互影响。(如果希望两个不同线程的任务参与同一个任务,需要考虑分布式事务)

异步方法的异常处理

  • 异步方法的异常会被捕获并封装成Futrue对象(或CompletableFuture对象),通过get方法获取异步任务的结果,如果有异常此时get方法会抛出异常,便于后续处理。
  • 若是异步方法中未捕获的异常还可以通过实现AsyncUncaughtExceptionHandler接口并重写handler逻辑定义处理逻辑。

ThreadLocal与@Async

  • 开启异步方法后切换到了其他线程,导致ThreadLocal中的内容无法被正确传递,解决方案:
    • 使用InheritableThreadLocal,原理是通过在子线程构造方法复制父线程的值实现值传递,但是在使用线程池时会错乱,因为线程池中的线程是复用的。
    • 使用TransmittableThreadLocal(TTL),TTL会在Runnable方法中保存子线程原来的ThreadLocal值并复制父线程的ThreadLocal值给子线程。