面试漫谈(十)- Spring篇

Posted by YiBo on December 17, 2020

SpringMVC的工作原理

由以下组件构成:

  • DispatcherServlet前段控制器:整个流程的控制中心,接受请求,响应结果
  • HandlerMapping处理器映射器:根据url寻找Handler
  • HandlerAdapter处理器适配器:按照规则去执行Handler
  • Handler处理器:Controller
  • ViewResolver 视图解析器

具体流程

  1. 用户发送请求到前端控制器DispatcherServlet
  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器
  3. 处理器映射器找到具体的处理器(注解或者xml配置),生成处理器以及处理器拦截器,返回给DispathcerServlet
  4. DispatcherServlet调用HandlerAdapter处理器适配器
  5. HandlerAdapter经过适配调用具体的处理器Controller
  6. Controller执行完成返回ModelAndView
  7. HandlerAdapter将Controller返回结果ModelAndView返回给DIspatcherServlet
  8. DispatcherServlet将ModelAndView传给ViewReslover试图解析器
  9. 视图解析器解析后返回具体的view
  10. DispatcherServlet根据view进行渲染
  11. DispatcherServlet 响应用户

Spring 常用注解


@Component 一个标准普通的spring bean类

@Repository 标注一个DAO组件类

@Service 标注一个业务逻辑组件类

@Controller 标注一个控制器组件类

以上四个注解的用法相同,功能相同,区别在于标识组件类型,component可以代替repository service Controller,因为这三个注解是被component标注的


@Autowired 可用于为类的属性,构造器,方法进行注值。增加@Qualifier进行限定,比如要引入一个接口,但是接口有好多实现类,就需要用Qualifier进行限定

@Resource 为目标bean指定协作者bean

@PostConstruct @PreDestroy 实现初始化和销毁bean之前进行的操作


@Bean 主要用于方法上,可以连续使用多种定义bean的注解,比如@Qualifier注解定义工厂方法的名称,用@Scope注解定义该bean的作用域范围,如singleton 还是 Prototype

@Configuration 配置类,标识可以被spring的IOC容器所使用


@Controller

@RequestMapping 将url映射到整个处理类或者特定的处理请求的方法

@RequestParam 请求的参数绑定到方法中的参数上

@PathVariable 用于修饰方法参数,会将修饰的方法的参数变为可供使用的url 变量

@RequestBody

@ResponseBody

@RestController 避免重复写 @RequestMapping @ResponseBody


@Transactional


@SpringBootApplication 包含了很多注解:

  • target 用于描述注解的使用范围
  • Retention 指定保留时间
  • Documented 可别javadoc等工具记录
  • Inherited 一个成员变量,父类被子类覆盖后无法继承,子类中可以继承父类中未被覆盖的父类注解的值
  • SpringBootConfiguration - Configuration - Componet 让Spring容器扫描
  • EnableAutoConfiguration 开启自动配置 - AutoConfigurationPackage 自动配置包含注解的类 import(AutoConfigurationImportSelector.class) 自动配置导入选项 –》 getCandidateConfigurations() –> 返回标注了 EnableAutoConfiguration 的所有包 ==》 将启动类所需的所有资源导入
  • ComponentScan 包扫描,默认扫描同级以及当前包下内容 - Repeatable 允许在同一申明类型(类,属性,方法)的多次使用同一个注解

Spring事务的传播行为了解吗

事务传播行为是某一个事务传播行为修饰的方法被嵌套进另一个方法时事务如何传播

1
2
3
4
5
6
7
8
9
 public void methodA(){
    methodB();
    //doSomething
 }

 @Transaction(Propagation=XXX)
 public void methodB(){
    //doSomething
 }

分为7种传播行为

