Spring学习笔记

原生Spring

列举一些重要的 Spring 模块?

Spring Core
核心模块, Spring 其他所有的功能基本都需要依赖于该类库,主要提供 IoC 依赖注入功能的支持。
Spring Aspects
该模块为与 AspectJ 的集成提供支持。
Spring AOP
提供了面向切面的编程实现。
Spring Data Access/Integration :
Spring Data Access/Integration 由 5 个模块组成:

  • spring-jdbc : 提供了对数据库访问的抽象 JDBC。不同的数据库都有自己独立的 API 用于操作数据库,而 Java 程序只需要和 JDBC API 交互,这样就屏蔽了数据库的影响。
  • spring-tx : 提供对事务的支持。
  • spring-orm : 提供对 Hibernate 等 ORM 框架的支持。
  • spring-oxm : 提供对 Castor 等 OXM 框架的支持。
  • spring-jms : Java 消息服务。

Spring Web
Spring Web 由 4 个模块组成:

  • spring-web :对 Web 功能的实现提供一些最基础的支持。
  • spring-webmvc : 提供对 Spring MVC 的实现。
  • spring-websocket : 提供了对 WebSocket 的支持,WebSocket 可以让客户端和服务端进行双向通信。
  • spring-webflux :提供对 WebFlux 的支持。WebFlux 是 Spring Framework 5.0 中引入的新的响应式框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步.

Spring Test
Spring 团队提倡测试驱动开发(TDD)。有了控制反转 (IoC)的帮助,单元测试和集成测试变得更简单。
Spring 的测试模块对 JUnit(单元测试框架)、TestNG(类似 JUnit)、Mockito(主要用来 Mock 对象)、PowerMock(解决 Mockito 的问题比如无法模拟 final, static, private 方法)等等常用的测试框架支持的都比较好。

Spring IOC & AOP

谈谈自己对于 Spring IoC 的了解

IoC(Inverse of Control:控制反转) 是一种 设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。不过, IoC 并非 Spirng 特有,在其他语言中也有应用。
为什么叫控制反转?

  • 控制 :指的是对象创建(实例化、管理)的权力
  • 反转 :控制权交给外部环境(Spring 框架、IoC 容器)

Spring学习笔记
对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。
IoC 容器就像是一个工厂一样, 当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。
如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。
在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好, 于是 SpringBoot 注解配置就慢慢开始流行起来。

⭐推荐阅读:面试被问了几百遍的 IoC 和 AOP ,还在傻傻搞不清楚?

什么是 IoC

IoC (Inversion of control )控制反转/反转控制。它是一种思想不是一个技术实现。描述的是:Java 开发领域对象的创建以及管理的问题。
例如:现有类 A 依赖于类 B

  • 传统的开发方式 :往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来
  • 使用 IoC 思想的开发方式不通过 new 关键字来创建对象,而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。我们需要哪个对象,直接从 IoC 容器里面过去即可。

从以上两种开发方式的对比来看:我们 “丧失了一个权力” (创建、管理对象的权力),从而也得到了一个好处(不用再考虑对象的创建、管理等一系列的事情)

IoC 解决了什么问题

IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。这样有什么好处呢?

  1. 对象之间的耦合度或者说依赖程度降低;
  2. 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。

例如:现有一个针对 User 的操作,利用 Service 和 Dao 两层结构进行开发
在没有使用 IoC 思想的情况下,Service 层想要使用 Dao 层的具体实现的话,需要通过 new 关键字在UserServiceImpl 中手动 new 出 IUserDao 的具体实现类 UserDaoImpl(不能直接 new 接口类)。

Spring学习笔记
很完美,这种方式也是可以实现的,但是我们想象一下如下场景:
开发过程中突然接到一个新的需求, 针对对IUserDao 接口开发出另一个具体实现类。因为 Server 层依赖了IUserDao的具体实现,所以我们需要修改UserServiceImpl中 new 的对象。如果只有一个类引用了IUserDao的具体实现,可能觉得还好,修改起来也不是很费力气, 但是如果有许许多多的地方都引用了IUserDao的具体实现的话,一旦需要更换IUserDao 的实现方式,那修改起来将会非常的头疼。
Spring学习笔记
使用 IoC 的思想,我们将对象的控制权(创建、管理)交有 IoC 容器去管理,我们在使用的时候直接向 IoC 容器 “要” 就可以了(通过 **@Autowired** 等的方式 )
Spring学习笔记

IoC 和 DI 别再傻傻分不清楚

IoC(Inverse of Control:控制反转)是一种 设计思想 或者说是某种模式。这个设计思想就是 将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。 IoC 在其他语言中也有应用,并非 Spring 特有。 IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
IoC 最常见以及最合理的实现方式叫做依赖注入(Dependency Injection,简称 DI)。
并且,老马(Martin Fowler)在一篇文章中提到将 IoC 改名为 DI,原文如下,原文地址:https://martinfowler.com/articles/injection.html

Spring学习笔记
老马的大概意思是 IoC 太普遍并且不表意,很多人会因此而迷惑,所以,使用 DI 来精确指名这个模式比较好。

谈谈自己对于 AOP 的了解

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关, 却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,(重复代码)。便于减少系统的 重复代码,降低模块间的 耦合度,并有利于未来的可拓展性和可维护性。

⭐推荐阅读:Spring AOP
本质上就是在业务方法的上文或者下文加上一些与业务无关的其他方法,比如:


这么写配置文件,就会在 com.xrq.aop.HelloWorld实现类的所有方法前后加上 printTime方法

public static void main(String[] args)
{
    ApplicationContext ctx =
            new ClassPathXmlApplicationContext("aop.xml");

    HelloWorld hw1 = (HelloWorld)ctx.getBean("helloWorldImpl1");
    HelloWorld hw2 = (HelloWorld)ctx.getBean("helloWorldImpl2");
    hw1.printHelloWorld();
    System.out.println();
    hw1.doPrint();

    System.out.println();
    hw2.printHelloWorld();
    System.out.println();
    hw2.doPrint();
}

运行结果:

