Spring

宋正兵 on 2021-06-22

Spring

Spring框架概述

Spring 是一种轻量级开发框架,旨在提高开发人员的开发效率以及系统的可维护性。我们一般讲的 Spring 框架指的是 Spring Framework,它是很多模块的集合,这些模块可以很方便地协助我们进行开发。

包括:核心模块、数据访问、Web、AOP、工具、消息和测试模块。

特征

  • 核心技术:依赖注入(DI)、AOP
  • 测试:模拟对象
  • 数据访问:事务、DAO支持、JDBC
  • Web支持:Spring MVC

列举一些重要的模块

  • Spring Core: 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 IoC 依赖注入功能。
  • Spring Aspects : 该模块为与 AspectJ 的集成提供支持。
  • Spring AOP :提供了面向切面的编程实现。
  • Spring JDBC : Java 数据库连接。
  • Spring JMS :Java 消息服务。
  • Spring ORM : 用于支持 Hibernate 等 ORM 工具。
  • Spring Web : 为创建 Web 应用程序提供支持。
  • Spring Test : 提供了对 JUnit 和 TestNG 测试的支持。

@RestController和@Controller

单独使用 @Controller 的话一般需要返回一个视图,属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。

@RestController 只返回对象,对象数据直接以 JSON 或 XML 形式写入 HTTP 响应(Response)中,这种情况属于 RESTful Web 服务,也是目前比较常用的(前后端分离)。

@Controller + @ResponseBody 的效果和 @RestController 的效果一样。

IoC和DI

概念

IoC

IoC 是指“反转控制”,是 Spring 中的一种设计思想以及重要特性。控制反转是一种通过描述(XML 或注解)并通过第三方去生产或获取特定对象的方式。在 Spring 中实现控制反转的是 IoC 容器,其实现方法是依赖注入(Dependency Injection,DI)。

Spring 容器在初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时再从 IoC 容器中取出需要的对象。

控制 指的是 IoC 容器控制了对象。传统程序设计,我们直接在对象内部通过 new 进行创建对象,是程序主动去创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 Ioc 容器来控制对象的创建。

反转 指的是由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象。传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象。

用图来说明,如下:

传统的程序设计

IOC 容器

image.png

DI

DI 指依赖注入,组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中

依赖注入的目的 并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

依赖 指的是应用程序需要 IoC 容器来提供对象需要的外部资源。

注入 指的是 IoC 容器注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

总结

传统的开发过程中,我们要实现某个功能时如果需要两个或以上的对象协作来完成,在没有使用 Spring 的情况下,需要在对象内将它合作的对象通过 new Object() 这样的语法新建出来,这种创建的方式是主动的,创建的主动权和创建的时机都掌握在我们手上。但是这样会使得类之间的耦合度变高,比如 A 对象需要 B 对象合作完成一件任务,A 需要 B,那么 A 就产生了对 B 的依赖,也就是说 A、B 之间产生了耦合关系。

使用了 Spring 之后,创建合作对象 B 的工作由 Spring 来完成,Spring 创建好合作对象 B 后会将其存放到容器当中,当 A 对象需要使用 B 对象的时候,Spring 就会从容器中找到并取出 B 对象,然后交给 A 对象使用。至于 Spring 是如何创建以及什么时候创建好的 B 对象,A 对象不需要去关心这些细节,在 A 对象得到 B 对象之后完成工作即可。

参考博客:

谈谈对Spring IOC的理解 - 孤傲苍狼 - 博客园 (cnblogs.com)

Spring IoC有什么好处呢? - Mingqi的回答 - 知乎

Spring IoC容器的初始化过程

  1. 设置容器的初始化状态,如:容器的启动时间,容器的激活状态
  2. 解析 bean.xml 配置文件,将配置文件中的信息解析封装为 BeanDefinition 对象
  3. 将 BeanDefinition 对象注册到 BeanFactory 容器中。此时还没有真正创建 bean 对象,只是解析封装 xml 配置文件的内容

BeanDefinition 中保存了我们的 Bean 信息,比如这个 Bean 指向的是哪个类、是否是单例的、是否懒加载、这个 Bean 依赖了哪些 Bean 等等。

参考博客:

Spring IOC 容器源码分析_Javadoop

Spring AOP

下面的都是动态织入,静态织入使用 AspectJ 在编译器织入,在这个期间使用 AspectJ 的编译器把 Aspect 类变编译成 class 字节码,然后在目标类编译的时候进行静态织入。

AOP 面向切面编程,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程

Spring AOP 是基于动态代理的,如果要代理的对象实现了某个接口(任意的),那么会使用 JDK Proxy 去实现 AOP,如果没有实现接口的对象,会使用 Cglib 去实现 AOP。

JDK 动态代理只能只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。

JDK Proxy

首先定义一个发送短信的接口,并创建一个发送短信的服务类实现该接口

1
2
3
4
5
6
7
8
9
public interface SmsService {
String send(String message);
}
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}

然后定义一个 JDK 动态代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DemoInvocationHandler implements InvocationHandler {
// 被代理的对象
private final Object target;
public DemoInvocationHandler(Object target) {
this.target = target;
}
// 动态代理对象替我们去调用被代理对象的原生方法
public Object invoke(Object proxy, Method method, Object[] args) throw Throwable {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return result;
}
}

创建获取代理对象的工厂类

1
2
3
4
5
6
7
8
9
10
public class JdkProxyFacotry {
// 获取target的代理对象
public static Object getProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类的类加载器
target.getClass().getInterfaces(), // 目标类所需要实现的接口,可以有多个
new DemoInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler
);
}
}

实际测试:

1
2
3
4
5
6
7
SmsService smsService = (SmsService) JdkProxyFacotry.getProxy(new SmsServiceImpl());
smsService.send("java");
/*
before method send
send message:java
after method send
*/

注意,如果用传入的类引用指向代理会报错,只能用接口引用指向代理。比如下面代码就会报错

1
2
3
4
5
6
SmsServiceImpl smsService = (SmsServiceImpl) JdkProxyFacotry.getProxy(new SmsServiceImpl());
smsService.send("java");
/*
Exception in thread "main" java.lang.ClassCastException: class $Proxy0 cannot be cast to class SmsServiceImpl ($Proxy0 and SmsServiceImpl are in unnamed module of loader 'app')
at Main.main(Main.java:11)
*/

JDK 动态代理的 Proxy.newProxyInstance() 方法就需要传递接口参数,猜测是因为 JDK 动态代理的原理是根据定义好的规则,用传入的接口创建一个新类。所以采用动态代理时只能用接口引用指向代理,而不能用传入的类引用指向代理。Cglib 不存在这种问题。

JDK动态代理为什么必须用接口以及与CGLIB的对比_魔术师的专栏-CSDN博客_jdk动态代理为什么必须实现接口

CGLib

JDK 动态代理只能代理实现了接口的类,如果想要代理没有实现接口的类,可以用 CGLib 动态代理机制来解决。CGLib 通过继承方式实现代理。

在 CGLib 动态代理机制中 MethodInterceptor 接口和 Enhancer 类是核心,我们需要去自定义 MethodInterceptor 并重写 intercept 方法,intercept 方法用于拦截增强被代理类的方法。

1
2
3
4
5
public interface MethodInterceptor extends Callback{
// 拦截被代理类中的方法
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable;
}
  1. obj :被代理的对象(需要增强的对象)
  2. method :被拦截的方法(需要增强的方法)
  3. args :方法入参
  4. methodProxy :用于调用原始方法

CGLib 动态代理的使用步骤

  1. 定义一个类
  2. 实现 MethodInteceptor 接口并自定义 intercept() 方法
  3. 通过 Enhancer 类的 create() 创建代理类

举例

CGLib 是一个开源项目,需要引入相关依赖

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

首先定义一个发送短信的类

1
2
3
4
5
6
public class SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}

然后自定义 MethodInterceptor 方法拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DemoMethodInterceptor implements MethodInterceptor {
/**
* @param o 被代理的对象(需要增强的对象)
* @param method 被拦截的方法(需要增强的方法)
* @param args 方法入参
* @param methodProxy 用于调用原始方法
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object object = methodProxy.invokeSuper(o, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return object;
}

}

获取代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CglibProxyFactory {

public static Object getProxy(Class<?> clazz) {
// 创建动态代理增强类
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new DebugMethodInterceptor());
// 创建代理类
return enhancer.create();
}
}

测试使用

1
2
3
4
5
6
7
SmsService smsService = (SmsService) CglibProxyFactory.getProxy(smsService.class);
smsService.send("java");
/*
before method send
send message:java
after method send
*/

AOP的使用

基于XML的方式

首先需要用 <bean> 标签将目标类和切面对象类的创建权交给 Spring

1
2
3
4
<!--    目标对象-->
<bean id="target" class="com.songzb.aop.Target"></bean>
<!-- 切面对象-->
<bean id="myAspect" class="com.songzb.aop.MyAspect"></bean>

然后配置织入关系,告诉Spring框架 哪些方法(切点)需要进行哪些增强(前置、后置。。。)

1
2
3
4
5
6
7
8
<!--    配置织入,告诉Spring框架 哪些方法(切点)需要进行哪些增强(前置、后置。。。)-->
<aop:config>
<!-- 声明切面-->
<aop:aspect ref="myAspect">
<!-- 切面:切点+通知-->
<aop:before method="before" pointcut="execution(public void com.songzb.aop.Target.save())"></aop:before>
</aop:aspect>
</aop:config>

基于注解的方式

基于注解的Spring AOP的配置和使用 - Study_Work - 博客园 (cnblogs.com)

  1. 利用 @Component 注解将目标类和切面类的对象创建权交给 Spring
  2. 使用 @Aspect 标注切面类
  3. 使用@通知注解【下边的】标注切面类的通知方法
  4. 在配置文件中开启 AOP 自动代理 <aop:aspectj-autoproxy/>
1
2
3
4
5
6
7
8
9
10
11
12
13
// 目标类
@Component("target")
public class Target implements TargetInterface {}
// 切面类
@Component("myAspect")
@Aspect // 标注当前MyAspect是一个切面类
public class MyAspect {
// 配置前置通知
@Before("execution(* com.songzb.anno.*.*(..))")
public void before(){
System.out.println("前置增强...");
}
}

AOP 常用注解:

注解 作用
@Before 前置通知:目标方法之前执行
@After 后置通知:目标方法之后执行(始终执行)
@AfterReturning 返回后通知:执行方法结束前执行(异常不执行)
@AfterThrowing 异常通知:出现异常时执行
@Around 环绕通知:环绕目标方法执行

Spring AOP和AspectJ AOP有什么区别?

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。Spring AOP 基于代理,而 AspectJ 基于字节码操作。

Spring AOP 已经集成了 AspectJ,AspectJ 比 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单。

如果切面少,两者性能差不多;如果切面太多,最好选择 AspectJ,它更快。

静态织入使用 AspectJ 在编译器织入,在这个期间使用 AspectJ 的编译器把 Aspect 类变编译成 class 字节码,然后在目标类编译的时候进行静态织入。

Spring Bean

Bean 就是由 IOC 实例化、组装、管理的一个对象。

Spring 如何管理 Bean 的

  • 通过读取 xml 文件,反射实例化对象,放在 FactoryBeanRegistrySupport 类的 factoryBeanObjectCache里面保存起来,该属性是一个 ConcurrentHashMap。
  • 通过 BeanFactory 实例化的 Bean 会在第一次真正使用的时候才初始化。
  • 通过 ApplicationContext 实例化的 Bean 会在创建的时候就初始化了。

Bean 的作用域

scope:指定对象的作用范围,取值如下:

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

Spring中的单例bean的线程安全问题

Spring 的容器本身没有提供 Bean 的线程安全策略,所以需要根据线程的作用域来分析 Bean 是否线程安全。

如果 Bean 是 prototype,那么不存在线程安全问题,因为每次请求 Bean 都是创建一个新的 Bean 对象,线程之间不存在 Bean 共享,所以是线程安全的。【前提是 Bean 里面没有类变量】

如果 Bean 是单例的,且我们给这个 Bean 赋予了状态,那么多线程环境下会产生线程安全问题。比如 Bean 中有一个 count 变量,并提供有 add 方法,在多线程环境下 不添加任何同步措施,count 的值将会是无法预测的。但如果这个 Bean 是无状态的,比如我们常用的 Controller、Service、Dao 这些 Bean 都是无状态的,它们不存在线程安全问题。

如果有需要保存数据的,可以在类中定义一个 ThreadLocal 成员变量,将数据保存在 ThreadLocal 中。

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
public class MyRunnable implements Runnable {
private ThreadLocal threadLocal = new ThreadLocal();
@Override
public void run() {
threadLocal.set((int)(Math.random()*100));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread t1 = new Thread(sharedRunnableInstance);
Thread t2 = new Thread(sharedRunnableInstance);
t1.start();
t2.start();
}
}
/*
99
25
*/

可以看到并没有出现线程安全问题“一个线程覆盖另一个线程设置的值”,而是各自存取的是不同的值。

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

  1. 作用对象不同,@Component 注解作用于类,@Bean 注解作用于方法。
  2. @Component 注解表明一个类会作为组件类,并告知 Spring 要为这个类创建 bean(可以用 @ComponentScan 注解来指定要扫描的路径),@Bean 注解告诉 Spring 这个方法将会返回一个对象,这个对象要注册为 Spring 应用上下文中的 bean。通常方法体中包含了最终产生 bean 实例的逻辑。
  3. 如果想将第三方的类变成组件,你又没有源代码,也就没办法使用 @Component 进行自动配置,这种时候使用 @Bean 就比较合适了。不过同样的也可以通过xml方式来定义。

@Bean 注解使用示例,一般配合 @Configuration 使用

1
2
3
4
5
6
7
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}

