跟随黑马面Java-4,框架篇

第四篇:框架篇

“求其上,得其中;求其中,得其下,求其下,必败”

新版Java面试专题视频教程,java八股文面试全套真题+深度详解(含大厂高频面试真题)_哔哩哔哩_bilibili

开篇


image-20240516162357487

Spring


Spring 框架中的单例bean是否线程安全?

Spring中的单例bean默认是单例模式(singleton),其不一定是线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
@RequestMapping("/user")
public class UserController{
private int count; // 成员变量需要考虑线程安全

@Autowired
private UserService userService;

@GetMapping("/getByld/{id}")
public User getById(@PathVariable("id") Integer id){
count++;
System.out.println(count);
return userService.getById(id);
}
}

在上述示例中,count是一个成员变量,需要考虑线程安全问题。而getById()方法中的形参id是局部变量,一般不会有线程安全问题。对于userService,通常情况下是无状态的类(Service/DAO),因此也不会存在线程安全问题。

单例bean被称为不安全的原因是因为在单例bean中定义了可变的成员变量,这些成员变量可能引发线程安全问题。要解决这个问题,可以采用多例模式(prototype)或者在单例bean中使用锁机制。

image-20240516164128760
Spring AOP的相关问题

AOP(面向切面编程)用于将那些与业务无关但对多个对象产生影响的公共行为和逻辑抽取并封装为一个可重用的模块,这个模块被称为“切面”(Aspect)。通过减少系统中的重复代码,AOP降低了模块间的耦合度,提高了系统的可维护性。

  • 常见的AOP使用场景

    • 记录操作日志

      1. 自定义注解Log

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        package com.rzcloud.annotation;

        import java.lang.annotation.*;

        /**
        * 自定义注解,用于记录日志信息。
        */
        @Target({ElementType.PARAMETER, ElementType.METHOD}) // 允许注解在参数和方法上使用
        @Retention(RetentionPolicy.RUNTIME) // 运行时保留注解
        @Documented // 注解会被包含在javadoc中
        public @interface Log {

        /**
        * 模块名称
        */
        String name() default "";
        }
      2. 创建切面类并配置

        1. 切点:注解com.rzcloud.annotation.Log

          1
          2
          3
          4
          @Pointcut("@annatation(com.rzcloud.annotation.Log)")
          private void pointcut(){

          }
        2. 通过环绕注解来获取用户名、登录的IP、时间、请求方式、访问的URL等信息,并将其保存到数据库中。

          1
          2
          3
          4
          5
          6
          @Around("pointcut()")
          public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
          // 获取用户名、登录IP、时间、请求方式、访问的URL等信息
          // 将信息保存到数据库中
          return joinPoint.proceed();
          }
    • 缓存处理

    • Spring中内置的事务处理

  • Spring中的事务是如何实现的

    Spring支持两种事务管理方式:编程式事务管理和声明式事务管理。

    • 编程式事务控制:使用TransactionTemplate实现,对业务代码有侵入性,因此项目中较少使用。

    • 声明式事务管理:建立在AOP之上,通过AOP功能对方法进行拦截,将事务处理的功能编织到方法中。在目标方法开始之前创建一个事务,在执行完目标方法后,根据执行情况提交或回滚事务。

      image-20240516170815925
image-20240516170913110
Spring中事务失效的场景