image-20201214214200875

  1. PROPAGATION_REQUIRED
    • 外围方法未开启事务的情况下,PROPAGATION_REQUIRED修饰的内部方法会开启自己的事务,互不干扰
    • (常用)外围方法开启事务的情况下,REQUIRED修饰的方法会加入到外围事务中区,只要有一个方法回滚(即使是被catch),整个事务均回滚
  2. PROPAGATION_REQUIRES_NEW
    • 外围未开启事务;内部方法会新开自己的事务,相互独立
    • 外围开启事务,同上
  3. PROPAGATION_NESTED
    • 外围未开启事务;新开自己的事务,互不干扰
    • 外围开启事务,外围回滚,子事务一定回滚。而内部事务可以单独catch回滚
  4. PROPAGATION_SUPPORTED

举个例子:用户积分失败,注册不会被打断。用户注册失败,用户积分一定回滚。用户积分日志失败,不会干扰外部积分和注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
   @Service
   public class MembershipPointServiceImpl implements MembershipPointService{

        @Transactional(propagation = Propagation.NESTED)
        public void addPoint(Point point){

            try {
                recordService.addRecord(Record record);
            } catch (Exception e) {
               //省略...
            }
            //省略...
        }
        //省略...
   }


@Service
   public class RecordServiceImpl implements RecordService{

        @Transactional(propagation = Propagation.NOT_SUPPORTED)
        public void addRecord(Record record){


            //省略...
        }
        //省略...
   }

另一个问题,在同一个类中调用带有transaction的类会失效,因为调用的是类本身的方法,而不是动态代理的增强类

SpringBoot是什么

相比于Spring,Springboot简化了Spring的过程,蚕蛹config的方式对spring进行配置,可以配置application.yml,整体思想是有定义的,自定义有限,否则走默认配置。俩种设计策略:

  • 开箱即用:在开发过程中,通过Maven项目的pom文件中添加相关依赖包,通过相应的注解来代替繁琐的XML配置以管理对象的生命周期

    主要说说pom文件,其中spring-boot-dependencies作为父工程,存放了springboot的依赖核心。所以在引入一些其他依赖,不需要指定版本,以及一些配置。比如spring-boot-starter-web,存放了自动配置相关的依赖,日志相关依赖,海域spring-core等依赖。springboot会将所有的功能场景都封装成一个个启动器

    1
    2
    3
    4
    5
    6
    7
    
    //@SpringBootApplication 标注,是一个SpringBoot应用
    @SpringBootApplication
    public class SpringbootdemoApplication {
    	public static void main(String[] args) {
    		SpringApplication.run(SpringbootdemoApplication.class, args);
    	}
    }
    

    @SpringBootApplication

    当我们的springboot项目启动的时候,会先导入AutoConfigurationImportSelector 这个类会帮我们选择所有候选的配置,我们需要导入的配置都是springboot帮我们写好的一个个配置类,那么这些配置类的位置,存在META-INF/spring.factories文件中,通过这个文件,spring可以找到这些配置类的位置,于是加载其中的配置

  • 约定大于配置:减少了开发人员需要作出决定的数量,减少了大量的XML配置,并且可以将代码编译,测试和打包等工作自动化

    在yml中配置的数据,通过@ConfigurationProperties映射到类中,然后再通过自动配置类,将这些属性配置到配置类中

为什么要有AOP

java是面向对象编程,需要引入公共部分的时候,会有大量重复代码,AOP可以横切代码封装到一个可重用的模块中,降低模块间的耦合度,有利于后续维护

AOP是什么

实现横切代码(权限,日志等)

比如我要实现一个接口的权限,只需要将这些代码放到切片中,定一下切片,连接点,和通知

  • pointCut 在哪里写入:execution(* com….aop.Service(..))用正则表达式来定义范围内的类,那些接口就会被当成切点
  • Advice 定义了AOP何时被调用
    • before
    • after
    • after-returning
    • after-throwing
    • around 在被通知的方法调用之前和调用之后调用
  • joinPoint 就是对应业务的方法对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Aspect