等同于

1
2
3
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

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

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

被添加了上述四种注解的类可以被 @Autowired 注解自动装配。

Bean 的生命周期

Spring Bean 的完整生命周期从创建 Spring 容器开始,直到最终 Spring 容器销毁 Bean。

一个 Bean 的生命周期分为四个阶段

如果熟悉过后,可以简单概括为:

Bean的生命周期可以概括为:
实例化 Bean对象并设置对象属性
检查 Aware 接口并设置相关依赖【Aware接口主要功能就是提醒容器将这个Bean需要依赖的对象注入】
BeanPostProcesser 前置处理
检查是否实现了 InitializingBean 接口以决定是否调用 afterPropertiesSet方法
检查是否配置了自定义的 init-method
BeanPostProcesser 后置处理
对象使用
检查是否实现 DisposableBean 以决定是否调用 destory 方法
检查是否配置了自定义的 destory-method

Spring基础复习:BeanFactory的使用_APlus-CSDN博客

实例化 Instantiation

  • Bean 容器找到配置文件中 Spring Bean 的定义,利用反射机制创建一个 Bean 的实例。

属性设置 Populate

  • 如果涉及到一些属性值,利用 set() 方法设置属性值。(依赖注入)
  • 如果 Bean 实现了 BeanNameAware anNameAware 接口,将会调用 setBeanName() 方法,获取配置文件中 Bean 的名字,即 id。
  • 如果 Bean 是实现了 BeanFactoryAware 接口,调用 setBeanFactory() 方法,获取 BeanFactory 对象的实例。
  • 类似的如果 Bean 是实现了其他的 *Aware 接口,就调用相应的方法。

初始化 Initialization

  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行它的初始化前处理方法。
  • 如果 Bean 实现了InitializingBean 接口,执行 afterPropertiesSet() 方法。
  • 如果 Bean 在配置文件中的定义包含有 init-method 属性,执行指定的方法。
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行它的初始化后处理方法。

BeanPostProcessor 针对 Spring 上下文中的所有 Bean 都生效。

销毁 Destruction

  • 如果实现了 DiposibleBean 接口,执行 destroy() 方法。
  • 如果 Bean 在配置文件中的定义包含有 destory-method 属性,执行指定的方法。

总结

详细理一理属性设置阶段和初始化阶段。

属性设置阶段

主要是配置文件中 Bean 的一个属性配置和 Bean 本身所实现了 Aware 接口,这些接口更多的是使用在 Spring 的框架代码中,实际开发环境不建议使用。

初始化阶段

如果有 BeanPostProcessor 对象并且 Bean 实现了InitializingBean 接口,那么初始化的顺序是:

BeanPostProcessor 对象的初始化前处理方法 postProcessBeforeInitialization() -> afterPropertiesSet() 方法 -> init-method 方法 -> BeanPostProcessor 对象的初始化后处理方法。

参考博客:

Bean初始化之postProcessBeforeInitialization、afterPropertiesSet、init-method、postProcessAfterInitialization等方法的加载 - Twelve_Eleven - 博客园 (cnblogs.com)

Spring Bean的生命周期(非常详细) - Chandler Qian - 博客园 (cnblogs.com)

Spring基础复习:BeanFactory的使用_APlus-CSDN博客

BeanFactory接口

BeanFactory 接口,是生产 Bean 的工厂,它负责生产和管理各个 Bean 实例。在 Spring 中,BeanFactory 是 IoC 容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。Spring 提供了许多它的实现,比如 ClassPathXmlApplicationContext、FileSystemXmlApplicationContext、AnnotationConfigApplicationContext 等。

BeanFactory 提供了如下行为:

方法 说明
getBean(String name) 根据 Bean 的名字,获取在 IoC 容器中的 Bean 实例
containsBean(String name) 根据 Bean 的名字进行检索,看看 IoC 容器中是否有这个 Bean
isSingleton(String name) 根据 Bean 的名字判断这个 Bean 是不是单例。相应的肯定有判断 prototype 等作用域的。
getType(String name) 得到 Bean 实例的 Class 类型

ApplicationContext接口

ApplicationContext 是 Spring 继 BeanFactory 之外的另一个核心接口或容器,允许容器通过应用程序上下文环境创建、获取、管理 Bean。

它的实现类:

  • FileSystemXmlApplicationContext:该容器从 XML 文件中加载已被定义的 bean。在这里,你需要提供给构造器 XML 文件的完整路径
  • ClassPathXmlApplicationContext:该容器从 XML 文件中加载已被定义的 bean。在这里,你不需要提供 XML 文件的完整路径,只需正确配置 CLASSPATH 环境变量即可,因为,容器会从 CLASSPATH 中搜索 bean 配置文件。
  • WebXmlApplicationContext:该容器会在一个 web 应用程序的范围内加载在 XML 文件中已被定义的 bean。

Spring 中 BeanFactory 和 ApplicationContext 的区别

  • BeanFactroy 采用的是延迟加载形式来注入 Bean 的,即只有在使用到某个 Bean 时(调用 getBean()),才对该 Bean 进行加载实例化,这样,我们就不能发现一些存在的 Spring 的配置问题。而 ApplicationContext 则相反,它是在容器启动时,一次性创建了所有的 Bean。这样,在容器启动时,我们就可以发现 Spring 中存在的配置错误。
  • BeanFactory 和 ApplicationContext 都支持 BeanPostProcessor、BeanFactoryPostProcessor 的使用,但两者之间的区别是: BeanFactory 需要手动注册,而 ApplicationContext 则是自动注册
  • ApplicationContext 包还提供了以下的功能:资源访问,如 URL 和文件;事件传播;载入多个(有继承关系)上下文;MessageSource,提供国际化的消息访问
  • 前者不支持依赖注解,后者支持

BeanFactory

  • 采用了工厂模式
  • 负责读取 bean 配置文档
  • 管理 bean 的加载,实例化,维护 bean 之间的依赖关系,负责 bean 的生命周期

ApplicationContext

  • 除了提供上述 BeanFactory 所能提供的功能之外,还提供了更完整的框架功能:国际化支持、aop、事务等
  • BeanFactory 在解析配置文件时并不会初始化对象,只有在使用对象 getBean() 才会对该对象进行初始化
  • ApplicationContext 在解析配置文件时对配置文件中的所有对象都初始化了

FactoryBean接口

FactoryBean 适用于 Bean 的创建过程比较复杂的场景,比如数据库连接池的创建。实现了这个接口的类也是一个 Bean,但是这个 Bean 可以生产其他 Bean 的特类。通过对接口方法的实现,这个 Bean 被附加了工厂行为和装饰器行为,而具有了生产能力。

FactoryBean 接口中的主要方法如下:

方法 说明
getObject() 获取对象
getObjectType() 获取对象类型
isSingleton() 是否是单例,如果要获取 FactoryBean 本身这个 Bean 的话,需要在根据名字传参时加一个前缀 &

对于实现了 FactoryBean 接口的类来讲,假设有 Person 类依赖一个很复杂的 Car 类。由于 Car 类的实例创建起来很麻烦,所以可以将这个复杂的创建过程包装起来,让 Car 类去实现 FactoryBean 接口,将这个复杂的实例化过程写到 getObject() 方法中,然后我们可以配置 Person 类的 Bean 直接依赖于这个 FactoryBean 即 Car 类就可以了,中间的过程 Spring 已经封装好了。

BeanDefinition接口

BeanDefinition 就是我们所说的 Spring 的 Bean,我们定义的各个 Bean 会转换成一个个的 BeanDefinition 存在于 Spring 的 BeanFactory 中。BeanDefinition 保存了 Bean 的信息,比如这个 Bean 指向的是哪个类、是否是单例的、是否懒加载、这个 Bean 依赖了哪些 Bean 等等。

Spring事务

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

2 种,编程式事务和声明式事务。

编程式事务管理使用 TransactionTemplate 来完成;声明式事务通过配置文件来进行配置,可以基于 XML 的声明,也可以基于注解的声明。

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

TransactionDefinition 接口中定义了五个表示隔离级别的常量:

  • 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的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

Spring事务中的事务传播行为

