IOC(DI)
IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”,还有些书籍翻译成为“控制反向”或者“控制倒置”。
1996年,Michael Mattson在一篇有关探讨面向对象框架的文章中,首先提出了IOC 这个概念。简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。
IOC的别名:依赖注入(DI Dependency Injection),通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦。类似于电脑主机的USB接口,遵循USB接口标准接入各种外设
IOC实现:反射(Reflection),就是根据给出的类名(字符串方式)来动态地生成对象。我们可以把IOC容器的工作模式看做是工厂模式的升华,可以把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中或者扫描注解给出定义,然后利用编程语言的的反射编程,根据配置文件中给出的类名生成相应的对象。从实现来看,IOC是把以前在工厂方法里写死的对象生成代码,改变为由配置文件或者注解来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。
bean概念:由IOC容器管理的那些组成你应用程序的对象我们就叫它Bean, Bean就是由Spring容器初始化、装配及管理的对象。Bean之间的依赖关系这就需要配置元数据,在Spring中由BeanDefinition代表
实现步骤:
- 加载并且保存Spring配置文件路径信息然后保存到configLocation中
- 刷新Spring上下文环境
- 创建并且载入DefaultListableBeanFactory(即BeanFactory)
- 根据DefaultListableBeanFactory创建XMLBeanDefinitionReader,用于后面读取xml配置文件信息
- 创建BeanDefinitionDelegate代理类,用于解析xml配置信息
- 解析xml中配置的
、 、 、 等不同的标签信息,以便于可以使用不同的解析器进行解析 - 通过XMLBeanDefinitionReader结合location路径信息读取Resources资源信息
- 使用BeanDefinitionDelegate代理类解析BeanDefinition bean元素并且依次进行实例化操作,实例化完毕之后将Bean信息注册(put)到BeanDefinitionMap中以便于可以下次继续使用
各个原理概念简介:
- BeanDefinition 对依赖翻转模式中管理对象依赖关系的数据抽象
- 实现依赖翻转功能的核心数据结构
- 依赖翻转功能都是围绕对BeanDefinition 处理完成的
- 有了这些BeanDefinition 基础数据结构,容器才能发挥作用
- BeanFactory 定义了IOC 容器的基本功能规范
- IOC容器最基本形式
- 遵守的基本契约
- 最底层最基本编程规范
- 仅仅是一个接口
DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等都是附加了某种功能的具体实现
DefaultListableBeanFactory 包含了ioc 的重要功能(容器系列中的一个基本产品)
- FactoryBean和BeanFactory
- 前者是一个Bean
- 后者是IOC容器(对象工厂),一个Factory
- XmlBeanFactory原理
- 提供了最基本的ioc 容器的功能
- 读取XML 形式 BeanDefinition 的ioc 容器
- XMLDefinitionReader 处理xml 形式的BeanDefinition
- 信息来源有 Resource类来给出
- Resource 类是封装io操作的类
- loadBeanDefinitions 方法:ioc 容器初始化重要方法
- BeanFactory 和ApplicationContext
- BeanFactory 实现是ioc 容器基本形式
- ApplicationContext 是高级形式(具有增强特性)
- IOC 容器初始化
- BeanDefinition 的Resource定位、载入和注册
- 这三个过程使用不同模块完成:
- BeanDefinition 的资源定位由ResourceLoader通过统一形式Resource接口完成
- BeanDefinition 存在形式:FileSystemResource、ClassPathResource等
- BeanDefinition 载入:把用户定义好的Bean表示成IOC容器的数据结构,即BeanDefinition
- POJO对象在IOC 容器内部的抽象
- BeanDefinition 注册:BeanDefinitionRegistry 接口的实现类来完成的
- IOC 容器内部是通过一个HashMap 持有这些对象的
- IOC 容器初始化和上下文初始化一般不包括依赖注入
- lazyInit属性,会在第一次getBean 时注入
- BeanDefinition 的资源定位由ResourceLoader通过统一形式Resource接口完成
beanFactory和ApplicationContext的区别:
- 通过ApplicationContext上下文容器:BeanFactory需要手动注册,而ApplicationContext则是自动注册,当在加载xml配置文件时,配置文件中的配置的bean已经被实例化
- BeanFactory:在加载配置文件时,配置文件中的bean不被实例化,只有当通过getBean(),获取bean实例的时候才被创建。BeanFactory提供了IOC容器最基本功能,而 ApplicationContext 则增加了更多支持企业级功能支持。ApplicationContext完全继承BeanFactory,因而BeanFactory所具有的语义也适用于ApplicationContext。
循环依赖处理
1 | public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, |
类图
BeanFactory
BeanDefinition
容器初始化
AOP
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个
热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
单一职责原则
使用场景
- 事务,通过抛出异常回滚
- 日志
- 权限
- 错误处理
- 处理缓存
相关概念
- Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
- Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
- Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
- Advice(增强,通知):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是环绕执行的代码。
- Target(目标对象):织入 Advice 的目标对象。
- AOP代理(AOP Proxy): AOP框架创建的对象,将通知应Advice用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象为目标对象的业务逻辑功能加上被切入的切面所形成的对象。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
- Weaving(织入):将 Aspect切面应用到Target对象, 并创建 Adviced object 的过程
在 Spring AOP 中 Joint point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 Joint point, 通过 point cut, 我们就可以确定哪些 Joint point 可以被织入 Advice.
AOP中的Joinpoint可以有多种类型:构造方法调用,字段的设置和获取,方法的调用,方法的执行,异常的处理执行,类的初始化。也就是说在AOP的概念中我们可以在上面的这些Joinpoint上织入我们自定义的Advice,但是在Spring中却没有实现上面所有的joinpoint,确切的说,Spring只支持方法执行类型的Joinpoint。
具体说就是需要做某些处理(如打印日志、处理缓存等等)的连接点,AspectJ中的注解:
- @annotation 限定有指定注解的连接点
- execution() 用于匹配是连接点的执行方法
- target 限定连接点匹配目标对象为指定类型
切面编程:
各个概念汇总:
类图:
Advice 的类型
- @Before ,before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)
- @AfterReturning, after return advice, 在一个 join point 正常返回后执行的 advice
- @AfterThrowing, after throwing advice, 当一个 join point 抛出异常后执行的 advice
- @After, after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.
- @Around, around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.
- introduction
代理对象
Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。
CGLIB和Java动态代理的区别
Java动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy,Java类继承机制不允许多重继承);CGLIB能够代理普通类;
Java动态代理使用Java原生的反射API进行操作,在生成类上比较高效;CGLIB使用ASM框架直接对字节码进行操作,在类的执行过程中比较高效
CGLIB:
ASM框架
ASM框架是一个致力于字节码操作和分析的框架,它可以用来修改一个已存在的类或者动态产生一个新的类。ASM提供了一些通用的字节码转换和分析算法,通过这些算法可以定制更复杂的工具。ASM提供了其它字节码工具相同的功能,但是它更关注执行效率,它被设计的更小更快,它被用于以下项目:
- openjdk,实现lambda表达式调用, Nashorn编译器
- Groovy和Kotlin编译器
- Cobertura 和Jacoco,测量代码范围
- CGLIB动态代理类
生成代理对象:
1 | final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable { |
代理对象织入
我们知道InvocationHandler是JDK动态代理的核心,生成的代理对象的方法调用都会委托到InvocationHandler.invoke()方法。
1 | final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable { |
获取可以应用到此方法上的通知链(Interceptor Chain),如果有,则应用通知,并执行joinpoint; 如果没有,则直接反射执行joinpoint。而这里的关键是通知链是如何获取的以及它又是如何执行的,下面逐一分析下。
从上面的代码可以看到,通知链是通过Advised.getInterceptorsAndDynamicInterceptionAdvice()这个方法来获取的,我们来看下这个方法的实现:
1 | public class AdvisedSupport extends ProxyConfig implements Advised { |
实际的获取工作其实是由AdvisorChainFactory. getInterceptorsAndDynamicInterceptionAdvice()这个方法来完成的,获取到的结果会被缓存:
1 | public class DefaultAdvisorChainFactory implements AdvisorChainFactory, Serializable { |
这个方法执行完成后,Advised中配置能够应用到连接点或者目标类的Advisor全部被转化成了MethodInterceptor
接下来我们再看下得到的拦截器链是怎么起作用的JdkDynamicAopProxy.java
:
1 | if (chain.isEmpty()) { |
从这段代码可以看出,如果得到的拦截器链为空,则直接反射调用目标方法,否则创建MethodInvocation,调用其proceed方法,触发拦截器链的执行,具体代码:
1 | public class ReflectiveMethodInvocation implements ProxyMethodInvocation, Cloneable { |
应用实例
日志:
1 |
|
1 |
|
注解
- Order 加载顺序调整
@Component
注解默认实例化的对象是单例,如果想声明成多例对象可以使用@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
。还有Controller层也加@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
可以保证每个请求进来都创建新实例比较浪费资源。还可以通过实现BeanFactoryPostProcessor
或者ApplicationContextAware
的工具类手动创建进行控制SpringUtils.getBean("test");
。通过@Component("abc")
指定别名abc,按需创建实例。@Repository
默认单例@Service
默认单例@Controller
默认多例
@Transactional
ACID
你要往两张表甚至多张表中插入数据或者修改数据,或者说多次更新操作,那么场景上要求,这些数据都必须保证一致,要么全成功要么全失败,不能有中间数据产生,这样的话,肯定得用事务了。当然根据情况可能程序还需要加锁(数据库里面还有一些概念,数据库自带一些锁机制:比如引入隔离性、悲观锁、乐观锁 通过UNDO日志检测之前是否被修改、分布式系统更复杂了,为了避免死锁出现了两阶段锁、死锁检测等),当个事务可以保值原子性,但是多个事务同时执行可能原始值100一个事务给值+100另一个事务看到的值还是之前的值又给加100可能最终结果是200,也可能出现300。
抛出异常会回滚事务,手动回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
,在try catch 异常后事务不会被回滚可以通过该方法在catch中手动回滚,当然也可以catch处理后继续向上层抛出异常
1 | try { |
单例与原型
单例对象实例注入多例对象实例时,由于单例对象在容器中只有一次初始化的机会,所以单例对象始终注入的都是同一个对象,这样不能满足我们需要多例的要求。可以手动new,也可以通过ApplicationContextAware或者applicationContext或者BeanFactoryPostProcessor ConfigurableListableBeanFactory组合做个工具类等。
1 |
|
SpringMVC的controller默认就是单例的。如果加上@Scope("prototype")
请求到Controller层的时候@Autowired
的对象会每次创建否则都是用同一个。
如果你bean配置多例了,依赖这个bean的bean也需要多例才行
在单例的bean中切记声明成员属性(如Map、List集合来缓存数据),是线程不安全的,得用线程安全的ConcurrentHashMap
一个单例模式创建的对象是可以同时被多个线程处理的,如果一个对象被多个线程同时处理的话,很有可能出现线程同步问题,如果两个线程同时访问一个函数的话,要不要加锁呢,加锁怎么加,不加又怎样?一个单例模式的方法可以同时被多个线程处理,多个线程如果不是同时处理一个对象的共有属性,则不会出现线程问题,即使是方法中的属性如果两个线程同时访问同一个方法的时候,如果这个方法中没有共有的属性,则不需要加锁,反之则需要加锁。
Java特性使用
ThreadLocal
RequestContextHolder 保存请求参数,request。这样程序中可以获取请求参数。
1 | public abstract class RequestContextHolder { |
在spring事务管理过程中会用到一些线程安全对象,这些对象都交由TransactionSynchronizationManager管理,TransactionSynchronizationManager把这些对象都保存在ThreadLocal中。
- 执行
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
DataSource加入TheadLocal - 执行
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator)
根据sessionFactory从ThreadLocal获取Session获取不到创建session()还是适用刚刚的DataSource创建,也就是配置文件配置的DataSource里面可以有多个数据源会有默认数据源具体看源码,能获取到直接用ThreadLocal获取到的SqlSession
1 | public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable { |
多数据源切换,注解获取到的数据源名称可以存在线程ThreadLocal中。
部分常用类说明
Assert
进行参数校验,在运行时识别程序错误。会抛出异常,外层还可以根据需要进行try catch操作,也可以不处理后期根据日志分析报错原因。
1 | Assert.notNull(clazz, "The class must not be null"); |
SpringBoot
什么是springboot?
它是一个服务于spring框架的框架,能够简化配置文件,快速构建web应用,内置tomcat,无需打包部署,直接运行。
约定优于配置指的是什么?
maven 的目录结构
a) 默认有 resources 文件夹存放配置文件
b) 默认打包方式为 jar
默认提供 application.properties/yml 文件
默认通过 spring.profiles.active 属性来决定运行环境时读取的配置文件
EnableAutoConfiguration 默认对于依赖的 starter 进行自动
@SpringBootApplication由哪几个注解组成,这几个注解分别表示什么作用
SpringBootApplication 本质上是由 3 个注解组成,分别是
@Configuration
@EnableAutoConfiguration
@ComponentScan
@Configuration //在启动类里面标注了@Configuration,意味着它其实也是一个 IoC 容器的配置类
@EnableAutoConfiguration:
springboot 应用把所有符合条件的@Configuration 配置
都加载到当前 SpringBoot 创建并使用的 IoC 容器中。
@ComponentScan:
ComponentScan 默认会扫描当前 package 下的的所有加了 @Component 、@Repository、@Service、@Controller的类到 IoC 容器中
1 |
|
@Import注解的作用
@Import注解是用来导入配置类或者一些需要前置加载的类
可以是普通java类,可以是加了@Configuration注解的,也可以是ImportBeanDefinitionRegistrar
自动配置,AutoConfigurationImportSelector执行过程
1 | public abstract class AbstractApplicationContext extends DefaultResourceLoader |
依赖一个spring-boot-starter-data-redis
的jar包,jar中定义了RedisConfiguration,当启动的时候spring会自动装载RedisConfiguration,那spring是如何知道配置类在哪里的呢?RedisConfiguration类的路径放在了classpath*META-INF/spring.factories的文件中,spring会加载这个文件中配置的configuration。
MyBatisPlus的spring.factories相关配置
1 | # Auto Configure |
@Configuration原理
@Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。用@Configuration加载spring,组合多个配置类。
注意:@Configuration注解的配置类有如下要求:
@Configuration不可以是final类型;
@Configuration不可以是匿名类;
嵌套的configuration必须是静态类。
一、用@Configuration加载spring
1.1、@Configuration配置spring并启动spring容器
1.2、@Configuration启动容器+@Bean注册Bean
1.3、@Configuration启动容器+@Component注册Bean
1.4、使用 AnnotationConfigApplicationContext 注册 AppContext 类的两种方法
1.5、配置Web应用程序(web.xml中配置AnnotationConfigApplicationContext)
二、组合多个配置类
2.1、在@configuration中引入spring的xml配置文件
2.2、在@configuration中引入其它注解配置
2.3、@configuration嵌套(嵌套的Configuration必须是静态类)
三、@EnableXXX注解
四、@Profile逻辑组配置
五、使用外部变量
@Configuration注解到底背后的工作原理是什么呢,笔者将从源码角度引入讲述@Configuration注解的工作原理,下面简单概述下Configuration注解是被初始化的流程:
AbstractApplicationContext::refresh–>AbstractApplicationContext::invokeBeanFactoryPostProcessors
–>ConfigurationClassPostProcessor::postProcessBeanFactory–>ConfigurationClassPostProcessor::enhanceConfigurationClasses
ConfigurationClassPostProcessor::enhanceConfigurationClasses这个方法是Configuration注解工作的核心方法,spring应用启动时所有的被@Configuration注解的类都会被spring cglib库生成cglib动态代理,然后其他地方通过@Autowired注解引入Student类对象就会被生成的configuration配置类生成的动态代理拦截处理完后再调用原configuration注解类的方法实例。
加上@Configuration注解主要是给我们的类加上了cglib代理。在执行我们的配置类的方法时,会执行cglib代理类中的方法,其中有一个非常重要的判断,当我们的执行方法和我们的调用方法是同一个方法时,会执行父类的方法new(cglib代理基于继承);当执行方法和调用方法不是同一个方法时会调用beanFactory.getBean获取。
在@Configuration中定义@Bean,Bean初始化的时候也就是相关功能模块进行初始化了(Bean里面还可以调用new xxx手动创建对象的方法),实现了配置模块的加载
1 |
|
@SpringBootApplication中包含@EnableAutoConfiguration,会自动加载 声明@SpringBootApplication的类的package及其子package下的@Configuration类,创建Bean,其他的不加载,如果是内部工程可以调整路径层级。所以这样对于第三方包还需要spring.factories中配置。
可能遇到问题
注入对象为空
可能是加载顺序导致的,可以用Order注解或者调整xml配置顺序。或者对象增加构造函数,在存在多层调用的情况在外层通过构造函数把对象传递进去。