Spring

365 阅读16分钟

Spring jar包运行原理

使用java -jar执行SpringBoot应用jar包,它会自动读取META-INF/MANIFEST.MF文件中的Main-Class,就表示了应用程序启动类的信息。然后它会创建类加载器来加载class文件,然后根据启动类信息,通过反射执行main方法,完成SpringBoot应用的启动。

Spring IOC

在普通的应用程序中,我们都是在类中主动创建对象,造成了类与类之间的强耦合。而Spring提供IOC容器,把创建和注入对象的控制器交给了IOC容器。Spring会先标记所有的Bean类,然后在运行期动态地将依赖关系注入到对应组件中。也就是说,所有类的创建、销毁都由交由Spring控制。IOC是在Spring容器启动时,也就是bean的初始化时实现的。

Spring AOP

AOP是面向切面编程,OOP是面向对象,AOP与OOP是对工程结构的不同视角。在OOP中,我们以class作为基本单元。在AOP中,我们以Aspect切面作为基本单元。切面由pointcut切点和advice通知组成,它既包含横切逻辑的定义,也包含了连接点的定义。而Spirng AOP就是负责实施切面的框架,它将切面所定义的横切逻辑,织入到切面所指定的连接点中。

切面:@Aspect注解修饰的类就是切面。

advice增强:由切面添加到特定的连接点(满足切入点规则的连接点)的一段代码,我们可以把advice理解为一个拦截器,可以在join point连接点上维护多个advice,进行层层拦截。advice有before、after return、after throwing、after final、around等。

join point连接点:程序运行中的一些时间点。比如一个方法的执行,或者一个异常的处理等。

point cut切点:advice与特定的切点相关联,并且在与point cut所提供的规则匹配的连接点中执行。advice增强是在join point上执行的,而point cut规定了哪些join point可以执行哪些advice,满足point cut规则的join point,就会被添加相应的advice操作。它们是不同维度上的东西。

Spring AOP采用运行时代理的方式来实现aspect切面,所以在为连接点织入advice后,会产生一个代理类。(JDK或CGLIB动态代理)SpringAOP是默认使用JDK动态代理的,但如果一个业务逻辑对象没有实现接口,那SpringAOP就会使用CGLIB来进行AOP代理。所以Spring AOP利用CGlib和JDK动态代理等方式,实现运行期的动态方法增强,将与业务无关的代码单独抽离出来,让业务逻辑本身变得更加内聚,降低系统的耦合性。

SpringAOP的使用

告诉Spring哪个是切面类(@Aspect),使用一个void方法来表示point cut切入点,然后为@~advice增强方法指定切入点表达式,告诉Spring通知是何时何地运行的。(不需要在配置类中开启AOP注解模式@EnableAspectJAutoProxy,因为引入AOP依赖后它默认开启)

强制使用CGLIB代理

<aop:aspectj-autoproxy proxy-target-class="true"/>

@EnableAspectJAutoProxy(proxyTargetClass=true)

SpringAOP的源码

@EnableAspectJAutoProxy中导入了AnnotationAwareApsectJAutoProxyCreator,他继承了AwareBeanPostProcessor,会在Spring初始化时被执行;它重写了父类的setBeanFactory()方法,完成了它的自动装配。

AnnotationAwareAspectJAutoProxyCreator会在所有bean创建时进行拦截,它会判断bean是否被增强(也就是该bean是否满足切点的规则)。若满足,就要为切面类创建一个代理对象。Spring AOP会判断是使用JDK动态代理还是CGLIB代理(对接口生成JDK动态代理、对类生成CGLIB代理),然后生成对应的代理bean。后续执行bean中的方法,就是执行被增强的方法了。

JDK动态代理和CGLIB动态代理

动态代理是在运行时动态生成代理类,由代理类完成具体方法的增强。

JDK动态代理面向接口

代理类和目标类实现同一个接口,代理类通过InvocationHandler+反射的方式(proxy.invoke()),实现对接口方法的拦截,完成对目标方法的代理与增强(Proxy.newProxyInstance(classLoader,implement[],handler)得到代理对象)

CGLIB动态代理基于类的继承

代理类继承目标类,代理类通过MethodInterceptor+反射的方式,实现对父类方法的拦截,完成对目标方法的代理和增强。

我们实现MethodInterceptor接口的interceptor(...)(proxy.invokeSuper()代理)方法。所以我们要通过Enhancer类设置代理类要继承的父类,进行类加载,调用interceptor(...)方法,最终返回被代理对象。

Spring bean作用域

singleton:在ioc容器中保持单例

prototype:每次getBean()时都会创建一个对象。