Spring 支持 7 种事务传播行为,确定客户端和被调用端的事务边界。(通俗讲就是多个具有事务控制的 service 相互调用时所形成的复杂的事务边界控制)。

传播行为 含义
PROPAGATION_REQUIRED(XML文件中为REQUIRED) 如果没有事务就新建事务,如果存在一个事务就加入这个事务
PROPAGATION_SUPPORTS(XML文件中为SUPPORTS) 支持当前事务,如果当前没有事务,就以非事务方式执行
PROPAGATION_MANDATORY(XML文件中为MANDATORY) 使用当前事务,如果没有事务就抛出异常
PROPAGATION_NESTED(XML文件中为NESTED) 如果当前存在事务,就在嵌套事务内执行,如果当前没有事务,则执行与 REQUIRED 类似的操作
PROPAGATION_NEVER(XML文件中为NEVER) 以非事务执行,若当前存在事务就抛出异常
PROPAGATION_REQUIRES_NEW(XML文件中为REQUIRES_NEW) 新建事务,如果当前存在事务,就把当前事务挂起,各自处理自己的事务
PROPAGATION_NOT_SUPPORTED(XML文件中为NOT_SUPPORTED) 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起

spring 事务传播行为实例分析_lldouble的博客-CSDN博客

事务失效

  • Spring 事务的原理是 AOP,失效的根本原因就是这个 AOP 不起作用了
  • 发生自调用,类里面使用 this 调用本类方法,此时这个 this 不是代理类,而是被代理对象【自己调用自己的方法,没办法给 AOP 一样织入方法】
  • 方法不是 public 的
  • 数据库不支持事务
  • 异常被 catch 了,事务不会回滚【catch 抓住了异常,然后又不抛出来,事务就不生效】
  • 抛出的异常没有被定义,默认是 RuntimeException【默认 RuntimeException 异常才回滚,其他类型异常不会回滚,如果想让它回滚,需要在注解上配置一下 @Transactional(rollbackFor = Exception.class)

(转)Spring事务失效的原因(7个) - 简书 (jianshu.com)

Spring 循环依赖

Spring 是如何解决循环依赖的? - 苏三说技术的回答 - 知乎

面试官:聊聊Spring源码的生命周期、循环依赖 (baidu.com)

循环依赖就是 A 依赖 B 的同时,B 也依赖了 A,它们之间的依赖关系构成了一个环形调用。

  1. 自己依赖自己的直接依赖,A 依赖 A。
  2. 两个对象之间的直接依赖,A 依赖 B,B 依赖 A。
  3. 多个对象之间的循环依赖,A 依赖 B,B 依赖 C,C 依赖 A。

什么情况下循环依赖可以被处理?

  • 单例的 setter 注入(能解决)
  • 多例的 setter 注入(不能解决)
  • 构造器注入(不能解决)
  • 单例的代理对象 setter 注入(有可能解决)
  • DependsOn 循环依赖(不能解决)

Spring 内部的三级缓存

  • singletionObjects 一级缓存【单例池】,用于保存实例化、注入、初始化完成的 bean 实例【创建好了的单例 bean】
  • earlySingletonObjects 二级缓存,用于保存实例化完成的 bean 实例【属性还未填充完,可能是代理对象,也可能是原始对象】
  • singletonFactories 三级缓存,存放可以生成 bean 的工厂,工厂主要用来生成 bean 的代理对象。

Spring 如何解决循环依赖?

1
2
3
4
5
6
7
8
9
10
11
@Service
public class TestService1 {
@Autowired
private TestService2 testService2;
}

@Service
public class TestService2 {
@Autowired
private TestService1 testService1;
}

Spring 通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象(earlySingletonObjects),三级缓存为早期曝光对象工厂(singletonFactories)。

当 A、B 两个类发生循环引用时,在 A 完成实例化后,就是用实例化后的对象去创建一个对象工厂,并添加到三级缓存中。如果 A 被 AOP 代理,那么通过这个工厂获取到的就是 A 代理后的对象,如果 A 没有被 AOP 代理,那么这个工厂获取到的就是 A 实例化的对象。

当 A 进行属性注入时,会去创建 B,同时 B 又依赖了 A,所以创建 B 的同时又会去调用 getBean(a) 来获取需要的依赖,此时的 getBean(a) 会从缓存中获取:(其实还有一步是先去一级缓存看看有没有创建好了的单例)

  1. 先获取到三级缓存中的工厂
  2. 调用对象工厂的 getObject() 方法来获取到对应的对象【没有被 AOP 代理就直接返回实例化的对象,有被 AOP 代理就返回代理后的对象】

得到这个对象后将其注入到 B 中。紧接着 B 会走完它的生命周期流程,包括初始化、后置处理器等。当 B 创建完后,会将 B 再注入到 A 中,此时 A 再完成它的整个生命周期。至此,循环依赖结束。

为什么要三级缓存?二级缓存能解决循环依赖吗?

当 A、B 两个类发生循环引用时,如果对 A 进行了 AOP 代理,那么在创建 B 并且注入依赖的时候,我们希望从容器中获取到的是 A 代理后的对象,而不是 A 本身。利用三级缓存,只有在真正发生循环依赖的时候,才去提前生成代理对象,否则只会创建一个工厂并将其放入三级缓存中,但不会去通过这个工厂真正创建对象。

如果要使用二级缓存解决循环依赖,意味着所有 bean 在实例化后就要完成 AOP 代理,这样违背了 Spring 设计的原则,Spring 在设计之初就是通过后置处理器来在 bean 生命周期的最后一步完成 AOP 代理,而不是在实例化后就立马进行 AOP 代理。

Spring的自动装配

自动装配指的是让我们定义的 Bean 自动注入属性。

Spring 中 Bean 有三种装配机制,分别是:

  • 在 xml 中显式装配;【在 xml 中指定要装配的依赖项】
  • 在 Java 中显式装配;
  • 隐式的自动装配;

自动装配

  • defalt:用 ref 指定注入的属性

    1
    2
    3
    4
    5
    6
    7
    <bean id="dog" class="com.zbsong.pojo.Dog"/>
    <bean id="cat" class="com.zbsong.pojo.Cat"/>

    <bean id="user" class="com.zbsong.pojo.User">
    <property name="cat" ref="cat"/>
    <property name="dog" ref="dog"/>
    </bean>
  • byName:根据 Bean 名字进行装配

    1
    2
    <bean id="user" class="com.zbsong.pojo.User" autowire="byName">
    </bean>

    效果和 ref 一样,当一个 bean 节点带有 autowire="byName" 的属性时,

    1. 查找其类中所有的 set 方法名,获得属性要注入的属性名
    2. 去 Spring 容器中寻找是否有 id 和此属性名对应的对象
    3. 如果有,就取出注入;如果没有,就报空指针异常
  • byType:根据 Bean 的类型进行装配

    1
    2
    <bean id="user" class="com.zbsong.pojo.User" autowire="byType">
    </bean>

    使用 autowire="byType" (按类型自动装配),需要首先保证同一个类型的对象,在 Spring 容器中唯一,如果不唯一则会抛出 NoUniqueBeanDefinitionException 异常。【即如果此时 Spring 容器中有两个 Cat 类型的对象 cat1 和 cat 2,那么按类型自动装配的时候就没办法确认去注入哪一个对象】

  • constructor:根据构造函数参数进行装配

    1
    2
    <bean id="user" class="com.zbsong.pojo.User" autowire="constructor">
    </bean>

    实际上时按照构造函数的参数类型自动装配

  • autodetect:有默认构造器的情况下通过 constructor 方式装配,否则使用 byType 进行装配

    1
    2
    <bean id="user" class="com.zbsong.pojo.User" autowire="autodetect">
    </bean>
  • @AutoWired 可以在字段、setter 方法、构造函数上使用,进行自动装配

    作用在方法上,如果方法有参数会在 Spring 容器中查找是否有对应类型的对象,并且会执行该方法

    作用在构造函数上可以明确成员变量的加载顺序。比如下面的情况:

    1
    2
    3
    4
    5
    6
    7
    @Autowired
    private User user;
    private String school;

    public UserAccountServiceImpl(){
    this.school = user.getSchool();
    }

    代码会先执行构造方法,然后再给标注了 @Autowired 的 user 注入值,所以在执行构造方法的时候会报错。

    解决办法是:

    1
    2
    3
    4
    5
    6
    7
    8
    private User user;
    private String school;

    @Autowired
    public UserAccountServiceImpl(User user){
    this.user = user;
    this.school = user.getSchool();
    }

    因为变量初始化的顺序为:静态变量或静态语句块 -> 实例变量或初始化语句块 -> 构造方法 -> @Autowired

Spring MVC

MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。

工作原理

image.png

流程说明:

  1. 客户端(浏览器)发起请求到前端控制器(DispatcherServlet)。
  2. 前端控制器请求处理器映射器(HandlerMapping)查找 Handler(可以根据xml配置、注解进行查找)。
  3. 处理器映射器解析到对应的 Handler 后,同拦截器(interceptor)一并构建执行链(HandlerExecutionChain)返回给前端控制器。
  4. 前端控制器调用处理器适配器(HandlerAdapter)去执行 Handler。
  5. 处理器适配器将会根据适配的结果去执行 Handler(执行的即我们平常讲的 Controller 控制器)。
  6. Handler 执行完成给处理器适配器返回 ModelAndView。
  7. 处理器适配器向前端控制器返回 ModelAndView。
  8. 前端控制器请求视图解析器去进行视图解析。
  9. 视图解析器向前端控制器返回View。
  10. 前端控制器进行视图渲染 ,将模型数据填充到 request 域中。
  11. 前端控制器向用户响应结果。

SpringMVC详细流程(一) - codedot - 博客园 (cnblogs.com)

Controller 和 RequestMapping 如何对应

Spring MVC 初始化的时候,会对所有的 Bean 扫描,添加了 @Controler 注解以及 @RequestMapping 注解的 Bean 添加到 Map 里面,他是一个 LinkedHashMap。key 是 url ,value 是 RequestMappingInfo。

Spring Boot

Spring Boot介绍

Spring 是 EJB(重量级企业开发框架)的替代品,通过依赖注入和面向切面编程,用简单的 Java 对象(POJO)实现了 EJB 的功能。

虽然 Spring 的组件代码是轻量级的,但它的配置却是重量级的(需要大量 XML 配置)。

Spring 2.5 引入了基于注解的组件扫描,消除了大量的 XML 配置的 <bean> 定义。Spring 3.0 引入了基于 Java 的配置,可以替代 XML。

尽管如此,在开启某些 Spring 特性,比如事务管理和 Spring MVC,还是需要用 XML 或 Java 进行显示配置。配置 Servlet 和过滤器(比如 Spring 的 DispacherServlet)同样需要在 web.xml 或 Servlet 初始化代码里进行显示配置。

从本质上来讲,Spring Boot 就是 Spring,它做了那些没有它你自己也会去做的 Spring Bean 配置。Spring Boot 旨在简化 Spring 开发。

