Spring

基础

Spring是什么?

一句话概括:Spring 是一个轻量级、非入侵式的控制反转 (IOC) 和面向切面(AOP) 的框架。

2002年,一位知名的软件工程师和企业家Rod Johnson(罗德·约翰逊)首次发布了轻量级的Java开发框架Spring,随后渐渐崛起,并淘汰了 EJB 这个传统的重装骑兵。

到了现在,企业级开发的标配基本就是 Spring5 + Spring Boot 2 + JDK 8

Spring有哪些特性(优点)呢?

1. IOC 和 DI 的支持

Spring 的核心就是一个大的工厂容器,可以维护所有对象的创建和依赖关系,Spring工厂用于生成 Bean,并且管理 Bean 的生命周期,实现高内聚低耦合的设计理念。

2. AOP 编程的支持

Spring 提供了面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等切面功能。

3. 声明式事务的支持

支持通过配置就来完成对事务的管理,而不需要通过硬编码的方式,以前重复的一些事务提交、回滚的JDBC代码,都可以不用自己写了。

4. 快捷测试的支持

Spring 对 Junit 提供支持,可以通过注解快捷地测试 Spring 程序。

5. 快速集成功能

方便集成各种优秀框架,Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如:Struts、Hibernate、MyBatis、Quartz 等)的直接支持。

6. 复杂API模板封装

Spring 对 JavaEE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等)都提供了模板化的封装,这些封装 API 的提供使得应用难度大大降低。

Spring有哪些模块呢?

Spring 框架是分模块存在,除了最核心的 Spring Core Container 是必要模块之外,其他模块都是 可选 ,大约有 20 多个模块。

最主要的七大模块:

1. Spring Core :

Spring 核心,它是框架最基础的部分,提供 IOC 和依赖注入 DI 特性。

2. Spring Context :

Spring 上下文容器,它是 BeanFactory 功能加强的一个子接口。

3. Spring Web :

它提供 Web 应用开发的支持。

4. Spring MVC :

它针对 Web 应用中 MVC 思想的实现。

5. Spring DAO :

提供对 JDBC 抽象层,简化了 JDBC 编码,同时,编码更具有健壮性。

6. Spring ORM :

它支持用于流行的 ORM 框架的整合,比如:Spring + Hibernate、Spring + iBatis、Spring + JDO 的整合等。

7. Spring AOP :

即面向切面编程,它提供了与 AOP 联盟兼容的编程实现。

Spring有哪些常用注解呢?

Spring有很多模块,甚至广义的SpringBoot、SpringCloud也算是Spring的一部分,我们来分模块,按功能来看一下一些常用的注解:

Web:

  • @Controller:组合注解(组合了@Component注解),应用在MVC层(控制层)。

  • @RestController:该注解为一个组合注解,相当于@Controller和@ResponseBody的组合,注解在类上,意味着,该Controller的所有方法都默认加上了@ResponseBody。

  • @RequestMapping:用于映射Web请求,包括访问路径和参数。如果是Restful风格接口,还可以根据请求类型使用不同的注解:

    • @GetMapping
    • @PostMapping
    • @PutMapping
    • @DeleteMapping
  • @ResponseBody:支持将返回值放在response内,而不是一个页面,通常用户返回json数据。

  • @RequestBody:允许request的参数在request体中,而不是在直接连接在地址后面。

  • @PathVariable:用于接收路径参数,比如@RequestMapping(“/hello/{name}”)申明的路径,将注解放在参数中前,即可获取该值,通常作为Restful的接口实现方法。

容器:

  • @Component:表示一个带注释的类是一个“组件”,成为Spring管理的Bean。当使用基于注解的配置和类路径扫描时,这些类被视为自动检测的候选对象。同时@Component还是一个元注解。

  • @Service:组合注解(组合了@Component注解),应用在service层(业务逻辑层)。

  • @Repository:组合注解(组合了@Component注解),应用在dao层(数据访问层)。

  • @Autowired:Spring提供的工具(由Spring的依赖注入工具(BeanPostProcessor、BeanFactoryPostProcessor)自动注入)。

  • @Qualifier:该注解通常跟 @Autowired 一起使用,当想对注入的过程做更多的控制,@Qualifier 可帮助配置,比如两个以上相同类型的 Bean 时 Spring 无法抉择,用到此注解。

  • @Configuration:声明当前类是一个配置类(相当于一个Spring配置的xml文件)。

  • @Value:可用在字段,构造器参数跟方法参数,指定一个默认值,支持 #{} 跟${} 两个方式。一般将 SpringbBoot 中的 application.properties 配置的属性值赋值给变量。

  • @Bean:注解在方法上,声明当前方法的返回值为一个Bean。返回的Bean对应的类中可以定义init()方法和destroy()方法,然后在@Bean(initMethod=”init”,destroyMethod=”destroy”)定义,在构造之后执行init,在销毁之前执行destroy。

  • @Scope:定义我们采用什么模式去创建Bean(方法上,得有@Bean) 其设置类型包括:Singleton 、Prototype、Request 、 Session、GlobalSession。