CurrentTime = 1446129611993
Enter HelloWorldImpl1.printHelloWorld()
CurrentTime = 1446129611993

CurrentTime = 1446129611994
Enter HelloWorldImpl1.doPrint()
CurrentTime = 1446129611994

CurrentTime = 1446129611994
Enter HelloWorldImpl2.printHelloWorld()
CurrentTime = 1446129611994

CurrentTime = 1446129611994
Enter HelloWorldImpl2.doPrint()
CurrentTime = 1446129611994

什么是 AOP

AOP:Aspect oriented programming 面向切面编程,AOP 是 OOP(面向对象编程)的一种延续。
下面我们先看一个 OOP 的例子。
例如:现有三个类,Horse、Pig、Dog,这三个类中都有 eat 和 run 两个方法。
通过 OOP 思想中的继承,我们可以提取出一个 Animal 的父类,然后将 eat 和 run 方法放入父类中,Horse、Pig、Dog通过继承Animal类即可自动获得 eat() 和 run() 方法。这样将会少些很多重复的代码。

Spring学习笔记
OOP 编程思想可以解决大部分的代码重复问题。但是有一些问题是处理不了的。比如在父类 Animal 中的多个方法的相同位置出现了重复的代码,OOP 就解决不了。
OOP 编程思想可以解决大部分的代码重复问题。但是有一些问题是处理不了的。比如在父类 Animal 中的多个方法的相同位置出现了重复的代码,OOP 就解决不了。
/**
 * 动物父类
 */
public class Animal {

    /** 身高 */
    private String height;

    /** 体重 */
    private double weight;

    public void eat() {
        // 性能监控代码
        long start = System.currentTimeMillis();

        // 业务逻辑代码
        System.out.println("I can eat...");

        // 性能监控代码
        System.out.println("执行时长:" + (System.currentTimeMillis() - start)/1000f + "s");
    }

    public void run() {
        // 性能监控代码
        long start = System.currentTimeMillis();

        // 业务逻辑代码
        System.out.println("I can run...");

        // 性能监控代码
        System.out.println("执行时长:" + (System.currentTimeMillis() - start)/1000f + "s");
    }
}

这部分重复的代码,一般统称为 横切逻辑代码。

Spring学习笔记
横切逻辑代码存在的问题:
  • 代码重复问题
  • 横切逻辑代码和业务代码混杂在一起,代码臃肿,不变维护

AOP 就是用来解决这些问题的
AOP 另辟蹊径,提出横向抽取机制,将横切逻辑代码和业务逻辑代码分离

Spring学习笔记
代码拆分比较容易,难的是如何在不改变原有业务逻辑的情况下,悄无声息的将横向逻辑代码应用到原有的业务逻辑中,达到和原来一样的效果。

AOP 解决了什么问题

通过上面的分析可以发现,AOP 主要用来解决: 在不改变原有业务逻辑的情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复。

AOP 为什么叫面向切面编程

:指的是横切逻辑, 原有业务逻辑代码不动,只能操作横切逻辑代码,所以面向横切逻辑
横切逻辑代码往往要影响的是很多个方法,每个方法如同一个点,多个点构成一个面。这里有一个面的概念

Spring AOP 就是基于 动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:

Spring学习笔记
当然你也可以使用 AspectJ !Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。

Spring bean

什么是 bean?

简单来说,bean 代指的就是那些被 IoC 容器所管理的对象。
我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。


下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。

Spring学习笔记
org.springframework.beansorg.springframework.context 这两个包是 IoC 实现的基础,如果想要研究 IoC 相关的源码的话,可以去看看

bean 的作用域有哪些?

Spring 中 Bean 的作用域通常有下面几种:

  • singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的,对单例设计模式的应用。
  • prototype : 每次请求都会创建一个新的 bean 实例。
  • request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
  • session : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。
  • global-session : 全局 session 作用域,仅仅在基于 portlet 的 web 应用中才有意义,Spring5 已经没有了。Portlet 是能够生成语义代码(例如:HTML)片段的小型 Java Web 插件。它们基于 portlet 容器,可以像 servlet 一样处理 HTTP 请求。但是,与 servlet 不同,每个 portlet 都有不同的会话。

如何配置 bean 的作用域呢?

  • xml 方式:

  • 注解方式:
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person personPrototype() {
    return new Person();
}

单例 bean 的线程安全问题了解吗?

大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。
常见的有两种解决办法:

  1. 在 bean 中尽量避免定义可变的成员变量。
  2. 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。

⭐来源:Java中的ThreadLocal详解
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。
ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。 ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个 ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示:

Spring学习笔记

不过,大部分 bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下, bean 是线程安全的。

@Component 和 @Bean 的区别是什么?

  1. @Component 注解作用于类,而 @Bean注解作用于方法。
  2. @Component通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean, @Bean告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。
  3. @Bean 注解比 @Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。

@Bean 注解使用示例:

@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }

}

上面的代码相当于下面的 xml 配置:


下面这个例子是通过 @Component 无法实现的。

@Bean
public OneService getService(status) {
    case (status)  {
        when 1:
                return new serviceImpl1();
        when 2:
                return new serviceImpl2();
        when 3:
                return new serviceImpl3();
    }
}

将一个类声明为 bean 的注解有哪些?

我们一般使用 @Autowired 注解自动装配 bean,要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,采用以下注解可实现:

  • @Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用 @Component 注解标注。
  • @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
  • @Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。

bean 的生命周期?

下面的内容整理自:https://yemengying.com/2016/07/14/spring-bean-life-cycle/(opens new window) ,除了这篇文章,再推荐一篇很不错的文章 :https://www.cnblogs.com/zrtqsk/p/3735273.html(opens new window)

  • Bean 容器找到配置文件中 Spring Bean 的定义。
  • Bean 容器利用 Java Reflection API 创建一个 Bean 的实例。
  • 如果涉及到一些属性值 利用 set()方法设置一些属性值。
  • 如果 Bean 实现了 BeanNameAware接口,调用 setBeanName()方法,传入 Bean 的名字。
  • 如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。
  • 如果 Bean 实现了 BeanFactoryAware 接口,调用 setBeanFactory()方法,传入 BeanFactory对象的实例。
  • 与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法。
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行 postProcessBeforeInitialization() 方法
  • 如果 Bean 实现了 InitializingBean接口,执行 afterPropertiesSet()方法。
  • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor对象,执行 postProcessAfterInitialization() 方法
  • 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
  • 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。

