🍊 Spring循环依赖
🎉 什么是循环依赖?
BeanA类依赖了BeanB类,BeanB类依赖了BeanC类,BeanC类依赖了BeanA类,形成了一个依赖闭环,我们把这种依赖关系就称之为循环依赖。
🎉 循环依赖会导致什么问题的出现?
启动项目,我们发现只要是有循环依赖关系的属性并没有自动赋值,而没有循环依赖关系的属性均有自动赋值,如下图所示:
IoC容器对Bean的初始化是根据BeanDefinition循环迭代,有一定的顺序。这样,在执行依赖注入时,需要自动赋值的属性对应的对象有可能还没初始化,没有初始化也就没有对应的实例可以注入。
🎉 Spring是怎么解决循环依赖导致的问题的?
Spring使用三级缓存解决循环依赖的过程
- 一级缓存存放实例化对象 。
- 二级缓存存放已经在内存空间创建好但是还没有给属性赋值的对象。
- 三级缓存存放对象工厂,用来创建提前暴露到bean的对象。
代码举例:
@Service public class TestService1 { @Autowired private TestService2 testService2; public void test1() { } }
@Service public class TestService2 { @Autowired private TestService1 testService1; public void test2() { } }
testService1先去一级缓存看有没有实例,发现没有,继续去二级缓存查看,发现没有,去三级缓存查看,发现没有实例就创建实例,在创建的过程中,提前暴露,添加到三级缓存里。
这个时候进行属性赋值,发现还有一个testService2,它没有赋值,是一个空的,就从一级缓存中去看testSerivce2有没有实例,发现没有,去二级查看发现没有,去三级缓存查看,发现没有,就创建实例,也提前暴露,添加到三级缓存里面。
这个时候testSerivce2对象里面发现testService1里面没有赋值,然后对testService1进行赋值,从一级缓存去查看,发现没有,去二级查看,发现没有,去三级查看,发现有,就把实例testService1从三级缓存添加到二级缓存里面,把实例testService1三级缓存的实例删除,这个时候,testService2里面有实例对象,对象里面的testService1也有值了,就是一个可以使用的实例对象了,就把这个对象移动到一级缓存里面,把三级缓存里面的testService2删除。
这个时候testService1里面的testService2属性就可以从一级缓存里面获取这个testService2实例了,把它进行赋值填充,testService1也完成了实例化,把testService1从二级缓存移动到一级缓存里面,把testService1在二级缓存的实例也删除。
想进一步了解代码实现的,这里分享一个地址:点击【java_wxid】进行查看。
🍊 Spring容器启动执行流程
部署一个web应用在web容器中,它会提供一个全局的上下文环境,这个上下文就是ServletContext,它为后面的IoC容器提供宿主环境,当web容器启动的时候,会执行web.xml中的ContextLoaderListener监听器初始化contextInitialized方法,调用父类的initWebApplicationContext方法。
这个方法里面执行了三个任务:
1.创建WebApplicationContext容器
2.加载context-param中spring配置文件
3.初始化配置文件并且创建配置文件中的bean。
监听器初始化完毕后,开始初始化web.xml中配置的servlet ,用DispatcherServlet举例,它是一个前端控制器,用来转发、匹配、处理每个servlet 请求。
DispatcherServlet上下文在初始化的时候会建立自己的上下文,先从ServletContext 中获取之前的WebApplicationContext作为自己上下文的父类上下文,有了这个父类上下文之后,再初始化自己持有的上下文,创建springmvc相关的bean,初始化处理器映射、视图解析等等,初始化完后,spring把Servlet的相关的属性作为属性key,存到servletcontext中,方便后面使用。这样每个Servlet 都持有自己的上下文,拥有自己独立的bean 空间,各个servlet 共享相同的bean,也就是根上下文定义的那些bean。
web容器停止时候会执行ContextLoaderListener的contextDestroyed方法销毁context容器。
🍊 Spring事务底层实现原理
当在某个类或者方法上使用@Transactional注解后,spring会基于该类生成一个代理对象,并将这个代理对象作为bean。当调用这个代理对象的方法时,如果有事务处理,则会先关闭事务的自动功能,然后执行方法的具体业务逻辑,如果业务逻辑没有异常,那么代理逻辑就会直接提交,如果出现任何异常,那么直接进行回滚操作。当然我们也可以控制对哪些异常进行回滚操作。
一个Bean在执行Bean的创建生命周期时,会经过InfrastructureAdvisorAutoProxyCreator的初始化后的方法,会判断当前当前Bean对象是否和BeanFactoryTransactionAttributeSourceAdvisor匹配,匹配逻辑为判断该Bean的类上是否存在@Transactional注解,或者类中的某个方法上是否存在@Transactional注解,如果存在则表示该Bean需要进行动态代理产生一个代理对象作为Bean对象。
源码执行流程:
- 加了@Transactional注解的类,或者类中拥有@Transactional注解的方法,都会生成代理对象作为bean。
代理对象执行方法时。 - 获取当前正在执行的方法上的@Transactional注解的信息TransactionAttribute。
- 查看@Transactional注解上是否指定了TransactionManager,如果没有指定,则默认获取TransactionManager类型的bean作为TransactionManager。对于TransactionManager有一个限制,必须是PlatformTransactionManager。
- 生成一个joinpointIdentification,作为事务的名字。
- 开始创建事务。
- 创建事务成功后执行业务方法。
- 如果执行业务方法出现异常,则会进行回滚,然后执行完finally中的方法后再将异常抛出。
- 如果执行业务方法没有出现异常,那么则会执行完finally中的方法后再进行提交。
🍊 Spring IOC容器加载过程
spring ioc容器的加载,大体上经过以下几个过程:
资源文件定位、解析、注册、实例化
🎉 资源文件定位
资源文件定位,一般是在ApplicationContext的实现类里完成的,因为ApplicationContext接口继承ResourcePatternResolver 接口,ResourcePatternResolver接口继承ResourceLoader接口,ResourceLoader其中的getResource()方法,可以将外部的资源,读取为Resource类。
🎉 解析DefaultBeanDefinitionDocumentReader
解析主要是在BeanDefinitionReader中完成的,最常用的实现类是XmlBeanDefinitionReader,其中的loadBeanDefinitions()方法,负责读取Resource,并完成后续的步骤。ApplicationContext完成资源文件定位之后,是将解析工作委托给XmlBeanDefinitionReader来完成的
解析这里涉及到很多步骤,最常见的情况,资源文件来自一个XML配置文件。首先是BeanDefinitionReader,将XML文件读取成w3c的Document文档。
DefaultBeanDefinitionDocumentReader对Document进行进一步解析。然后DefaultBeanDefinitionDocumentReader又委托给BeanDefinitionParserDelegate进行解析。如果是标准的xml namespace元素,会在Delegate内部完成解析,如果是非标准的xml namespace元素,则会委托合适的NamespaceHandler进行解析最终解析的结果都封装为BeanDefinitionHolder,至此解析就算完成。
后续会进行细致讲解。
🎉 注册
然后bean的注册是在BeanFactory里完成的,BeanFactory接口最常见的一个实现类是DefaultListableBeanFactory,它实现了BeanDefinitionRegistry接口,所以其中的registerBeanDefinition()方法,可以对BeanDefinition进行注册这里附带一提,最常见的XmlWebApplicationContext不是自己持有BeanDefinition的,它继承自AbstractRefreshableApplicationContext,其持有一个DefaultListableBeanFactory的字段,就是用它来保存BeanDefinition。
所谓的注册,其实就是将BeanDefinition的name和实例,保存到一个Map中。刚才说到,最常用的实现DefaultListableBeanFactory,其中的字段就是beanDefinitionMap,是一个ConcurrentHashMap。
🎉 实例化
注册也完成之后,在BeanFactory的getBean()方法之中,会完成初始化,也就是依赖注入的过程。
想进一步了解代码实现的,这里分享一个地址:点击【java_wxid】进行查看。
🍊 SpringAOP实现原理
Spring的AOP的应用场景一般在日志记录、权限验证、事务管理等业务场景。
它是运行期进行织入,生成字节码,再加载到虚拟机中,JDK是利用反射原理,CGLIB使用了ASM原理。
初始化时会看目标类有没有实现InvocationHandler接口或者是Proxy类。
如果实现了接口,就使用JDK动态代理,通过反射来接收被代理的类。
如果没实现就利用cglib进行AOP动态代理,CGLIB是通过继承的方式做的动态代理,是一个代码生成的类库,可以在运行时动态的生成某个类的子类,将目标对象转变为代理对象对事务进行操作。
所以在初始化的时候,已经将目标对象进行代理,放入到spring 容器中。
想进一步了解代码实现的,这里分享一个地址:点击【java_wxid】进行查看。
🍊 Spring的自动装配
使用@Autowired注解自动装配指定的bean,在启动spring IoC时,容器自动加载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowied的时候,就会在IoC容器自动查找需要的bean,并且注入对象的属性,使用@Autowired的时候,首先在容器中查询对应类型的bean,如果查询结果刚好为一个,就将这个bean装配给@Autowired指定的数据,如果查询的结果不止一个,那么@Autowired会根据名称来查找,如果上述查找的结果为空,那么会抛出异常,解决方法可以使用required=false。如果使用@Resource它默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。
在Spring中共有5种自动装配模式:
(1)no:这是Spring的默认设置,在该设置下自动装配是关闭的,开发者需要自行在Bean定义中用标签明确地设置依赖关系。
(2)byName:该模式可以根据Bean名称设置依赖关系。当向一个Bean中自动装配一个属性时,容器将根据Bean的名称自动在配置文件中查询一个匹配的Bean。如果找到就装配这个属性,如果没找到就报错。
(3)byType:该模式可以根据Bean类型设置依赖关系。当向一个Bean中自动装配一个属性时,容器将根据Bean的类型自动在配置文件中查询一个匹配的Bean。如果找到就装配这个属性,如果没找到就报错。
(4)constructor:和byType模式类似,但是仅适用于有与构造器相同参数类型的Bean,如果在容器中没有找到与构造器参数类型一致的Bean,那么将会抛出异常。
(5)autodetect:该模式自动探测使用constructor自动装配或者byType自动装配。首先会尝试找合适的带参数的构造器,如果找到就是用构造器自动装配,如果在Bean内部没有找到相应的构造器或者构造器是无参构造器,容器就会自动选择byType模式。
🍊 Spring Boot自动装配
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。
启动类的@SpringBootApplication注解由@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan三个注解组成,三个注解共同完成自动装配;
- @SpringBootConfiguration 注解标记启动类为配置类
- @ComponentScan 注解实现启动时扫描启动类所在的包以及子包下所有标记为bean的类由IOC容器注册为bean
- @EnableAutoConfiguration通过 @Import 注解导入 AutoConfigurationImportSelector类,然后通过AutoConfigurationImportSelector 类的 selectImports 方法去读取需要被自动装配的组件依赖下的spring.factories文件配置的组件的类全名,并按照一定的规则过滤掉不符合要求的组件的类全名,将剩余读取到的各个组件的类全名集合返回给IOC容器并将这些组件注册为bean
🍊 Spring Boot启动过程
一、SpringBoot启动的时候,会构造一个SpringApplication的实例,构造SpringApplication的时候会进行初始化的工作,初始化的时候会做以下几件事:
1、把参数sources设置到SpringApplication属性中,这个sources可以是任何类型的参数.
2、判断是否是web程序,并设置到webEnvironment的boolean属性中.
3、创建并初始化ApplicationInitializer,设置到initializers属性中 。
4、创建并初始化ApplicationListener,设置到listeners属性中 。
5、初始化主类mainApplicatioClass。
二、SpringApplication构造完成之后调用run方法,启动SpringApplication,run方法执行的时候会做以下几件事:
1、构造一个StopWatch计时器,用来记录SpringBoot的启动时间 。
2、初始化监听器,获取SpringApplicationRunListeners并启动监听,用于监听run方法的执行。
3、创建并初始化ApplicationArguments,获取run方法传递的args参数。
4、创建并初始化ConfigurableEnvironment(环境配置)。封装main方法的参数,初始化参数,写入到 Environment中,发布 ApplicationEnvironmentPreparedEvent(环境事件),做一些绑定后返回Environment。
5、打印banner和版本。
6、构造Spring容器(ApplicationContext)上下文。先填充Environment环境和设置的参数,如果application有设置beanNameGenerator(bean)、resourceLoader(加载器)就将其注入到上下文中。调用初始化的切面,发布ApplicationContextInitializedEvent(上下文初始化)事件。
7、SpringApplicationRunListeners发布finish事件。
8、StopWatch计时器停止计时,日志打印总共启动的时间。
9、发布SpringBoot程序已启动事件(started())
10、调用ApplicationRunner和CommandLineRunner
11、最后发布就绪事件ApplicationReadyEvent,标志着SpringBoot可以处理就收的请求了(running())
🍊 Spring MVC的工作原理
1、用户向服务器发送请求,请求被SpringMVC的前端控制器DispatcherServlet截获。
2、DispatcherServlet对请求的URL(统一资源定位符)进行解析,得到URI(请求资源标识符),然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象,包括Handler对象以及Handler对象对应的拦截器,这些对象都会被封装到一个HandlerExecutionChain对象当中返回。
3、DispatcherServlet根据获得的Handler,选择一个合适的HandlerAdapter。HandlerAdapter的设计符合面向对象中的单一职责原则,代码结构清晰,便于维护,最为重要的是,代码的可复制性高。HandlerAdapter会被用于处理多种Handler,调用Handler实际处理请求的方法。
4、提取请求中的模型数据,开始执行Handler(Controller)。在填充Handler的入参过程中,根据配置,spring将帮助做一些额外的工作消息转换:将请求的消息,如json、xml等数据转换成一个对象,将对象转换为指定的响应信息。数据转换:对请求消息进行数据转换,如String转换成Integer、Double等。 数据格式化:对请求的消息进行数据格式化,如将字符串转换为格式化数字或格式化日期等。数据验证:验证数据的有效性如长度、格式等,验证结果存储到BindingResult或Error中。
5、Handler执行完成后,向DispatcherServlet返回一个ModelAndView对象,ModelAndView对象中应该包含视图名或视图模型。
6、根据返回的ModelAndView对象,选择一个合适的ViewResolver(视图解析器)返回给DispatcherServlet。
7、ViewResolver结合Model和View来渲染视图。
8、将视图渲染结果返回给客户端。
🍊 Mybatis的缓存机制
首先,Mybatis里面设计了二级缓存来提升数据的检索效率,避免每次数据的访问都需要去查询数据库。
一级缓存,是SqlSession级别的缓存,也叫本地缓存,因为每个用户在执行查询的时候都需要使用SqlSession来执行,
为了避免每次都去查数据库,Mybatis把查询出来的数据保存到SqlSession的本地缓存中,后续的SQL如果命中缓存,就可以直接从本地缓存读取了。
如果想要实现跨SqlSession级别的缓存?那么一级缓存就无法实现了,因此在Mybatis里面引入了二级缓存,就是当多个用户
在查询数据的时候,只有有任何一个SqlSession拿到了数据就会放入到二级缓存里面,其他的SqlSession就可以从二级缓存加载数据。
每个一级缓存的具体实现原理是:
在SqlSession 里面持有一个Executor,每个Executor中有一个LocalCache对象。
当用户发起查询的时候,Mybatis会根据执行语句在Local Cache里面查询,如果没命中,再去查询数据库并写入到LocalCache,否则直接返回。
所以,以及缓存的生命周期是SqlSessiion,而且在多个Sqlsession或者分布式环境下,可能会导致数据库写操作出现脏数据。
二级缓存的具体实现原理是:
使用CachingExecutor装饰了Executor,所以在进入一级缓存的查询流程之前,会先通过CachingExecutor进行二级缓存的查询。
开启二级缓存以后,会被多个SqlSession共享,所以它是一个全局缓存。因此它的查询流程是先查二级缓存,再查一级缓存,最后再查数据库。
另外,MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时缓存粒度也能够到 namespace 级别,并且还可以通过 Cache 接口实现类不同的组合,对 Cache 的可控性也更强。
🌟 Spring Cloud知识点
🍊 微服务构建
🎉 微服务架构设计原则
一般来说,在设计微服务体系结构的时候,遵循业务边界的概念,按照业务进行拆分、同时隐藏实现细节、把内容组件化模块化、可伸缩性可扩展性要求较高、并且可以实现隔离应用故障,避免整体系统不可用、要求独立部署,持续交付。
对于用户激增、并发量较高、数据量较大还得考虑:
- 水平复制因素,就是单个服务多运行几个实例。
- 数据分区因素,按照用户区域进行数据分区,比如北京、上海、广州等多建几个集群。
- 并发请求因素,如果产品数量多,用户群体多的情况下,使用多级缓存处理高并发,数据量小,数据访问很高,适合存储热点数据的,可以用Nignx缓存、数据量一般,访问量大,比较热门的数据,比如首页这种的,可以使用本地缓存、数据量很大,访问量一般,比如一般的商品这种,可以使用Redis缓存。产品数量不多,用户群体大,可以考虑将页面静态化,把页面放到CDN里面,图片放到云存储里面。
🎉 微服务构建技术选型
一般来说,利用Spring Boot快速构建应用,利用Spring Cloud Alibaba Nacos实现动态服务发现、服务配置管理、服务及流量管理,利用Open feign实现与其他系统进行交互,利用Hystrix 实现熔断和错误处理,利用Ribbon实现客户端负载均衡,利用 Nginx 实现服务端负载均衡,利用 Gateway管理外部系统访问、利用Spring Security Oauth2作为权限框架进行请求校验,权限拦截、利用Seata作为分布式事务组件、利用Zipkin/Skywalking作为链路追踪、利用Sentinel作为服务降级、使用arthas/VM作为Java诊断工具、引入swagger作为在线文档、使用Redis作为分布式缓存、使用MySQL作为关系型数据库、使用MongoDB作为非关系型数据库、使用ElasticSearch作为全文搜索、有大数据的情况下使用Spark或者炎凰数仓进行读时建模。
🎉 前后端分离架构
对于一些老的项目或者特定业务的项目可能还是没有分离前后端,不过目前主流基本都是前后端分离,前后端交互更清晰,就剩下了接口模型,后端的接口更简洁明了,更容易维护,前端多渠道集成场景更容易。
后端采用统一的数据模型,支持多个前端,比如:H5前端、PC前端、安卓前端、IOS前端。
对于请求方式,比如GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源,当然也需要根据自己项目的实际情况出发,请求方式尽量统一起来。另外需要考虑后期接口参数是否会新增,如果后期参数不确定,尽量使用POST,方便后期扩展参数。
对于返回响应的实体类,后端响应统一起来,不能每个后端都用自己的响应实体类,这样前端会炸的。另外对于后端抛出来的错也需要拦截封装,给到前端一个友好的响应,而不是直接抛出去,这样前端那边的感知不友好。
对于前端请求,一般会携带几个校验参数放到请求头中,比如:App私钥(前后端约定好)、时间戳、Token令牌、校验码等等,后端一般会在网关服务或者权限服务里面,将更新数据的请求,比如POST,PUT,DELETE请求方式,对这些请求进行校验,请求头里App私钥、时间戳、Token令牌通过某种运算或者算法计算出结果,最后通过加密的方式加密这个结果,作为最终的校验码,对比前后端的校验码是否一致,判断这个请求是否合法、请求的Token是否过期/失效。
🎉 项目部署安全方面
对于项目系统部署安全方面,建议改私有网络+堡垒机+密码复杂度,建立锁机机制使用第三方知名云厂商托管数据库,降低运维复杂度,将项目部署在VPC私网内,使用cloudwatch Log+lambda+Network Firewall服务,检查mysql连续登录失败的IP次数,触发lambda执行脚本更新Network Firewall规则,禁用该IP访问。出于安全考虑,服务器只允许通过堡垒机进行运维。在没有提供安全访问策略表的情况下,除了被堡垒机访问之外,所有虚拟机无法访问任何主机,也无法被任何主机访问。