Spring Boot优点

  1. Spring Boot 不需要编写大量样板代码、XML 配置和注释。
  2. Spring Boot 可以很容易地与 Spring 生态系统集成,如 Spring JDBC、Spring ORM等。
  3. Spring Boot 遵循默认配置,以减少开发工作(默认配置可以修改)。
  4. Spring Boot 应用程序提供嵌入式 HTTP 服务器,如 Tomcat,可以轻松地开发和测试 web 应用程序。

spring ORM是什么,spring的七大模块有哪些_小爷欣欣-CSDN博客

Spring Boot 如何创建项目

可以通过 https://start.aliyun.com/bootstrap.html 来生成一个 Spring Boot 项目。也可以通过 IDEA 创建一个。

Spring Boot 项目结构

  • src 文件夹:存放工程代码的地方
    • main/java 文件夹:存放源代码
    • main/resources
      • static 文件夹:存放静态文件,比如图片、css、js等
      • templates 文件夹:存放模板文件,比如 jsp、thymeleaf 等
    • test 文件夹:存放测试文件

src/main/java 中 Application.java 是项目的启动类,需要放到最外层,不然会导致一些类无法被正确扫描到。

  1. domain 目录主要用于实体(Entity)与数据访问层(Repository)
  2. service 层主要是业务类代码
  3. controller 负责页面访问控制
  4. config 目录主要放一些配置类

@SpringBootApplication 注解

1
2
3
4
5
6
7
@SpringBootApplication
public class HelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldApplication.class, args);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration // 1
@ComponentScan // 2
@EnableAutoConfiguration // 3
public @interface SpringBootApplication {
}

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

Spring Boot 的核心注解 @SpringBootApplication 可以被看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。这三个注解的作用分别是:

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

Spring 如何读取配置文件

我们常将一些常用的配置信息放入配置文件当中,Spring 支持我们通过以下方式读取这些配置信息。

通过 @Value 注解读取

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

1
2
@Value("${username}")
String username;

通过 @ConfigurationProperties 读取

  1. 使用 @ConfigurationProperties 和 @Component 注解到 bean 定义类上,这里 @Component 代指同一类实例化 Bean 的注解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 将类定义为一个bean的注解,比如 @Component,@Service,@Controller,@Repository
    // 或者 @Configuration
    @Component
    // 表示使用配置文件中前缀为user的属性的值初始化该bean定义产生的的bean实例的同名属性
    // 在使用时这个定义产生的bean时,其属性name会是Tom
    @ConfigurationProperties(prefix = "user")
    public class User {
    private String name;
    private int age;
    // 省略getter/setter方法
    }

    对应application.properties配置文件内容如下:

    1
    2
    user.name=Tom
    user.age=11

    在此种场景下,当 Bean 被实例化时,@ConfigurationProperties 会将对应前缀的后面的属性与 Bean 对象的属性匹配。符合条件则进行赋值。

  2. 使用 @ConfigurationProperties 和 @Bean 注解在配置类的Bean定义方法上。以数据源配置为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class DataSourceConfig {
    @Primary
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix="spring.datasource.primary")
    public DataSource primaryDataSource() {
    return DataSourceBuilder.create().build();
    }
    }

    这里便是将前缀为“spring.datasource.primary”的属性,赋值给 DataSource 对应的属性值。

    @Configuration 注解的配置类中通过 @Bean 注解在某个方法上将方法返回的对象定义为一个 Bean,并使用配置文件中相应的属性初始化该 Bean 的属性。

  3. 使用 @ConfigurationProperties 注解到普通类,然后再通过 @EnableConfigurationProperties 定义为 Bean。

    1
    2
    3
    4
    5
    @ConfigurationProperties(prefix = "user1")
    public class User {
    private String name;
    // 省略getter/setter方法
    }

    这里 User 对象并没有使用 @Component 相关注解。而该 User 类对应的使用形式如下:

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableConfigurationProperties({User.class})
    public class Application {
    public static void main(String[] args) throws Exception {
    SpringApplication.run(Application.class, args);
    }
    }

    上述代码中,通过 @EnableConfigurationProperties 对 User 进行实例化时,便会使用到 @ConfigurationProperties 的功能,对属性进行匹配赋值。

Spring Boot 如何处理异常

Spring全局异常处理 - 叮叮叮叮叮叮当 - 博客园 (cnblogs.com)

使用 @ControllerAdvice 和 @ExceptionHandler 处理全局异常。

  1. 自定义异常类型,继承 RuntimeException 类。
  2. 创建异常处理类,类上添加 @ControllerAdvice 注解,可以添加 assignableTypes 参数指定只处理特定 Controller 类的异常。
  3. 在异常处理方法上添加 @ExceptionHandler(value = Exception.class),value 可以指定要处理的是哪一类异常,此处表示所有异常都走这里。

1. 新建异常信息实体类

非必要的类,主要用于包装异常信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/main/java/com/twuc/webApp/exception/ErrorResponse.java
public class ErrorResponse {
private String message;
private String errorTypeName;

public ErrorResponse(Exception e) {
this(e.getClass().getName(), e.getMessage());
}

public ErrorResponse(String errorTypeName, String message) {
this.errorTypeName = errorTypeName;
this.message = message;
}
......省略getter/setter方法
}

2. 自定义异常类型

一般我们处理的都是 RuntimeException ,所以如果你需要自定义异常类型的话直接集成这个类就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/main/java/com/twuc/webApp/exception/ResourceNotFoundException.java
public class ResourceNotFoundException extends RuntimeException {
private String message;

public ResourceNotFoundException() {
super();
}

public ResourceNotFoundException(String message) {
super(message);
this.message = message;
}

@Override
public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

3. 新建异常处理类

我们只需要在类上加上 @ControllerAdvice 注解这个类就成为了全局异常处理类,当然你也可以通过 assignableTypes 指定特定的 Controller 类,让异常处理类只处理特定类抛出的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/main/java/com/twuc/webApp/exception/GlobalExceptionHandler.java
@ControllerAdvice(assignableTypes = {ExceptionController.class})
@ResponseBody
public class GlobalExceptionHandler {

ErrorResponse illegalArgumentResponse = new ErrorResponse(new IllegalArgumentException("参数错误!"));
ErrorResponse resourseNotFoundResponse = new ErrorResponse(new ResourceNotFoundException("Sorry, the resourse not found!"));

@ExceptionHandler(value = Exception.class)// 拦截所有异常, 这里只是为了演示,一般情况下一个方法特定处理一种异常
public ResponseEntity<ErrorResponse> exceptionHandler(Exception e) {

if (e instanceof IllegalArgumentException) {
return ResponseEntity.status(400).body(illegalArgumentResponse);
} else if (e instanceof ResourceNotFoundException) {
return ResponseEntity.status(404).body(resourseNotFoundResponse);
}
return null;
}
}

4. controller模拟抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/main/java/com/twuc/webApp/web/ExceptionController.java
@RestController
@RequestMapping("/api")
public class ExceptionController {

@GetMapping("/illegalArgumentException")
public void throwException() {
throw new IllegalArgumentException();
}

@GetMapping("/resourceNotFoundException")
public void throwException2() {
throw new ResourceNotFoundException();
}
}

5. 测试

使用 Get 请求 localhost:8080/api/resourceNotFoundException ,服务端返回的 JSON 数据如下:

1
2
3
4
{
"message": "Sorry, the resourse not found!",
"errorTypeName": "com.twuc.webApp.exception.ResourceNotFoundException"
}

也可以通过 MockMvc 类在 test 中模拟 Http 请求,MockMvc 由 org.springframework.boot.test 包提供。

JPA

JPA 是 Java persistence API 的简称,Java 持久层 API,是 JDK 5.0 注解或 XML 描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。

spring boot 中使用 jpa以及jpa介绍_ 深存少年梦-CSDN博客_jpa

如何使用JPA在数据库中非持久化一个字段?

假设有一个类有以下属性

1
2
3
4
5
6
7
Entity(name="USER")
public class User {
private Long id;
private String userName;
private String password;
private String secrect;
}

如果想要让 secrect 这个字段不被持久化,即不被数据库存储怎么办呢?可以采用下面几种方法:

1
2
3
4
5
static String transient1; // not persistent because of static
final String transient2 = “Satish”; // not persistent because of final
transient String transient3; // not persistent because of transient
@Transient
String transient4; // not persistent because of @Transient

过滤器

介绍

Filter 过滤器主要是用来过滤用户请求的,它允许我们对用户请求进行前置处理和后置处理,比如过滤非法请求。Filter 过滤器是面向切面编程——AOP 的具体实现(AOP 切面编程只是一种编程思想而已)。

如果想要自定义 Filter 的话非常简单,只需要实现 javax.Servlet.Filter 接口,然后重写里面的 3 个方法即可。

1
2
3
4
5
6
7
8
9
10
// Filter.java
public interface Filter {

//初始化过滤器后执行的操作
default void init(FilterConfig filterConfig) throws ServletException {}
// 对请求进行过滤
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
// 销毁过滤器后执行的操作,主要用户对某些资源的回收
default void destroy() {}
}

如何实现过滤的?

Filter 接口中有一个叫做 doFilter() 的方法,这个方法实现了对用户请求的过滤。具体流程:

  1. 用户发送请求到 web 服务器,请求会先到过滤器。
  2. 过滤器对请求进行一些处理,比如过滤请求参数、修改返回给客户端的 response 内容、判断是否让用户访问该接口等等,再发送给目标资源。
  3. 用户请求响应完毕后,响应会先到过滤器,对响应内容进行一些处理,再把响应发送给客户端。

自定义Filter

多个 Filter 可以通过设置优先级来决定它们的执行顺序。

手动注册配置实现

自定义的 Filter 需要实现 javax.Servlet.Filter 接口,并重写接口中定义的 3 个方法。

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
@Component
public class MyFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(MyFilter.class);

@Override
public void init(FilterConfig filterConfig) {
logger.info("初始化过滤器:", filterConfig.getFilterName());
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//对请求进行预处理
logger.info("过滤器开始对请求进行预处理:");
HttpServletRequest request = (HttpServletRequest) servletRequest;
String requestUri = request.getRequestURI();
System.out.println("请求的接口为:" + requestUri);
long startTime = System.currentTimeMillis();
//通过 doFilter 方法实现过滤功能,它会自动的去寻找下一个过滤器,执行完后返回这里,可以看作是一个递归的过程
filterChain.doFilter(servletRequest, servletResponse);
// 上面的 doFilter 方法执行结束后用户的请求已经返回
long endTime = System.currentTimeMillis();
System.out.println("该用户的请求已经处理完毕,请求花费的时间为:" + (endTime - startTime));
}

@Override
public void destroy() {
logger.info("销毁过滤器");
}
}

