文章目录
- 6. Spring框架
- 6.1 Spring和SpringBoot的区别
- 6.2 IOC
- 6.2.1 概念
- 6.2.2 DI:依赖注入
- 6.2.3 IOC的理解
- 6.3 AOP
- 6.3.1 概念
- 6.3.2 涉及的概念
- 6.3.3 AOP的原理
- 6.4 三级缓存解决循环依赖的问题
- 6.5 Spring Bean的生命周期
- 6.6 Spring启动过程
- 6.7 SpringBoot的启动过程
- 6.8 Springboot SPI机制
- 6.9 Springboot 事务传播机制
- 6.10 SpringMVC工作流程
- 6.11 SpringBoot中的常用注解
- 6.12 Spring应用的设计模式
- 6.13 @Autowired和@Resource的区别
- 6.14 在事务中切换数据源的操作
6. Spring框架
6.1 Spring和SpringBoot的区别
- 1.Springboot 将原有的 xml 配置,简化为 java 注解
- 2.使用 IDE 可以很方便的搭建一个 springboot 项目,选择对应的 maven 依赖,简化Spring应用的初始搭建以及开发过程
- 3.springboot 有内置的 tomcat 服务器,可以 jar 形式启动一个服务,可以快速部署发布 web 服务
- 4.springboot 使用 starter 依赖自动完成 bean 配置,解决 bean 之间的冲突,并引入相关的 jar 包(这一点最重要)
- 5.@SpringBootConfiguration(内部为@Configuration):被标注的类等于在spring的XML配置文件中(applicationContext.xml),装配所有bean事务,提供了一个spring的上下文环境
6.2 IOC
6.2.1 概念
- 使用者之前使用B对象的时候都需要自己去创建和组装,而现在这些创建和组装都交给spring容器去给完成了,使用者只需要去spring容器中查找需要使用的对象就可以了;这个过程中B对象的创建和组装过程被反转了,之前是使用者自己主动去控制的,现在交给spring容器去创建和组装了,对象的构建过程被反转了,所以叫做控制反转;IOC是是面相对象编程中的一种设计原则,主要是为了降低系统代码的耦合度,让系统利于维护和扩展。
6.2.2 DI:依赖注入
- 依赖注入是spring容器中创建对象时给其设置依赖对象的方式,比如给spring一个清单,清单中列出了需要创建B对象以及其他的一些对象(可能包含了B类型中需要依赖对象),此时spring在创建B对象的 时候,会看B对象需要依赖于哪些对象,然后去查找一下清单中有没有包含这些被依赖的对象,如果有 就去将其创建好,然后将其传递给B对象;可能B需要依赖于很多对象,B创建之前完全不需要知道其他 对象是否存在或者其他对象在哪里以及被他们是如何创建,而spring容器会将B依赖对象主动创建好并将 其注入到B中去,比如spring容器创建B的时候,发现B需要依赖于A,那么spring容器在清单中找到A的 定义并将其创建好之后,注入到B对象中。
6.2.3 IOC的理解
- IOC控制反转,是一种设计理念,将对象创建和组装的主动控制权利交给了spring容器去做,控制的动作被反转了,降低了系统的耦合度,利于系统维护和扩展,主要就是指需要使用的对象的组装控制权被反转了,之前是自己要做的,现在交给spring容器做了。
6.3 AOP
6.3.1 概念
- AOP:面向切面编程。AOP是一种新的编程方式,它和OOP不同,OOP把系统看作多个对象的交互,AOP把系统分解为不同的关注点,或者称之为切面(Aspect)
6.3.2 涉及的概念
- 目标对象(target): 目标对象指将要被增强的对象,即包含主业务逻辑的类对象。
- 连接点(JoinPoint): 连接点,程序运行的某一个点,比如执行某个方法,在Spring AOP中Join Point总是表示一个方法的执行
- 代理对象(Proxy): AOP中会通过代理的方式,对目标对象生成一个代理对象,代理对象中会加入需要增强功能,通过代理对象来间接的方式目标对象,起到增强目标对象的效果。
- 通知(Advice): 需要在目标对象中增强的功能,如上面说的:业务方法前验证用户的功能、方法执行之后打印方法的执行日志。
- 切入点(PointCut):用来指定需要将通知使用到哪些地方,比如需要用在哪些类的哪些方法上,切入点就是做这个配置的。
- 切面(Aspect):通知(Advice)和切入点(Pointcut)的组合。切面来定义在哪些地方(Pointcut)执行什么操作 (Advice)。
- 顾问(Advisor):Advisor 其实它就是 Pointcut 与 Advice 的组合,Advice 是要增强的逻辑,而增强的逻辑要在什么地方 执行是通过Pointcut来指定的,所以 Advice 必需与 Pointcut 组合在一起,这就诞生了 Advisor 这个 类,spring Aop中提供了一个Advisor接口将Pointcut 与 Advice 的组合起来。
6.3.3 AOP的原理
- 在Java平台上,对于AOP的织入,有3种方式:
- 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现织入;
- 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;
- 运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。
- 最简单的方式是第三种,Spring的AOP实现就是基于JVM的动态代理。由于JVM的动态代理要求必须实现接口,如果一个普通类没有业务接口,就需要通过CGLIB或者Javassist这些第三方库实现。
- AOP技术看上去比较神秘,但实际上,它本质就是一个动态代理,让我们把一些常用功能如权限检查、日志、事务等,从每个业务方法中剥离出来。
6.4 三级缓存解决循环依赖的问题
- 第一级缓存:也叫单例池,存放已经经历了完整生命周期的Bean对象。
- 第二级缓存:存放早期暴露出来的Bean对象,实例化以后,就把对象放到这个Map中。(Bean可能只经过实例化,属性还未填充)。
- 第三级缓存:存放早期暴露的Bean的工厂。
- 只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。
- 为了解决第二级缓存中AOP生成新对象的问题,Spring就提前AOP,比如在加载b的流程中,如果发送了循环依赖,b依赖了a,就要对a执行AOP,提前获取增强以后的a对象,这样b对象依赖的a对象就是增强以后的a了。
- 二三级缓存就是为了解决循环依赖,且之所以是二三级缓存而不是二级缓存,主要是可以解决循环依赖对象需要提前被aop代理,以及如果没有循环依赖,早期的bean也不会真正暴露,不用提前执行代理过程,也不用重复执行代理过程。
- 示例:
- 使用三级缓存解决循环依赖的问题步骤:
- creatingSet.add(‘AService’) 在set集合中添加一个正在初始化的字段。
- 实例化(new Aservice()),创建AService原始对象–>放入第三级缓存中
- 填充bService属性–>从单例池找–>找不到–>创建bService的Bean
- BService的Bean生命周期
- 2.1 实例化(new BService()),创建BService原始对象
- 2.2 填充aService属性–>从单例池找–>找不到–>在第二级缓存中找–>找不到–>在creatingSet寻找–>出现了依赖循环–>在第三级缓存中找原始对象,提前进行AOP,创建aService的代理对象–>放入第二级缓存中
- 2.3填充其他属性
- 2.4做其他事情(AOP)
- 2.5放入单例池 Bean对象
- 3.填充其他属性
- 4.做其他重要的事情(AOP,判断一下:AService提前做过AOP没有)
- 在第二级缓存中找一下AService的代理对象
- 5.放入单例池 AService代理对象
- 6.creatingSet.remove(‘AService’)
6.5 Spring Bean的生命周期
- Bean的定义
- 资源定位的过程。
如使用@ComponentScan定义的扫描路径去找@Component的类(PS:现在启动类@SpringBootApplication会直接扫描本包内的@Compnent注释的类) - 解析资源并保存定义。
- Spring IoC容器装载Bean定义。
- Bean的生命周期
- 解析Bean定义后会将定义保存在BeanDefinition实例中。
- 依赖注入例如@Autowired注入的资源。
- 默认情况下,在Spring IoC容器初始化的时候,Bean将会自动实例化和注入。如果不想跟随Ioc容器初始化,在Spring web开发中可以通过以下步骤进行延迟加载(缺少一个都不能实现):
- 首先在对应实体类上面加上@Lazy注解。
- 然后在@Autowired注解引用该Bean的地方同时再加上@Lazy注解。
- 使用@Bean注入的地方(如果有)也要加上@Lazy。
6.6 Spring启动过程
- spring启动的流程
- a. 定位:在spring中,使用统一的资源表现方式Resource,定位到spring配置文件。
- b. 加载:在加载这个过程中,主要工作是读取spring配置文件,解析配置文件中的内容,将这些信息转换成为Spring内容可以理解、使用的BeanDefinition。
- c. 注册:加载过配置文件后,就将BeanDefinition信息注册到BeanDefinitionRegistry接口中,通常情况下Spring容器的实现类都实现这个接口。注册其实就是把beanName和beanDefinition作为键值对放到beanFactory对象的map。
6.7 SpringBoot的启动过程
- 首先从main找到run()方法,在执行run()方法之前new一个SpringApplication对象
- 进入run()方法,创建应用监听器SpringApplicationRunListeners开始监听
- 然后加载SpringBoot配置环境(ConfigurableEnvironment),然后把配置环境(Environment)加入监听对象中
- 然后加载应用上下文(ConfigurableApplicationContext),当做run方法的返回对象
- 最后创建Spring容器,refreshContext(context),实现starter自动化配置和bean的实例化等工作。
6.8 Springboot SPI机制
- SPI的全名为Service Provider Interface.这个是针对厂商或者插件的。
- SPI的思想:系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制
6.9 Springboot 事务传播机制
- 指封装在数据库事务之上的一种事务处理机制。其管理方法有两种,分别是编程式事务以及声明式事务。一般我们使用@Transactional进行声明式事务。
- 事务的传播,是指一个方法调用另一个方法并将事务传递给它。事务的转播机制主要针对被调用者而言,控制它是否被传播或者被怎样传播。spring事务的传播机制有七种:
6.10 SpringMVC工作流程
- Web容器启动,通知Spring进行初始化容器,加载Bean的类信息,初始化单例Bean
- 遍历所有Bean,获取每一个Controller中每一个方法的 URL,把 Controller和相应的 URL保存到一个Map集合中
- 把集合转发给 DispatcherServlet(前端控制处理器),DispatcherServlet请求 HandlerMapping找到所有标注了 Controller的类和 RequestMapping的方法和类,生成 Handler(处理器)和 HandlerInterceptor(拦截器)(如果有的话生成),再以HandlerExcutionChain(处理器执行链)的形式返回。
- 根据 Handler找到 HandlerApapter(处理器适配器),执行方法处理请求并返回 ModelAndView给 DispatcherServlet
- DispatcherServlet将 ModelAndView转发给 ViewResolver(视图解析器)进行视图的解析于渲染。并将渲染后的视图返回给 DispatcherServlet
- DispatcherServlet 将视图返回给客户端
6.11 SpringBoot中的常用注解
- 1:@SpringbootBootApplication:@SpringBootApplication是springboot中最核心的注解,写在启动类的上面。它是@Configuration、@EnableAutoConfiguration和@ComponentScan的组合注解。@Configuration指示一个类声明一个或者多个@Bean 声明的方法并且由Spring容器管理,@EnableAutoConfiguration将SpringBoot中所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器,@ComponentScan扫描定义路径下的bean。
- 2:@ImportResource:用来加载xml配置文件
- 3:@AutoWired:自动导入依赖的bean。
- 4:@Value:注入springboot中application.properties配置文件中配置的属性值。
- 5:@RestController:主要作用于Controller的类上,它是@Controller和@ResponseBody的组合注解,主要用于返回json数据。
- 6:@ResponseBody:主要作用于控制层的类上,主要用于返回json数据。
- 7:@Data:主要作用于实体类上,编译后可以自动加上get、set、toString、equals方法等,减少我们实体类代码的书写,增加可阅读性。
- 8:@Bean:用@Bean标注方法等价于XML中配置的bean。放在方法上面,而不是类,产生一个bean,交给spring管理。
- 9:@RequestMapping:主要作用于Controller类及方法上,主要作用是请求地址的映射,当然,其中还有method属性等,method属性主要是请求类型,比如post、get等,value = RequestMethod.GET。
- 10:@Mapper:主要作用于DAO接口上,可以自动生成接口的实现类。
- 11:@Id:表示该属性为主键
- 12:@MapperScan:主要作用于启动类上,用于生成DAO接口的实现类,如果DAO接口比较多,推荐使用@MapperScan注解,写法如@MapperScan(“com.example.demo.dao”).
- 13:@PathVariable:主要是用于取url中的变量的值,比如 @RequestMapping(“/student/{studentName}”),那么在对应的方法入参中可以写成:(@PathVariable String studentName).
- 14:@RequestParam:将请求参数绑定到Controller的方法上面,@RequestParam(value=”参数名”)。
6.12 Spring应用的设计模式
- 工厂模式 : Spring 使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
- 代理模式 : Spring AOP 功能的实现。
- 单例模式 : Spring 中的 Bean 默认都是单例的。
- 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
- 装饰模式 : 项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式可以根据客户的需求能够动态切换不同的数据源。
- 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式 : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配 Controller。
6.13 @Autowired和@Resource的区别
- 1、 @Autowired与@Resource都可以用来装配bean. 都可以写在字段上,或写在setter方法上。
- 2、 @Autowired默认按类型装配(这个注解是属业spring的),默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用
- 3、@Resource(这个注解属于J2EE的),默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行安装名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
- 推荐使用:@Resource注解在字段上,这样就不用写setter方法了,并且这个注解是属于J2EE的,减少了与spring的耦合。这样代码看起就比较优雅。
6.14 在事务中切换数据源的操作
- spring是通过DataSourceTransactionManager对事务进行管理的,当我们在执行事务方法时,会通过AOP机制先执行DataSourceTransactionManager的doBegin()方法;
- 其中doBegin方法中的Connection newCon = obtainDataSource().getConnection()是用来获取具体的数据库connection,随着断点对obtainDataSource().getConnection()进行跟进,发现它最终会调用AbstractRoutingDataSource的getConnection()方法;
- 看到这个方法,我们应该就会很熟悉了,这个方法中的determineTargetDataSource()上面已经说过,它是根据determineCurrentLookupKey()决定当前线程使用哪个数据源。
- 通过上面的分析我们可以知道事务方法开始前就会根据当前数据源获取一个数据库连接connection,这个connection在当前线程的上下文中进行缓存,事务方法结束前都是可复用的,不然如何保证数据的事务特性,所以我们在事务的过程中是不需要更新connection的,不更新connection也就不会执行AbstractRoutingDataSource的getConnection()方法,从而不会更新数据源。这里需要说明的是:spring事务不仅仅针对加了@Transactional注解的方法,对数据库进行增删改的时候,spring也是通过DataSourceTransactionManager进行管理的。
- 上图是mybatis执行器,比如SimpleExecutor,创建Statement获取connection时的一段源码分析,通过分析可以发现,执行器在获取connection时,会根据当前线程上下文是否有connection,来决定进行connection复用,还是通过fetchConnection()获取connection,其中fetchConnection()是根据AbstractRoutingDataSource的getConnection()方法获取connection。因此,我们要想切换数据源只能在事务开启前进行切换,我们知道事务也是基于AOP实现的,这样我们可以把切换数据源的操作也放在AOP中,然后设置该AOP的order高于事务即可,比如,在该AOP中添加@Order(-1)注解。