AOP:

  • @Aspect:声明一个切面(类上) 使用@After、@Before、@Around定义建言(advice),可直接将拦截规则(切点)作为参数。

  • @After :在方法执行之后执行(方法上)。

  • @Before : 在方法执行之前执行(方法上)。

  • @Around : 在方法执行之前与之后执行(方法上)。

  • @PointCut : 声明切点 在java配置类中使用@EnableAspectJAutoProxy注解开启Spring对AspectJ代理的支持(类上)。

事务:

  • @Transactional:在要开启事务的方法上使用@Transactional注解,即可声明式开启事务。

Spring 中应用了哪些设计模式呢?

1. 工厂模式 :

Spring 容器本质是一个大工厂,使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。

2. 代理模式 :

Spring AOP 功能功能就是通过代理模式来实现的,分为动态代理和静态代理。

3. 单例模式 :

Spring 中的 Bean 默认都是单例的,这样有利于容器对Bean的管理。

4. 模板模式 :

Spring 中 JdbcTemplate、RestTemplate 等以 Template结尾的对数据库、网络等等进行操作的模板类,就使用到了模板模式。

5. 观察者模式 :

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

6. 适配器模式 :

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

7. 策略模式 :

Spring中有一个Resource接口,它的不同实现类,会根据不同的策略去访问资源。

IOC

说一说什么是IOC?什么是DI?

Java 是面向对象的编程语言,一个个实例对象相互合作组成了业务逻辑,原来,我们都是在代码里创建对象和对象的依赖。

IOC(控制反转):就是由容器来负责控制对象的生命周期和对象间的关系。以前是我们想要什么,就自己创建什么,现在是我们需要什么,容器就给我们送来什么。

DI(依赖注入):指的是容器在实例化对象的时候把它依赖的类注入给它。有的说法IOC和DI是一回事,有的说法是IOC是思想,DI是IOC的实现。

使用IOC主要是为了解耦,硬编码会造成对象间的过度耦合,使用IOC之后,我们可以不用关心对象间的依赖,专心开发应用就行。

能简单说一下Spring IOC的实现机制吗

  1. 配置文件或注解:在Spring中,可以使用XML配置文件、Java注解或Java类依赖关系等方式来定义Bean及其依赖关系。
  2. BeanDefinition解析:Spring容器将读取并解析配置文件或注解,然后根据定义创建BeanDefinition对象。BeanDefinition对象包含了Bean的名称、类型、作用范围和属性等信息。
  3. BeanFactory实例化:当应用程序需要访问Bean时,Spring容器会实例化一个BeanFactory。BeanFactory是一个工厂模式,它负责创建、组装和管理Bean对象。
  4. Bean实例化:当容器实例化BeanFactory后,它将根据BeanDefinition创建相应的Bean实例。默认情况下,Spring使用无参构造函数来实例化Bean,但你也可以通过工厂方法、构造函数参数或其他方式来自定义Bean的实例化。
  5. 依赖注入:一旦Bean实例化完成,容器会扫描Bean的属性,并将它们注入到所需的依赖项中。这个过程通常使用setter方法、构造函数参数或字段注入来完成。
  6. 生命周期管理:Spring容器负责Bean的生命周期管理,它使用各种回调接口(例如InitializingBean和DisposableBean)来处理Bean的初始化和销毁操作。

说说BeanFactory和ApplicantContext?

BeanFactory和ApplicationContext都是Spring框架中用于管理Bean对象的容器。

BeanFactory是一个基本的Bean容器,提供了最基本的依赖注入和生命周期管理功能。它的主要功能是加载、实例化、配置和管理Bean对象。当应用程序需要访问Bean时,BeanFactory会实例化一个Bean实例,并按需提供给应用程序。BeanFactory通常使用延迟加载策略,也就是说只有在访问Bean时才会实例化它。

ApplicationContext是BeanFactory的超集,提供了更多的企业级服务,如AOP、事务管理、国际化和事件处理等。ApplicationContext不仅支持BeanFactory的所有功能,而且还提供了更高级别的功能和特性,例如自动装配、注解驱动的Bean定义和更方便的资源管理。ApplicationContext通常使用预先加载策略,也就是说在启动时就会实例化所有Bean。

以下是BeanFactory和ApplicationContext的一些区别:

  1. 功能差异:BeanFactory提供了基本的Bean管理和依赖注入功能,而ApplicationContext除了提供BeanFactory的所有功能以外,还提供了更高级别的功能和特性。
  2. 初始化方式:BeanFactory通常使用延迟加载策略,只有在实际使用时才会初始化Bean,而ApplicationContext则通常使用预先加载策略,在启动时就完成所有Bean的初始化。
  3. 性能差异:由于ApplicationContext需要在启动时初始化所有Bean,因此它比BeanFactory更加耗费内存和CPU资源。但是通过使用缓存和其他机制,Spring可以在一定程度上优化应用程序的性能。
  4. 适用范围:BeanFactory适用于基本的应用程序,而ApplicationContext更适用于大型企业级应用程序。