在配置中注册自定义的过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class MyFilterConfig {
@Autowired
MyFilter myFilter;
@Bean
public FilterRegistrationBean<MyFilter> registFilter() {
FilterRegistrationBean<MyFilter> filterRegistrationBean = new FilterRegistrationBean<>();
// 注册自定义过滤器
filterRegistrationBean.setFilter(myFilter);
// 指定过滤路径
filterRegistrationBean.setUrlPatterns(new ArrayList<>(Arrays.asList("/api/*")));
// 设置优先级
// filterRegistrationBean.setOrder(1);//优先级,1最顶级,越小越优先
return filterRegistrationBean;
}
}

通过提供好的一些注解实现

在自定义的过滤器类上加上注解 @WebFilter,然后再这个注解中通过它提供好的一些参数进行配置

1
2
3
4
@WebFilter(filterName = "MyFilterWithAnnotation", urlPatterns = "/api/*")
public class MyFilterWithAnnotation implements Filter {
......
}

另外,为了能让 Spring 找到它,你需要在启动类上加上 @ServletComponentScan 注解。

拦截器

拦截器(Interceptor)同过滤器 Filter 一样,它们都是面向切面编程。在 Spring 中,当请求发送到 Controller 时,在被 Controller 处理之前,它必须经过 Interceptors(0 个或多个)。Spring Interceptor 是一个非常类似于 Servlet Filter 的概念 。

自定义 Interceptor

如果需要自定义 Interceptor,必须实现 org.springframework.web.servlet.HandlerInterceptor 接口或者继承 org.springframework.web.servlet.handler.HandlerInterceptorAdapter 类,并且需要重写下面 3 个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler)


public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView)


public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex)

每个请求可能会通过许多拦截器。下图说明了这一点。

LogInterceptor 用于过滤所有请求

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
35
36
37
38
39
public class LogInterceptor extends HandlerInterceptorAdapter {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
long startTime = System.currentTimeMillis();
System.out.println("\n-------- LogInterception.preHandle --- ");
System.out.println("Request URL: " + request.getRequestURL());
System.out.println("Start Time: " + System.currentTimeMillis());

request.setAttribute("startTime", startTime);

return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, //
Object handler, ModelAndView modelAndView) throws Exception {

System.out.println("\n-------- LogInterception.postHandle --- ");
System.out.println("Request URL: " + request.getRequestURL());

// You can add attributes in the modelAndView
// and use that in the view page
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, //
Object handler, Exception ex) throws Exception {
System.out.println("\n-------- LogInterception.afterCompletion --- ");

long startTime = (Long) request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
System.out.println("Request URL: " + request.getRequestURL());
System.out.println("End Time: " + endTime);

System.out.println("Time Taken: " + (endTime - startTime));
}
}

配置拦截器

1
2
3
4
5
6
7
8
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// LogInterceptor apply to all URLs.
registry.addInterceptor(new LogInterceptor());
}
}

可以通过 addPathPatterns() 指定拦截哪个请求,excludePathPatterns() 指定不拦截哪个请求。

拦截器和过滤器的区别

  • 过滤器 Filter:运行在所有Servlet之前。是Java EE体系中的组件,无论是否使用其它框架都可以使用过滤器来实现拦截的效果。
    • 当你有一堆东西的时候,你只希望选择符合你要求的东西。定义这些要求的工具就是过滤器。
  • 拦截器 Interceptor:运行在 DispatcherServlet 之后。是 Spring MVC 框架中的组件,只有项目中使用了Spring MVC 框架才可以使用拦截器。
    • 在一个流程正在进行的时候,你希望干预它的进展,甚至终止它,这是拦截器做的事情。
  • 拦截器可以获取 Spring 容器中的各个 bean,而过滤器不行

拦截器功在对请求权限鉴定方面确实很有用处,在我所参与的这个项目之中,第三方的远程调用每个请求都需要参与鉴定,所以这样做非常方便,而且他是很独立的逻辑,这样做让业务逻辑代码很干净。

拦截器和过滤器的区别 - THISISPAN - 博客园 (cnblogs.com)

Spring Boot 自动装配原理

口水话回答

自动装配就是可以通过注解或者一些简单的配置,就能在 Spring Boot 的帮助下实现某块功能,可以省去很多配置。具体的做法是引入一个 xxx-starter 第三方包,然后就可以使用了。

核心原理就是通过 Spring 扫描 META-INF/spring.factories 文件,识别需要自动装配的类,然后经过条件过滤,把最终需要自动装配的类加载到 IoC 容器中。

它的实现原理是通过核心注解 @SpringBootApplication 中的 @EnableAutoConfiguration 注解当中导入的加载自动装配类 AutoConfigurationImportSelector 实现的。该类继承了 ImportSelector 接口并实现了接口的 selectImport() 方法,通过该方法可以获取所有符合条件的类的全限定名,把这些类加载到 IoC 容器中。自动装配的步骤如下:

  1. 判断自动装配开关是否打开

    默认spring.boot.enableautoconfiguration=true,可在 application.properties 或 application.yml 中设置

  2. 调用 getAutoConfigurationEntry() 方法,获取所有需要装配的 bean:

    1. 获取 EnableAutoConfiguration 注解中的 exclude 和 excludeName
    2. 读取 META-INF/spring.factories,目的是获取需要自动装配的所有配置类
    3. 进行筛选,配置类上会有条件注解,满足所有条件注解 @ConditionalOnXXX 的配置类才会被注入到 IoC 容器中

什么是 Spring Boot 自动装配?

Spring Boot 在启动时扫描外部引用 jar 包中地 META-INF/spring.factories 文件,将文件中配置地类型信息加载到 Spring 容器,并执行类中定义的各种操作。对外部 jar 来说,只需要按照 Spring Boot 定义的标准,就能将自己的功能装进 Spring Boot中。(这个格式是 自动配置的类全名.条件=值

大白话来讲就是:通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。

自动装配可以用来引入第三方包,只需要引入一个 xxx-starter 就可以了,省去了很多的配置。

Spring Boot 是如何实现自动装配的?

通过核心注解 @SpringBootApplication 中的 @EnableAutoConfiguration 注解中导入的加载自动装配类 AutoConfigurationImportSelector 实现的。该类继承了 ImportSelector 接口,实现了该接口的 selectImports() 方法,该方法主要用于获取所符合条件的类的全限定类名,这些类需要被加载到 IoC 容器中,步骤如下:

  1. 判断自动装配开关是否打开

    默认spring.boot.enableautoconfiguration=true,可在 application.properties 或 application.yml 中设置

  2. 获取所有需要装配的 bean,getAutoConfigurationEntry() 方法:

    1. 获取 EnableAutoConfiguration 注解中的 exclude 和 excludeName
    2. 获取需要自动装配的所有配置类,读取 META-INF/spring.factories
    3. 筛选,满足所有条件注解 @ConditionalOnXXX 的类才会生效

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


1
2
3
4
5
6
7
8
9
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration // 1
@ComponentScan // 2
@EnableAutoConfiguration // 3
public @interface SpringBootApplication {
}

Spring Boot 的核心注解 @SpringBootApplication 可以被看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。这三个注解的作用分别是:

  • @EnableAutoConfiguration:启用 Spring Boot 的自动装配机制
  • @Configuration:这个注解实际上就是代表了一个配置类,相当于一个 xml 文件
  • @ComponentScan:扫描被 @Component(@Service、@Controller)注解的 bean,默认会扫描启动类所在包下的所有类。

@EnableAutoConfiguration 是实现自动装配的重要注解,从它入手。

1
2
3
4
5
6
7
8
9
10
11
@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 {};
}

@EnableAutoConfiguration 只是一个简单的注解,自动装配核心功能的实现就是由 AutoConfigurationImportSelector 类实现的。

AutoConfigurationImportSelector 加载自动装配类

1
2
3
4
5
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 容器中。它的具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static final String[] NO_IMPORTS = new String[0];

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

现在我们结合 getAutoConfigurationEntry() 的源码来详细分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();

AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
// <1>.判断自动装配开关是否打开。默认spring.boot.enableautoconfiguration=true,可在 application.properties 或 application.yml 中设置
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
// <2>.用于获取EnableAutoConfiguration注解中的 exclude 和 excludeName
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
//<3>.获取需要自动装配的所有配置类,读取META-INF/spring.factories
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//<4>.筛选,@ConditionalOnXXX 中的所有条件都满足,该类才会生效。
configurations = this.removeDuplicates(configurations);
Set<String> 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);
}
}

Spring Boot 提供的条件注解

  • @ConditionalOnBean:当容器里有指定 Bean 的条件下
  • @ConditionalOnMissingBean:当容器里没有指定 Bean 的情况下
  • @ConditionalOnSingleCandidate:当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean
  • @ConditionalOnClass:当类路径下有指定类的条件下
  • @ConditionalOnMissingClass:当类路径下没有指定类的条件下
  • @ConditionalOnProperty:指定的属性是否有指定的值
  • @ConditionalOnResource:类路径是否有指定的值
  • @ConditionalOnExpression:基于 SpEL 表达式作为判断条件
  • @ConditionalOnJava:基于 Java 版本作为判断条件
  • @ConditionalOnJndi:在 JNDI 存在的条件下差在指定的位置
  • @ConditionalOnNotWebApplication:当前项目不是 Web 项目的条件下
  • @ConditionalOnWebApplication:当前项目是 Web 项 目的条件下

自定义starter

第一步,创建threadpool-spring-boot-starter工程

第二步,引入 Spring Boot 相关依赖

第三步,创建 ThreadPoolAutoConfiguration 类,添加 @Confuguration注解,在类中创建线程池的方法上添加注解 @Bean 和 @ConditionOnClass(ThreadPoolExecutor.class)

第四步,在 threadpool-spring-boot-starter 工程的 resources 包下创建 META-INF/spring.factories 文件

第五步,新建工程引入 threadpool-spring-boot-starter

img

第六步,测试

Bean映射工具

用于将不同的两个对象实例进行属性复制,从而基于源对象的属性信息进行后续操作,而不改变源对象的属性信息。

这里边有涉及到了浅拷贝和深拷贝。如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝

Apache 的 BeanUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PersonSource  {
private Integer id;
private String username;
private String password;
private Integer age;
// getters/setters omiited
}
public class PersonDest {
private Integer id;
private String username;
private Integer age;
// getters/setters omiited
}
public class TestApacheBeanUtils {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
//下面只是用于单独测试
PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);
PersonDest personDest = new PersonDest();
BeanUtils.copyProperties(personDest,personSource);
System.out.println("persondest: "+personDest);
}
}
// persondest: PersonDest{id=1, username='pjmike', age=21}

从上面的例子可以看出,对象拷贝非常简单,BeanUtils 最常用的方法就是:

1
2
3
4
//将源对象中的值拷贝到目标对象
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
BeanUtilsBean.getInstance().copyProperties(dest, orig);
}

默认情况下,使用 org.apache.commons.beanutils.BeanUtils 对复杂对象的复制是引用,这是一种浅拷贝。并且由于对拷贝对象加了很多检验,造成了性能比较差,不推荐使用。

Spring 的 BeanUtils