Spring学习笔记

Spring MVC

说说自己对于 Spring MVC 了解?

MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。

Spring学习笔记
Model 1 时代
很多学 Java 后端比较晚的朋友可能并没有接触过 Model 1 时代下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。
这个模式下 JSP 即是控制层(Controller)又是表现层(View)。显而易见,这种模式存在很多问题。比如控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;再比如前端和后端相互依赖,难以进行测试维护并且开发效率极低。
Spring学习笔记
Model 2 时代
学过 Servlet 并做过相关 Demo 的朋友应该了解”Java Bean(Model)+ JSP(View)+Servlet(Controller) “这种开发模式,这就是早期的 JavaWeb MVC 开发模式。
  • Model:系统涉及的数据,也就是 dao 和 bean。
  • View:展示模型中的数据,只是用来展示。
  • Controller:处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。

Spring学习笔记
Model2 模式下还存在很多问题,Model2 的抽象和封装程度还远远不够,使用 Model2 进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。
于是,很多 JavaWeb 开发相关的 MVC 框架应运而生比如 Struts2,但是 Struts2 比较笨重。

Spring MVC 时代
随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。
MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。
Spring MVC 下我们一般把后端项目分为:

  • Service 层(处理业务)
  • Dao 层(数据库操作)
  • Entity 层(实体类)
  • Controller 层(控制层,返回数据给前台页面)

SpringMVC 工作原理了解吗?

Spring MVC 原理如下图所示:
SpringMVC 工作原理的图解我没有自己画,直接图省事在网上找了一个非常清晰直观的,原出处不明。

Spring学习笔记
流程说明(重要):
  1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet
  2. DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler
  3. 解析到对应的 Handler(也就是我们平常说的 Controller控制器)后,开始由 HandlerAdapter 适配器处理。
  4. HandlerAdapter会根据 Handler来调用真正的处理器开处理请求, 并处理相应的业务逻辑。
  5. 处理器处理完业务后,会返回一个 ModelAndView 对象, Model 是返回的数据对象, View 是个逻辑上的 View
  6. ViewResolver 会根据逻辑 View 查找实际的 View
  7. DispaterServlet 把返回的 Model 传给 View(视图渲染)。
  8. View 返回给 请求者(浏览器)

Spring 框架中用到了哪些设计模式?

关于下面一些设计模式的详细介绍,可以看笔主前段时间的原创文章《面试官:”谈谈 Spring 中都用到了那些设计模式?”。》

工厂设计模式

  • Spring 使用工厂模式通过 BeanFactoryApplicationContext创建 bean 对象。

Spring IOC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。

两者对比:

  • BeanFactory :延迟注入(使用到某个 bean 的时候才会注入),相比于BeanFactory来说会占用更少的内存,程序启动速度更快。
  • ApplicationContext :容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。

代理设计模式

Spring AOP 功能的实现。

单例设计模式

Spring 中的 Bean 默认都是单例的。

模板方法模式

  • Spring 中 **jdbcTemplate** **hibernateTemplate** 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
  • 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。

观察者模式

Spring 事件驱动模型就是观察者模式很经典的一个应用。
Spring 的事件流程总结

  1. 定义一个事件: 实现一个继承自 ApplicationEvent,并且写相应的构造函数;
  1. 定义一个事件监听者:实现 ApplicationListener 接口,重写 onApplicationEvent() 方法;
  1. 使用事件发布者发布消息: 可以通过 ApplicationEventPublisher 的 publishEvent() 方法发布消息。

Example:

// 定义一个事件,继承自ApplicationEvent并且写相应的构造函数
public class DemoEvent extends ApplicationEvent{
    private static final long serialVersionUID = 1L;

    private String message;

    public DemoEvent(Object source,String message){
        super(source);
        this.message = message;
    }

    public String getMessage() {
         return message;
          }

// 定义一个事件监听者,实现ApplicationListener接口,重写 onApplicationEvent() 方法;
@Component
public class DemoListener implements ApplicationListener{

    //使用onApplicationEvent接收消息
    @Override
    public void onApplicationEvent(DemoEvent event) {
        String msg = event.getMessage();
        System.out.println("接收到的信息是:"+msg);
    }

}
// 发布事件,可以通过ApplicationEventPublisher  的 publishEvent() 方法发布消息。
@Component
public class DemoPublisher {

    @Autowired
    ApplicationContext applicationContext;

    public void publish(String message){
        //发布事件
        applicationContext.publishEvent(new DemoEvent(this, message));
    }
}

当调用 DemoPublisher 的 publish() 方法的时候,比如 demoPublisher.publish(“你好”) ,控制台就会打印出:接收到的信息是:你好 。

适配器模式

Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了 适配器模式适配 Controller
spring MVC中的适配器模式

  • 在Spring MVC中,DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。
  • 解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter 适配器处理。
  • HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配, *Controller 作为需要适配的类。

为什么要在 Spring MVC 中使用适配器模式?

  • Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。
  • 如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样:
if(mappedHandler.getHandler() instanceof MultiActionController){
   ((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
    ...

}else if(...){
   ...

}

假如我们再增加一个 Controller类型就要在上面代码中再加入一行 判断语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。

Spring 事务

事务的特性(ACID)了解么?

  • 原子性(Atomicity): 一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
  • 一致性(Consistency): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
  • 隔离性(Isolation): 数据库允许多个并发事务同时对其数据进行读写和修改的能力, 隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为 不同级别,包括:
  • 未提交读(Read uncommitted)
  • 提交读(read committed)
  • 可重复读(repeatable read)
  • 串行化(Serializable)。
  • 持久性(Durability): 事务处理结束后, *对数据的修改就是永久的,即便系统故障也不会丢失。

详谈 Spring 对事务的支持

再提醒一次:你的程序是否支持事务首先取决于数据库 ,比如使用 MySQL 的话,如果你选择的是 innodb 引擎,那么恭喜你,是可以支持事务的。但是,如果你的 MySQL 数据库使用的是 myisam 引擎的话,那不好意思,从根上就是不支持事务的。

这里再多提一下一个非常重要的知识点: MySQL 怎么保证原子性的?

我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行 回滚,在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先先记录到这个回滚日志中,然后再执行相关的操作。
如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!
并且, 回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况, 当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。

Spring 管理事务的方式有几种?

  • 编程式事务 : 在代码中硬编码(不推荐使用) : 通过 TransactionTemplate或者 TransactionManager 手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。

TransactionTemplate示例代码:

@Autowired
private TransactionTemplate transactionTemplate;

public void testTransaction() {

        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {

                try {

                    // ....  业务代码
                } catch (Exception e){
                    //回滚
                    transactionStatus.setRollbackOnly();
                }

            }
        });
}

使用 TransactionManager 进行编程式事务管理的示例代码如下:

@Autowired
private PlatformTransactionManager transactionManager;

public void testTransaction() {

  TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
          try {
               // ....  业务代码
              transactionManager.commit(status);
          } catch (Exception e) {
              transactionManager.rollback(status);
          }
}

  - **声明式事务** : 在 XML 配置文件中配置或者直接基于注解(推荐使用) : 实际是通过 AOP 实现(基于@Transactional的全注解方式使用最多), 示例代码:
@Transactional(propagation=propagation.PROPAGATION_REQUIRED)
public void aMethod {
  //do something
  B b = new B();
  C c = new C();
  b.bMethod();
  c.cMethod();
}

Spring 事务管理接口介绍

Spring 框架中,事务管理相关最重要的 3 个接口如下:

  • **PlatformTransactionManager**: (平台)事务管理器,Spring 事务策略的核心。
  • **TransactionDefinition**: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
  • **TransactionStatus**: 事务运行状态。

⭐详细:Spring 事务管理接口介绍

PlatformTransactionManager :事务管理接口

Spring 并不直接管理事务,而是提供了多种事务管理器 。Spring 事务管理器的接口是: **PlatformTransactionManager**
通过这个接口,Spring 为各个平台如

  • JDBC(DataSourceTransactionManager)
  • Hibernate(HibernateTransactionManager)
  • JPA(JpaTransactionManager)

等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

TransactionDefinition :事务属性

事务管理器接口 **PlatformTransactionManager** 通过 **getTransaction(TransactionDefinition definition)** 方法来得到一个事务,这个方法里面的参数是 **TransactionDefinition** 类 ,这个类就定义了一些基本的事务属性。
那么什么是 事务属性 呢?
事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。
事务属性包含了 5 个方面:

Spring学习笔记
TransactionDefinition 接口中定义了 5 个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等。
事务超时属性

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1。 **Transcational** 注解中也有 **timeout** 属性。

事务只读属性

对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中

很多人就会疑问了,为什么我一个数据查询操作还要启用事务支持呢?
拿 MySQL 的 innodb 举例子,根据官网 https://dev.mysql.com/doc/refman/5.7/en/innodb-autocommit-commit-rollback.html(opens new window) 描述:

MySQL 默认对每一个新建立的连接都启用了 autocommit模式。在该模式下,每一个发送到 MySQL 服务器的sql语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。

但是,

  • 如果你给方法加上了 @Transactional注解的话, 这个方法执行的所有sql会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。
  • 如果不加 @Transactional,每条sql会开启一个单独的事务, *中间被其它事务改了数据,都会实时读取到最新值。

分享一下关于事务只读属性,其他人的解答:

  1. 如果你一次执行单条查询语句, 则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性;
  2. 如果你一次执行 多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前, 数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该 启用事务支持.

TransactionStatus :事务状态

TransactionStatus接口用来记录事务的状态 该接口定义了一组方法,用来获取或判断事务的相应状态信息。
PlatformTransactionManager.getTransaction(…)方法返回一个 TransactionStatus 对象。

⭐事务属性详解

实际业务开发中,大家一般都是使用 _ _@Transactional_ _注解来开启事务,很多人并不清楚这个参数里面的参数是什么意思,有什么用。为了更好的在项目中使用事务管理,强烈推荐好好阅读一下下面的内容。

事务传播行为

事务传播行为是为了解决业务层方法之间互相调用的事务问题
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

Spring 事务中哪几种事务传播行为?

⭐示例: 举个例子

事务传播行为是为了解决业务层方法之间互相调用的事务问题

⭐注意:不同类之间的事务

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
正确的事务传播行为可能的值如下:
1. **TransactionDefinition.PROPAGATION_REQUIRED**
使用的最多的一个事务传播行为,我们平时经常使用的@Transactional注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

Class A {
    @Transactional(propagation=propagation.PROPAGATION_REQUIRED)
    public void aMethod {
        //do something
        B b = new B();
        b.bMethod();
    }
}

Class B {
    @Transactional(propagation=propagation.PROPAGATION_REQUIRED)
    public void bMethod {
       //do something
    }
}

如果我们上面的 aMethod()bMethod()使用的都是 PROPAGATION_REQUIRED传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。

  • *⭐:相互影响

2. **TransactionDefinition.PROPAGATION_REQUIRES_NEW**
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务, Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

Class A {
    @Transactional(propagation=propagation.PROPAGATION_REQUIRED)
    public void aMethod {
        //do something
        B b = new B();
        b.bMethod();
    }
}

Class B {
    @Transactional(propagation=propagation.REQUIRES_NEW)
    public void bMethod {
       //do something
    }
}

举个例子:如果我们上面的 bMethod()使用 PROPAGATION_REQUIRES_NEW事务传播行为修饰, aMethod还是用 PROPAGATION_REQUIRED修饰的话。