你知道Spring容器启动阶段会干什么吗?

Spring的IOC容器工作的过程,其实可以划分为两个阶段:容器启动阶段和Bean实例化阶段

其中容器启动阶段主要做的工作是加载和解析配置文件,保存到对应的Bean定义中。

容器启动开始,首先会通过某种途径加载Congiguration MetaData,在大部分情况下,容器需要依赖某些工具类(BeanDefinitionReader)对加载的Congiguration MetaData进行解析和分析,并将分析后的信息组为相应的BeanDefinition。

最后把这些保存了Bean定义必要信息的BeanDefinition,注册到相应的BeanDefinitionRegistry,这样容器启动就完成了。

能说一下Spring Bean生命周期吗?

  1. 实例化Bean对象:当Spring容器启动时,它会根据配置文件或注解来创建BeanDefinition对象,并使用反射机制实例化Bean对象。
  2. 设置Bean属性:一旦Bean对象被实例化,Spring容器会自动扫描Bean的属性,并将其设置为相应的依赖项,也就是所需要引用的其他Bean或常量值。
  3. BeanPostProcessor的前置处理:在Bean对象完成依赖注入之后,Spring容器会调用注册的BeanPostProcessor实例来为Bean提供额外的处理逻辑。这些处理逻辑通常包括检查Bean对象是否合法、修改Bean属性值等。
  4. 初始化Bean:在Bean对象完成属性注入后,Spring容器会回调Bean的init方法(例如InitializingBean接口的afterPropertiesSet()方法),来执行Bean的自定义初始化逻辑。
  5. BeanPostProcessor的后置处理:在Bean对象完成初始化后,Spring容器再次调用已注册的BeanPostProcessor实例来对Bean进行后置处理,例如添加代理、修改属性值等。
  6. 使用Bean:在Bean对象完成初始化之后,Spring容器会将其提供给应用程序使用。此时,Bean对象进入可用状态,应用程序可以使用其中的属性和方法。
  7. 销毁Bean:当Spring容器关闭或者Bean不再被使用时,它会调用Bean的destroy方法(例如DisposableBean接口的destroy()方法),来执行Bean的自定义销毁逻辑。

Bean定义和依赖定义有哪些方式?

有三种方式:直接编码方式、配置文件方式、注解方式

  • 直接编码方式:我们一般接触不到直接编码的方式,但其实其它的方式最终都要通过直接编码来实现。

  • 配置文件方式:通过xml、propreties类型的配置文件,配置相应的依赖关系,Spring读取配置文件,完成依赖关系的注入。

  • 注解方式:注解方式应该是我们用的最多的一种方式了,在相应的地方使用注解修饰,Spring会扫描注解,完成依赖关系的注入。

有哪些依赖注入的方法?

  1. Setter注入:这是最常用的依赖注入方式。容器通过调用Bean的setter方法来设置所需的依赖项,例如:

    1
    2
    3
    4
    5
    6
    7
    public class MyService {
    private MyDao myDao;

    public void setMyDao(MyDao myDao) {
    this.myDao = myDao;
    }
    }
  2. 构造函数注入:这是另一种常用的依赖注入方式。容器通过调用Bean的构造函数来创建Bean,并传递所需的依赖项作为参数,例如:

    1
    2
    3
    4
    5
    6
    7
    public class MyService {
    private MyDao myDao;

    public MyService(MyDao myDao) {
    this.myDao = myDao;
    }
    }
  3. 字段注入:使用@Autowired或@Inject注解,在字段上标记需要注入的依赖项。

    1
    2
    3
    4
    public class MyService {
    @Autowired
    private MyDao myDao;
    }
  4. 接口注入:将依赖项定义为接口类型,在运行时动态找到实现该接口的Bean并注入。

  5. 基于注解的注入:使用自定义注解来标记需要注入的依赖项,然后通过扫描等机制实现注入。

Spring有哪些自动装配的方式?

  1. 组件扫描(Component Scan):通过在配置类上添加@ComponentScan注解,Spring会自动扫描指定包下的所有组件,并将其注册到容器中。可以使用其他注解如@Controller@Service@Repository等来进一步细化扫描范围。
  2. 自动装配(Autowiring):当一个Bean依赖另一个Bean时,Spring可以自动将所依赖的Bean注入到该Bean中。通过在依赖的属性上添加@Autowired注解来实现自动装配,或者在构造函数上添加@Autowired注解来实现构造函数自动装配。
  3. Java配置(Java Configuration):通过在配置类中使用@Bean注解来定义Bean,Spring会自动将这些Bean注册到容器中。
  4. 基于XML配置的自动装配:在XML配置文件中使用<context:component-scan>元素来开启组件扫描。使用<bean>元素来定义Bean,并使用autowire="byType"autowire="byName"来实现自动装配。

