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)
- 目标类实现了接口时使用JDK动态代理
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的内部状态是可变的(成员变量可以被修改),就可能出现并发安全问题。解决方案包括
- 线程使用自己的局部变量,而不是使用Bean中的共享变量(即尽量使用无状态的Bean)
- 如果Bean确实需要保存可变状态,通过Synchronized关键字或Lock接口或ThreadLocal保证线程安全
- 将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值给子线程。