request:每次HTTP请求都会创建一个对象,但只在本次请求中有效。

session:在http session中保持单例。

globalSession:在全局session中保持单例。 设置单例: ,在applicationConext.xml中。

@Scope(value="")

@Autowired的原理

在Bean加载的过程中,A类的生命周期执行到了依赖注入,则会先拿到所有同类型的候选类,然后根据@Qualifier(name)->@Primary(bean)→@Priority(1、2)→同类型的优先级顺序选择候选类,注入到成员变量中。若不为循环依赖,则使用BeanFactory反射构造出被依赖的Bean,然后把被依赖的Bean填充入加载Bean的成员变量中。若为循环依赖... 补充:@Resource(name="",type="")是依据同name属性的bean类注入。

@Configuration和@Component的区别

1.@Comonent:Spring容器加载时会扫描@Component类并把它作为bean对象放入Spring容器中,但是没有后置处理器处理,于是就以正常形式完成bean的生命周期。

2.@Configuration类会在Spring初始化时被解析,然后放入Spring容器中。在Spring加载后置处理器时,有个configurationClassPostProcessor专门处理@Configuration注解的类,该后置处理器使用cglib动态代理增强类中的@Bean方法,并把@Bean方法的返回值放入Spring容器中,然后返回代理对象。所以@Configuration类在完成bean生命周期后生成的是代理类bean对象。

3.@Component中的@Bean方法返回值不会被注入到Spring容器中,@Configuration中的@Bean方法返回值会被注入导Spring容器中。

Spring事务

Spring事务包含两种配置方式:一种是使用XML进行模糊匹配,绑定事务管理;一种是使用@Transactional注解,为每个需要进行事务处理的方法单独配置。Spring在初始化时,会扫描到使用事务的方法,然后创建一个当前Bean对应的代理对象,使用TransactionInterceptor拦截代理方法。当执行代理类的目标方法时,会触发拦截器底层的invoke(),进行事务操作。成功则提交,失败则把捕获到的异常与@Transaction()注解中的rollbackFor属性中配置的异常做对比,比对成功则回滚。

Spring事务的7种传播行为

propagation_required:若上下文已经存在了事务,就加入事务中执行;若上下文中不存在事务,则新建事务执行。Spring默认的事务传播行为。

propagation_supports:若上下文已经存在事务,则加入事务中执行;若上下文中不存在事务,则使用非事务的方式执行。

propagation_manatory:要求上下文中必须存在事务,若不存在则抛异常。

propagation_requires_new:每次执行都要新创建一个事务,并且将上下文中的事务挂起。执行完新事务后,再恢复上下文事务的执行(发红包场景:系统的初始化与校验等、发红包、记录日志。那么发红包的子事务不会影响到父事务的提交和回滚)

propagation_not_supported:每次执行都要判断当前上下文是否有事务,若有事务则挂起,然后执行自己的逻辑,结束后再恢复上下文事务。可以将事务的范围缩小,避免一些不必要的异常。

propagation_never:上下文一旦有事务,就抛出异常。

propagation_nested:若上下文中存在事务,则嵌套事务执行;若不存在事务,则新建事务。(嵌套事务就是子事务在父事务中执行,进入子事务之前,父事务会建立一个回滚点,然后执行子事务,子事务执行结束后,父事务继续执行。只有在父事务提交后,字事务才会被随着提交。)

@Transactional失效场景

①在protect、private修饰符的方法上失效

动态代理只作用于public方法,因为private方法代理类访问不到,而JDK动态代理不支持protected,所以Spring事务就干脆只支持public方法了。

②在非@Transactional方法中调用事务方法,事务方法的事务代理不会生效

因为非事务方法就说明Spring并没有扫描到这个方法的@Transactional,也就不会为这个方法创建代理类,也就用略过其中的AOP事务。

解决;使用BeanPostProcessor / ThreaLocal 把目标bean改为代理类类型。

@Transactional默认只对RuntimeException和Error回滚,不对受检异常回滚(IOExecption等)

若我们调用@Transaction方法A,A会抛异常,我们在A之外捕获处理,Spring依然会回滚,因为异常已经抛到了代理层。若一个事务方法中有ab两个方法,a没有异常,b有异常但未处理,则会抛到代理层从而回滚a的正确结果,我们可以改变@Teansactional的传播行为为propagation_requires_new,让b方法开辟一个独立的子事务执行。

bean加载

①Spring初始化时,会实例化Spring单例池和bean工厂,然后扫描并解析配置类和组件类。然后把扫描到的类信息封装到BeanDefinition对象中,把BD对象放入BeanDefinitionMap中。