  • 如果 aMethod()发生异常回滚, bMethod()不会跟着回滚,因为 bMethod()开启了独立的事务。
  • 但是,如果 bMethod()抛出了未被捕获的异常并且这个异常满足事务回滚规则的话, aMethod()同样也会回滚,因为这个异常被 aMethod()的事务管理机制检测到了。
  • *⭐:外部不影响内部,内部会影响外部

3. **TransactionDefinition.PROPAGATION_NESTED**
如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED

Class A {
    @Transactional(propagation=propagation.PROPAGATION_REQUIRED)
    public void aMethod {
        //do something
        B b = new B();
        b.bMethod();
        b.bMethod2();
    }
}

Class B {
    @Transactional(propagation=propagation.PROPAGATION_NESTED)
    public void bMethod {
       //do something
    }
    @Transactional(propagation=propagation.PROPAGATION_NESTED)
    public void bMethod2 {
       //do something
    }
}

  • 如果 aMethod()回滚的话, bMethod()bMethod2()都要回滚
  • bMethod()回滚的话,并不会造成 aMethod()bMethod()2回滚。
  • *⭐:外部影响内部,内部不影响外部,也不影响平行关系的代码

4. **TransactionDefinition.PROPAGATION_MANDATORY**
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
这个使用的很少。
若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚:

  • **TransactionDefinition.PROPAGATION_SUPPORTS**: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • **TransactionDefinition.PROPAGATION_NOT_SUPPORTED**: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • **TransactionDefinition.PROPAGATION_NEVER**: 以非事务方式运行,如果当前存在事务,则抛出异常。

Spring 事务中的隔离级别有哪几种?

和事务传播行为这块一样,为了方便使用,Spring 也相应地定义了一个枚举类: Isolation

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);

    private final int value;

    private Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

怎么用? @Transactional注解定义如下:

...

public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;
    ....

里面有 Isolation类型的属性,因此在使用 @Transactional时,只需传入就好

@Override
@Transactional(rollbackFor = Exception.class,
               isolatoin = [Isolation.DEFAULT, Isolation.READ_UNCOMMITTED ....]选一个)
public JSONObject methond(Integer args) {
    ...

}
  • TransactionDefinition.ISOLATION_DEFAULT :使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别.

  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED :最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更, 可能会导致脏读、幻读或不可重复读

  • TransactionDefinition.ISOLATION_READ_COMMITTED : 允许读取并发事务已经提交的数据, 可以阻止脏读,但是幻读或不可重复读仍有可能发生
  • TransactionDefinition.ISOLATION_REPEATABLE_READ : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改, 可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • TransactionDefinition.ISOLATION_SERIALIZABLE : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说, 该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

@Transcational

@Transactional(rollbackFor = Exception.class)注解了解吗?

Exception分为运行时异常 RuntimeException和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。
@Transactional 注解作用于类上时, 该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解, 那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。
@Transactional注解中如果不配置rollbackFor属性, 那么事务只会在遇到 **RuntimeException** 的时候才会回滚,加上 **rollbackFor=Exception.class**,可以让事务在遇到非运行时异常时也回滚。

@Transactional 事务注解原理

面试中在问 AOP 的时候可能会被问到的一个问题。简单说下吧!
我们知道, **@Transactional** 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。
如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。

如果一个类或者一个类中的 public 方法上被标注 @Transactional注解的话,Spring 容器就会在启动的时候为其创建一个 代理类,在调用被 @Transactional 注解的 public 方法的时候,实际调用的是, TransactionInterceptor 类中的 invoke()方法。
这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。

TransactionInterceptor 类中的 invoke()方法内部实际调用的是 TransactionAspectSupport类的 invokeWithinTransaction()方法。

Spring AOP 自调用问题

若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有 @Transactional 注解的方法的事务会失效。
这是由于Spring AOP代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。
MyService 类中的 method1()调用 method2()就会导致 method2()的事务失效。

应该是因为调用 method1()不会启动AOP代理,导致 method2()失效吧。

@Service
public class MyService {

private void method1() {
     method2();
     //......

}
@Transactional
 public void method2() {
     //......

  }
}

解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理。

@Transactional 的使用注意事项总结

  1. @Transactional 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;
  2. 避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效;
  3. 正确的设置 @TransactionalrollbackForpropagation 属性,否则事务可能会回滚失败; (不同类之间)
  4. @Transactional注解的方法所在的类必须被 Spring 管理,否则不生效;
  5. 底层使用的数据库必须支持事务机制,否则不生效;

Spring / SpringBoot 常用注解总结

1. @SpringBootApplication

这里先单独拎出@SpringBootApplication 注解说一下,虽然我们一般不会主动去使用它。

Guide 哥:这个注解是 Spring Boot 项目的基石,创建 SpringBoot 项目之后会默认在主类加上。

@SpringBootApplication
public class SpringSecurityJwtGuideApplication {
      public static void main(java.lang.String[] args) {
        SpringApplication.run(SpringSecurityJwtGuideApplication.class, args);
    }
}

我们可以把 @SpringBootApplication看作是

  • @Configuration: 扫描被 @Component (@Service, @Controller)注解的 bean,注解默认会扫描该类所在的包下所有的类。、
  • @EnableAutoConfiguration: 启用 SpringBoot 的自动配置机制、
  • @ComponentScan: 允许在 Spring 上下文中注册额外的 bean 或导入其他配置类

注解的集合。

2. Spring Bean 相关

2.1. @Autowired

自动导入对象到类中,被注入进的类同样要被 Spring 容器管理比如:Service 类注入到 Controller 类中。

@Service
public class UserService {
  ......

}

@RestController
@RequestMapping("/users")
public class UserController {
   @Autowired
   private UserService userService;
   ......

}

2.2. @Component , @Repository , @Service , @Controller

我们一般使用 @Autowired 注解让 Spring 容器帮我们自动装配 bean。要想把类标识成可用于 @Autowired注解自动装配的 bean 的类,可以采用以下注解实现:

  • @Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用 @Component 注解标注。
  • @Repository: 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
  • @Controller : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。

2.3. @RestController