@Component
public class AuthAspect{
  // 切点 "execution(* com.aop.*Service.*(..)"
  // advice : before
  @Before("execution(* com.aop.*Service.*(..)")
  // 连接点 joinPoint
  public void before1(JoinPoint joinPoint){
    MethodSingnation singature = (MethodSignature)joinPoint.getSingnature();
    Mehtod method = singature.getMethod();
  	// 通过自定义注解,将实际的参数传递过来  
    AuthPermission autoAnno = method.getAnnotation(AuthPermission.class)
    ...
  }
}

@Retention(value = RententionPolicy.RUNTIME)
@Documented
public @interface AuthPermission{
  int idx() defualt 0;
}

@AuthPermission
public void buyItem(int userId){
  
}

@SpringBootApplication
public class SpringDemoApplication{
  public static void mian(Stirng[] args){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringDemoApplication.calss);
		....    
  }
}

Spring AOP 做了什么

首先AnnotationConfigApplicationContext是一个用来管理注解bean的容器,所以我可以用该容器取的我定义了@Service注解的类的实例

CGLIB是一种动态代理技术,先将代理对象类的class文件加载进来,之后通过修改其字节码并生成子类

是在容器生成的时候,扫描@Aspect注解标识的类,构建成一个Advisor对象,保存进BeanFdactoryAspectJAdvisorsBuilder.advisorsCache中….

通过AbstractAutoProxyCreator构建强化对象调用了createAopProxy好眼熟,如果targetClass有时限接口或者是Proxy的子类,使用JDK动态代理,如果不是会使用CGLIB。springboot2.0开始默认CGLIB代理

JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,无法代理一个没有接口的类

CGLIB主要对指定的类生成一个子类,并覆盖其中的方法

为什么不用静态代理

需要构造多个proxy类

JDK动态代理原理

1
2
3
JdkProxy proxy = new JkdProxy(new Hello());
IHello hello = proxy.getProxy();
hello.say();

hello调用say的时候,hello已经被代理了,所以在调用say函数的时候其实调用的是jdkProxy类中的invoke函数,invoke函数先实现了before函数才实现了主方法

CGLIB动态代理原理

1
2
Hello helloProxy = CGlibProxy.getInstance().getProxy(Hello.class);
helloProxy.say();

say函数调用CGlibProxy类中的intercept函数

这里和JDK不一样,JDk是通过method.invoke(target,args)触发目标函数,去对应的method以及args.因为传过来的obj不是hello对象,而是被代理过的对象,无法像JDK直接通过反射搞定

为什么JDK动态代理需要实现接口

JKDPROXY类取的代理类的方法是: (T)Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this)

而CGlibProxy获取代理的方式(T)Enhancer.create(cls,this)

IOC怎么理解

IOC Inversion of Control 是一个技术思想,不是一个技术实现

IOC思想下的开发方式:不用自己去new对象了,而是通过IOC容器去帮主我们实例化对象并管理它

控制:指的是对象创建的权利 反转:控制权交给了外部环境IOC容器

解决了什么问题:

  • 解决了对象之前的耦合问题

DI Dependancy Injection 和 IOC描述的是同一件事情,角度不同。IOC是站在了对象的角度,对象实例化其管理的权利交给了容器,DI是站在容器的角度,会把对象依赖的其他对象注入

IOC容器初始化的流程

通过ClasspathXmlApplicationContext为例

  1. 未设置延迟加载的前提下,bean的创建是在容器初始化工程中完成的
  2. 对象的构造函数调用时机:AbstractApplicationContext#refresh方法的finishBeanFactoryInitialization(beanFactory)处
  3. InitializingBean 之 afterPropertiesSet : 同上
  4. 分析BeanFactoryPostProcessor 初始化和调用情况
  5. 分析 BeanPostProcessor 初始化和调用情况

image-20201222115955744

Spring IOC 容器初始化主流程