1
2
3
4
5
6
7
8
9
10
public class TestSpringBeanUtils {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {

//下面只是用于单独测试
PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);
PersonDest personDest = new PersonDest();
BeanUtils.copyProperties(personSource,personDest);
System.out.println("persondest: "+personDest);
}
}

Spring 下的 BeanUtils 也是使用 copyProperties 方法进行拷贝,只不过它的实现方式非常简单,就是对两个对象中相同名字的属性进行简单的 get/set,仅检查属性的可访问性。

成员变量赋值是基于目标对象的成员列表,并且会跳过 ignore 的以及在源对象中不存在,所以这个方法是安全的,不会因为两个对象之间的结构差异导致错误,但是必须保证同名的两个成员变量类型相同

Spring Boot 定时任务

玩转SpringBoot之定时任务详解 - Java学习之道 (mmzsblog.cn)

使用SpringBoot创建定时任务非常简单,目前主要有以下三种创建方式:

  1. 基于注解(@Scheduled)
  2. 基于接口(SchedulingConfigurer) 前者相信大家都很熟悉,但是实际使用中我们往往想从数据库中读取指定时间来动态执行定时任务,这时候基于接口的定时任务就派上用场了。
  3. 基于注解设定多线程定时任务

静态:基于注解

基于注解 @Scheduled 默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响,可以理解为顺序执行。

创建定时器

使用 Spring Boot 基于注解来创建定时任务非常简单,只需要几行代码便可完成。

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Configuration //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling // 2.开启定时任务
public class SaticScheduleTask {
//3.添加定时任务
@Scheduled(cron = "0/5 * * * * ?")
//或直接指定时间间隔,例如:5秒
//@Scheduled(fixedRate=5000)
private void configureTasks() {
System.err.println("执行静态定时任务时间: " + LocalDateTime.now());
}
}

cron 表达式的参数分别表示:

  • 秒(0~59) 例如0/5表示每5秒
  • 分(0~59)
  • 时(0~23)
  • 日(0~31)的某天,需计算
  • 月(0~11)
  • 周几( 可填1-7 或 SUN/MON/TUE/WED/THU/FRI/SAT)

@Scheduled 注解的参数还支持简单的延时操作,例如 fixedDelay,fixedRate 填写相应的毫秒数即可。

千万不能忘了添加 @EnableScheduling 注解,在启动类上添加【官网这么讲的】

缺点

虽然使用很方便,但是当我们想要调整执行周期的时候,需要重启应用才能生效。所以为了达到实时生效的效果,可以使用接口来完成定时任务。

动态:基于接口 SchedulingConfigurer

需要创建数据库,指定 cron_id 和 cron 字段。

1
2
3
4
5
6
7
8
9
DROP DATABASE IF EXISTS `socks`;
CREATE DATABASE `socks`;
USE `SOCKS`;
DROP TABLE IF EXISTS `cron`;
CREATE TABLE `cron` (
`cron_id` varchar(30) NOT NULL PRIMARY KEY,
`cron` varchar(30) NOT NULL
);
INSERT INTO `cron` VALUES ('1', '0/5 * * * * ?');

创建定时器

数据库准备好后,编写定时任务。定义一个配置类,实现 SchedulingConfigurer 接口的 configureTasks() 方法。

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
35
36
37
38
@Mapper
public interface CronMapper {
@Select("select cron from cron limit 1")
public String getCron();
}

@Component
@Configuration // 1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling // 2.开启定时任务
public class DynamicScheduleTask implements SchedulingConfigurer {

@Autowired //注入mapper
@SuppressWarnings("all")
CronMapper cronMapper;

/**
* 执行定时任务.
*/
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 给它 runnable 任务和时间 trigger
taskRegistrar.addTriggerTask(
//1.添加任务内容(Runnable)
() -> System.out.println("执行动态定时任务: " + LocalDateTime.now().toLocalTime()),
//2.设置执行周期(Trigger)
triggerContext -> {
//2.1 从数据库获取执行周期
String cron = cronMapper.getCron();
//2.2 合法性校验.
if (StringUtils.isEmpty(cron)) {
// Omitted Code ..
}
//2.3 返回执行周期(Date)
return new CronTrigger(cron).nextExecutionTime(triggerContext);
}
);
}
}

修改执行周期

通过修改数据库的数据,下一次定时任务执行时会自动的读取到我们最新的配置,然后设置好间隔时间。

如果在数据库修改时格式出现错误,则定时任务会停止,即使重新修改正确,此时只能重新启动项目才能恢复。

多线程

基于注解设定多线程定时任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//@Component注解用于对那些比较中立的类进行注释;
//相对与在持久层、业务层和控制层分别采用 @Repository、@Service 和 @Controller 对分层中的类进行注释
@Component
@EnableScheduling // 1.开启定时任务
@EnableAsync // 2.开启多线程
public class MultithreadScheduleTask {
@Async // 这个注解很重要
@Scheduled(fixedDelay = 1000) //间隔1秒
public void first() throws InterruptedException {
System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
System.out.println();
Thread.sleep(1000 * 10);
}

@Async
@Scheduled(fixedDelay = 2000)
public void second() {
System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
System.out.println();
}
}

常用注解

@SpringBootApplication

1
2
3
4
5
6
7
@SpringBootApplication
public class HelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldApplication.class, args);
}

}

@SpringBootApplication 注解是 Spring Boot 项目的基石,创建 Spring Boot 项目之后会默认在主类加上该注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration // 1
@ComponentScan // 2
@EnableAutoConfiguration // 3
public @interface SpringBootApplication {
}

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

Spring Boot 的核心注解 @SpringBootApplication 可以被看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。这三个注解的作用分别是:

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

Spring Bean 相关

@Autowired

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

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class UserService {
......
}

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

@Component,@Repository,@Service,@Controller

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

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

@RestController

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

单独使用 @Controller 一般用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。

@Controller + @ResponseBody 返回 JSON 或 XML 形式数据。

@Scope

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

1
2
3
4
5
@Bean
@Scope("singleton")
public Person personSingleton() {
return new Person();
}

@Configuration

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

1
2
3
4
5
6
7
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}

处理常见的 HTTP 请求类型

4 种常见的请求类型:

  • GET:请求从服务器获取特定资源。
  • POST:在服务器上创建一个新的资源。
  • PUT:更新服务器上的资源。
  • DELETE:从服务器删除特定的资源。

GET请求

@GetMapping("/user") 等价于 @RequestMapping(value="/user", method=RequestMethod.GET)

1
2
3
4
@GetMapping("/users")
public ResponseEntity<List<User>> getAllUsers() {
return userRepository.findAll();
}

POST请求

@PostMapping("/users") 等价于 @RequestMapping(value="/users", method=RequestMethod.POST)

1
2
3
4
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) {
return userRespository.save(user);
}

PUT请求

@PutMapping("/users/{userId}") 等价于 @RequestMapping(value="/users/{userId}", method=RequestMethod.PUT)

1
2
3
4
5
@PutMapping("/users/{userId}")
public ResponseEntity<User> updateUser(@PathVariable(value = "userId") Long userId,
@Valid @RequestBody UserUpdateRequest userUpdateRequest) {
......
}

DELETE请求

@DeleteMapping("/users/{userId}") 等价于 @RequestMapping(value="/users/{userId}", method=RequestMethod.DELETE)

1
2
3
4
@DeleteMapping("/users/{userId}")
public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){
......
}

前后端传值

@PathVariable和@RequestParam

1
2
3
4
5
6
@GetMapping("/klasses/{klassId}/teachers")
public List<Teacher> getKlassRelatedTeachers(
@PathVariable("klassId") Long klassId,
@RequestParam(value = "type", required = false) String type ) {
...
}

如果我们请求的 url 是:/klasses/{123456}/teachers?type=web

那么我们服务获取到的数据就是:klassId=123456,type=web

@RequestBody

用于读取 Request 请求的 body 部分,并且 ContentType 为 application/json 格式的数据,接收到数据之后自动将数据绑定到 java 对象上。

演示:

注册一个接口

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

UserRegisterRequest 对象

1
2
3
4
5
6
7
8
9
10
11
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterRequest {
@NotBlank
private String userName;
@NotBlank
private String password;
@NotBlank
private String fullName;
}

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

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

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

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

读取配置信息

很多时候我们需要将一些常用的配置信息放到配置文件中。Spring 提供了一些方法可以读取这些配置文件。

通过 @Value 注解读取

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

1
2
@Value("${username}")
String username;

通过 @ConfigurationProperties 读取

  1. 使用 @ConfigurationProperties 和 @Component 注解到 bean 定义类上,这里 @Component 代指同一类实例化 Bean 的注解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 将类定义为一个bean的注解,比如 @Component,@Service,@Controller,@Repository
    // 或者 @Configuration
    @Component
    // 表示使用配置文件中前缀为user的属性的值初始化该bean定义产生的的bean实例的同名属性
    // 在使用时这个定义产生的bean时,其属性name会是Tom
    @ConfigurationProperties(prefix = "user")
    public class User {
    private String name;
    private int age;
    // 省略getter/setter方法
    }

    对应application.properties配置文件内容如下:

    1
    2
    user.name=Tom
    user.age=11

    在此种场景下,当 Bean 被实例化时,@ConfigurationProperties 会将对应前缀的后面的属性与 Bean 对象的属性匹配。符合条件则进行赋值。

  2. 使用 @ConfigurationProperties 和 @Bean 注解在配置类的Bean定义方法上。以数据源配置为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class DataSourceConfig {
    @Primary
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix="spring.datasource.primary")
    public DataSource primaryDataSource() {
    return DataSourceBuilder.create().build();
    }
    }

    这里便是将前缀为“spring.datasource.primary”的属性,赋值给 DataSource 对应的属性值。

    @Configuration 注解的配置类中通过 @Bean 注解在某个方法上将方法返回的对象定义为一个 Bean,并使用配置文件中相应的属性初始化该 Bean 的属性。

  3. 使用 @ConfigurationProperties 注解到普通类,然后再通过 @EnableConfigurationProperties 定义为 Bean。

    1
    2
    3
    4
    5
    @ConfigurationProperties(prefix = "user1")
    public class User {
    private String name;
    // 省略getter/setter方法
    }

    这里 User 对象并没有使用 @Component 相关注解。而该 User 类对应的使用形式如下:

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableConfigurationProperties({User.class})
    public class Application {
    public static void main(String[] args) throws Exception {
    SpringApplication.run(Application.class, args);
    }
    }

    上述代码中,通过 @EnableConfigurationProperties 对 User 进行实例化时,便会使用到 @ConfigurationProperties 的功能,对属性进行匹配赋值。

全局处理Controller层异常

  • @ControllerAdvice:注解定义全局异常处理类,可以添加 assignableTypes 参数指定只处理特定 Controoler 类的异常。
  • @ExceptionHandler:注解声明异常处理方法,value 可以指定要处理的是哪一类异常。

JSON 数据处理

过滤JSON数据

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