@RestController注解是 @Controller@ResponseBody的合集,表示这是个控制器 bean,并且是将函数的返回值直接填入 HTTP 响应体中,是 REST 风格的控制器。

Guide 哥:现在都是前后端分离,说实话我已经很久没有用过@Controller。如果你的项目太老了的话,就当我没说。

单独使用 @Controller不加 @ResponseBody的话一般是用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。 @Controller + @ResponseBody 返回 JSON 或 XML 形式数据

  • Controller
  • 返回一个页面( Thymeleaf 模板引擎生成,Spring 默认会去 resources 目录下 templates 目录下找,所以建议把页面放在 resources/templates目录下)
  • @Controller+ @ResponseBody返回 JSON 格式数据
  • RestController:返回JSON格式 *(Spring 4 之后新加的注解)

关于 @RestController@Controller的对比,请看这篇文章:@RestController vs @Controller

2.4. @Scope

声明 Spring Bean 的作用域,使用方法:

@Bean
@Scope("singleton")
public Person personSingleton() {
    return new Person();
}

四种常见的 Spring Bean 的作用域:

  • singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
  • prototype : 每次请求都会创建一个新的 bean 实例。
  • request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
  • session : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。

2.5. @Configuration

一般用来声明配置类,可以使用 @Component注解替代,不过使用 @Configuration注解声明配置类更加语义化。

@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }

}

3. 处理常见的 HTTP 请求类型

5 种常见的请求类型:

  • GET :请求从服务器获取特定资源。举个例子:GET /users(获取所有学生), 用 @GetMapping注解
  • POST :在服务器上创建一个新的资源。举个例子:POST /users(创建学生),用 @PostMapping注解
  • PUT :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /users/12(更新编号为 12 的学生),用 @PutMapping注解
  • DELETE :从服务器删除特定的资源。举个例子:DELETE /users/12(删除编号为 12 的学生),用 @DeleteMapping注解
  • PATCH :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。用 @PatchMapping注解

. 前后端传值

掌握前后端传值的正确姿势,是你开始 CRUD 的第一步!

4.1. @PathVariable 和 @RequestParam

@PathVariable用于获取路径参数, @RequestParam用于获取查询参数。
举个简单的例子:

@GetMapping("/klasses/{klassId}/teachers")
public List getKlassRelatedTeachers(
         @PathVariable("klassId") Long klassId,
         @RequestParam(value = "type", required = false) String type ) {
...

}

如果我们请求的 url 是: /klasses/123456/teachers?type=web
那么我们服务获取到的数据就是: klassId=123456,type=web

4.2. @RequestBody

用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且 Content-Type 为 application/json 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用 HttpMessageConverter或者自定义的 HttpMessageConverter将请求的 body 中的 json 字符串转换为 java 对象。
我用一个简单的例子来给演示一下基本使用!
我们有一个注册的接口:

@PostMapping("/sign-up")
public ResponseEntity signUp(@RequestBody @Valid UserRegisterRequest userRegisterRequest) {
  userService.save(userRegisterRequest);
  return ResponseEntity.ok().build();
}

UserRegisterRequest对象

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterRequest {
    @NotBlank
    private String userName;
    @NotBlank
    private String password;
    @NotBlank
    private String fullName;
}

我们发送 post 请求到这个接口,并且 body 携带 JSON 数据:

{
 "userName":"coder",
 "fullName":"shuangkou",
 "password":"123456"
}

这样我们的后端就可以直接把 json 格式的数据映射到我们的 UserRegisterRequest​类上。

Spring学习笔记
👉 需要注意的是: 一个请求方法只可以有一个 **@RequestBody** ,但是可以有多个 **@RequestParam** **@PathVariable**。 如果你的方法必须要用两个 @RequestBody来接受数据的话, 大概率是你的数据库设计或者系统设计出问题了!

5. 读取配置信息(springboot读取 application.yml )

很多时候我们需要将一些常用的配置信息比如阿里云 oss、发送短信、微信认证的相关配置信息等等放到配置文件中。
下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。
我们的数据源 application.yml内容如下:

wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油!

my-profile:
  name: Guide哥
  email: koushuangbwcx@163.com

library:
  location: 湖北武汉加油中国加油
  books:
    - name: 天才基本法
      description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。
    - name: 时间的秩序
      description: 为什么我们记得过去,而非未来?时间"流逝"意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。
    - name: 了不起的我
      description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻?

⭐5.1. @Value(常用)

使用 @Value("${property}")​读取比较简单的配置信息:

@Value("${wuhan2020}")
String wuhan2020;

⭐5.2. @ConfigurationProperties(常用)

通过@ConfigurationProperties读取配置信息并与 bean 绑定。

@Component
@ConfigurationProperties(prefix = "library")
class LibraryProperties {
    @NotEmpty
    private String location;
    private List books;

    @Setter
    @Getter
    @ToString
    static class Book {
        String name;
        String description;
    }
  省略getter/setter
  ......

}

你可以像使用普通的 Spring bean 一样,将其注入到类中使用。

5.3. @PropertySource (不常用)

@PropertySource读取指定 properties 文件

@Component
@PropertySource("classpath:website.properties")

class WebSite {
    @Value("${url}")
    private String url;

  省略getter/setter
  ......

}

更多内容请查看这篇文章:《10 分钟搞定 SpringBoot 如何优雅读取配置文件?》 。

6. 参数校验

数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。

JSR(Java Specification Requests) 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便!

校验的时候我们实际用的是 Hibernate Validator 框架。Hibernate Validator 是 Hibernate 团队最初的数据校验框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现。

SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。如下图所示(通过 idea 插件—Maven Helper 生成):

:更新版本的 spring-boot-starter-web 依赖中不再有 hibernate-validator 包(如2.3.11.RELEASE),需要自己引入 spring-boot-starter-validation 依赖。
具体可以查看这篇文章, 写的非常好:《如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!》。