有上述分析,SpringIOC 容器初始化的关键环节就在 AbstractApplicationContext#refresh()方法中

  1. 刷新前的预处理
  2. 获取BeanFactory 加载beanDefinition注册到 BeanDefinitionRegistry
  3. BeanFactory的预备工作(进行一些设置,比如Context的类加载器)
  4. beanFactory准备工作完成后进行的后置处理工作
  5. 实例化并调用实现了BeanFactoryPostProcessor接口的bean
  6. 注册BeanPostProcessor 在创建bean的前后执行
  7. 初始化 MessageSource 组件(做国际化功能)
  8. 初始化事件派发器
  9. 子类重写方法,在容器刷新时可以自定义逻辑
  10. 注册应用的监听器
  11. 初始化所有剩下的非懒加载的单例bean
    1. 初始化创建非懒加载方式的单例bean实例
    2. 填充属性
    3. 初始化方法调用(比如afterPropertiesSet init-method方法)
    4. 调用BeanPostProcessor对实例bean进行后置处理
  12. 完成Context的刷新

BeanFactory和ApplicationContext的区别

BeanFactory是Spring框架中IOC容器的顶层接口,它只是用来定义一些基础的功能,实现一些基础规范,而ApplicationContext是它的一个子接口,比BeanFactory要有更多的功能,比如说国际化支持和资源访问(xml等)等等

image-20201222112108177

ApplicationContext下还有三个比较重要的类,也是Spring容器的启动方式:

  • ClassPathXmlApplicationContext 从类的根路径下加载配置文件0
  • FileSystemXmlApplicationContext 从磁盘路径上加载配置文件
  • AnnotationConfigApplicationContext 纯注解模式下启动Spring容器

Spring IOC高级功能

  1. Lazy-init 延迟加载

    Bean的延迟加载,ApplicationContext容器的默认行为是在启动服务器时将所有singleton bean 提前进行实例化

    Spring启动会把所有的bean信息(包括XML和注解)解析转化成Spring能够识别的Beandefinition并存到hashmap里,供下面的初始化使用,然后对对每个beandefinition进行处理,如果是懒加载在容器初始化阶段不处理,其他的则在容器初始化阶段进行初始化并依赖注入

  2. FactoryBean 和 BeanFactory

    BeanFactory接口是容器的顶级接口,定义了容器的一些基础行为,负责生产和管理bean的一个工程,比如ApplicationContext

    Spring中bean有俩种,一种是普通的bean,一种是工厂bean,FactoryBean可以生成某一个类型的Bean实例,也就是可以借助于自定义Bean的创建工程。Bean创建的三种方法:静态方法,实例化方法,工厂方法,FactoryBean使用较多

  3. 后置处理器

    Spring提供了俩种后置处理bean的扩展接口,分别是 BeanPostProcessor 和 BeanFactoryPostProcessor

    • BeanFactoryPostProcessor

      BeanFactory -》 Bean对象

      在BeanFactory初始化之后可以shiy9ongBeanFactoryPostProcessor进行后置处理一些事情

      是针对整个bean的工厂进行处理。 该接口提供了一个方法,参数为 ConfigurableListableBeanFacotry ,有个实现方法:getBeanDefinition,可以根据此方法,找到我们定义bean的BeanDefinition对象。然后可以对定义的属性进行修改。

      ​ beanDinition 对象: 我们在XML中定义的bean标签,Spring解析bean标签成为一个javaBean这个javabean就是BeanDefinition

      调用BeanFactoryPostProcessor方法是,这个bean还没有被实例化,此时bean刚被解析成beanDefinition对象

    • BeanPostProcessor

      在bean对象实例化(并不是bean的整个生命周期完成)之后可以使用BeanPostProcessor进行后置处理一些事情

      该接口提供了来个方法,分别在bean的初始化方法之前和初始化之后执行,具体这初始化的方法可以是我们在定义了bean时,定义了init-method所指定的方法。处理是发生在Spring容器的实例化和依赖注入之后