TODO:
1.看下极客时间的Spring课程
2.https://www.zhihu.com/question/438612354
设计思想
约定大于配置
- 在使用 Spring 开发的项目中,配置往往会比较复杂、繁琐。比如,我们利用 Spring MVC 来开发 Web 应用,需要配置每个 Controller 类以及 Controller 类中的接口对应的 URL。
如何来简化配置呢?一般来讲,有两种方法,一种是基于注解,另一种是基于约定。这两种配置方式在 Spring 中都有用到。Spring 在最小化配置方面做得淋漓尽致,有很多值得我们借鉴的地方。 - 基于注解的配置方式,我们在指定类上使用指定的注解,来替代集中的 XML 配置。比如,我们使用 @RequestMapping 注解,在 Controller 类或者接口上,标注对应的 URL;使用 @Transaction 注解表明支持事务等。
- 基于约定的配置方式,也常叫作“约定优于配置”或者“规约优于配置”(Convention over Configuration)。通过约定的代码结构或者命名来减少配置。说直白点,就是提供配置的默认值,优先使用默认值。程序员只需要设置那些偏离约定的配置就可以了。
比如,在 Spring JPA(基于 ORM 框架、JPA 规范的基础上,封装的一套 JPA 应用框架)中,我们约定类名默认跟表名相同,属性名默认跟表字段名相同,String 类型对应数据库中的 varchar 类型,long 类型对应数据库中的 bigint 类型等等。 - 基于刚刚的约定,代码中定义的 Order 类就对应数据库中的“order”表。只有在偏离这一约定的时候,例如数据库中表命名为“order_info”而非“order”,我们才需要显示地去配置类与表的映射关系(Order 类 ->order_info 表)。
- 实际上,约定优于配置,很好地体现了“二八法则”。在平时的项目开发中,80% 的配置使用默认配置就可以了,只有 20% 的配置必须用户显式地去设置。所以,基于约定来配置,在没有牺牲配置灵活性的前提下,节省了我们大量编写配置的时间,省掉了很多不动脑子的纯体力劳动,提高了开发效率。除此之外,基于相同的约定来做开发,也减少了项目的学习成本和维护成本。
低侵入松耦合
- 框架的侵入性是衡量框架好坏的重要指标。所谓低侵入指的是,框架代码很少耦合在业务代码中。低侵入意味着,当我们要替换一个框架的时候,对原有的业务代码改动会很少。相反,如果一个框架是高度侵入的,代码高度侵入到业务代码中,那替换成另一个框架的成本将非常高,甚至几乎不可能。这也是一些长期维护的老项目,使用的框架、技术比较老旧,又无法更新的一个很重要的原因。
- Spring 提供的 IOC 容器,在不需要 Bean 继承任何父类或者实现任何接口的情况下,仅仅通过配置,就能将它们纳入进 Spring 的管理中。如果我们换一个 IOC 容器,也只是重新配置一下就可以了,原有的 Bean 都不需要任何修改。
- 除此之外,Spring 提供的 AOP 功能,也体现了低侵入的特性。在项目中,对于非业务功能,比如请求日志、数据采点、安全校验、事务等等,我们没必要将它们侵入进业务代码中。因为一旦侵入,这些代码将分散在各个业务代码中,删除、修改的成本就变得很高。而基于 AOP 这种开发模式,将非业务代码集中放到切面中,删除、修改的成本就变得很低了。
模块化轻量级
-
Spring 在分层、模块化方面做得非常好。每个模块都只负责一个相对独立的功能。模块之间关系,仅有上层对下层的依赖关系,而同层之间以及下层对上层,几乎没有依赖和耦合。除此之外,在依赖 Spring 的项目中,开发者可以有选择地引入某几个模块,而不会因为需要一个小的功能,就被强迫引入整个 Spring 框架。所以,尽管 Spring Framework 包含的模块很多,已经有二十几个,但每个模块都非常轻量级,都可以单独拿来使用。正因如此,到现在,Spring 框架仍然可以被称为是一个轻量级的开发框架。
image.png
再封装、再抽象
- Spring 不仅仅提供了各种 Java 项目开发的常用功能模块,而且还对市面上主流的中间件、系统的访问类库,做了进一步的封装和抽象,提供了更高层次、更统一的访问接口。
- 比如,Spring 提供了 spring-data-redis 模块,对 Redis Java 开发类库(比如 Jedis、Lettuce)做了进一步的封装,适配 Spring 的访问方式,让编程访问 Redis 更加简单。
- 比如Spring Cache,实际上也是一种再封装、再抽象。它定义了统一、抽象的 Cache 访问接口,这些接口不依赖具体的 Cache 实现(Redis、Guava Cache、Caffeine 等)。在项目中,我们基于 Spring 提供的抽象统一的接口来访问 Cache。这样,我们就能在不修改代码的情况下,实现不同 Cache 之间的切换。
- 除此之外,在模板模式中,讲的 JdbcTemplate ,实际上,它也是对 JDBC 的进一步封装和抽象,为的是进一步简化数据库编程。不仅如此,Spring 对 JDBC 异常也做了进一步的封装。封装的数据库异常继承自 DataAccessException 运行时异常。这类异常在开发中无需强制捕获,从而减少了不必要的异常捕获和处理。除此之外,Spring 封装的数据库异常,还屏蔽了不同数据库异常的细节(比如,不同的数据库对同一报错定义了不同的错误码),让异常的处理更加简单。
IOC 控制反转
不需要考虑对象的创建问题
ioc容器实际上就是一个<String, Bean>的map 存放的各种对象
可以理解 Spring是包含了众多工具方法的Ioc容器
DI属于Ioc的一种实现方式 是否还有其他实现方式 ==》 Servlet、Akka
在Servlet应用程序中,任何实现Servlet 接口的类 都将由Servlet容器管理。
在Akka应用程序中,IoC容器称为 ActorSystem,托管对象是扩展特征Actor的类的实例, 并通过称为Props的配置对象创建
控制反转四问
1.谁控制了谁
2.控制了什么
3.为何是反转
4.哪些方面反转了
1.谁控制谁:老的模式下直接new一个对象 是由我们直接控制对象的创建 入参代表以来的对象 ioc模式下依赖的对象在创建的时候不需要你控制 由Ioc容器来做这个事情 所以是Ioc容器控制对象 包括他的依赖
2.控制了什么:控制了对象
3.为何是反转: 正转就是我们在创建的对象里 根据入参去创建以来的对象 有了ioc之后依赖的对象是由ioc容器创建之后注入进去的 原来是主动创建 现在是被动获取 所以是反转
4.哪些方面反转了:所依赖对象的获取反转了 主动获取变成被动获取 。
AOP面向切面编程
@Autowired 和 @Resource 的区别是什么?
@Autowired 是 Spring 提供的注解,@Resource 是 JDK 提供的注解。
Autowired是byType注入的 先根据接口类型去匹配
Resource是根据名称去匹配的
如果AService BService同时实现了Service借口 这个时候AutoWired注入会报错 最好选用Reource
FactoryBean
BeanFactory和FactoryBean的区别
Bean 的作用域有哪些?
Spring 中 Bean 的作用域通常有下面几种:
- singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
-
prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续
getBean()
两次,得到的是不同的 Bean 实例。 - request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
- session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
- application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
-
websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
注解或者xml配置的时候可以指定作用域 未指定情况下的默认作用域为singleton
singleton的bean类因为存在资源竞争情况 可能有线程安全问题
对于有状态单例 Bean 的线程安全问题,常见的有两种解决办法:在 Bean 中尽量避免定义可变的成员变量。在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式 这种方式在多线程情况会出问题)。
SpringMVC核心组件
-DispatcherServlet:核心的中央处理器,负责接收请求、分发,并给予客户端响应。
-HandlerMapping:处理器映射器,根据 URL 去匹配查找能处理的 Handler ,并会将请求涉及到的拦截器和 Handler 一起封装。
-HandlerAdapter:处理器适配器,根据 HandlerMapping 找到的 Handler ,适配执行对应的 Handler;
-Handler:请求处理器,处理实际请求的处理器。
-ViewResolver:视图解析器,根据 Handler 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 DispatcherServlet 响应客户端
自动装配
自动装配可以简单理解为:通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。
如何实现的自动装配
SpringBootApplication
@SpringBootApplication看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是:
@EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
@Configuration:允许在上下文中注册额外的 bean 或导入其他配置类
@ComponentScan:扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。如下图所示,容器中将排除TypeExcludeFilter和AutoConfigurationExcludeFilter。
EnableAutoConfiguration 只是一个简单地注解,自动装配核心功能的实现实际是通过 AutoConfigurationImportSelector类。
AutoConfigurationImportSelector类
AutoConfigurationImportSelector 类实现了 ImportSelector接口,也就实现了这个接口中的 selectImports方法,该方法主要用于获取所有符合条件的类的全限定类名,这些类需要被加载到 IoC 容器中。
selectImports函数
getAutoConfigurationEntry函数 主要负责加载自动配置类
getCandidateConfigurations函数
Spring事务在什么情况下会失效?
1.访问权限问题
java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。
如果事务方法的访问权限不是定义成public,这样会导致事务失效,因为spring要求被代理方法必须是public
的。
翻开源码,可以看到,在AbstractFallbackTransactionAttributeSource
类的computeTransactionAttribute
方法中有个判断,如果目标方法不是public,则返回null,即不支持事务。
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
...
}
2. 方法用final修饰
如果事务方法用final修饰,将会导致事务失效。因为spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。
但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
同理,如果某个方法是static的,同样无法通过动态代理,变成事务方法。
3.对象没有被spring管理
使用spring事务的前提是:对象要被spring管理,需要创建bean实例。如果类没有加@Controller、@Service、@Component、@Repository等注解,即该类没有交给spring去管理,那么它的方法也不会生成事务。
4.表不支持事务
如果MySQL使用的存储引擎是myisam,这样的话是不支持事务的。因为myisam存储引擎不支持事务。
5.方法内部调用
如下代码所示,update方法上面没有加 @Transactional
注解,调用有 @Transactional
注解的 updateOrder 方法,updateOrder 方法上的事务会失效。
因为发生了自身调用,调用该类自己的方法,而没有经过 Spring 的代理类,只有在外部调用事务才会生效。
@Service
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
this.updateOrder(order);
}
@Transactional
public void updateOrder(Order order) {
// update order
}
}
解决方法:
1、再声明一个service,将内部调用改为外部调用
2、使用编程式事务
3、使用AopContext.currentProxy()获取代理对象
@Servcie
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
((OrderService)AopContext.currentProxy()).updateOrder(order);
}
@Transactional
public void updateOrder(Order order) {
// update order
}
}
6.未开启事务
如果是spring项目,则需要在配置文件中手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。
如果是springboot项目,那么不需要手动配置。因为springboot已经在DataSourceTransactionManagerAutoConfiguration
类中帮我们开启了事务。
7.吞了异常
有时候事务不会回滚,有可能是在代码中手动catch了异常。因为开发者自己捕获了异常,又没有手动抛出,把异常吞掉了,这种情况下spring事务不会回滚。
如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。
Spring怎么解决循环依赖的问题?
首先,有两种Bean注入的方式。
构造器注入和属性注入。
对于构造器注入的循环依赖,Spring处理不了,会直接抛出BeanCurrentlylnCreationException
异常。
对于属性注入的循环依赖(单例模式下),是通过三级缓存处理来循环依赖的。
而非单例对象的循环依赖,则无法处理。
下面分析单例模式下属性注入的循环依赖是怎么处理的:
首先,Spring单例对象的初始化大略分为三步:
-
createBeanInstance
:实例化bean,使用构造方法创建对象,为对象分配内存。 -
populateBean
:进行依赖注入。 -
initializeBean
:初始化bean。
Spring为了解决单例的循环依赖问题,使用了三级缓存:
singletonObjects
:完成了初始化的单例对象map,bean name --> bean instance
earlySingletonObjects
:完成实例化未初始化的单例对象map,bean name --> bean instance
singletonFactories
: 单例对象工厂map,bean name --> ObjectFactory,单例对象实例化完成之后会加入singletonFactories。
在调用createBeanInstance进行实例化之后,会调用addSingletonFactory,将单例对象放到singletonFactories中。
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
假如A依赖了B的实例对象,同时B也依赖A的实例对象。
- A首先完成了实例化,并且将自己添加到singletonFactories中
- 接着进行依赖注入,发现自己依赖对象B,此时就尝试去get(B)
- 发现B还没有被实例化,对B进行实例化
- 然后B在初始化的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects和二级缓存earlySingletonObjects没找到,尝试三级缓存singletonFactories,由于A初始化时将自己添加到了singletonFactories,所以B可以拿到A对象,然后将A从三级缓存中移到二级缓存中
- B拿到A对象后顺利完成了初始化,然后将自己放入到一级缓存singletonObjects中
- 此时返回A中,A此时能拿到B的对象顺利完成自己的初始化
由此看出,属性注入的循环依赖主要是通过将实例化完成的bean添加到singletonFactories来实现的。而使用构造器依赖注入的bean在实例化的时候会进行依赖注入,不会被添加到singletonFactories中。比如A和B都是通过构造器依赖注入,A在调用构造器进行实例化的时候,发现自己依赖B,B没有被实例化,就会对B进行实例化,此时A未实例化完成,不会被添加到singtonFactories。而B依赖于A,B会去三级缓存寻找A对象,发现不存在,于是又会实例化A,A实例化了两次,从而导致抛异常。
总结:1、利用缓存识别已经遍历过的节点; 2、利用Java引用,先提前设置对象地址,后完善对象。