6.1. 一些常用的字段验证的注解

  • @NotEmpty被注释的字符串的不能为 null 也不能为空
  • @NotBlank被注释的字符串非 null,并且必须包含一个非空白字符
  • @Null 被注释的元素必须为 null
  • @NotNull被注释的元素必须不为 null
  • @AssertTrue 被注释的元素必须为 true
  • @AssertFalse被注释的元素必须为 false
  • @Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式
  • @Email 被注释的元素必须是 Email 格式。
  • @Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=)被注释的元素的大小必须在指定的范围内
  • @Digits(integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
  • @Past被注释的元素必须是一个过去的日期
  • @Future 被注释的元素必须是一个将来的日期

6.2. 验证请求体(RequestBody)

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {

    @NotNull(message = "classId 不能为空")
    private String classId;

    @Size(max = 33)
    @NotNull(message = "name 不能为空")
    private String name;

    @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围")
    @NotNull(message = "sex 不能为空")
    private String sex;

    @Email(message = "email 格式不正确")
    @NotNull(message = "email 不能为空")
    private String email;

}

我们在需要验证的参数上加上了 @Valid注解,如果验证失败,它将抛出 MethodArgumentNotValidException

6.3. 验证请求参数(Path Variables 和 Request Parameters)

一定一定不要忘记在类上加上 **@Validated ** 注解了,这个参数可以告诉 Spring 去校验方法参数。

@RestController
@RequestMapping("/api")
@Validated
public class PersonController {

    @GetMapping("/person/{id}")
    public ResponseEntity getPersonByID(@Valid
                                                @PathVariable("id")
                                                @Max(value = 5,
                                                     message = "超过 id 的范围了")
                                                Integer id)
    {
        return ResponseEntity.ok().body(id);
    }
}

7. 全局处理 Controller 层异常

介绍一下我们 Spring 项目必备的全局处理 Controller 层异常。
相关注解:

  1. @ControllerAdvice:注解定义全局异常处理类
  2. @ExceptionHandler:注解声明异常处理方法

如何使用呢?拿我们在第 5 节参数校验这块来举例子。如果方法参数不对的话就会抛出 MethodArgumentNotValidException,我们来处理这个异常。

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    /**
     * 请求参数异常处理
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request) {
       ......

    }
}

⭐更多关于 Spring Boot 异常处理的内容: 使用枚举简单封装一个优雅的 Spring Boot 全局异常处理!

8. jpa

⭐推荐文章:一文搞懂如何在 Spring Boot 正确中使用 JPA

9. 事务 @Transactional

在要开启事务的方法上使用 @Transactional注解即可!

@Transactional(rollbackFor = Exception.class)
public void save() {
  ......

}

我们知道 Exception 分为:

  • 运行时异常 RuntimeException
  • 非运行时异常

@Transactional注解中如果不配置 rollbackFor属性,那么事务只会在遇到 RuntimeException的时候才会回滚,加上 rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚。

@Transactional注解一般可以作用在类或者方法上。

  • 作用于类:当把 @Transactional 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息。
  • 作用于方法:当类配置了 @Transactional,方法也配置了 @Transactional,方法的事务会覆盖类的事务配置信息。

⭐更多关于 Spring 事务的内容请查看:

  1. 可能是最漂亮的 Spring 事务管理详解(opens new window)
  2. 一口气说出 6 种 @Transactional 注解失效场景

10. json 数据处理

10.1. 过滤 json 数据

**@JsonIgnoreProperties​** 作用在类上用于过滤掉特定字段不返回或者不解析。

//生成json时将userRoles属性过滤
@JsonIgnoreProperties({"userRoles"})
public class User {

    private String userName;
    private String fullName;
    private String password;
    private List userRoles = new ArrayList<>();
}

**@JsonIgnore** 一般用于类的属性上,作用和上面的 **@JsonIgnoreProperties** 一样。

public class User {

    private String userName;
    private String fullName;
    private String password;
   //生成json时将userRoles属性过滤
    @JsonIgnore
    private List userRoles = new ArrayList<>();
}

10.2. 格式化 json 数据

@JsonFormat一般用来格式化 json 数据。
比如:

@JsonFormat(shape=JsonFormat.Shape.STRING,
            pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
            timezone="GMT")
private Date date;

10.3. 扁平化对象

@Getter
@Setter
@ToString
public class Account {
    private Location location;
    private PersonInfo personInfo;

  @Getter
  @Setter
  @ToString
  public static class Location {
     private String provinceName;
     private String countyName;
  }
  @Getter
  @Setter
  @ToString
  public static class PersonInfo {
    private String userName;
    private String fullName;
  }
}

未扁平化之前:

{
    "location": {
        "provinceName":"湖北",
        "countyName":"武汉"
    },
    "personInfo": {
        "userName": "coder1234",
        "fullName": "shaungkou"
    }
}

使用 @JsonUnwrapped&#x200B;扁平对象之后:

@Getter
@Setter
@ToString
public class Account {
    @JsonUnwrapped
    private Location location;
    @JsonUnwrapped
    private PersonInfo personInfo;
    ......

}
{
  "provinceName":"湖北",
  "countyName":"武汉",
  "userName": "coder1234",
  "fullName": "shaungkou"
}

  1. 测试相关
    **@ActiveProfiles** 一般作用于测试类上, 用于声明生效的 Spring 配置文件。
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
@Slf4j
public abstract class TestBase {
  ......

}
  • **@Test** 声明一个方法为测试方法
  • **@Transactional** 被声明的测试方法的数据会回滚,避免污染测试数据。
  • **@WithMockUser** *Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限。
    @Test
    @Transactional
    @WithMockUser(username = "user-id-18163138155",
                  authorities = "ROLE_TEACHER")
    void should_import_student_success() throws Exception {
        ......

    }

Spring Boot 自动装配原理

每次问到 Spring Boot, 面试官非常喜欢问这个问题: “讲述一下 SpringBoot 自动装配原理?”
我觉得我们可以从以下几个方面回答:

  1. 什么是 SpringBoot 自动装配?
  2. SpringBoot 是如何实现自动装配的?如何实现按需加载?
  3. 如何实现一个 Starter?

前言

使用过 Spring 的小伙伴,一定有被 XML 配置统治的恐惧。即使 Spring 后面引入了基于注解的配置,我们在开启某些 Spring 特性或者引入第三方依赖的时候,还是需要用 XML 或 Java 进行显式配置。

举个例子。没有 Spring Boot 的时候,我们写一个 RestFul Web 服务,还首先需要进行如下配置。

@Configuration
public class RESTConfiguration
{
    @Bean
    public View jsonTemplate() {
        MappingJackson2JsonView view = new MappingJackson2JsonView();
        view.setPrettyPrint(true);
        return view;
    }

    @Bean
    public ViewResolver viewResolver() {
        return new BeanNameViewResolver();
    }
}

spring-servlet.xml


但是,Spring Boot 项目,我们只需要 添加相关依赖,无需配置,通过启动下面的 main 方法即可。

并且,我们通过 Spring Boot 的全局配置文件 application.propertiesapplication.yml即可对项目进行设置比如更换端口号,配置 JPA 属性等等。

SpringBoot 是如何实现自动装配的?

我们先看一下 SpringBoot 的核心注解 @SpringBootApplication

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@ComponentScan
@EnableAutoConfiguration
public @interface SpringBootApplication {

}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration //实际上它也是一个配置类
public @interface SpringBootConfiguration {
}

大概可以把 @SpringBootApplication看作是 @Configuration@EnableAutoConfiguration@ComponentScan 注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是:

  • @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
  • @Configuration:允许在上下文中注册额外的 bean 或导入其他配置类
  • @ComponentScan: 扫描被 @Component (@Service, @Controller)注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。如下图所示,容器中将排除 TypeExcludeFilterAutoConfigurationExcludeFilter

Spring学习笔记
@EnableAutoConfiguration 是实现自动装配的重要注解,我们以这个注解入手。

@EnableAutoConfiguration: 实现自动装配的核心注解

EnableAutoConfiguration 只是一个简单地注解, 自动装配核心功能的实现实际是通过 **AutoConfigurationImportSelector** 类。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage //作用:将main包下的所欲组件注册到容器中
@Import({AutoConfigurationImportSelector.class}) //加载自动装配类 xxxAutoconfiguration
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class[] exclude() default {};

    String[] excludeName() default {};
}

AutoConfigurationImportSelector :加载自动装配类

AutoConfigurationImportSelector类的继承体系如下:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {

}

public interface DeferredImportSelector extends ImportSelector {

}

public interface ImportSelector {
    String[] selectImports(AnnotationMetadata var1);
}

可以看出, AutoConfigurationImportSelector 类实现了 ImportSelector接口,也就实现了这个接口中的 selectImports方法,该方法主要用于 获取所有符合条件的类的全限定类名,这些类需要被加载到 IoC 容器中

private static final String[] NO_IMPORTS = new String[0];

public String[] selectImports(AnnotationMetadata annotationMetadata) {
        // .判断自动装配开关是否打开
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
          //.获取所有需要装配的bean
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }

这里我们需要重点关注一下 **getAutoConfigurationEntry()** 方法,这个方法主要负责加载自动配置类的。

该方法调用链如下:

Spring学习笔记
现在我们结合 getAutoConfigurationEntry()的源码来详细分析一下:
private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();

AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
        //.<<<<<<<<<<<<<<<<<<<<<<<<<<
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
            //.<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            //.<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            //.<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            configurations = this.removeDuplicates(configurations);
            Set exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.filter(configurations, autoConfigurationMetadata);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
        }
    }

  • 第 1 步: *判断自动装配开关是否打开
if (!this.isEnabled(annotationMetadata)){...}

判断自动装配开关是否打开。默认 spring.boot.enableautoconfiguration=true,可在 application.propertiesapplication.yml 中设置

Spring学习笔记
  • 第 2 步 : *获取包括的、被排除的组件名

用于获取 EnableAutoConfiguration注解中的 excludeexcludeName

AnnotationAttributes attributes = this.getAttributes(annotationMetadata);

attributes里有两个字段,其形如:

"attributes":{
  "exclude":{
    "key": "exclude",
    "value": String类型变量
  },
  "excludeName":{
    "key": "excludeName",
    "value": Sting类型变量
  }
}
  • 第 3 步:获取需要自动装配的所有配置类,读取 **META-INF/spring.factories**
List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);

根据 excludeexcludeName获取需要自动装配的所有配置类,读取 META-INF/spring.factories:

  • spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories

Spring学习笔记

从下图可以看到这个文件的配置内容都被我们读取到了。 XXXAutoConfiguration的作用就是 按需加载组件

Spring学习笔记
不光是这个依赖下的 META-INF/spring.factories被读取到, 所有 Spring Boot Starter 下的 **META-INF/spring.factories** 都会被读取到。
所以,你可以清楚滴看到, druid 数据库连接池的 Spring Boot Starter 就创建了 META-INF/spring.factories文件。
Spring学习笔记
  • 第 4 步 : *按需加载

到这里可能面试官会问你:” spring.factories中这么多配置,每次启动都要全部加载么?”。
很明显,这是不现实的。我们 debug 到后面你会发现,configurations 的值变小了。

configurations = this.removeDuplicates(configurations);

因为,这一步有经历了一遍筛选, **@ConditionalOnXXX** 中的所有条件都满足,该类才会生效。

@Configuration
// 检查相关的类:RabbitTemplate 和 Channel是否存在
// 存在才会加载
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {
}

如何实现一个Starter

查看:如何实现一个 Starter

总结

Spring Boot :

  • 通过 @EnableAutoConfiguration开启 自动装配
  • 通过 SpringFactoriesLoader 最终加载 META-INF/spring.factories中的自动配置类实现自动装配,
  • 自动配置类其实就是通过 @Conditional按需加载的配置类,
  • 想要其生效必须引入 spring-boot-starter-xxx包实现起步依赖

参考

JavaGuide Spring

Original: https://www.cnblogs.com/excelsiorly/p/15884263.html
Author: Excelsiorly
Title: Spring学习笔记

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/580720/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球