一、Bean
Spring 3 中为 Bean 定义了 5 种作用域,分别为
singleton(单例):全局唯一。线程不安全,默认类型。bean是无状态时,线程安全。
如何避免线程不安全?
Bean中避免定义可变的成员变量;
类中定义ThreadLocal,将可变的成员变量放到threadLocal中。)
prototype(原型):每次使用创建。通过Spring容器获取bean时,新建一个。线程安全;适合有状态的bean使用.
有状态会话bean/有状态:一个对像处于不同的状态,调用相同的方法时产生不同的行为
e.g. 每个用户有自己特有的一个实例,在用户的生存期内,bean保持了用户的信息;即有数据存储功能。
数据库链接
request:一次http请求创建一个,请求结束实例销毁。
session :一次http session中,容器返回bean的同一个实例。仅在当前session内有效。,请求结束实例销毁。
session vs request
session可包含多个request请求
http无状态协议,为实现会话跟踪,服务端使用session保存上下文,对每个用户创建一个sessionId,区分不同用户,并把ID传回给cookie。当下次访问时带着sessionID,服务端校验是哪一个用户。Cookie可以保存到硬盘上,即使关闭浏览器也不影响,除非删除,当sessionID过期时服务端删除session,下次重新创建。
global session:一个全局的http session中,容器会返回统一实例。
仅使用 portlet context 时有效。portlet:java web组件
二、IOC
2.1 IOC:控制反转(Inversion of Control)
指的是对象的创建和生命周期的管理,全部托管给Spring容器,而传统对象的创建都是通过业务方使用关键字new或反射来创建的;控制反转是把控制权从业务方交给了Spring容器,这样做的最大好处就是实现解耦和面向接口编程。
Spring提供IOC两种实现方式:BeanFactory和ApplicationContext接口。
BeanFactory:IOC容器基本实现,是Spring里面一个内部使用的接口,不提供给开发人员使用。(加载配置文件的时候不会去创建对象,在获取对象(使用)的时候才会创建对象)。
ApplicationContext:BeanFactory接口的子接口,提供更多更强大的功能,一般是由开发人员使用的。(在加载配置文件时就会创建对象)
2.2 DI:依赖注入(dependcy Injection)
指的是获得依赖对象的过程由自身管理变为由IOC容器主动注入,就是由IOC容器在运行期间,动态的将某种依赖关系注入到对象中。
bean对象需要依赖其他组件或对象,依赖关系由容器运行时确定。 两种方式注入:基于配置文件, 基于注解
Spring如何解决循环依赖?
使用三级缓存,通过构造方法创建半成品解决
- 一级 singletonObjects,即存放最终bean
- 二级 earlySingeltonObjects
- 三级 singletonFactory
如果构造方法中依赖实例对象,则无法解决;
不支持prototype(原型)bean注入循环依赖,因为Spring容器只有在需要时才会初始化prototype
参考:一文告诉你Spring是如何利用"三级缓存"巧妙解决Bean的循环依赖问题的【享学Spring】-腾讯云开发者社区-腾讯云
Autowired注解与Resource注解的区别
这两个注解的作用都一样,都是在做bean的注入,在使用过程中,两个注解有时候可以替换使用。
相同点:
- 1.@Resource注解和@Autowired注解都可以用作bean的注入;
- 2.在接口只有一个实现类的时候,两个注解可以互相替换,效果相同
不同点:
- @Resource注解是Java自身的注解;@Autowired注解是Spring的注解;
- @Resource注解有两个重要的属性,分别是name和type,如果name属性有值,则使用byName的自动注入策略,将值作为需要注入bean的名字;如果type有值,则使用byType自动注入策略,将值作为需要注入bean的类型。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略,即@Resource注解默认按照名称进行匹配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名,按照名称查找,当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
- @Autowired注解是spring的注解,此注解只根据type进行注入,不会去匹配name。但是如果只根据type无法辨别注入对象时,就需要配合使用@Qualifier注解或者@Primary注解使用。
- 举个栗子:若有一个UserService接口,同时创建了两个此接口的实现类userServiceImpl01和userServiceImpl02,然后用UserController类来测试两个注解的不同用法。那么此时,需要@Autowired注解与@Qualifier注解一起使用,以@Qualifier(“userServiceImpl01”)指定注入的bean的名称;而Resource有name属性(@Resource(name = “userServiceImpl01”)),可以区分要注入哪一个实现类。
三、AOP
AOP 把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务。AOP 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
织入(weave):将切面应用到目标对象并导致代理对象创建的过程
引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
3.1 Spring AOP vs AspectJ AOP
AspectJ 是一个基于 Java 语言的 AOP 框架,提供了强大的 AOP 功能,AspectJ 中的很多语法结构基本上已成为 AOP 领域的标准。
Spring AOP 是Spring实现的AOP框架,采用动态代理的模式进行切面的织入,目前Spring 同时支持原生的Spring AOP与AspectJ的AOP
Spring AOP | AspectJ AOP | |
简介 | 通过Spring IoC提供简单的AOP实现 | 最原始,最完善,更健壮,更复杂 |
织入方式 | 基于动态代理的AOP。要实现目标对象的切面,就要创建目标对象的代理类。 2种实现方式: JDK动态代理——实现相应的接口,底层基于反射 CGLib代理 ——未实现接口的使用该方式 运行时织入 | 三种类型织入: 1.编译期:切面和目标类放一起用ajc编译 2.编译后:目标类/切面已经打成jar包,此时仍可用ajc命令对jar包再一次编织 3.类加载时:jvm加载目标类时,做字节码替换 |
使用方法 | 无须额外配置,Spring自带功能 | 需要使用AJC编译器进行编译 |
性能 | AspectJ一般采用编译期织入,而Spring AOP一般采用类加载期使用动态代理织入,由于切面代码执行的逻辑是一样的,因此少量切面的时间开销会很接近,但考虑到动态代理产生的开销,一般认为AspectJ的性能要优于Spring AOP。 有人做过相关的性能分析,大家可以参考一下AOP 底层技术性能测试与比较 |
注意:
1). AspectJ compiler(AJC) 编译器不支持lombok 注解
2). Spring AOP采用的是动态代理模式,因此仅支持Public方法的切面,私有方法的aop请使用AspectJ
3.2 AOP两种代理方式
Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。
3.2.1 JDK动态接口代理
JDK 动态代理主要涉及到 java.lang.reflect 包中的两个类:Proxy 和 InvocationHandler。InvocationHandler是一个接口,通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编制在一起。Proxy 利用 InvocationHandler 动态创建一个符合某一接口的实例,生成目标类的代理对象.
3.2.2 CGLib 动态代理
CGLib 全称为 Code Generation Library,是一个强大的高性能,高质量的代码生成类库,可以在运行期扩展 Java 类与实现 Java 接口,CGLib 封装了 asm,可以再运行期动态生成新的 class。和 JDK 动态代理相比较:JDK 创建代理有一个限制,就是只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,则可以通过 CGLib 创建动态代理。
3.3 多数据源
Spring 提供了两种事务管理方式,编程式事务管理和声明式事务管理,声明式事务管理是通过 AOP 实现的,代码侵入性最小,使用 @Transactional 声明。
四、Spring事务
4.1 传播机制(7种)
事务传播行为是为了解决业务层方法之间互相调用的事务问题。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
Spring 定义了以下几种事务传播行为:
嵌套事务:外部事务的子事务,外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务。
4.2 事务隔离级别(4种+1默认)
TransactionDefinition.ISOLATION_DEFAULT:
使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别。
TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 读为提交
最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
TransactionDefinition.ISOLATION_READ_COMMITTED: 读已提交
允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
TransactionDefinition.ISOLATION_REPEATABLE_READ: 可重复读
对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
TransactionDefinition.ISOLATION_SERIALIZABLE: 序列化
最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
4.3 @Transactional事务不生效的场景
1).事务方法内部调用同一类内事务方法
方法拥有事务能力是因为 Spring AOP 生成代理了对象,但是这种方法直接调用了 this 对象的方法,所以内部方法不会生成事务。
2).非public方法
Spring 要求被代理方法必须是 public 的。@Transactional修饰的方法必需是public
3). 方法被定义成 final 的
方法被定义成了 final 的,这样会导致 Spring AOP 生成的代理对象不能覆写该方法,而导致事务失效。
4). 当前实体没有被 Spring 管理
5).错误的Spring事务传播特性 @Transactional(propagation = Propagation.NEVER)
这种类型的传播特性不支持事务,如果有事务则会抛异常
6).数据库不支持事务
mysql的myISAM引擎不支持事务,使用myISAM引擎的相关表事务失效。
7).自己吞掉异常
@Transactional修饰的方法体里,使用try catch捕获异常,没有再抛出,则事务的 AOP 无法捕获异常,事务也不会回滚。
8).抛出异常不正确
@Transactional(rollbackFor = Exception.class)
如果没有声明rollbackFor的异常类型,开发人员自己捕获了异常,又抛出了异常 Exception,事务也不会回滚。因为 Spring 事务默认情况下只会回滚 RuntimeException(运行时异常)和 Error(错误),不会回滚 Exception。
9).多线程调用
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.doAnotherThing();
}).start();
}
@Service
public class RoleService {
@Transactional(propagation = Propagation.NESTED)
public void doAnotherThing() {
System.out.println("保存role表数据");
}
}
Spring 的事务是通过数据库连接来实现的。当前线程中保存了一个 map,key 是数据源,value 是数据库连接:
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接是不一样的,所以是不同的事务。因此当doAnotherThing发生异常回滚时,不会引起add()方法回滚。
10). 嵌套事务,如何只回滚一部分
在代码中手动把内部嵌套事务放在 try/catch 中,并且不继续往外抛出异常