考察对Spring框架的深入理解、复杂业务的编码经验。

  • 异常捕获处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Transactional
    public void update(Integer from, Integer to, Double money) {
    try {
    // 转账的用户不能为空
    Account fromAccount = accountDao.selectByld(from);
    // 判断用户的钱是否够转账
    if (fromAccount.getMoney() - money >= 0) {
    fromAccount.setMoney(fromAccount.getMoney() - money);
    accountDao.updateByld(fromAccount);
    }
    // 异常
    int a = 1/0;
    // 被转账的用户
    Account toAccount = accountDao.selectByld(to);
    toAccount.setMoney(toAccount.getMoney() + money);
    accountDao.updateByld(toAccount);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    上述代码不会进行事务回滚,因为事务通知只有捉到了自标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉。解决方法就是在catch块添加throw new RuntimeException(e)抛出。

  • 抛出检查异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Transactional
    public void update(Integer from, Integer to, Double money) throws FileNotFoundException {
    // 转账的用户不能为空
    Account fromAccount = accountDao.selectByld(from);
    // 判断用户的钱是否够转账
    if (fromAccount.getMoney() - money >= 0) {
    fromAccount.setMoney(fromAccount.getMoney() - money);
    accountDao.updateByld(fromAccount);
    // 读取文件
    new FileInputStream("dddd");
    // 被转账的用户
    Account toAccount = accountDao.selectByld(to);
    toAccount.setMoney(toAccount.getMoney() + money);
    accountDao.updateByld(toAccount);
    }
    }

    上述代码不会进行事务回滚,即使其抛出了异常。因为Spring默认只会回滚非检查异常,而FileNotFoundException是检查异常;解决方法是配置rollbackFor属性:@Transactional(rollbackFor=Exception.class)。这样,只要有异常就都会回滚。

  • 非public方法导致的事务失效

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Transactional(rollbackFor = Exception.class)
    void update(Integer from, Integer to, Double money) throws FileNotFoundException {
    // 转账的用户不能为空
    Account fromAccount = accountDao.selectByld(from);
    // 判断用户的钱是否够转账
    if (fromAccount.getMoney() - money >= 0) {
    fromAccount.setMoney(fromAccount.getMoney() - money);
    accountDao.updateByld(fromAccount);
    // 读取文件
    new FileInputStream("dddd");
    // 被转账的用户
    Account toAccount = accountDao.selectByld(to);
    toAccount.setMoney(toAccount.getMoney() + money);
    accountDao.updateByld(toAccount);
    }
    }

    上述代码不会进行事务回滚,即使其抛出了异常。原因是Spring为方法创建代理、添加事务通知、前提条件都是该方法是public的。解决方案:改为public方法。

  • 同类中方法相互调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MyService {
    @Transactional
    public void methodA() {
    methodB(); // methodB的事务不会生效
    }

    @Transactional
    public void methodB() {
    // 事务操作
    }
    }
image-20240516190739807
Spring中事务传播行为
  1. PROPAGATION_REQUIRED: 如果存在一个事务,则加入该事务;否则创建一个新的事务(默认值)。

  2. PROPAGATION_REQUIRES_NEW: 总是创建一个新的事务,如果存在一个事务,则挂起它。

  3. PROPAGATION_SUPPORTS: 如果存在一个事务,则加入;否则以非事务方式执行。

  4. PROPAGATION_NOT_SUPPORTED: 以非事务方式执行,如果存在事务则挂起。

  5. PROPAGATION_NEVER: 不支持事务,如果存在事务则抛出异常。

  6. PROPAGATION_MANDATORY: 必须在一个现有的事务中执行,否则抛出异常。

  7. PROPAGATION_NESTED: 如果存在一个事务,则在嵌套事务中执行;否则创建一个新的事务。


Spring中bean的生命周期
  • BeanDefinition

    Spring容器在进行实例化时,会将xml配置的<bean>的信息封装成一个BeanDefinition对象,Spring根据BeanDefinition来创建Bean对象,里面有很多的属性用来描述Bean

    1
    2
    3
    4
    <bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl" lazy-init="true"/>
    <bean id="userService" class="com.itheima.service.UserServiceImpl" scope="singleton">
    <property name="userDao" ref="userDao"/>
    </bean>
    image-20240516191306328
    • beanClassName:bean的类名

    • initMethodName:初始化方法名称

    • propertyValues:bean的属性值

    • scope:作用域

    • lazyInit:延迟初始化

    image-20240516193005947
    1. 构造函数:Bean实例首先通过构造函数创建,这是根据BeanDefinition中的定义生成的。

    2. 依赖注入:在构造函数之后,Spring会进行依赖注入,为Bean注入所需的依赖对象。

    3. Aware接口:如果Bean实现了某些Aware接口(如BeanNameAwareBeanFactoryAwareApplicationContextAware),Spring会调用这些接口的方法,给Bean提供相应的上下文信息。

    4. BeanPostProcessor#before:在Bean初始化之前,Spring会调用所有注册的BeanPostProcessorpostProcessBeforeInitialization方法,对Bean进行进一步处理。

    5. 初始化方法:Bean的初始化方法包括实现了InitializingBean接口的afterPropertiesSet方法,或者在配置文件中指定的自定义初始化方法。

    6. BeanPostProcessor#after:初始化方法之后,Spring会调用所有BeanPostProcessorpostProcessAfterInitialization方法,对Bean进行进一步处理或包装。

    7. AOP:此时,Spring可能会为Bean创建代理对象(如果配置了AOP),代理可以是JDK动态代理或CGLIB动态代理,以实现切面编程。

    上图描述了从Bean实例化、依赖注入、初始化到AOP代理的整个生命周期过程。

image-20240516193258301
Spring Bean的循环引用

常见:实例化A的Bean需要先实例化B的Bean,实例化B的Bean又要先实例化A的Bean。

image-20240516193426126
  • 什么是Spring的循环依赖

    image-20240516193603933
  • 三级缓存解决循环依赖

    循环依赖容易导致死循环,而Spring解决循环依赖是通过三级缓存,对应的三级缓存如下:

    1
    2
    3
    4
    5
    6
    7
    8
    // 单实例对象注册器
    public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;

    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
    }
    缓存名称 源码名称 作用
    一级缓存 singletonObjects 单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
    二级缓存 earlySingletonObjects 缓存早期的bean对象(生命周期还没走完)
    三级缓存 singletonFactories 缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

    一级缓存作用:限制bean在beanFactory中只存一份,即实现singleton scope,它解决不了循环依赖。

    • 要想打破循环依赖,就需要一个中间人的参与,这个中间人就是二级缓存。

      1. 在实例化A,这时候拿到的是原始对象A,它还是一个半成品对象。和之前不太一样,它会被存入二级缓存。

        image-20240516194947690
      2. 这时候A需要注入B,但B不存在,则实例化之,然后将半成品B也放入二级缓存。

        image-20240516195026133
      3. 现在B又需要注入A,此时将会从二级缓存中获取半成品A并注入B,此时B创建成功,存入单例池(一级缓存)中,然后从二级缓存中删除B。

        image-20240516195215185
      4. 此时B已经有了,那么就可以顺利注入给A,然后A完成实例化,存入单例池,然后删除二级缓存中的A。

        image-20240516195350810
    • 如果一个对象是代理对象,那么就需要借助三级缓存了。

      1. 实例化A,然后原始对象A会生成一个ObjectFactory对象,然后将此对象放入三级缓存中。

        image-20240516200214324
      2. 此时A对象需要注入B,B不存在,实例化。原始对象B也会生成一个ObjectFactory对象,也会将其放入三级缓存。

        image-20240516200325115
      3. B此时需要注入A,于是从三级缓存中获取A对象的ObjectFactory对象。然后通过A的ObjectFactory对象创建代理对象,然后将其创建的代理对象存入二级缓存。

        image-20240516200551311
      4. 此时,能够将创建好的A的代理对象,注入给对象B,那么B就能够创建成功,并存入一级缓存。

        image-20240516200735402
      5. 现在,B创建好了,就能够注入给A了,A也就能创建成功并存入一级缓存。注意,此时创建的不是A的原对象,而是代理对象。

        image-20240516200935385
    • 有时候,Spring的缓存无法解决循环依赖,比如构造函数中的循环依赖。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      @Component
      public class A {

      // B成员变量
      private B b;

      public A(B b){
      System.out.println("A的构造方法执行了...");
      this.b = b;
      }
      }


      @Component
      public class B {

      // A成员变量
      private A a;

      public B(A a){
      System.out.println("B的构造方法执行了...");
      this.a = a;
      }
      }

      上方AB两个类在构造函数中相互调用,会导致循环依赖。Spring只能解决Setter方法导致的循环依赖,而现在是构造器注入。会导致如下报错:

      image-20240516201728710

      解决方法也很简单,在A的构造方法上添加@Lazy注解懒加载,使其在真正需要的时候再注入B。

      1
      2
      3
      4
      public A(@Lazy B b){
      System.out.println("A的构造方法执行了...");
      this.b = b;
      }
image-20240516202025572 image-20240516202058628
SpringMVC 的HTTP处理流程

SpringMVC经历过两个阶段:视图阶段(JSP)、前后端分离阶段(接口开发、异步)

  • 视图阶段(JSP)

    1. 现在有一个请求http://localhost:8080/user/getById/1,一旦浏览器请求成功发送,就会后台有一前端控制器接受此请求。这个前端控制器叫DispatcherServlet,所有的请求都会先经过此控制器,它是被Tomcat容器初始化的。这个类被加载之后,就加载了一些组件类,这里面有HandlerMapping(处理器映射器)、HandlerAdaptor(处理器适配器)、ViewResolver(视图解析器)。

    2. 如果这个请求到了前端控制器,那么将会到达处理器映射器。处理器映射器将会通过请求路径来寻找对应类中的对应的方法。

    3. 处理器映射器找到对应的类中的对应的方法后,返回处理器执行链(HandlerExecutionChain)给前端控制器。 因为在执行某个方法的时候,可能出现拦截器,如果有,则会将拦截器和方法名包含在执行链中。

    4. 如果没有拦截器,将会去找合适的处理器适配器来执行handler。此时处理器适配器将能够找到具体的方法去执行。

      此时handler还是指的是控制器中的某一个方法,它执行完成后 ,将会返回给处理器适配器。

      在处理器适配器中,做了两件重要的事情,一是处理参数,二是处理返回值。对于不同的Controller,参数和返回值各有不同,那你如何能够正常接收呢?其实这是因为处理器适配器中有一些对应类型参数的类型转换器,最终你的方法才能正确的去接收这些参数。返回值类型多种多样,那它又是如何响应给前端呢?也是处理器适配器去处理的。

      处理器适配器的作用清晰了,也就是处理参数和返回值。

    5. 当Handler处理完成后,将会返回一个ModelAndView给处理器适配器,然后处理器适配器再返回给前端控制器。

    6. 前端控制器处理完成后 ,再去找视图解析器,最终让视图解析器将逻辑视图解析为真正的视图并返回给前端控制器。

    7. 然后前端控制器去找到一个视图,这个视图中的一些jsp表达式将会被替换成真正的数据。

    8. 最终由前端控制器返回给用户的是一个带有数据的真正的页面。

    image-20240516204643109
  • 前后端开发阶段(接口开发,异步请求)

    1. 和之前一样,来请求了还是前端控制器先调用处理器映射器,找到对应的方法去执行。
    2. 然后交给处理器适配器去执行具体的handler。如果handler加了@ResponseBody之类的注解,则通过HttpMessageConverter来返回结果转换JSON。
    3. 由前端控制器将结果响应给前端。
    image-20240516205754299
image-20240516210043386 image-20240516210116914
SpringBoot自动配置原理
1
2
3
4
5
6
@SpringBootApplication
public class UserApplication{
public static void main(String args[]){
SpringApplication.run(UserApplication.class,args);
}
}

SpringBootApplication相当于下面三者之和:

  • @SpringBootConfiguration:该注解与@Configuration注解作用相同,用来声明当前也是一个配置类。

  • @ComponentScan:组件扫描,默认扫描当前引导类所在包及其子包。

  • @EnableAutoConfiguration:SpringBoot实现自动化配置的核心注解。

@EnableAutoConfiguration中,有一条@Import({AutoConfigurationImprotSelector.class}),这是自动配置的选择器。在这个选择器中,它会自动加载配置文件(该项目和项目引用的Jar包中的classpath下的META-INF/spring.factories),文件中的内容会根据一定条件(@ConditionalOnClass),统一加载到Spring容器中。

这个文件中,标明了很多的以AutoConfiguration结尾的类。这个文件中的自动配置类不是全都会加载进来。

image-20240516211103399
  • 接下来以Redis的configuration来分析。

    • 其中,@Configuration表示是一个配置类
    • @ConditionalOnClass表示是否有对应的字节码;什么时候会有此字节码呢?当我们导入了其相关依赖。
    • 如果有这个字节码,那么就会加载RedisAutoConfiguration到Spring容器中,否则不加载。
    • 注意看redisTemplate()方法,它将会返回一个RedisTemplate对象,并将其放到Spring容器中。
image-20240516212546529
Spring中常见的注解
  • Spring中常见的注解

    注解 说明
    @Component、@Controller、@Service、@Repository 使用在类上用于实例化Bean
    @Autowired 使用在字段上用于依赖类型注入
    @Qualifier 结合@Autowired一起使用,用于根据名称进行依赖注入
    @Scope 标注Bean的作用范围
    @Configuration 指定当前类是一个Spring配置类,当创建容器时会从该类上加载注解
    @ComponentScan 用于指定Spring在初始化容器时要扫描的包
    @Bean 使用在方法上,标注将该方法的返回值存储到Spring容器中
    @Import 使用@Import导入的类会被Spring加载到ioc容器中
    @Aspect、@Before、@After、@Around、@Pointcut 用于切面编程(AOP)
  • SpringMVC中常见的注解

    下表整理了各个注解及其用途:

    注解 说明
    @RequestMapping 用于映射请求路径,可以定义在类上和方法上。用于类上,则表示类中的所有的方法都是以该地址作为父路径
    @RequestBody 注解实现接收HTTP请求的JSON数据,将JSON转换为Java对象
    @RequestParam 指定请求参数的名称
    @PathVariable 从请求路径中获取请求参数(例如 /user/{id}),传递给方法的形式参数
    @ResponseBody 注解实现将Controller方法返回对象转化为JSON对象响应给客户端
    @RequestHeader 获取指定的请求头数据
    @RestController @Controller + @ResponseBody 的组合注解,表示该类中的每个方法都返回JSON对象
  • SpringBoot中常见的注解

    下表整理了各个注解及其用途:

    注解 说明
    @SpringBootConfiguration 组合了 @Configuration 注解,实现配置文件的功能
    @EnableAutoConfiguration 启用自动配置的功能,也可以关闭某个自动配置选项
    @ComponentScan Spring组件扫描

Mybatis


Mybatis执行流程
image-20240516214841075
  1. 加载 Mybatis 核心配置文件 mybatis-config.xml,其中包含了数据库连接配置和映射文件的位置声明。

  2. 构建会话工厂 SqlSessionFactory

  3. 使用会话工厂创建会话 SqlSession 对象,其中包含了执行 SQL 语句的所有方法。

  4. 通过执行器 Executor 操作数据库,执行 SQL 语句并维护查询缓存。执行器的执行方法中需要一个 MappedStatement 类型的参数,它封装了 SQL 语句的映射信息。

  5. SQL 语句在 Mapper 文件中以一条条的 SQL 语句表示,并由一个个 MappedStatement 对象读取存储。每个 MappedStatement 对象代表 Mapper 文件中的一个标签,负责处理输入输出参数,即 JavaType 和 JDBCType 的相互转换。

    image-20240516214918437 image-20240516215006700
  6. 最后,才能进行数据库操作。

image-20240516215435718
Mybatis是否支持延迟加载

Mybatis是支持延迟加载的,但是默认没有开启。

  • 什么是延迟加载

    假如我们有一个订单表和用户表,用户的实体类中有一条List<Order> orderList存储着用户的订单。

    image-20240516215852442

    查询用户的时候,把用户所属的订单数据也查询出来,这个是立即加载;

    查询用户的时候,暂时不查询订单数据,当需要订单的时候,再查询订单,这个就是延迟加载。

  • 如何设置延迟加载

    fetchType设置为lazy即可。

    image-20240516220405686

    要开启全局延迟加载,修改配置文件中lazyLoadingEnabledtrue

    image-20240516220604162
  • 延迟加载的原理

    1. 使用CGLIB创建目标对象的代理对象
    2. 当调用目标方法user.getOrderList()时,进入拦截器invoke方法,发现user.getOrderList()为null,执行sql查询order表
    3. 把order查询上来,然后调用user.setOrderList(List<Order> orderList),接着完成user.getOrderList()方法的调用。
    image-20240516220920635
image-20240516221047432
Mybatis一级二级缓存
image-20240516221146799

Mybatis都是将缓存保存在本地的,这是一个基于PerpetualCache的缓存,本质是一个HashMap。一级缓存作用域是session级别的;二级缓存作用域是namespace和mapper的,不依赖于session。

  • 一级缓存

    基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当Session进行flush或close之后,该Session中的所有cache就将清空,默认打开一级缓存。

    image-20240516221513006
  • 二级缓存

    二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQL session。默认也是采用 PerpetualCache,基于HashMap。默认关闭。

    • 下方代码因为处于不同的SqlSession中,一级缓存失效,所以会执行两条SQL:

      image-20240516221812957
    • 二级缓存默认是关闭的,需要手动打开,一共两步。

      1. 打开全局配置:

        image-20240516221935504
      2. 映射文件

        使用<cache/>标签让当前mapper生效二级缓存

        image-20240516222118220
    • 注意事项

      1. 对于缓存数据更新机制,当某一作用域(一级缓存Session/二级缓存Namespaces)执行了新增、修改、删除操作后,默认该作用域下所有select中的缓存将被清除。

      2. 二级缓存需要缓存的数据必须实现 Serializable 接口

      3. 只有会话提交或关闭后,一级缓存中的数据才会转移到二级缓存中。

image-20240516222424331