Spring 是一种轻量级开发框架,旨在提高开发人员的开发效率以及系统的可维护性。
我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是:核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。
比如:Core Container 中的 Core 组件是 Spring 所有组件的核心,Beans 组件和 Context 组件是实现 IOC(控制反转) 和 DI(依赖注入) 的基础,AOP 组件用来实现面向切面编程。
Spring 官网 列出的 Spring 的6个特征:
下图对应 Spring4.x 版本,目前最新的5.x版本中 web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。
IOC(Inversion of Control:控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。IOC 在其它语言中也有应用,并非 Spring 特有,IOC 容器是 Spring 用来实现 IOC 的载体,IOC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
将对象之间的相互依赖关系交给 IOC 容器来管理,并由 IOC 容器完成对象的注⼊。这样可以很⼤程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IOC 容器就像是⼀个工厂一样,当我们需要创建⼀个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中⼀个 Service 类可能有几百甚⾄上千个类作为它的底层,假如我们需要实例化这个Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把⼈逼疯。如果利⽤ IOC 的话,你只需要配置好,然后在需要的地⽅引用就行了,这大大增加了项目的可维护性且降低了开发难度。
举个例子:
中间位置的第三方,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的转动全部依靠第三方,全部对象的控制权都上缴给了第三方(IOC容器),所以,IOC容器成了整个系统的关键核心,它起到了一种类似于粘合剂的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个粘合剂,对象与对象之间会彼此失去联系,这就是有人把 IOC 容器比喻成粘合剂的由来。
把上图中间的IOC容器拿掉,然后再来看看这套系统:
这时候A、B、C、D这4个对象之间已经没有了耦合关系,彼此毫无联系,这样的话,当你在实现A的时候,根本无须再去考虑B、C和D了,对象之间的依赖关系已经降低到了最低程度。所以,如果真能实现IOC容器,对于系统开发而言,这将是一件多么美好的事情,参与开发的每一成员只要实现自己的类就可以了,跟别人没有任何关系!
对比一下:
没有引入IOC容器之前:对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
引入IOC容器之后:对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。
Spring 时代我们⼀般通过 XML 文件来配置 Bean,后来开发⼈员觉得 XML 文件来配置不太好,于是
SpringBoot 注解配置就慢慢开始流⾏起来。
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Spring AOP就是基于动态代理
如下图所示:
当然你也可以使⽤ AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。使用 AOP 之后我们可以把⼀些通⽤功能抽象出来,在需要⽤到的地方直接使⽤即可,这样大大简化了、代码量。我们需要增加新功能时也方便,这样也提⾼了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。
Spring AOP 和 AspectJ AOP 的区别
Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是Spring AOP 相对来说更简单.
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。
在spring的配置文件中注册UserService,将UserDaoJdbc通过 constructor-arg 标签注入到UserService的某个有参数的构造方法
<!-- 注册userService -->
<bean id="userService" class="com.lyu.spring.service.impl.UserService">
<constructor-arg name="userDao" ref="userDaoJdbc"></constructor-arg>
<constructor-arg name="user" ref="user"></constructor-arg>
</bean>
<!-- 注册实体User类,用于测试 -->
<bean id="user" class="com.lyu.spring.entity.User"></bean>
<!-- 注册jdbc实现的dao -->
<bean id="userDaoJdbc" class="com.lyu.spring.dao.impl.UserDaoJdbc"></bean>
public class UserService implements IUserService {
private IUserDao userDao;
private User user;
public UserService(IUserDao userDao, User user) {
this.userDao = userDao;
this.user = user;
}
public void loginUser() {
userDao.loginUser();
}
}
<!-- 注册userService -->
<bean id="userService" class="com.lyu.spring.service.impl.UserService">
<!-- 写法一 -->
<!-- <property name="UserDao" ref="userDaoMyBatis"></property> -->
<!-- 写法二 -->
<property name="userDao" ref="userDaoMyBatis"></property>
</bean>
<!-- 注册mybatis实现的dao -->
<bean id="userDaoMyBatis" class="com.lyu.spring.dao.impl.UserDaoMyBatis"></bean>
注:上面这两种写法都可以,spring会将name值的每个单词首字母转换成大写,然后再在前面拼接上 “set” 构成一个方法名,然后去对应的类中查找该方法,通过反射调用,实现注入。
public class UserService implements IUserService {
private IUserDao userDao1;
public void setUserDao(IUserDao userDao1) {
this.userDao1 = userDao1;
}
public void loginUser() {
userDao1.loginUser();
}
}
还有一点需要注意:如果通过set方法注入属性,那么spring会通过默认的空参构造方法来实例化对象,所以如果在类中写了一个带有参数的构造方法,一定要把空参数的构造方法写上,否则spring没有办法实例化对象,导致报错。
在介绍注解注入的方式前,先简单了解 bean 的一个属性 autowire,autowire 主要有三个属性值:constructor,byName,byType。
(1)主要有四种注解可以注册 bean,每种注解可以任意使用,只是语义上有所差异:
(2)描述依赖关系主要有两种
@Resource
@Qualifier("userDaoMyBatis")
private IUserDao userDao;
public UserService(){
}
// candidateBeans 为上一步通过类型匹配到的多个bean,该 Map 中至少有两个元素。
protected String determineAutowireCandidate(Map<String, Object> candidateBeans, DependencyDescriptor descriptor) {
// requiredType 为匹配到的接口的类型
Class<?> requiredType = descriptor.getDependencyType();
// 1. 先找 Bean 上有@Primary 注解的,有则直接返回
String primaryCandidate = this.determinePrimaryCandidate(candidateBeans, requiredType);
if (primaryCandidate != null) {
return primaryCandidate;
} else {
// 2.再找 Bean 上有 @Order,@PriorityOrder 注解的,有则返回
String priorityCandidate = this.determineHighestPriorityCandidate(candidateBeans, requiredType);
if (priorityCandidate != null) {
return priorityCandidate;
} else {
Iterator var6 = candidateBeans.entrySet().iterator();
String candidateBeanName;
Object beanInstance;
do {
if (!var6.hasNext()) {
return null;
}
// 3. 再找 bean 的名称匹配的
Entry<String, Object> entry = (Entry)var6.next();
candidateBeanName = (String)entry.getKey();
beanInstance = entry.getValue();
} while(!this.resolvableDependencies.values().contains(beanInstance) && !this.matchesBeanName(candidateBeanName, descriptor.getDependencyName()));
return candidateBeanName;
}
}
}
determineAutowireCandidate 方法的逻辑是:
可以简单的理解为先以 ByType 的方式去匹配,如果匹配到了多个再以 ByName 的方式去匹配,找到了对应的 bean 就去注入,没找到就抛出异常。
还有一点要注意:如果使用了 @Qualifier 注解,那么当自动装配匹配到多个 bean 的时候就不会进入 determineAutowireCandidate 方法(亲测),而是直接查找与 @Qualifer 指定的 bean name 相同的 bean 去注入,找到了就直接注入,没有找到则抛出异常。
tips:大家如果认真思考可能会发现 ByName 的注入方式和 @Qualifier 有点类似,都是在自动装配匹配到多个 bean 的时候,指定一个具体的 bean,那它们有什么不同呢?
ByName 的方式需要遍历,@Qualifier 直接一次定位。在匹配到多个 bean 的情况下,使用 @Qualifier 来指明具体装配的 bean 效率会更高一下。
当通过spring容器创建一个Bean实例时,不仅可以完成Bean实例的实例化,还可以为Bean指定特定的作用域。Spring支持如下5种作用域:
Spring 中单例 bean 的线程安全问题
大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。
常见的两种解决方法:
Spring 容器负责创建应用程序中的 bean 同时通过 ID 来协调这些对象之间的关系。作为开发人员,我们需要告诉 Spring 要创建哪些 bean 并且如何将其装配到一起。
关于下面一些设计模式的详细介绍,可以参考文章《⾯试官:“谈谈Spring中都用到了哪些设计模式?”》
声明式事务又分为两种:
TransactionDefinition 接口中定义了五个表示隔离级别的常量:
TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ隔离级别,Oracle 默认采用的 READ_COMMITTED隔离级别。
TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发⽣。
TransactionDefinition.ISOLATION_SERIALIZABLE: 最⾼的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
(1)支持当前事务的情况:
(2)不支持当前事务的情况:
(3)其它情况:
@Transactional(rollbackFor = Exception.class)
我们知道:Exception分为运行时异常RuntimeException和非运行时异常。事务管理对于企业应⽤来说是至关重要的,即使出现异常情况,它也可以保证数据的⼀致性。
当 @Transactional 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类⾥⾯的方法抛出异常,就会回滚,数据库里面的数据也会回滚。
在 @Transactional 注解中如果不配置 rollbackFor 属性,那么事物只会在遇到 RuntimeException 的时候才会回滚,加上 rollbackFor=Exception.class ,可以让事物在遇到非运行时异常时也回滚。
参考: 曲健磊、Jack方、JavaGuide面试突击