②Spring会取出DBMap中的BeanDefinition,调用各种后置处理器对它们进行验证。在验证完成后,Spring会把beanName放入一个叫SingletonsCurrentlyInCreation集合中,表示此bean正在创建,用于解决循环依赖。

③然后Spring会为每个BeanDefinition反射创建出Java对象,封装到BD对象中,然后封装入BDFactory工厂。此外还会判断BeanDefinition是否支持循环依赖,若支持则放入singletonFactories三级缓存中。

④完成BeanDefinition对象的依赖注入。

⑤回调各种方法:Aware接口→BeanPostProcessor→生命周期方法(Aware→ PostProcessBeforeIntialization() → [@PostConstruct→InitializingBean接口→《init-method》] → postProcessAfterInitialization())

⑥进行AOP代理,得到bean对象并放入singletonObjects单例池中

⑦发布ContextRefreshedEvent事件。ApplicationListener监听器可以监听此事件

循环依赖

概念

两个类在初始化时都需要把自己注入到对象的属性中,发生循环依赖。只有单例对象才会发生循环依赖,因为原型对象在Spring容器初始化时不会被创建,只有在使用它时才会创建,即不存在循环依赖。

流程

Spring初始化的过程中,A类的生命周期执行到了依赖注入,那么Spring会判断A是否支持循环依赖,若支持则把A封装成工厂并放入三级缓存中。然后进行A的依赖注入。假设A依赖B,但B并没有初始化完成,即getBean(B)失败。那么Spirng会去判断B是否处于SingletonsCurrentlyInCreation集合中 && 是否处于三级缓存中。

若不在,则开始执行B的生命周期。依旧是执行到依赖注入时,得到A的bean对象会失败,然后判断A是否处于三级缓存中,是!所以从三级缓存中通过bean工厂得到A的实例对象注入B中,完成B的生命周期。此时再返回到A的生命周期,注入B,完成循环依赖。

若在,则从三级缓存中取出B的实例对象,直接注入到A中,完成循环依赖。

三级缓存

一级缓存:SingletonObjects,也就是Spring单例池,存放已经创建好的bean对象。

二级缓存:earlySingletonObjects,属于三级缓存的缓存,存放bean工厂和bean对象。如果bean对象被AOP代理,那么每次从三级缓存中拿工厂时,工厂会得到一个代理类,继而创建一个新的代理对象,破坏的bean的单例。所以使用二级缓存,每次只取二级缓存中缓存的bean对象,保证bean的单例。

三级缓存:singletonFactories,存放bean工厂,提前暴露对象来解决循环依赖、为因为循环依赖注入而被提前注入的对象进行AOP。

Spring MVC执行流程

①Spring启动时会初始化DispatcherServlet,加载DispatcherServlet.properties中的配置类,然后执行八个初始化方法。其中的两个init()方法会把HandlerMapping排序放入HandlerMappings集合中,把HandlerAdapter排序放入HandlerAdapters。

②客户端发起请求会映射到@RequestMapping上,然后DispatcherServlet会接收到此请求,从HandlerMappings集合中拿到处理此请求的HnadlerMapping,取出其中的执行器放入执行器链中;然后再从HandlerAdapters集合中取出对应的HandlerAdapter子类,执行这个执行器链:

对于Controller接口类型的控制器:直接把请求对象强转成Controller对象并执行接口方法。

对于@Controller类型的控制器:Spring在初始化时会扫描@Controller,并把所有的url - method映射放入一个UrlLookup的Map中。所以我们可以通过请求的URL得到Method对象,根据@RequestParam到的参数,通过策略模式进行形参匹配,然后反射执行方法,处理请求。 ③处理请求后得到返回值,对应的解析器订阅了不同类型的返回值(观察者模式),那么我们得到了返回值解析器。解析器的内部就是通过责任链模型来选择各种MessageConverter()消息处理器来返回的JSON或ModelAndView。

SpringBoot

SpringBoot提供了什么:SpringBoot内置了Tomcat、SpringBoot提供了很多现成的Starters、SpringBoot可以简化配置(Application.yml->/config->classpath:/)。

自动配置原理

前置知识:SpringBootStarter就是约定大于配置,我们只要在maven中引入starter的依赖,那SpringBoot就是自动扫描到starter所依赖的所有信息,然后通过自动装配为我们把这些依赖加载到Spring容器中。(starter需要@EnableConfigurationProperties 与 @ConditionalOnXXX,还要把..EnableAutoConfiguration - 类路径写到Spring.factories中代表它是自动装配的配置信息)

@SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan

①Spring启动时会扫描到@SpringBootApplication,@EnableAutoConfiguration中有注解@Import(AutoConfigurationImportSelector.class),它会先去扫描META-INF/spring-autoconfigure-metadata.properties下的元数据信息,然后再去使用SpringFactoriesLoader去扫描META-INF/Spring.factories下的候选加载类。然后Spring会根据每个候选加载类的元数据信息@ConditionalOnXXX,过滤掉一些不满足条件注解的候选类(比如这个候选类需要依赖另一个类,而没有找到另一个类)。

那经过过滤后,就剩下需要自动注入的候选类了。这些候选类上还会有@EnableConfigurationProperties(),Spring会把注解中的Properties类加入进Spring容器中,然后绑定到对应的后置处理器上,用于回调处理。最后

Springboot定时任务

@EnableScheduling + @Scheduled(cron="")

@EnableScheduling是通过@Import把后置处理器放入Spring容器中,后置处理器扫描@Scheduled,然后把对应的任务封装成事件并发布,由Spirngboot事件机制得到任务,通过一个@bean(name="taskScheduler")的单任务Scheduler线程池执行任务。

@Scheduled对线程池类型调用的优先级是:SchedulingConfigurer接口中的自定义线程池 → @Bean()+ThreadPoolTaskScheduler类型的线程池 → @Bean()+ScheduledThreadPoolExecutor类型的线程池 → 默认bean。

只使用@Scheduled(),它默认是单线程的,所以多个定时任务会引发线程阻塞。那加上@Async和@EnableAsync,但它仅仅是开辟其他线程来进行异步操作,但@Scheduled内部还是默认的SingleThreadScheduledExecutor,所以并没有改变它的单线程性质!而且如果任务的执行时间超过了调度时间,那异步线程也不会管,而是直接开启下一个任务的异步操作。所以就可能导致任务交叉。

所以我就重写了@Scheduled的线程池,使用自定义的ThreadPoolTaskSchedule线程池@Bean(name="taskScheduler")来代替默认的单线程线程池。

Spring整合Mybatis

Spring在启动时会触发BeanDefinitionRegisterPostProcessor后置处理器,它会手动创建一个BeanDefinition,封装成一个工厂Bean。而且在执行此方法时传入了@MapperScan的元数据,那也就意味着扫描到了Mapper.class。所以Spring把MapperClass对象放入这个BeanDefinition中当作参数。FactoryBean把MapperClass封装到有参构造函数中,Spring就会完成自动注入了。那这个FactoryBean就得到了所有的Mapper,然后后置处理器会把这个BeanDefinition放入BeanDefinitionMap中,完成Spring初始化后,Mapper也就进入Spring容器了。

过滤器和拦截器的区别

image.png 过滤器是在请求进入tomcat容器,但还没进入servlet生命周期时触发的。拦截器是在进入servlet生命周期后执行的。

所以过滤器只能在容器初始化时被调用一次,拦截器可以被执行多次。

过滤器是基于职责链的函数回调形式实现的,而拦截器是基于Java反射实现的。

过滤器无法获取spring bean,而拦截器可以获取(HandlerInterceptor-preHandler(res,repo,handler))。

使用层面

两个bean相同类型,应该如何注入?:Qualify(value="") 指定、@Autowired(byteName="")

从URL地址里获取参数用哪个注解

@PathVariable:/user/getUserById/2

@RequestParam:/user/getUserById?id=2

MyBatis使用:

① MyBatis二级缓存 

1) 二级缓存以namespace为单位,增删改会清空namespace下的所有缓存。而如果有两个namespace对某个表都有操作,就会导致缓存结果不一致。

② 说说你对MyBatis的理解?

③  JDBC连接数据库步骤

1) 加载JDBC驱动类、创建数据库连接、创建PreparedStatement执行SQL、若有结果集则遍历操作、关闭结果集、PS、Conn

④ mybatis怎么创建一个数据库的?使用Mybatis-Generator

⑤ MyBatis中#{}和${}的区别?

1) #{}是?占位符,使用SQL预编译,可以防止SQL注入

2) ${}是+拼接,使用SQL拼接直接编译

⑥ Maven使用过吧,讲一下Maven与远程仓库的登录校验要在哪里完成

1) Settings.xml中的标签中

⑦ Mybatis的自增主键怎么设置,如何设置表的关联

1) 

2) @Insert() + @Options(useGeneratedKeys=true,keyProperty=””,keyColumn=””)

(2) 同类中的方法相互调用是否会被AOP、Transactional拦截?

① 不会,因为拦截是由代理bean拦截的。而在一个方法类中调用另一个方法,是执行的this.xxx(),也就是由原来的bean调用,不是代理对象。