Spring 中的 Bean 的作用域有哪些**?**

  • singleton : 在Spring容器仅存在一个Bean实例,Bean以单实例的方式存在,是Bean默认的作用域。

  • prototype : 每次从容器重调用Bean时,都会返回一个新的实例。

以下三个作用域于只在Web应用中适用:

  • request : 每一次HTTP请求都会产生一个新的Bean,该Bean仅在当前HTTP Request内有效。

  • session : 同一个HTTP Session共享一个Bean,不同的HTTP Session使用不同的Bean。

  • globalSession :同一个全局Session共享一个Bean,只用于基于Protlet的Web应用,Spring5中已经不存在了。

Spring 中的单例 Bean 会存在线程安全问题吗?

首先结论在这:Spring中的单例Bean不是线程安全的。

因为单例Bean,是全局只有一个Bean,所有线程共享。如果说单例Bean,是一个无状态的,也就是线程中的操作不会对Bean中的成员变量执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。

假如这个Bean是有状态的,也就是会对Bean中的成员变量进行写操作,那么可能就存在线程安全的问题。单例Bean线程安全问题怎么解决呢?

常见的有这么些解决办法:

1. 将Bean定义为多例

这样每一个线程请求过来都会创建一个新的Bean,但是这样容器就不好管理Bean,不能这么办。

2. 在Bean对象中尽量避免定义可变的成员变量

削足适履了属于是,也不能这么干。

3. 将Bean中的成员变量保存在ThreadLocal中⭐

我们知道ThredLoca能保证多线程下变量的隔离,可以在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal里,这是推荐的一种方式。

什么是循环依赖?

循环依赖指的是两个或多个Bean相互依赖,形成了一个循环引用的关系。这种情况下,当Spring容器尝试去创建这些Bean时,会出现无法解决依赖关系的情况。

例如,Bean A依赖于Bean B,而Bean B又依赖于Bean A。在创建Bean A时,Spring发现它依赖于Bean B,因此开始创建Bean B。但是,在创建Bean B时,Spring发现它依赖于Bean A,而Bean A还未创建完成,因此无法注入实例,于是创建Bean B的过程也被中断了。这样就导致了一个死循环。

循环依赖可能会导致应用程序崩溃,因为它破坏了对象图中的树状结构,使得对象之间的关系变得混乱不清。为了避免循环依赖的问题,可以使用构造函数注入方式来代替属性注入方式,或者使用@Lazy注解延迟加载Bean,或者通过重构代码来消除循环依赖。

Spring怎么解决循环依赖的呢?

Spring采用了“提前暴露”的方式来解决循环依赖的问题。具体来说,当Spring发现两个Bean之间存在循环依赖时,它会先创建实例对象,然后将对方的引用提前注入到实例中,再完成剩下的属性注入。

下面是一个简单的例子来说明Spring是如何处理循环依赖的:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
private B b;
public void setB(B b) {
this.b = b;
}
}

class B {
private A a;
public void setA(A a) {
this.a = a;
}
}

在这个例子中,类A和类B相互依赖。当Spring容器开始创建Bean时,它会先创建A的实例,但此时无法注入B,因为B还未创建完成。为了解决这个问题,Spring会先创建B的实例,然后注入A的引用到B中,最后才将B注入到A中。

需要注意的是,只有通过构造函数注入或者@Autowired注解的依赖可以解决循环依赖。如果使用setter方法注入依赖,则无法解决循环依赖问题。另外,在使用setter方法进行属性注入时,如果要避免循环依赖问题,可以使用@Lazy注解进行延迟初始化。

@Autowired的实现原理?

@Autowired是Spring中用于进行自动装配的注解,它的实现原理主要涉及到以下两个方面:

  1. 自动装配的处理过程

当Spring容器初始化一个Bean时,会检查该Bean的依赖关系,并尝试为其注入依赖的Bean。如果发现了被@Autowired注解标记的属性或构造函数参数,则会根据这些依赖的类型来查找对应的Bean,并将其注入到目标对象中。

  1. 注入Bean的方式

当Spring找到了需要注入的Bean时,会将其注入到目标对象中。具体注入的方式取决于所使用的注入器(如AutowiredAnnotationBeanPostProcessor),通常有以下几种方式:

  • 属性注入:将被注入的Bean直接赋值给目标对象中对应的属性。
  • 构造函数注入:通过调用目标对象的构造函数来注入Bean。
  • 方法注入:通过调用目标对象的setter方法来注入Bean。

在进行自动装配时,Spring会优先按照类型查找Bean,如果存在多个匹配的Bean,则会按照名称进行匹配。可以使用@Qualifier注解指定要注入的Bean的名称。

需要注意的是,当通过@Autowired注入依赖时,如果找不到对应的Bean,则会抛出异常。可以通过将required属性设置为false来避免这种情况,此时如果找不到对应的Bean,该属性将为null