1
2
3
4
5
6
7
8
9
//生成json时将userRoles属性过滤
@JsonIgnoreProperties({"userRoles"})
public class User {
private String userName;
private String fullName;
private String password;
@JsonIgnore
private List<UserRole> userRoles = new ArrayList<>();
}

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

1
2
3
4
5
6
7
8
public class User {
private String userName;
private String fullName;
private String password;
//生成json时将userRoles属性过滤
@JsonIgnore
private List<UserRole> userRoles = new ArrayList<>();
}

格式化 JSON 数据

@JsonFormat 一般用来格式化 JSON 数据。

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

扁平化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Getter
@Setter
@ToString
public class Account {
@JsonUnwrapped
private Location location;
@JsonUnwrapped
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;
}
}

未扁平化之前:

1
2
3
4
5
6
7
8
9
10
{
"location": {
"provinceName":"湖北",
"countyName":"武汉"
},
"personInfo": {
"userName": "coder1234",
"fullName": "shaungkou"
}
}

使用 @JsonUnwrapped 扁平对象之后:

1
2
3
4
5
6
7
8
9
10
@Getter
@Setter
@ToString
public class Account {
@JsonUnwrapped
private Location location;
@JsonUnwrapped
private PersonInfo personInfo;
......
}
1
2
3
4
5
6
{
"provinceName":"湖北",
"countyName":"武汉",
"userName": "coder1234",
"fullName": "shaungkou"
}

测试相关

@Test 声明一个方法为测试方法。

@Transactional 被声明的测试方法的数据会回滚,避免污染测试数据。

@WithMockUser 由Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限。

1
2
3
4
5
6
@Test
@Transactional
@WithMockUser(username = "user-id-18163138155", authorities = "ROLE_TEACHER")
void should_import_student_success() throws Exception {
......
}

@ActiveProfiles 一般作用于测试类上,用于声明生效的 Spring 配置文件。

1
2
3
4
5
6
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
@Slf4j
public abstract class TestBase {
......
}

@Autowired 可以加载构造方法上吗

@Autowired 可以对成员变量、方法以及构造函数进行注释。

作用在成员变量上,相当于在配置文件中配置 bean,并且使用 setter 注入。作用在成员变量上要等到类完全加载完,才会将相应的 bean 注入。

作用在普通方法上,会在注入的时候调用一次该方法,如果方法中有实体参数,会对参数进行装配,可以用来在自动注入的时候做一些初始化操作。

作用在构造函数上可以解决如下问题:

1
2
3
4
5
6
7
@Autowired
private User user;
private String school;

public UserAccountServiceImpl(){
this.school = user.getSchool();
}

这段代码不能运行成功,因为作用在成员变量上要等到类完全加载完,才会将相应的 bean 注入。这里会限制性构造方法,然后再给注解了 @Autowired 的 user 注入值,所以在执行构造方法的时候,会报错。

Java变量的初始化顺序为:静态变量或静态语句块–>实例变量或初始化语句块–>构造方法–>@Autowired

解决方法:

1
2
3
4
5
6
7
8
private final User user; // final 去确保单例
private String school;

@Autowired
public UserAccountServiceImpl(User user){
this.user = user;
this.school = user.getSchool();
}

Spring事务

事务的特性ACID

原子性 Atomicity:一个事务中的所有操作,要么全部完成,要么全部不完成。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

一致性 Consistency:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。举个例子说 A 向 B 转了 100 块钱,B 的账户里应该多 100,如果 A 的账户没有减 100,那么就是不一致的。

隔离性 Isolation:数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务的隔离级别分为:未提交读(Read Uncommitted)、提交读(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。

持久性 Durability:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

Spring对事务的支持

是否支持事务取决于数据库,比如使用 MySQL 的话,需要选择 innodb 引擎才可以支持事务。

如何实现回滚?

在 MySQL 中,恢复机制是通过回滚日志(undo log)来实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。

如果执行过程中遇到异常,直接利用回滚日志中的信息将数据回滚到修改之前的样子即可。并且由于回滚日志会先于数据持久化到磁盘上,这就保证了即使遇到数据库突然宕机,当用户再次启动数据库时,数据库还能够通过查询回滚日志来完成之前没有完成的事务。

Spring支持两种方式的事务

1)编程式事务管理

通过 TransactionTemplate 或者 TransactionManager 手动管理事务,实际应用中用的比较少。

TransactionTemplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}
}
});
}

TransactionManager:

1
2
3
4
5
6
7
8
9
10
11
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}

2)声明式事务管理

推荐使用,因为其代码侵入性最小,实际是通过 AOP 实现。

使用 @Transactional 注解进行事务管理的示例:

1
2
3
4
5
6
7
8
@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:事务运行状态。

PlatformTransactionManager 会根据 TransactionDefinition 的定义(比如事务超时时间、隔离级别、传播行为等)来进行事务管理,而 TransactionStatus 接口则提供了一些方法来获取事务相应的状态(比如是否新事务、是否可以回滚等等)。

PlatformTransactionManager:事务管理接口

Spring 并不直接管理事务,而是提供了多种事务管理器。Spring 事务管理器的接口是 PlatformTransactionManager。

通过这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)等都提供了对应的事务管理器,但是具体的实现是各个平台自己的事情了。

该接口定义了三个方法:

1
2
3
4
5
6
7
8
9
10
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface PlatformTransactionManager {
//获得事务
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//提交事务
void commit(TransactionStatus var1) throws TransactionException;
//回滚事务
void rollback(TransactionStatus var1) throws TransactionException;
}

TransactionDefinition:事务属性

事务管理器 PlatformTransactionManager 通过getTransaction(TransactionDefinition definition) 方法来得到一个事务,TransactionDefinition 类就是用来定义一些基本的事务属性。

事务属性包括 5 种:隔离级别、传播行为、回滚原则、是否只读、事务超时。

TransactionStatus:事务状态

TransactionStatus 接口用来记录事务的状态,该接口定义了一组方法用于获取或判断事务的相应状态信息。

PlatformTransactionManager.getTransaction(...) 方法返回一个 TransactionStatus 对象。

1
2
3
4
5
6
7
public interface TransactionStatus{
boolean isNewTransaction(); // 是否是新的事务
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 设置为只回滚
boolean isRollbackOnly(); // 是否为只回滚
boolean isCompleted; // 是否已完成
}

事务属性详解

事务传播行为

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

Spring 支持 7 种事务传播行为,确定客户端和被调用端的事务边界。(通俗讲就是多个具有事务控制的 service 相互调用时所形成的复杂的事务边界控制)。

传播行为 含义
PROPAGATION_REQUIRED(XML文件中为REQUIRED) 表示当前方法必须在一个具有事务的上下文中运行,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚)【默认】
PROPAGATION_SUPPORTS(XML文件中为SUPPORTS) 表示当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行
PROPAGATION_MANDATORY(XML文件中为MANDATORY) 表示当前方法必须在一个事务中运行,如果没有事务,将抛出异常
PROPAGATION_NESTED(XML文件中为NESTED) 表示如果当前方法正有一个事务在运行中,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同PROPAGATION_REQUIRED的一样
PROPAGATION_NEVER(XML文件中为NEVER) 表示当方法务不应该在一个事务中运行,如果存在一个事务,则抛出异常
PROPAGATION_REQUIRES_NEW(XML文件中为REQUIRES_NEW) 表示当前方法必须运行在它自己的事务中。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。
PROPAGATION_NOT_SUPPORTED(XML文件中为NOT_SUPPORTED) 表示该方法不应该在一个事务中运行。如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行

spring 事务传播行为实例分析_lldouble的博客-CSDN博客

太难了~面试官让我结合案例讲讲自己对Spring事务传播行为的理解。 (qq.com)

事务隔离级别

TransactionDefinition 接口中定义了五个表示隔离级别的常量:

隔离级别 介绍
ISOLATION_DEFAULT 使用后端数据库默认的隔离级别,MySQL 默认采用 REPEATABLE_READ 级别。
ISOLATION_READ_UNCOMMITTED 最低的隔离级别,允许读取尚未提交的数据,可能会导致脏读不可重复读幻读
ISOLATION_READ_COMMITTED 允许读已经提交的数据,可以阻止脏读,但是仍然可能发生不可重复读和幻读
ISOLATION_REPEATABLE_READ 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己修改。可以阻止脏读和不可重复读,但仍有可能发生幻读
ISOLATION_SERIALIZABLE 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行。

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)

幻读:关于幻读,可重复读的真实用例是什么? - 知乎用户的回答 - 知乎
https://www.zhihu.com/question/47007926/answer/222348887

事务超时属性

事务超时,指一个事务所允许执行的最长时间,如果超过了该时间限制还没有完成,则自动回滚事务。

在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为 -1。

事务只读属性

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

事务回滚规则

回滚规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下只有遇到运行时异常(RuntimeException)时才会回滚,Error 也会导致事务回滚。遇到检查(Checked)异常时不会回滚。

通过 rollbackFor 可以指定回滚特定的异常类型:

1
@Transactional(rollbackFor= MyException.class)

@Transactional 注解使用详解

作用范围

  • 方法上:只能加到 public 方法上,否则不生效
  • 类:加到类上表明对该类中所有的 public 方法都生效
  • 接口:不推荐

常用配置参数

属性名 说明
propagation 事务的传播行为,默认值为 REQUIRED
isolation 事务的隔离级别,默认值为 DEFAULT,使用后端数据库默认的隔离级别
timeout 事务的超时时间,默认值为 -1(不会超时)。超过该时间但事务还未完成,自动回滚事务。
readOnly 指定事务是否为只读事务,默认为 false
rollbackFor 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型

@Transacntional 事务注解原理

@Transacntional 注解的工作机制是基于 AOP 实现的,AOP 是利用动态代理实现。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现接口,会使用 CGLIB 动态代理。

如果一个类或者一个类的 public 方法上被标注有 @Transacntional 注解,Spring 容器就会在启动的时候为其创建一个代理类,在调用被 @Transacntional 注解的 public 方法时,会现在目标方法之前开启事务,方法执行过程中如果遇到异常会回滚事务,方法调用完之后提交事务,这些都是通过 AOP 实现的。

Spring AOP 自调用问题

若同一类中的其他没有 @Transacntional 注解的方法内部调用有 @Transacntional 注解的方法,有 @Transacntional 注解的方法的事务会失效。

这是由于 Spring AOP 代理的原因造成的,因为只有当 @Transacntional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class MyService {
// method1() 调用 method2() 会导致 method2() 的事务失效
private void method1() {
method2();
//......
}
@Transactional
public void method2() {
//......
}
}

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