AOP

说说什么是AOP

AOP:面向切面编程。简单说,就是把一些业务逻辑中的相同的代码抽取到一个独立的模块中,让业务逻辑更加清爽。

我们可以把 日志记录数据校验 可重用的功能模块分离出来,然后在程序的执行的合适的地方动态地植入这些代码并执行。这样就简化了代码的书写。

业务逻辑代码中没有参和通用逻辑的代码,业务模块更简洁,只包含核心业务代码。实现了业务逻辑和通用逻辑的代码分离,便于维护和升级,降低了业务逻辑和通用逻辑的耦合性。

AOP 可以将遍布应用各处的功能分离出来形成可重用的组件。在编译期间、装载期间或运行期间实现在不修改源代码的情况下给程序动态添加功能。从而实现对业务逻辑的隔离,提高代码的模块化能力。

AOP 的核心其实就是动态代理,如果是实现了接口的话就会使用 JDK 动态代理,否则使用 CGLIB 代理,主要应用于处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等。

AOP有哪些核心概念?

  • 切面(Aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象

  • 连接点(Joinpoint):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器

  • 切点(Pointcut):对连接点进行拦截的定位

  • 通知(Advice):所谓通知指的就是指拦截到连接点之后要执行的代码,也可以称作增强

  • 目标对象 (Target):代理的目标对象

  • 织入(Weabing):织入是将增强添加到目标类的具体连接点上的过程。

    • 编译期织入:切面在目标类编译时被织入

    • 类加载期织入:切面在目标类加载到JVM时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。

    • 运行期织入:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面。

      Spring采用运行期织入,而AspectJ采用编译期织入和类加载器织入。

  • 引介(introduction):引介是一种特殊的增强,可以动态地为类添加一些属性和方法

AOP有哪些环绕方式?

AOP 一般有 5 种环绕方式:

  • 前置通知 (@Before)

  • 返回通知 (@AfterReturning)

  • 异常通知 (@AfterThrowing)

  • 后置通知 (@After)

  • 环绕通知 (@Around)

多个切面的情况下,可以通过 @Order 指定先后顺序,数字越小,优先级越高。

说说你平时有用到AOP吗?

这里给出一个小例子,SpringBoot项目中,利用AOP打印接口的入参和出参日志,以及执行时间,还是比较快捷的。

  • 引入依赖:引入AOP依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starteraop</artifactId>
</dependency>
  • 自定义注解:自定义一个注解作为切点

    1
    2
    3
    4
    5
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    @Documented
    public @interface WebLog {
    }
  • 配置AOP切面:

    • @Aspect:标识切面
    • @Pointcut:设置切点,这里以自定义注解为切点,定义切点有很多其它种方式,自定义注解是比较常用的一种。
    • @Before:在切点之前织入,打印了一些入参信息
    • @Around:环绕切点,打印返回参数和接口执行时间
    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
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    @Aspect
    @Component
    public class WebLogAspect {
    private final static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
    /**
    * 以自定义 @WebLog 注解为切点
    **/
    @Pointcut("@annotation(cn.fighter3.spring.aop_demo.WebLog)
    ")
    public void webLog() {}

    /**
    * 在切点之前织入
    */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws
    Throwable {
    // 开始打印请求日志
    ServletRequestAttributes attributes = (ServletRequestAttributes)
    RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();
    // 打印请求相关参数
    logger.info("==========================================
    Start ==========================================");
    // 打印请求 url
    logger.info("URL : {}",
    request.getRequestURL().toString());
    // 打印 Http method
    logger.info("HTTP Method : {}",
    request.getMethod());
    // 打印调用 controller 的全路径以及执行方法
    logger.info("Class Method : {}.{}",
    joinPoint.getSignature().getDeclaringTypeName(),
    joinPoint.getSignature().getName());
    // 打印请求的 IP
    logger.info("IP : {}",request.getRemoteAddr());
    // 打印请求入参
    logger.info("Request Args : {}",new ObjectMapper().writeValueAsString(joinPoint.getArgs()));
    }

    /**
    * 在切点之后织入
    * @throws Throwable
    */
    @After("webLog()")
    public void doAfter() throws Throwable {
    // 结束后打个分隔线,方便查看
    logger.info("===========================================
    End ===========================================");
    }
    /**
    * 环绕
    */
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint
    proceedingJoinPoint) throws Throwable {
    //开始时间
    long startTime = System.currentTimeMillis();
    Object result = proceedingJoinPoint.proceed();
    // 打印出参
    logger.info("Response Args : {}", new
    ObjectMapper().writeValueAsString(result));
    // 执行耗时
    logger.info("Time-Consuming : {} ms",
    System.currentTimeMillis() - startTime);
    return result;
    }
    }
  • 使用:只需要在接口上加上自定义注解

    1
    2
    3
    4
    5
    @GetMapping("/hello")
    @WebLog(desc = "这是一个欢迎接口")
    public String hello(String name){
    return "Hello "+name;
    }

说说JDK 动态代理和 CGLIB 代理 ?

Spring的AOP是通过动态代理来实现的,动态代理主要有两种方式JDK动态代理和Cglib动态代理,这两种动态代理的使用和原理有些不同。

JDK 动态代理

1. Interface :对于 JDK 动态代理,目标类需要实现一个Interface。

2. InvocationHandler :InvocationHandler是一个接口,可以通过实现这个接口,定义横切逻辑,再通过反射机制(invoke)调用目标类的代码,在次过程,可能包装逻辑,对目标方法进行前置后置处理。

3. Proxy :Proxy利用InvocationHandler动态创建一个符合目标类实现的接口的实例,生成目标类的代理对象。

CgLib 动态代理

1. 使用JDK创建代理有一大限制,它只能为接口创建代理实例,而CgLib 动态代理就没有这个限制。

2. CgLib 动态代理是使用字节码处理框架 ASM ,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。

3. CgLib 创建的动态代理对象性能比 JDK 创建的动态代理对象的性能高不少,但是 CGLib 在创建代理对象时所花费的时间却比 JDK 多得多,所以对于单例的对象,因为无需频繁创建对象,用 CGLib 合适,反之,使用 JDK 方式要更为合适一些。同时,由于 CGLib 由于是采用动态创建子类的方法,对于 final 方法,无法进行代理。

说说Spring AOPAspectJ AOP 区别**?**

Spring AOP

Spring AOP 属于 运行时增强 ,主要具有如下特点:

1. 基于动态代理来实现,默认如果使用接口的,用 JDK 提供的动态代理实现,如果是方法则使用 CGLIB 实现

2. Spring AOP 需要依赖 IOC 容器来管理,并且只能作用于 Spring 容器,使用纯Java 代码实现

3. 在性能上,由于 Spring AOP 是基于 动态代理 来实现的,在容器启动时需要生成代理实例,在方法调用上也会增加栈的深度,使得 Spring AOP 的性能不如AspectJ 的那么好。

4. Spring AOP 致力于解决企业级开发中最普遍的 AOP(方法织入)。

AspectJ

AspectJ 是一个易用的功能强大的 AOP 框架,属于 编译时增强 , 可以单独使用,也可以整合到其它框架中,是 AOP 编程的完全解决方案。AspectJ 需要用到单独的编译器 ajc。

AspectJ 属于静态织入,通过修改代码来实现,在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的,一般有如下几个织入的时机:

1. 编译期织入(Compile-time weaving):如类 A 使用 AspectJ 添加了一个属性,类 B 引用了它,这个场景就需要编译期的时候就进行织入,否则没法编译类 B。

2. 编译后织入(Post-compile weaving):也就是已经生成了 .class 文件,或已经打成 jar 包了,这种情况我们需要增强处理的话,就要用到编译后织入。

3. 类加载后织入(Load-time weaving):指的是在加载类的时候进行织入,要实现这个时期的织入,有几种常见的方法

整体对比如下:

事务

Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring

是无法提供事务功能的。Spring 只提供统一事务管理接口,具体实现都是由各数据

库自己实现,数据库事务的提交和回滚是通过数据库自己的事务机制实现。

Spring 事务的种类?

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

编程式事务

编程式事务管理使用 TransactionTemplate,需要显式执行事务。

声明式事务

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

优点是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过 @Transactional 注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。

Spring 的事务隔离级别?

Spring的接口TransactionDefinition中定义了表示隔离级别的常量,当然其实主要还是对应数据库的事务隔离级别:

1. ISOLATION_DEFAULT:使用后端数据库默认的隔离界别,MySQL 默认可重复读,Oracle 默认读已提交。

2. ISOLATION_READ_UNCOMMITTED:读未提交

3. ISOLATION_READ_COMMITTED:读已提交

4. ISOLATION_REPEATABLE_READ:可重复读

5. ISOLATION_SERIALIZABLE:串行化

Spring 的事务传播机制?

Spring 事务的传播机制说的是,当多个事务同时存在的时候——一般指的是多个事务方法相互调用时,Spring 如何处理这些事务的行为。

事务传播机制是使用简单的 ThreadLocal 实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。

Spring默认的事务传播行为是PROPAFATION_REQUIRED,它适合绝大多数情况,如果多个ServiceX#methodX()都工作在事务环境下(均被Spring事务增强),且程序中存在调用链Service1#method1()->Service2#method2()->Service3#method3(),那么这3个服务类的三个方法通过Spring的事务传播机制都工作在同一个事务中。

声明式事务实现原理了解吗?

就是通过AOP/动态代理。

  • 在Bean初始化阶段创建代理对象:Spring容器在初始化每个单例bean的时候,会遍历容器中的所有BeanPostProcessor实现类,并执行其postProcessAfterInitialization方法,在执行AbstractAutoProxyCreator类的postProcessAfterInitialization方法时会遍历容器中所有的切面,查找与当前实例化bean匹配的切面,这里会获取事务属性切面,查找@Transactional注解及其属性值,然后根据得到的切面创建一个代理对象,默认是使用JDK动态代理创建代理,如果目标类是接口,则使用JDK动态代理,否则使用Cglib。

  • 在执行目标方法时进行事务增强操作:当通过代理对象调用Bean方法的时候,会触发对应的AOP增强拦截器,声明式事务是一种环绕增强,对应接口为MethodInterceptor ,事务增强对该接口的实现为TransactionInterceptor ,类图如下:

    事务拦截器 TransactionInterceptor 在 invoke 方法中,通过调用父类TransactionAspectSupport 的 invokeWithinTransaction 方法进行事务处理,包括开启事务、事务提交、异常回滚。

声明式事务在哪些情况下会失效?

MVC

Spring MVC 的核心组件?

1. DispatcherServlet :前置控制器,是整个流程控制的 核心 ,控制其他组件的执行,进行统一调度,降低组件之间的耦合性,相当于总指挥。

2. Handler :处理器,完成具体的业务逻辑,相当于 Servlet 或 Action。

3. HandlerMapping :DispatcherServlet 接收到请求之后,通过 HandlerMapping 将不同的请求映射到不同的 Handler。

4. HandlerInterceptor :处理器拦截器,是一个接口,如果需要完成一些拦截处理,可以实现该接口。

5. HandlerExecutionChain :处理器执行链,包括两部分内容:Handler 和HandlerInterceptor(系统会有一个默认的 HandlerInterceptor,如果需要额外设置拦截,可以添加拦截器)。

6. HandlerAdapter :处理器适配器,Handler 执行业务方法之前,需要进行一系列的操作,包括表单数据的验证、数据类型的转换、将表单数据封装到 JavaBean等,这些操作都是由 HandlerApater 来完成,开发者只需将注意力集中业务逻辑的处理上,DispatcherServlet 通过 HandlerAdapter 执行不同的 Handler。

7. ModelAndView :装载了模型数据和视图信息,作为 Handler 的处理结果,返回给 DispatcherServlet。

8. ViewResolver :视图解析器,DispatcheServlet 通过它将逻辑视图解析为物理视图,最终将渲染结果响应给客户端

Spring MVC 的工作流程?

1. 客户端向服务端发送一次请求,这个请求会先到前端控制器DispatcherServlet(也叫中央控制器)。

2. DispatcherServlet接收到请求后会调用HandlerMapping处理器映射器。由此得知,该请求该由哪个Controller来处理(并未调用Controller,只是得知)

3. DispatcherServlet调用HandlerAdapter处理器适配器,告诉处理器适配器应该要去执行哪个Controller

4. HandlerAdapter处理器适配器去执行Controller并得到ModelAndView(数据和视图),并层层返回给DispatcherServlet

5. DispatcherServlet将ModelAndView交给ViewReslover视图解析器解析,然后返回真正的视图。

6. DispatcherServlet将模型数据填充到视图中

7. DispatcherServlet将结果响应给客户端

Spring MVC 虽然整体流程复杂,但是实际开发中很简单,大部分的组件不需要开发人员创建和管理,只需要通过配置文件的方式完成配置即可,真正需要开发人员进行处理的只有 HandlerController) 、ViewModel

当然我们现在大部分的开发都是前后端分离,Restful风格接口,后端只需要返回Json数据就行了。

SpringMVC Restful风格的接口的流程是什么样的呢?

我们都知道Restful接口,响应格式是json,这就用到了一个常用注解:

@ResponseBody

1
2
3
4
5
@GetMapping("/user")
@ResponseBody
public User user(){
return new User(1,"张三");
}

加入了这个注解后,整体的流程上和使用ModelAndView大体上相同,但是细节上有一些不同:

1. 客户端向服务端发送一次请求,这个请求会先到前端控制器DispatcherServlet

2. DispatcherServlet接收到请求后会调用HandlerMapping处理器映射器。由此得知,该请求该由哪个Controller来处理

3. DispatcherServlet调用HandlerAdapter处理器适配器,告诉处理器适配器应该要去执行哪个Controller

4. Controller被封装成了ServletInvocableHandlerMethod,HandlerAdapter处理器适配器去执行invokeAndHandle方法,完成对Controller的请求处理

5. HandlerAdapter执行完对Controller的请求,会调HandlerMethodReturnValueHandler去处理返回值,主要的过程:

5.1. 调用RequestResponseBodyMethodProcessor,创建ServletServerHttpResponse(Spring对原生ServerHttpResponse的封装)实例

5.2.使用HttpMessageConverter的write方法,将返回值写入ServletServerHttpResponse的OutputStream输出流中

5.3.在写入的过程中,会使用JsonGenerator(默认使用Jackson框架)对返回值进行Json序列化

6. 执行完请求后,返回的ModealAndView为null,ServletServerHttpResponse里也已

经写入了响应,所以不用关心View的处理

过滤器和拦截器有什么区别

过滤器和拦截器在软件开发中通常用于对请求进行处理和控制,它们虽然有相似的作用,但在具体的实现和使用上有一些区别。

  1. 过滤器(Filter):

    • 过滤器通常指的是在Web应用中对请求和响应进行过滤和处理的组件,它可以拦截请求和响应,并对其进行预处理或后处理。
    • 在Java的Web开发中,过滤器是Servlet规范中的一部分,可以用于对请求和响应进行过滤和处理,比如日志记录、字符编码转换、安全控制等。
    • 过滤器的实现依赖于特定的框架或技术,比如Java Servlet中的Filter接口。
  2. 拦截器(Interceptor):

    • 拦截器通常指的是在面向对象编程中,用于拦截方法调用的一种机制,它可以在方法执行前或执行后进行特定的处理。
    • 在Spring框架中,拦截器是一种AOP(面向切面编程)的实现,可以用于在方法执行前后进行一些额外的处理,比如日志记录、权限控制等。
    • 拦截器通常与框架或库相关联,比如Spring框架中的HandlerInterceptor接口。

总的来说,过滤器通常针对Web请求和响应进行处理,而拦截器通常用于方法调用的拦截和处理。在不同的技术栈和应用场景中,它们的具体实现和用途会有所不同。

Spring Boot

介绍一下SpringBoot,有哪些优点?

Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。

Spring Boot 以 约定大于配置 核心思想开展工作,相比Spring具有如下优势:

1. Spring Boot 可以快速创建独立的Spring应用程序。

2. Spring Boot 内嵌了如Tomcat,Jetty和Undertow这样的容器,也就是说可以直接跑起来,用不着再做部署工作了。

3. Spring Boot 无需再像Spring一样使用一堆繁琐的xml文件配置。

4. Spring Boot 可以自动配置(核心)Spring。SpringBoot将原有的XML配置改为Java配置,将bean注入改为使用注解注入的方式(@Autowire),并将多个xml、properties配置浓缩在一个appliaction.yml配置文件中。

5. Spring Boot 提供了一些现有的功能,如量度工具,表单数据验证以及一些外部配置这样的一些第三方功能。

6. Spring Boot 可以快速整合常用依赖(开发库,例如spring-webmvc、jackson-json、validation-api和tomcat等),提供的POM可以简化Maven的配置。当我们引入核心依赖时,SpringBoot会自引入其他依赖。

SpringBoot自动配置原理了解吗

SpringBoot开启自动配置的注解是 @EnableAutoConfiguration ,启动类上的注解 @SpringBootApplication 是一个复合注解,包含了@EnableAutoConfiguration:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @AutoConfigurationPackage //将main同级的包下的所有组件注册到容器

    @Import({AutoConfigurationImportSelector.class}) //加载自动
    装配类 xxxAutoconfiguration
    public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY =
    "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
    }
  • AutoConfigurationImportSelector 实现了 ImportSelector 接口,这个接口的作用就是收集需要导入的配置类,配合 @Import() 就可以将相应的类导入到Spring容器中

  • 获取注入类的方法是selectImports(),它实际调用的是getAutoConfigurationEntry ,这个方法是获取自动装配类的关键,主要流程可以分为这么几步:

    1. 获取注解的属性,用于后面的排除

    2. 获取所有需要自动装配的配置类的路径 :这一步是最关键的,从META-INF/spring.factories获取自动配置类的路径

    3. 去掉重复的配置类和需要排除的重复类,把需要自动加载的配置类的路径存储起来

如何自定义一个SpringBoot Srarter?

知道了自动配置原理,创建一个自定义SpringBoot Starter也很简单。

1. 创建一个项目,命名为demo-spring-boot-starter,引入SpringBoot相关依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configurationprocessor</artifactId>
<optional>true</optional>
</dependency>

2. 编写配置文件

这里定义了属性配置的前缀

1
2
3
4
5
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {
private String name;
//省略getter、setter
}

3. 自动装配

创建自动配置类HelloPropertiesConfigure

1
2
3
4
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloPropertiesConfigure {
}

4. 配置自动类

在 /resources/META-INF/spring.factories 文件中添加自动配置类路径

1
2
3
4
org.springframework.boot.autoconfigure.EnableAutoConfigura
tion=\
cn.fighter3.demo.starter.configure.HelloPropertiesConfigur
e

Springboot 启动原理?

SpringApplication 这个类主要做了以下四件事情:

1. 推断应用的类型是普通的项目还是 Web 项目

2. 查找并加载所有可用初始化器 , 设置到 initializers 属性中

3. 找出所有的应用程序监听器,设置到 listeners 属性中

4. 推断并设置 main 方法的定义类,找到运行的主类

SpringBoot 启动大致流程如下 :

0%