使用总结

  1. @Transactional 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;
  2. 避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效;
  3. 正确的设置 @Transactional 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败

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

  • 工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
  • 代理设计模式 : Spring AOP 功能的实现。
  • 单例设计模式 : Spring 中的 Bean 默认都是单例的。
  • 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate`等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
  • 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
  • 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
  • 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。

面试官:“谈谈Spring中都用到了那些设计模式?”。 (qq.com)

MyBatis

#{}和${}的区别是什么

  • ${} 是 properties 文件中的变量占位符,它可以用于标签属性值和 sql 内部,属于静态文本替换,比如 ${driver} 会被静态替换为 com.mysql.jdbc.Driver
  • #{} 是 sql 的参数占位符,MyBatis 会将 sql 中的 #{} 替换为 ? 号,在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的 ? 号占位符设置参数值,比如 ps.setInt(0, parameterValue)${item.name} 的取值方式为使用反射从参数对象中获取 item 对象的 name 属性值,相当于 param.getItem().getName()
1
2
Select ID,COMMAND from Message where COMMAND=#{command}
Select ID,COMMAND from Message where COMMAND=‘${command}’

前者解析为:

Select ID,COMMAND from Message where COMMAND=?具有预编译效果

后者解析为:

Select ID,COMMAND from Message where COMMAND=段子 不具有预编译效果

XML 映射文件中,除了常见的select|insert|update|delete标签之外,还有哪些标签

还有 <resultMap>(将查询结果映射到一个结果集)、<where>(where 条件)、<if>(if 判断)、<foreach>(循环)、<sql>(sql片段抽取,达到重用的目的,使用 <inlcude>

通常一个 XML 映射文件都会写一个 Dao 接口与之对应,这个 Dao 接口的工作原理是什么?

Dao 接口,就是熟悉的 Mapper 接口。接口的全限定名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中的 MappedStatement(对应一个 select|insert|update|delete 节点) 的 id 值,接口方法内的参数,就是传递给 sql 的参数。

Mapper 接口是没有实现类的,当调用接口方法时,接口全限定名+方法名拼接的字符串作为 key 值,可以唯一定位一个 MappedStatement。在 MyBatis 中,每一个<select><insert><update><delete>标签,都会被解析为一个 MappedStatement 对象。

Dao 接口里的方法,参数不同时方法能重载吗?

Dao 接口方法可以重载,但是需要满足以下条件:

  1. 仅有一个无参方法和一个有参方法。
  2. 多个有参方法时,参数数量必须一致。且使用相同的 @Param 注解。

Dao 接口里的方法可以重载,但是 Mybatis 的 XML 里面的 id 不允许重复。

1
2
3
4
5
6
7
8
9
/**
* Mapper接口里面方法重载
*/
public interface StuMapper {

List<Student> getAllStu();

List<Student> getAllStu(@Param("id") Integer id);
}

然后在 StuMapper.xml 中利用 Mybatis 的动态 sql 就可以实现。

1
2
3
4
5
6
7
8
<select id="getAllStu" resultType="com.pojo.Student">
select * from student
<where>
<if test="id != null">
id = #{id}
</if>
</where>
</select>

MyBatis 是如何进行分页的?分页插件的原理是什么?

通常我们进行分页,都是传递参数给 sql 语句,使用 limit 来进行分页查询的。(物理分页,在 sql 中指定 limit 和 offset 值)

MyBatis 提供了内置的专门处理分页的 RowBounds 类。(逻辑分页会将所有的结果都查询到,然后根据 RowBounds 中提供的 offset 和 limit 值来获取最后的结果)

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
package org.apache.ibatis.session;
public class RowBounds {
public static final int NO_ROW_OFFSET = 0;
public static final int NO_ROW_LIMIT = Integer.MAX_VALUE;
public static final RowBounds DEFAULT = new RowBounds();
// 偏移量,即从第几行开始读取,起始位是0
private final int offset;
// 限制条数
private final int limit;

public RowBounds() {
this.offset = NO_ROW_OFFSET;
this.limit = NO_ROW_LIMIT;
}
public RowBounds(int offset, int limit) {
this.offset = offset;
this.limit = limit;
}
public int getOffset() {
return offset;
}
public int getLimit() {
return limit;
}
}

该类使用起来极为方便,先给接口增加一个 RowBounds 参数

1
List<Student> findStudentByRowBounds(@Param("name") String name, RowBounds rowBounds);

然后配置普通的 SQL

1
2
3
<select id="findStudentByRowBounds" resultMap="studentMapper">
SELECT * FROM studuent WHERE name LIKE CONCAT('%', #{name}, '%')
</select>

MyBatis 会自动识别 RowBounds,并根据你传递的 RowBounds 参数进行分页。

分页插件的基本原理是使用 MyBatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。

MyBatis 执行插入操作后,返回数据库主键

在定义 XML 映射器时设置属性 useGeneratedKeys 值为 true,并分别指定属性 keyProperty 和 keyColumn 为对应的数据库记录主键字段与 Java 对象的主键属性。

MyBatis 动态 sql

MyBatis 动态 sql 可以让我们在 XML 映射文件内,以标签的形式编写动态 sql,完成逻辑判断和动态拼接 sql 的功能,MyBatis 提供了 9 种动态 sql 标签 trim|where|set|foreach|if|choose|when|otherwise|bind

trim 标签:我的理解是它是一个格式化标记,通过 prefix 设置前缀xxx,suffix 设置后缀xxx,prefixoverride 去除第一个前缀xxx,suffixOverrides 去除最后一个后缀xxx,xxx表示属性引号中的值。

动态sql标签trim的用法 - 程序员大本营 (pianshen.com)

mybatis动态sql中的trim标签的使用 - 西风恶 - 博客园 (cnblogs.com)

其执行原理为,从 sql 参数对象中计算表达式的值,根据表达式的值动态拼接 sql,以此来完成动态 sql 的功能。

MyBatis 是如何将 sql 执行结果封装为目标对象并返回的?都有哪些映射形式?

第一种:使用 <resultMap> 标签,逐一定义列名和对象属性名之间的映射关系。第二种:使用 sql 列的别名功能,将列别名书写为对象属性名。

有了列名与属性名的映射关系后,MyBatis 通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。

Servlet

Servlet 是用 Java 编写的服务器端程序,主要功能在于交互式地浏览和修改数据,生成动态 Web 内容。侠义的 Servlet 是指 Java 语言实现的一个接口,广义的 Servlet 是指实现了这个 Servlet 接口的类,一般情况下人们将 Servlet 理解为后者。

Tomcat 和 Servlet 的关系 & 工作流程

Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器。Tomcat 作为 Servlet 容器,负责处理客户请求,把请求传递给 Servlet,并将 Servlet 的响应传递回给客户。Servlet 是一种运行在支持 Java 语言的服务器上的组件,用于交互式地浏览和修改数据,生成动态 Web 内容。

从 HTTP 协议中的请求和响应可以得知,浏览器发出的请求是一个请求文本,浏览器接收到的是一个响应文本。

  1. Tomcat 将 HTTP 请求文本接收并解析,然后封装成 HttpServletRequest 类型的 request 对象,所有的 HTTP 头数据都可以通过 request 对象调用对应的方法查询到。
  2. Tomcat 同时会把要响应的信息封装到为 HttpServletResponse 类型的 response 对象,通过设置 response 属性就可以控制要输出到浏览器的内容,然后将 response 交给 Tomcat,Tomcat 就会将其变成响应文本的格式发送给浏览器。

Servlet 工作原理

1
2
3
4
5
6
7
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig(); // 这个方法会返回由Servlet容器传给init()方法的ServletConfig对象
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo(); // 这个方法会返回Servlet的一段描述,可以返回一段字符串
void destroy();
}

Servlet 接口定义了 Servlet 与 Servlet 容器之间的契约。

Serlvet 容器将 Servlet 类载入内存,并产生 Servlet 实例【单例】。用户请求时,Serlvet 容器调用对应 Servlet 的 service() 方法,并传入一个 ServletRequest 对象和一个 ServletResponse 对象。

ServletRequest 对象和一个 ServletResponse 对象都是由 Servlet 容器(例如 Tomcat)封装好的,不需要程序员去实现,可以直接使用它们。ServletRequest 中封装了当前的 HTTP 请求,所以开发人员不必解析和操作原始的 HTTP 数据。ServletResponse 表示当前用户的 HTTP 响应,程序员只需要直接操作 ServletResponse 对象就能把响应轻松的发回给用户。

对于每一个应用程序,Servlet 容器还会创建一个 ServletContext 对象。这个对象中封装了上下文(应用程序)的环境详情【每个应用程序只有一个 ServletContext】。每个 Servlet 对象也都有一个封装 Servlet 配置的 ServletConfig 对象。

Servlet 的生命周期

在 Servlet 接口的定义中,init()service()destroy() 是定义 Servlet 生命周期的方法。代表了 Servlet 从“出生”到“工作”再到“死亡”的过程。Servlet 容器(例如 TomCat)会根据下面的规则来调用这三个方法:

  • 当 Servlet 第一次被请求时,Servlet 容器就会开始调用 init() 方法来初始化一个 Servlet 对象出来,但是这个方法在后续请求中不会在被 Servlet 容器调用。调用这个方法时,Servlet 容器会传入一个 ServletConfig 对象进来从而对 Servlet 对象进行初始化
  • 每当请求 Servlet 时,Servlet 容器就会调用 service() 方法,执行主要的业务逻辑
  • 当要销毁 Servlet 时,Servlet 容器就会调用 destory() 方法,执行一些后处理逻辑

编写一个 Servlet

1.创建一个 MyServlet 继承 HttpServlet 抽象类,重写 doGet()doPost() 方法【这里的 HttpServlet 是实现了 Servlet 接口的抽象类 GenericServlet 的子类,利用 HttpServlet 抽象类可以直接在 doGet() 中处理 GET 请求,doPost() 中处理 POST 请求,而不用在 service() 方法中条件判断。实际上 HttpServlet 的 service() 方法就是做了这么一件事情,判断请求的方式去调用不同的处理方法。】

1
2
3
4
5
6
7
8
9
10
public class MyServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("处理get请求");
}
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("处理post请求");
}
}

2.在 web.xml 中配置 MyServlet,这一步的目的是为了让 tomcat 知道将封装好的 request 找到对应的 Servlet。

1
2
3
4
5
6
7
8
9
10
11
12
<servlet>
<!--Servlet的名字,一般和类名相同-->
<servlet-name>MyServlet</servlet-name>
<!--Servlet的全限定类名-->
<servlet-class>top.zbsong.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<!--跟上面的Servlet名字相同,对应-->
<servlet-name>MyServlet</servlet-name>
<!--浏览器通过该url找到Servlet-->
<url-pattern>/MyServlet</url-pattern>
</servlet-mapping>

3.通过浏览器访问 http://localhost:8080/ServletDemo/MyServlet

请求转发和重定向的区别

  • 从数据共享来看,请求转发中目标页面和转发到的页面共享 request 里的数据;重定向不共享任何数据
  • 从运用场景来看,请求转发一般用于用户登录,根据角色转发到相应的模块;重定向一般用于用户注销登录时返回主页面和跳转到其他网站等
  • 从效率上来看,请求转发高;重定向低
  • 请求转发是服务器调用不同的资源处理同一请求,始终是处理的同一个请求;重定向是让浏览器再次向服务器发送请求,前后是两个不同的请求。

请求转发:forward,重定向:redirect。