Skip to content

Spring

前记

不想学java啊啊啊啊啊啊啊啊

IOC

  IOC(Inversion of Control)即控制反转,是一种设计原则,指的是将对象的创建和管理交给容器来处理,而不是由程序员直接控制。Spring框架通过IOC容器来实现这一原则,使得开发者可以更专注于业务逻辑的实现,而不需要担心对象的生命周期和依赖关系。

  Spring提供了一个容器,称为IOC容器,它负责创建、配置和管理对象。开发者可以通过配置文件或注解来定义对象的依赖关系,IOC容器会根据这些定义自动创建和注入对象。被IOC容器管理的对象称为Bean,管理Service\Controller\Repository等组件。

  • 目标:解耦合,提高代码的可维护性和可测试性。
  • 实现方式:通过依赖注入(Dependency Injection)来实现对象之间的依赖关系。
  • 最终效果:开发者不需要关心对象的创建和管理,IOC容器会自动处理这些细节,使得代码更加简洁和灵活。

bean

  在Spring框架中,Bean是由IOC容器管理的对象。Bean可以是任何一个Java对象,通常是一个类的实例。通过配置文件或注解,开发者可以定义Bean的属性、依赖关系以及生命周期等信息。

xml
<!-- 这个文件位于src/main/resources目录下,命名为applicationContext.xml -->
<bean id="myBean" name="alias" class="com.example.MyClass" scope="prototype" init-method="init" destroy-method="destroy">
    <property name="property1" value="value1"/>
    <property name="property2" ref="anotherBean"/>
</bean>

  在上面的XML配置中,定义了一个Bean,id为"myBean",别名为"alias",类为"com.example.MyClass",作用域为"prototype",初始化方法为"init",销毁方法为"destroy"。同时,还定义了两个属性,一个是简单值,另一个是引用另一个Bean。   bean默认是单例,设置scope="prototype"后,每次获取都会创建一个新的实例。适合交给容器进行管理的对象有:

  • Service:业务逻辑组件,处理具体的业务需求。
  • Controller:控制器组件,处理用户请求并返回响应。
  • Repository:数据访问组件,负责与数据库进行交互。
  • Component:通用组件,可以用于任何需要被IOC容器管理的对象。

  在正常Java中,我们使用如下方法获取Bean:

java
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyClass myBean = (MyClass) context.getBean("myBean");

构造方法实例化

  构造方法是指通过类的构造函数来创建Bean实例。这种方式适用于需要在创建对象时进行一些初始化操作的情况。构造方法可以有参数,也可以没有参数。

xml
<bean id="myBean" class="com.example.MyClass">
    <constructor-arg value="value1"/>
    <constructor-arg ref="anotherBean"/>
</bean>

  Spring构造的时候,使用的是无参构造函数,之后通过setter方法注入属性值。也可以使用有参构造函数直接注入属性值。无参构造方法如果不存在,将抛出异常BeanCreationException。

Spring Boot

  现代开发中,如果没有无参构造函数,Spring Boot会使用构造器注入来创建Bean实例(寻找带参构造函数),而不是使用无参构造函数和setter方法。这种方式更符合依赖注入的原则,能够更好地支持不可变对象和更清晰的依赖关系。

  其实就是上面的正常方法:

  • 原理: Spring 直接动用 Java 的反射机制,去调用你类里的构造方法(默认是无参构造)来把对象 new 出来。
  • 适用场景: 你自己写的业务类(比如 UserService、BookDao),直接交给 Spring 管。
  • 现代 Spring Boot 写法: 直接在类头上加 @Component、@Service、@Repository 即可。

静态工厂实例化(了解)

  静态工厂方法是指通过一个静态方法来创建Bean实例,而不是直接使用构造函数。这种方式可以提供更灵活的对象创建逻辑,例如根据不同的条件返回不同的实例。

xml
<bean id="myBean" class="com.example.MyFactory" factory-method="createInstance"/>

  在上面的XML配置中,定义了一个Bean,类为"com.example.MyFactory",通过静态工厂方法"createInstance"来创建Bean实例。静态工厂方法可以根据需要返回不同的对象实例,提供了更大的灵活性。

  • 原理: 你告诉 Spring:“别去调那个类的构造方法了,去调这个工厂类的 static 静态方法,把那个方法返回的结果当做 Bean 给我存起来。”
  • 适用场景: 整合遗留系统或早期的第三方组件时,这些组件强制要求用静态方法获取对象。

实例工厂实例化

  实例工厂方法是指通过一个实例方法来创建Bean实例。这种方式适用于需要在创建对象时依赖于其他对象的情况。

xml
<bean id="myFactory" class="com.example.MyFactory"/>
<bean id="myBean" factory-bean="myFactory" factory-method="createInstance"/>

  在上面的XML配置中,首先定义了一个工厂Bean,类为"com.example.MyFactory",然后通过实例工厂方法"createInstance"来创建Bean实例。实例工厂方法可以依赖于工厂Bean的状态或其他属性,提供了更大的灵活性。

  其实原因是这次第三方库提供的工厂方法不是静态的(没有 static 修饰)。这意味着,你必须先 new 出一个工厂对象,然后通过这个工厂对象去调用它的实例方法,才能拿到最终的目标产品。

  • 原理: 分两步走。第一步,先让 Spring 把“工厂”当成一个普通的 Bean 造出来;第二步,告诉 Spring 去调用这个已存在的“工厂 Bean”的某个方法,把产出的结果当做真正的目标 Bean 存起来。
  • 适用场景: 同样是整合那些设计比较特殊的第三方框架。

实例工厂实例化方法优化

  出现了一个新的泛型类FactoryBean<T>

java
public interface FactoryBean<T> {

    // 1. 核心灵魂:你要造的那个复杂的对象,到底长什么样?全写在这个方法里!
    T getObject() throws Exception;

    // 2. 告诉 Spring:你造出来的这个产品,属于什么类型(Class)?
    Class<?> getObjectType();

    // 3. 告诉 Spring:你造的这个产品是单例(Singleton)还是多例(Prototype)?默认是单例。
    default boolean isSingleton() {
        return true;
    }
}

  当你写了一个类实现 FactoryBean 并把它交给 Spring 管理时,Spring 会在后台做一件非常神奇的“掉包”操作。

神奇的“掉包”魔法(最重要的机制)

  假设你写了一个 MyCarFactoryBean,实现了 FactoryBean<Car> 接口,并在里面写好了组装一辆 Car 对象的复杂逻辑。然后你把它放进 Spring 容器,名字叫 myCar。

  • 常规认知: 如果你向 Spring 要 myCar,它应该把 MyCarFactoryBean 这个“机床”本身给你。
  • 真实情况(掉包): 当你向 Spring 请求 myCar 时,Spring 发现这是一台“特种机床”(它实现了 FactoryBean)。于是,Spring 不会把机床给你,而是默默调用了机床的 getObject() 方法,把生产出来的那辆真正的 Car 拿出来交给了你!

  与此同时,配置也会变得更简单了:

xml
<bean id="myCar" class="com.example.MyCarFactoryBean"/>

bean生命周期

  生命周期是指Bean从创建到销毁的整个过程。在Spring框架中,Bean的生命周期包括以下几个阶段:

  1. 实例化:Spring容器通过调用构造方法或工厂方法创建Bean实例。
  2. 属性注入:Spring容器将配置文件或注解中定义的属性值注入到Bean实例中。
  3. 初始化:如果Bean实现了InitializingBean接口,Spring容器会调用afterPropertiesSet()方法进行初始化操作。或者,如果在配置文件中指定了init-method属性,Spring容器会调用指定的方法进行初始化。
  4. 使用:Bean实例可以被应用程序使用。
  5. 销毁:如果Bean实现了DisposableBean接口,Spring容器会调用destroy()方法进行销毁操作。或者,如果在配置文件中指定了destroy-method属性,Spring容器会调用指定的方法进行销毁。

关于销毁

  1. 单例Bean:Spring容器在关闭时会销毁单例Bean,调用其销毁方法。此处要调用context.close()来触发销毁过程。对于单例 Bean,Spring 容器(上下文 Context)就像是一艘大船,单例 Bean 就是船上的水手。只要大船还在航行,水手就不能下船。只有当大船沉没(也就是调用 context.close() 关闭上下文)时,Spring 才会统一触发所有单例 Bean 的销毁逻辑(比如关闭数据库连接、释放内存等)。
  2. 多例Bean:Spring容器不会自动销毁多例Bean,因为它无法追踪这些Bean的生命周期。开发者需要手动调用销毁方法来销毁多例Bean。此时,生死完全交给了GC(垃圾回收器)。多例 Bean 就像是船上的乘客,他们可以随时上船下船。Spring 只负责把乘客送上船(创建 Bean),但不负责送他们下船(销毁 Bean)。当乘客不再需要时,GC 会自动回收他们占用的资源。
  3. Web 专属作用域(Request / Session):它们是独立生灭的。
xml
<bean id="myBean" name="alias" class="com.example.MyClass" scope="prototype" init-method="init" destroy-method="destroy" />

  对于这个bean,Spring给了方法可以省去init-method和destroy-method属性的麻烦。只要让MyClass实现InitializingBean和DisposableBean接口,并重写afterPropertiesSet()和destroy()方法,Spring就会自动调用这些方法来完成初始化和销毁的过程。

Bean 依赖注入

xml
<bean id="myBean" name="alias" class="com.example.MyClass" scope="prototype" init-method="init" destroy-method="destroy">
    <property name="property1" value="value1"/>
    <property name="property2" ref="anotherBean"/>
</bean>

  对于以上代码,Spring会在创建myBean实例时,自动注入anotherBean实例到myBean的property2属性中,自动注入value1这个值到property1属性中,类型会自动判断。这个过程就是依赖注入(Dependency Injection)。

  其实在这里,这个ref="anotherBean"就像是告诉Spring:“嘿,我的这个属性需要一个叫 anotherBean 的对象,你帮我找一下,找到后把它注入到我的这个属性里去。” Spring 就会根据这个提示,在 myBean 被创建的时候,调用 setProperty2() 方法,把 anotherBean 的实例传进去。

Bean 构造器注入

xml
<bean id="myBean" class="com.example.MyClass">
    <constructor-arg value="value1"/>
    <constructor-arg name="args1" ref="anotherBean"/>
</bean>

  对于以上代码,Spring会在创建myBean实例时,自动调用MyClass的构造方法,并将value1作为第一个参数,anotherBean实例作为第二个参数传入。这个过程就是构造器注入(Constructor Injection)。

  构造器注入就像是告诉 Spring:“嘿,我这个类的构造方法需要两个参数,第一个是一个简单的值,第二个是一个叫 anotherBean 的对象。你帮我把这两个东西准备好,然后在创建 myBean 的时候,把它们传进去。” Spring 就会根据这个提示,在创建 myBean 的时候,找到合适的构造方法,并把 value1 和 anotherBean 的实例传进去。这个name对应的是形参名。

name参数

  这个是为了在调用构造方法时,能够根据参数名来匹配对应的值或引用。这样做的好处是,即使构造方法有多个参数,Spring也能正确地将配置文件中的值或引用注入到对应的参数中,而不需要依赖参数的顺序。**但是这样做高度耦合。**可用的替代方案:

  • type参数,type参数是根据参数类型来匹配的,减少了对参数名的依赖,提高了配置的灵活性和可维护性。
  • index参数,index参数是根据参数在构造方法中的位置来匹配的,这种方式比较简单,但容易出错,因为如果构造方法的参数顺序发生变化,配置文件也需要相应地修改。

依赖自动装配

  依赖自动装配(Autowiring)是指Spring容器根据Bean之间的依赖关系,自动将一个Bean注入到另一个Bean中,而不需要在配置文件中显式地指定。这种方式可以减少配置文件的冗余,提高开发效率。

xml
<bean id="myBean" class="com.example.MyClass" autowire="byName"/>

  在上面的XML配置中,定义了一个Bean,类为"com.example.MyClass",并设置了autowire属性为"byName",表示Spring容器将根据Bean的名称来自动装配依赖关系。Spring会查找与myBean属性名称相同的Bean,并将其注入到myBean中。autowire可选值:

  • no:默认值,不进行自动装配。
  • byName:根据Bean的名称进行自动装配。
  • byType:根据Bean的类型进行自动装配。
  • constructor:根据构造方法的参数类型进行自动装配。

  需要注意的是,使用byType时,如果容器中存在多个同类型的Bean,Spring将无法确定要注入哪个Bean,从而抛出异常。此时,可以使用@Qualifier注解来指定要注入的Bean。

  该方法只能用于引用了类型依赖注入,不能对简单类型进行操作。

集合注入

  集合注入是指Spring容器将一个集合类型的Bean注入到另一个Bean中。Spring支持多种集合类型的注入,包括List、Set、Map和Properties等。

java
private int[] myArray;
private List<String> myList;
private Set<String> mySet;
private Map<String, String> myMap;
private Properties myProperties;

  上面对应的配置如下:

xml
<bean id="myBean" class="com.example.MyClass">
    <property name="myArray">
        <array>
            <value>1</value>
            <value>2</value>
            <value>3</value>
        </array>
    </property>
    <!-- 这里的name属性对应的是变量名。在这里,myList是MyClass中的一个属性,类型是List<String>。 -->
    <property name="myList">
        <list>
            <value>item1</value>
            <value>item2</value>
            <value>item3</value>
        </list>
    </property>
    <property name="mySet">
        <set>
            <value>item1</value>
            <value>item2</value>
            <value>item3</value>
        </set>
    </property>
    <property name="myMap">
        <map>
            <entry key="key1" value="value1"/>
            <entry key="key2" value="value2"/>
            <entry key="key3" value="value3"/>
        </map>
    </property>
    <property name="myProperties">
        <props>
            <prop key="key1">value1</prop>
            <prop key="key2">value2</prop>
            <prop key="key3">value3</prop>
        </props>
    </property>
</bean>

获取 Bean

  1. 使用Bean名称获取
java
// 注意!!!这里要强制类型转换,getBean返回的是一个Object类型的对象。
MyClass myBean = (MyClass) context.getBean("myBean");
  1. 使用Bean类型获取
java
MyClass myBean = context.getBean(MyClass.class);
  1. 使用Bean名称和类型获取
java
MyClass myBean = context.getBean("myBean", MyClass.class);

alt text

context

  context(上下文)是Spring框架中的一个核心概念,指的是一个容器,用于管理Bean的生命周期和依赖关系。Context提供了一个统一的接口,允许开发者访问和操作Bean,以及获取应用程序的配置信息。

context.registerShutdownHook()

  调用时,Spring 会利用 Java 底层的 Runtime.getRuntime().addShutdownHook() 机制确保即使 JVM 准备退出了,它也会先触发 Spring 的销毁逻辑,确保资源被正确释放。感觉和defer context.close()差不多,都是在程序结束时调用销毁方法。

加载类路径下的配置文件

  在Spring框架中,加载类路径下的配置文件是一个常见的操作。Spring提供了多种方式来加载配置文件,其中最常用的是使用ClassPathXmlApplicationContext类。这个类会从类路径中加载指定的XML配置文件,并创建一个Spring容器来管理Bean的生命周期和依赖关系。

java
// 1. 加载类路径下的配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

// 2. 从文件系统下加载配置文件(不咋用)
ApplicationContext context = new FileSystemXmlApplicationContext("C:/path/to/applicationContext.xml");

加载properties文件

  感觉就跟.env很像,一些重要的配置直接写到properties文件里,Spring在启动时会自动加载这些配置,并且可以通过@Value注解或者Environment对象来访问这些配置值。这样做的好处是可以将配置与代码分离,方便管理和修改,同时也提高了应用程序的灵活性和可维护性。

  例如,对于以下配置:

xml
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

  讲这些重要东西放到jdbc.properties文件里:

properties
jdbc.url=jdbc:mysql://localhost:3306/mydb
jdbc.username=root
jdbc.password=123456

然后在xml中需要加载这个properties文件:

xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 在这里可以添加system-properties-mode="override"属性,来指定系统属性的优先级。默认情况下,Spring会优先使用系统属性,如果你想让properties文件中的属性覆盖系统属性,可以设置system-properties-mode="override"。 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!-- 其他 Bean 定义 -->
</beans>

  在上面代码中:

  • <beans>标签是Spring配置文件的根元素,定义了一个Spring容器。
  • xmlns:contextxsi:schemaLocation属性用于引入Spring的上下文命名空间和相关的XML Schema定义,以便在配置文件中使用Spring提供的上下文功能。
  • <context:property-placeholder>标签用于加载位于类路径下的jdbc.properties文件。通过${}占位符,Spring会自动将properties文件中的值注入到Bean的属性中。这样做不仅使得配置更加灵活和易于管理,还提高了应用程序的安全性,因为敏感信息(如数据库密码)可以从代码中分离出来,避免硬编码在源代码中。

命名空间

  命名空间(Namespace)是XML中的一个概念,用于区分不同元素和属性的来源。在Spring框架中,命名空间用于区分不同的配置元素,例如beans、context、aop等。通过使用命名空间,开发者可以更清晰地组织和管理配置文件,同时也可以避免元素和属性之间的冲突。

  xmlns代表着XML Namespace

  1. xmlns="http://www.springframework.org/schema/beans"

  这是默认命名空间,表示该XML文件中的元素和属性属于Spring的beans命名空间。这个命名空间定义了Spring Bean的配置元素,例如<bean><property>等。

  大白话就是:Spring 解析器你好,在这个文件里,凡是不带任何前缀的普通标签(比如你下面写的 <bean><property>),统统都属于 beans 这个基础核心库。

  1. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  这是XML Schema实例命名空间,表示该XML文件使用了XML Schema定义。这个命名空间定义了XML Schema相关的元素和属性,例如xsi:schemaLocation。引入这个命名空间,仅仅是为了使用下面那个 xsi:schemaLocation 属性。有了它,XML 才知道该去哪里寻找语法校验规则。

  1. xmlns:context="http://www.springframework.org/schema/context"

  这是Spring的上下文命名空间,表示该XML文件中的元素和属性属于Spring的context命名空间。这个命名空间定义了Spring上下文相关的配置元素,例如<context:property-placeholder><context:component-scan>等。

  1. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"

  上面那些 xmlns 里的网址,其实只是一个虚假的“名字”,Spring 并不会真的去那个网址下载东西。而 schemaLocation 才是真正的映射对照表

  它是成对出现的:

  • 第一对: 把 .../schema/beans 这个名字,映射到了 .../spring-beans.xsd 这个实体文件上。
  • 第二对: 把 .../schema/context 这个名字,映射到了 .../spring-context.xsd 这个实体文件上。

  其作用极其巨大: .xsd 文件全称是 XML Schema Definition(XML 结构定义)。它里面写死了 <bean> 标签下只能嵌套 <property>,不能嵌套 <context>。这也是为什么你在 IDEA 里如果不小心把 <bean> 拼写成了 <been>,IDEA 能立刻给你标红报错的原因——IDEA 在底层默默读取了这个 .xsd 文件帮你做了语法校验。

容器相关

  • BeanFactory

  是Spring框架中最基本的容器,提供了基本的Bean管理功能。它负责创建、配置和管理Bean的生命周期,但不提供高级功能如AOP、事件发布等。初始化BeanFactory时,Bean实例不会被立即创建,而是在第一次请求时才会创建(懒加载)。

  • ApplicationContext

  是BeanFactory的子接口,提供了更高级的功能,如国际化支持、事件发布、AOP等。它在初始化时会创建所有单例Bean实例(预加载),因此启动速度较慢,但运行时性能更好。它提供了基础的bean操作相关方法,如getBean()、containsBean()等,通过其他接口扩展功能。常用的初始化类有

  1. ClassPathXmlApplicationContext
  2. FileSystemXmlApplicationContext

注解开发

  注解开发是指使用Java注解来配置和管理Spring Bean的方式。通过使用注解,开发者可以在代码中直接定义Bean的属性、依赖关系和生命周期等信息,而不需要在XML配置文件中进行繁琐的配置。这种方式提高了开发效率和代码的可读性,同时也使得配置更加灵活和易于维护。

  之前通过XML配置文件来定义Bean的属性和依赖关系,现在可以直接在Java类中使用注解来完成这些配置。例如,使用@Component注解来标识一个类为Spring Bean,使用@Autowired注解来自动注入依赖关系,使用@PostConstruct@PreDestroy注解来定义初始化和销毁方法等。例如,对于以下XML配置:

xml
<bean id="myBean" class="com.example.MyClass">
    <property name="property1" value="value1"/>
    <property name="property2" ref="anotherBean"/>
</bean>

  可以使用注解来替代XML配置:

java
// 这里如果想起名称,可以在@Component注解里加上一个value属性,来指定这个Bean的名称。比如 @Component("myBean"),这样就相当于在XML里写了 <bean id="myBean" class="com.example.MyClass">。
@Component
public class MyClass {

    @Value("value1")
    private String property1;

    @Autowired
    private AnotherBean property2;

    // 其他代码...
}

  除此之外,得让Spring知道要来这里搜索,所以在xml上要加上context:component-scan标签,告诉Spring去扫描这个包下的类,找到那些被注解标记为Bean的类,并把它们注册到Spring容器中:

xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.example"/>

    <!-- 其他 Bean 定义 -->
</beans>

  对于@Component注解,还有一些衍生注解,它们分别用于标识不同类型的Bean,提供了更具体的语义信息,帮助开发者更好地组织和管理代码结构:

  • @Component:通用组件,适用于任何需要被Spring管理的类。
  • @Service:业务逻辑组件,适用于处理具体的业务需求的类。
  • @Controller:控制器组件,适用于处理用户请求并返回响应的类,通常用于Web应用程序。
  • @Repository:数据访问组件,适用于与数据库进行交互的类,通常用于持久层。

纯注解开发

  Spring3.0 升级了纯注解开发模式,使用JAVA代码来配置Spring容器,完全不需要XML配置文件。这种方式被称为Java-based Configuration(基于Java的配置),它通过使用@Configuration@Bean注解来定义和管理Spring Bean。

  我们使用config文件夹下的SpringConfig来代替原先的applicationContext.xml文件,来定义和管理Spring Bean。例如:

java
@Configuration
@ComponentScan("com.example")
@Scope("singleton") // 这里的单例是指在整个Spring容器中只会创建一个myBean实例,所有对myBean的请求都会返回同一个实例。
public class SpringConfig {

    @Bean
    public MyClass myBean() {
        MyClass myBean = new MyClass();
        myBean.setProperty1("value1");
        myBean.setProperty2(anotherBean());
        return myBean;
    }

    @Bean
    public AnotherBean anotherBean() {
        return new AnotherBean();
    }

    @PostConstruct
    public void init() {
        // 初始化逻辑
    }

    @PreDestroy
    public void destroy() {
        // 销毁逻辑
    }
}

  在上面的代码中:

  • @Configuration:表示这个类是一个Spring配置类。
  • @ComponentScan:告诉Spring去扫描指定的包来寻找被注解标记为Bean的类。如果要搜索多个包:@ComponentScan({"com.example", "com.dao"})
  • @Bean:用于定义一个Bean的方法,方法的返回值就是这个Bean的实例。
  • @Scope:用于定义Bean的作用域,例如单例(singleton)或多例(prototype)。
  • @PostConstruct:用于定义Bean的初始化方法,在Bean创建完成后自动调用。
  • @PreDestroy:用于定义Bean的销毁方法,在Bean销毁之前自动调用。

  在这个情况下,我们通过AnnotationConfigApplicationContext类来加载这个配置类,并获取Bean实例:

java
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
MyClass myBean = context.getBean(MyClass.class);

关于@Component(@Service等)和@Bean

  1. 相同点
  • 核心目标一致: 都是为了将对象交由 Spring IoC 容器统一管理,使其成为容器中的一个 Bean。
  • 默认作用域一致: 默认情况下,两者向容器注册的 Bean 都是单例(Singleton)。
  1. 不同点
对比维度@Component (及其衍生 @Service/@Controller)@Bean
作用位置标在 类 (Class) 的上方标在配置类的 方法 (Method) 上方
主要场景你自己编写的类(你有权限修改源码并在类头加注解)第三方包的类(无法改源码,只能手动 new),或需要复杂构建逻辑的对象
装配机制全自动:配合 @ComponentScan,Spring 自动扫描并利用反射(通常是无参构造)创建对象。半自动:你必须自己在方法里写代码把对象 new 出来并配好属性,Spring 只负责把方法 return 的结果放进容器。
灵活性较低(交给 Spring 走标准流程全权打理)极高(可以在方法内部写任意自定义的初始化和装配逻辑)

  具体的内容参考下面章节:管理第三方Bean。

自动装配

  在Java-based Configuration中,自动装配(Autowiring)同样适用。我们可以使用@Autowired注解来自动注入Bean的依赖关系。例如:

java
@Service

public class MyService {

    @Autowired
    private MyClass myBean;

    // 其他代码...
}

  在上面的代码中,@Autowired注解告诉Spring自动注入一个类型为MyClass的Bean实例到myBean属性中。Spring会根据类型来查找匹配的Bean,并将其注入到myBean中。需要注意的是,在这里甚至不需要自己提供set方法,Spring会通过反射机制直接将依赖注入到属性中,这也是Java-based Configuration的一大优势,简化了代码并提高了开发效率。

  如果在容器中存在多个同类型的Bean,Spring将无法确定要注入哪个Bean,从而抛出异常。此时,可以使用@Qualifier注解来指定要注入的Bean。例如:

java
@Service
public class MyService {

    @Autowired
    @Qualifier("myBean1") // 指定要注入的Bean名称
    private MyClass myBean;

    // 其他代码...
}

简单类型注入

  对于简单类型的注入,我们可以使用@Value注解来注入基本数据类型和字符串。例如:

java
@Component
public class MyClass {

    @Value("value1")
    private String property1;

    // 其他代码...
}

  如果这个简单类型的值需要从properties文件中获取,我们可以使用${}占位符来引用properties文件中的值。例如:

java
@Component
@PropertySource("classpath:jdbc.properties") // 加载properties文件
public class MyClass {

    @Value("${jdbc.url}")
    private String property1;

    // 其他代码...
}

管理第三方Bean

  在Java-based Configuration中,如果我们需要管理第三方库中的Bean(即我们无法修改其源码来添加注解的类),我们可以使用@Bean注解来定义一个工厂方法,手动创建并配置这个Bean。例如:

java
@Configuration
@ComponentScan("com.example")
public class SpringConfig {

    @Bean
    public MyClass myBean() {
        MyClass myBean = new MyClass();
        myBean.setProperty1("value1");
        myBean.setProperty2(anotherBean());
        return myBean;
    }

    @Bean
    public AnotherBean anotherBean() {
        return new AnotherBean();
    }
}

  在上面的代码中,我们定义了一个名为myBean的Bean,它是通过调用myBean()方法创建的。在这个方法内部,我们手动创建了一个MyClass对象,并设置了它的属性。然后我们将这个对象返回,Spring会将其注册为一个Bean并管理它的生命周期。

  然后,我们可以通过@Autowired注解来注入这个Bean到其他组件中。例如:

java
@Service
public class MyService {

    @Autowired
    private MyClass myBean;

    // 其他代码...
}

关于@ComponentScan

  1. 相同点
  • 核心目标一致: 都是为了将对象交由 Spring IoC 容器统一管理,使其成为容器中的一个 Bean。
  • 默认作用域一致: 默认情况下,两者向容器注册的 Bean 都是单例(Singleton)。
  1. 不同点
对比维度@Component (及其衍生 @Service/@Controller)@Bean
作用位置标在 类 (Class) 的上方标在配置类的 方法 (Method) 上方
主要场景你自己编写的类(你有权限修改源码并在类头加注解)第三方包的类(无法改源码,只能手动 new),或需要复杂构建逻辑的对象
装配机制全自动:配合 @ComponentScan,Spring 自动扫描并利用反射(通常是无参构造)创建对象。半自动:你必须自己在方法里写代码把对象 new 出来并配好属性,Spring 只负责把方法 return 的结果放进容器。
灵活性较低(交给 Spring 走标准流程全权打理)极高(可以在方法内部写任意自定义的初始化和装配逻辑)

  如果括号里什么都不写(直接用 @ComponentScan),它绝不会扫描整个项目!

  • 默认行为: 它只会扫描 当前配置类所在的包,以及该包下面的子包。
  • 避坑指南: 如果你把配置类放在了 com.example.config 包下,那放在 com.example.service 里的业务类就绝对扫不到。这也是为什么 Spring Boot 的主启动类必须放在项目最外层的根目录(比如 com.example) 的根本原因。

@Import

  @Import注解是Spring框架中用于导入其他配置类的注解。相较于@ComponentScan,它提供了一种更直接和明确的方式来组合多个配置类。通过使用@Import,我们可以将一个配置类中的Bean定义导入到另一个配置类中,从而实现配置的模块化和重用。我感觉就是更直观的确保某个配置类被加载了,而不需要担心它是否在扫描范围内。

  例如,假设我们有两个配置类:DataSourceConfigServiceConfig,我们可以使用@Import将它们组合在一起:

java
@Configuration
@Import({DataSourceConfig.class, ServiceConfig.class})
public class AppConfig {
    // 其他配置...
}

第三方bean依赖注入

  对于第三方bean,要是它依赖其他bean,可以直接在形参里写上这个依赖,Spring会自动注入。例如:

java
@Configuration
public class AppConfig {

    // 1. 先把第三方数据源放进容器
    @Bean
    public DruidDataSource dataSource() {
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl("jdbc:mysql://localhost:3306/mydb");
        return ds;
    }

    // 2. 重点来了!要把第三方的 JdbcTemplate 放进容器
    @Bean
    // 魔法在这里:我们在括号里写上需要的依赖 (DruidDataSource ds)
    public JdbcTemplate jdbcTemplate(DruidDataSource ds) { 
        
        // 此时,传进来的 ds 就是上面那个已经配好的数据源
        // 我们直接把它塞给 JdbcTemplate 的构造方法
        JdbcTemplate template = new JdbcTemplate(ds); 
        
        return template;
    }
}

AOP

  AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的一个重要特性,它允许开发者将横切关注点(如日志记录、事务管理、安全控制等)从核心业务逻辑中分离出来,以提高代码的模块化和可维护性。

  通过使用AOP,开发者可以在不修改核心业务代码的情况下,动态地添加或修改功能,从而实现更灵活和可扩展的应用程序设计。

alt text

核心概念

  • 连接点(Join Point):程序执行的特定点,如方法调用、异常抛出等。
  • 切入点(Pointcut):定义了在哪些连接点上应用切面逻辑的规则。一个切入点可以只描述一个具体方法,也可以匹配多个方法。
    • 一个具体方法:无返回值的save方法。
    • 多个方法:所有以save开头的方法。
  • 通知(Advice):切面中定义的具体操作,可以在连接点之前、之后或抛出异常时执行。
  • 通知类:定义通知的类。
  • 切面(Aspect):一个模块化的关注点,通常是一个类,包含了横切逻辑和切点定义。

  AOP的相关坐标:

xml
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>

  具体实现方式:

java
@Component
@Aspect // 表示这个类是一个切面类,里面定义了切点和通知。其实就是告诉Spring这是AOP。
public class MyAdvice {
    // @Pointcut("execution(* com.example.service.*.*(..))") 定义切入点,匹配com.example.service包下的所有方法
    @Pointcut("execution(void com.example.service.*.save())")
    private void saveMethod() {}

    @Before("saveMethod()") // 在saveMethod方法执行之前执行
    public void beforeSave() {
        System.out.println("Before save: Saving data...");
    }
}

  除此之外,在Congifuration的类上还需要加上@EnableAspectJAutoProxy注解,来启用Spring的AOP功能:

java
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
    // 其他配置...
}

AOP的工作流程

  1. Spring容器启动
  2. 解析配置类,识别切面类和切点定义
  3. 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
  • 匹配失败:正常创建bean实例,不进行增强。
  • 匹配成功:创建一个代理对象,代理对象会在调用目标方法时执行通知逻辑。
  1. 获取bean执行方法
  • 获取bean,调用方法并执行,完成操作
  • 获取的bean是代理对象时,调用方法时会先执行通知逻辑,再执行目标方法,最后返回结果。

上面的黑话

  1. 增强:其实就是“通知(Advice)”产生的实际效果。大白话就是:你的 save() 方法原本只会“存数据库”。现在,AOP 把“打印日志”和“开启事务”的代码强行塞到了它的前后。对于 save() 方法来说,它的功能变多了,这就叫“它被增强了”。
  2. 代理对象:当一个类的方法被切入点匹配到时,Spring会创建一个代理对象来替代原始的Bean。这个代理对象会在调用目标方法时执行通知逻辑,然后再调用原始方法。对于调用者来说,他们拿到的其实是这个代理对象,而不是原始的Bean实例。

有趣的是,你就算打印这个得到的对象,也看不见这层Proxy,因为内部重写了toString方法,直接把它伪装成了原始对象的样子。你只能通过反射或者调试工具才能发现它其实是个代理对象。

  其实就是造一个高仿的“替身”,也就是“代理对象”。

  • 真身(目标对象 Target): 你自己写的 UserService 实例,它只会傻傻地存数据库。
  • 替身(代理对象 Proxy): Spring 在内存里瞬间凭空捏造出来的一个对象。它长得和真身一模一样(有着一样的方法名),但它的肚子里大有乾坤。

  这个代理模式感觉和Vue的Proxy很像,都是在原对象的基础上,创建一个“代理对象”,来实现一些额外的功能(Vue是响应式数据,Spring是AOP增强)。不过它们的实现原理和应用场景是完全不同的。

AOP && 接口

  如果AOP 的切点瞄准了一个“接口”,那么容器里所有实现了这个接口的类,统统都会被 Spring 自动套上代理。

切入点表达式

execution( [修饰符] 返回值类型 包名.类名.方法名(参数类型) )   切入点表达式是用来定义在哪些连接点上应用切面逻辑的规则。例如:

java
@Pointcut("execution(void com.example.service.*.save())")
@Pointcut("execution(* com.example.service.*.*(..))")
@Pointcut("execution(* *..*Service+.*(..))")

  在上面的代码中:

  • execution(void com.example.service.*.save()):匹配com.example.service包下所有类的save方法,且这个方法必须是无返回值的。
  • execution(* com.example.service.*.*(..)):匹配com.example.service包下所有类的所有方法,不论返回值类型和参数类型。
  • execution(* *..*Service+.*(..)):匹配所有以Service结尾的类(无论在哪个包下)的所有方法,不论返回值类型和参数类型。

  其中:

  • *:表示任意类型或任意方法名,但只能有一个(不能是零个)。
  • ..:表示任意数量的参数(包括零个参数),也可以代表多个连续任意符号。
  • +:表示匹配当前类型及其子类型,或者当前接口及其所有的实现类。

AOP通知类型

  AOP通知类型主要有以下几种:

  1. 前置通知(Before Advice):在目标方法执行之前执行。@Before("saveMethod()")
java
@Before("saveMethod()")
public void beforeSave() {
    System.out.println("Before save: Saving data...");
}
  1. 后置通知(After Advice):在目标方法执行之后执行,无论方法是正常返回还是抛出异常。@After("saveMethod()")
java
@After("saveMethod()")
public void afterSave() {
    System.out.println("After save: Data saved.");
}
  1. 环绕通知(Around Advice)(重点):环绕通知可以在目标方法执行之前和之后执行,并且可以控制目标方法是否执行。@Around("saveMethod()")

  可以在这里进行权限校验等操作。

java
@Around("saveMethod()")
// 这里一定要设定为Object类型,除非返回值是void,否则就丢不了结果了。
// 而且这个方法必须抛出Throwable异常,因为pjp.proceed()可能会抛出任何异常,我们必须捕获并向外抛出。
public Object aroundSave(ProceedingJoinPoint pjp) throws Throwable {
    // 在目标方法执行之前执行
    System.out.println("Before save: Saving data...");

    // 执行目标方法
    Object result = pjp.proceed();

    // 在目标方法执行之后执行
    System.out.println("After save: Data saved.");

    // 如果原方法有个返回值,我们得保证环绕完还得丢出去
    return result;
}
  1. 返回通知(After Returning Advice)(了解):在目标方法正常返回之后执行。
  2. 异常通知(After Throwing Advice)(了解):在目标方法抛出异常之后执行。

AOP通知获取数据

  在AOP通知中,我们可以通过切入点表达式来获取目标方法的参数、返回值和异常等数据。例如:

java
@Before("saveMethod()")
public void beforeSave(JoinPoint joinPoint) {
    // 获取目标方法的参数
    Object[] args = joinPoint.getArgs();
    System.out.println("Before save: Saving data with arguments: " + Arrays.toString(args));
}

// returning仅适用于After Returning Advice,表示这个通知只会在目标方法正常返回时执行,并且可以获取到目标方法的返回值。
@AfterReturning(pointcut = "saveMethod()", returning = "result")
public void afterReturningSave(JoinPoint joinPoint, Object result) {
    // 获取目标方法的参数
    Object[] args = joinPoint.getArgs();
    // 获取目标方法的返回值
    System.out.println("After save: Data saved with result: " + result);
}

  在上面的代码中,@AfterReturning注解的returning属性指定了一个参数名(result),这个参数会接收目标方法的返回值。通过JoinPoint对象,我们可以获取目标方法的参数等信息。

JointPoint和ProceedingJoinPoint

  1. JoinPoint:是AOP通知方法的参数类型之一,表示一个连接点。它提供了获取连接点相关信息的方法,如获取方法参数、目标对象等。适用于前置通知、后置通知、返回通知和异常通知等。
  2. ProceedingJoinPoint:是JoinPoint的子接口,专门用于环绕通知。它除了提供JoinPoint的功能外,还提供了一个proceed()方法,用于执行目标方法并获取其返回值。适用于环绕通知。

事务管理

  事务管理是Spring框架中的一个重要特性,它允许开发者在应用程序中定义和管理事务,以确保数据的一致性和完整性。通过使用Spring的事务管理功能,开发者可以轻松地控制事务的边界,处理事务的提交和回滚,从而提高应用程序的可靠性和健壮性。

  Spring提供了两种主要的事务管理方式:编程式事务管理和声明式事务管理。编程式事务管理需要开发者在代码中显式地控制事务的开始、提交和回滚,而声明式事务管理则通过配置来定义事务的行为,开发者无需直接操作事务对象。

  在Spring中,声明式事务管理通常使用@Transactional注解来实现。通过在方法或类上添加@Transactional注解(一般写在接口上),开发者可以指定该方法或类中的所有方法都需要进行事务管理。例如:

java
@Service
// @Transactional 一般不写在实现类上,而是写在接口上,这样就算有多个实现类,它们都能共享同一套事务配置。
public class MyService {

    // 写在这里勉强能生效,但不推荐,因为如果这个类有多个实现类,那么它们就不能共享同一套事务配置了。
    @Transactional
    public void saveData() {
        // 业务逻辑代码
    }
}

  然后,在配置类上:

  1. 使用@EnableTransactionManagement注解来启用Spring的事务管理功能:
  2. 配置数据源和事务管理器,例如使用DataSourceTransactionManager来管理基于JDBC的数据源事务:
java
@Configuration
@EnableTransactionManagement
public class AppConfig {
    @Bean
    public DataSource dataSource() {
        // 配置数据源,例如使用DruidDataSource
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl("jdbc:mysql://localhost:3306/mydb");
        ds.setUsername("root");
        ds.setPassword("password");
        return ds;
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

  通过合理地使用Spring的事务管理功能,开发者可以确保应用程序的数据一致性和完整性,同时也可以提高应用程序的性能和可维护性。

  使用示例:

java
public interface UserService {
    @Transactional(readOnly = true, rollbackFor = {IOException.class}) // 这里的rollbackFor属性表示当抛出任何类型的异常(包括检查异常)时,事务都会回滚。
    void saveUser(User user);
}

为什么会有rollbackFor?

  事务并不是说抛出异常就一定回滚的。默认情况下,Spring只会在抛出运行时异常(RuntimeException)错误(Error)时才会回滚事务,而对于检查异常(Checked Exception),Spring默认不会回滚事务。这是因为检查异常通常表示可预见的错误情况,开发者可以通过捕获和处理这些异常来恢复程序的正常运行,而不需要回滚整个事务。

  如果你希望在抛出检查异常时也回滚事务,可以使用rollbackFor属性来指定哪些异常类型应该触发事务回滚。例如:

java
@Transactional(rollbackFor = Exception.class)
public void saveUser(User user) throws Exception {
    // 业务逻辑代码
    if (someCondition) {
        throw new Exception("Something went wrong");
    }
}

  在上面的代码中,rollbackFor = Exception.class表示当抛出任何类型的异常(包括检查异常)时,事务都会回滚。这样可以确保在发生任何错误时,数据的一致性和完整性得到维护。

propagation传播行为

  如果一个@Transactional注解的方法被另一个@Transactional注解的方法调用了,那么就会涉及到事务的传播行为(Propagation)。传播行为定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播和处理。

  大白话就是:事务A中调用了事务B、C、D,此时默认情况下,事务A、B、C、D会共享同一个事务,如果其中任何一个方法抛出异常导致事务回滚,那么整个事务(包括A、B、C、D)都会回滚。在这里,A是事务管理员,B、C、D是事务协调员,它们都在A的事务里。

  但是如果D需要独立的事务,这时就可以通过设置传播行为来实现。具体的例子如下:

java
@Transactional(propagation = Propagation.REQUIRES_NEW) // 这个注解表示D方法需要一个新的事务,独立于A、B、C的事务。
public void D() {
    // 业务逻辑代码
}

  在上面的代码中,Propagation.REQUIRES_NEW表示D方法需要一个新的事务,独立于A、B、C的事务。这样,即使A、B、C中的任何一个方法抛出异常导致事务回滚,D方法的事务也不会受到影响,仍然会正常提交。

  除此之外,propagation还有其他的选项:

MyBatis

  MyBatis是一个流行的Java持久层框架,它提供了一个简单而强大的方式来进行数据库操作。MyBatis通过使用XML或注解来定义SQL语句和映射关系,使得开发者可以轻松地将Java对象与数据库表进行映射,从而实现数据的持久化。

  其实际功能类似于gorm,但是gorm开发会更迅速,因为无需用户自己编写SQL语句,而是通过ORM的方式来操作数据库。MyBatis则需要开发者自己编写SQL语句,这虽然增加了开发的复杂度,但也提供了更大的灵活性和控制力,特别是在处理复杂查询和性能优化方面。

  具体的使用方法如下:

  1. 添加MyBatis依赖:
xml
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>
  1. 配置数据源和MyBatis:
java
@Configuration
// 这个是关键
@MapperScan("com.example.mapper") // 扫描Mapper接口所在的包
public class MyBatisConfig {

    @Bean
    public DataSource dataSource() {
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl("jdbc:mysql://localhost:3306/mydb");
        ds.setUsername("root");
        ds.setPassword("password");
        return ds;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        return sessionFactory.getObject();
    }
}
  1. 定义实体类:
java
public class User {
    private Long id;
    private String name;
    private String email;

    // getters and setters
}
  1. 定义Mapper接口:
java
import org.apache.ibatis.annotations.*;

// @Mapper 告诉 Spring:这是一个 MyBatis 的接口,请帮我在内存里捏造一个代理实现类!
@Mapper 
public interface UserMapper {

    // 1. 查 (Select)
    // #{id} 就是占位符,它会自动提取方法参数里传进来的 id
    @Select("SELECT * FROM users WHERE id = #{id}")
    User findById(Integer id);

    // 2. 删 (Delete)
    @Delete("DELETE FROM users WHERE id = #{id}")
    int deleteById(Integer id); // 返回值 int 表示影响了数据库里多少行数据

    // 3. 改 (Update)
    @Update("UPDATE users SET age = #{age} WHERE name = #{name}")
    int updateAgeByName(User user); 

    // 4. 增 (Insert)
    // 注意:传进来的是个 User 对象,#{name} 会自动调用 user.getName() 获取值
    @Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")
    int insertUser(User user);
}

  如果方法有多个零散的参数,极其容易报错,因为MyBatis无法确定哪个参数对应哪个占位符。这时,我们可以使用@Param注解来明确指定参数的名称。例如:

java
@Update("UPDATE users SET age = #{age} WHERE name = #{name}")
int updateAgeByName(@Param("name") String name, @Param("age") Integer age);

  通过以上步骤,我们就可以使用MyBatis来进行数据库操作了。MyBatis提供了丰富的功能,如动态SQL、缓存机制等,可以帮助开发者更高效地进行数据访问和管理。

SpringMVC

  SpringMVC是Spring框架中的一个模块,专门用于构建Web应用程序。它基于Model-View-Controller(MVC)设计模式,将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。通过使用SpringMVC,开发者可以轻松地处理HTTP请求、管理会话、进行数据绑定和验证等,从而构建功能丰富、可维护的Web应用程序。

  学学就好,不需要深入了解它的实现原理和细节。我们只需要知道它是一个基于MVC设计模式的Web框架,可以帮助我们处理HTTP请求和响应,管理会话,进行数据绑定和验证等。

  使用步骤:

  1. 添加SpringMVC依赖:
xml
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.3.9</version>
</dependency>
  1. 配置SpringMVC:
java
@Configuration
@EnableWebMvc // 启用SpringMVC的功能
@ComponentScan("com.example.controller") // 扫描Controller所在的包
public class WebConfig implements WebMvcConfigurer {
    // 其他配置...
}
  1. 初始化Servlet容器:(复制就好啦)
java
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;

public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer {

    // 1. 加载 SpringMVC 环境(大堂经理前线部)
    @Override
    protected WebApplicationContext createServletApplicationContext() {
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        ctx.register(SpringMvcConfig.class); // 注册你自己的 SpringMVC 图纸
        return ctx;
    }

    // 2. 设置 SpringMVC 拦截的请求路径
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"}; // 拦截所有请求
    }

    // 3. 加载 Spring 环境(后勤总指挥部)
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null; 
    }

    // 4. 乱码处理(可选)
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
        encodingFilter.setEncoding("UTF-8");
        encodingFilter.setForceEncoding(true);
        return new Filter[]{encodingFilter};
    }
}
  1. 定义Controller:
java
// @Controller
// @ResponseBody // 这个也可以写在方法上,表示这个方法的返回值直接作为HTTP响应体返回,而不是解析为视图名称。
@RestController // 这个注解相当于@Controller和@ResponseBody的组合,表示这个类是一个控制器,并且所有方法的返回值都直接作为HTTP响应体返回。
@RequestMapping("/users") // 定义请求路径的前缀
public class UserController {

    // 1. 路径传参 (:id) -> 极其精准地找某个人
    // 请求样子:GET http://localhost:8080/users/123
    @GetMapping("/{id}") 
    public User getUserById(@PathVariable("id") Integer id) {
        return userService.findById(id);
    }
    
    // ==========================================
    // 请求样子:GET http://localhost:8080/users/search?name=张三&age=25
    // ==========================================
    @GetMapping("/search") 
    public List<User> searchUsers(
            // @RequestParam 就是专门用来接 ? 后面的参数的
            @RequestParam("name") String userName, 
            
            // 进阶玩法:如果不传 age,默认当做 18 岁处理,且不报错
            @RequestParam(value = "age", required = false, defaultValue = "18") Integer age
    ) {
        System.out.println("前端传来的搜索条件:姓名=" + userName + ", 年龄=" + age);
        // 业务逻辑代码,例如根据条件去数据库搜索列表
        return userService.findByNameAndAge(userName, age);
    }

    // 3. 请求体传参 (JSON) -> 接收一大坨复杂数据来新建用户
    // 请求样子:POST http://localhost:8080/users/ 并在 Body 里塞 JSON
    @PostMapping
    public String createUser(@RequestBody User user) {
        userService.save(user);
        return "User created successfully";
    }

    // 4. 日期传参 -> 接收一个日期字符串来查询某一天的订单
    // 请求样子:GET http://localhost:8080/orders?date=2024-06-01
    @GetMapping("/orders")
    public List<Order> getOrdersByDate(@RequestParam("date") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDate date2) {
        System.out.println("前端传来的日期参数:" + date);
        // 业务逻辑代码,例如根据日期去数据库查询订单列表
        return orderService.findByDate(date);
    }
}

@EnableWebMvc

  @EnableWebMvc注解是Spring框架中用于启用SpringMVC功能的注解。当我们在配置类上添加@EnableWebMvc注解时,Spring会自动配置和启用SpringMVC的相关组件和功能,如请求映射、视图解析、数据绑定等。这个注解相当于告诉Spring:“嘿,我要使用SpringMVC了,请帮我把它的功能都打开吧!”

  开启的功能包括:

  1. 请求映射:SpringMVC会自动扫描并注册使用@RequestMapping@RequestBody等注解标注的控制器方法,以便能够处理HTTP请求。
  2. Json解析:SpringMVC会自动配置一个消息转换器(如Jackson),用于将Java对象转换为JSON格式,反之亦然。这使得我们可以轻松地处理RESTful API的请求和响应。

@ResponseBody

  @ResponseBody注解是SpringMVC中用于将控制器方法的返回值直接作为HTTP响应体返回的注解。当我们在控制器方法上添加@ResponseBody注解时,Spring会自动将方法的返回值转换为适当的格式(对象转json(这个是jackson干的),字符串直接返回)并写入HTTP响应体中,而不是将其解析为视图名称。

  如果没有这个注解,可能会return web.jsp,(jsp和jsx类似,在html中塞入java)然后被解析成一个页面。

SpringMVC管理的Bean

  SpringMVC 是一个纯粹的 Web 层框架。它包揽了所有和 HTTP 协议、网络通信、数据格式转换、接口路由相关的“前台脏活累活”,以此来保证你的 Controller 可以极其清爽地只写纯 Java 代码。

  因此,SpringMVC 管理的 Bean 只能是 Controller 以及和它相关的组件(如 HandlerInterceptor、ViewResolver 等)。而 Service、Repository、Component 等其他类型的 Bean 则应该由 Spring 的核心容器来管理,而不是 SpringMVC。

  但这就出现了问题,当我们开发Service和Dao层时,很有可能会不小心调用到SpringMVC的组件(比如说调用了SpringMVC的RequestContextHolder来获取当前请求的信息),这时就会导致这些Service和Dao层的Bean被SpringMVC管理了。虽然它们仍然可以正常工作,但从设计上来说,这种耦合是不合理的。因此:

  1. SpringMVC加载的Bean应该只包含Controller和与HTTP请求处理相关的组件。
  2. Service、Repository、Component等其他类型的Bean应该由Spring的核心容器来管理,而不是SpringMVC。
  • 要么设定扫描范围为整个项目,然后排除掉controller包内的bean:
java
@Configuration
// 正则
@ComponentScan(value = "com.example", excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.example.controller.*"))
// 注解
// @ComponentScan(value = "com.example", excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class))
public class AppConfig {
    // 其他配置...
}
  • 要么设定扫描范围为精准范围:
java
@Configuration
@ComponentScan({"com.example.service", "com.example.repository"})
public class AppConfig {
    // 其他配置...
}

  其实也可以都加载在一个环境里面。

总的来说,上面都太复杂了!!!

  上面的第三步实际上有简化写法:

java
public class ServletContainersInitConfig extends AbstractAnnotaionConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{AppConfig.class}; // 直接把 Spring 的配置类放在这里就好了
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{SpringMvcConfig.class}; // 直接把 SpringMVC 的配置类放在这里就好了
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

异常处理器

  在SpringMVC中,我们可以通过定义一个全局异常处理器来统一处理控制器层抛出的异常。这样可以避免在每个控制器方法中都写重复的try-catch代码,提高代码的可维护性和一致性。所有的异常均抛出到表现层进行处理。

  在这里,我们可以使用AOP的方式来实现全局异常处理器。具体的实现如下:

java
// 在controller中创建的一个异常类
@RestControllerAdvice // 这个注解表示这是一个全局异常处理器,Spring会自动扫描并注册它。
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class) // 这个注解表示当控制器层抛出任何类型的异常时,都会调用这个方法来处理。
    public Result handleException(Exception ex) {
        // 这里可以进行日志记录、错误信息封装等操作
        System.out.println("发生异常了,异常信息:" + ex.getMessage());
        // 这个Result类是你自己定义的一个统一的响应格式,可以根据需要进行修改和扩展。
        return new Result("error", ex.getMessage()); // 返回一个统一的错误响应
    }
}

项目异常处理方案

  在实际项目中,我们通常会定义一个统一的异常处理方案,以便在发生异常时能够返回一致的错误响应格式。下面是一个常见的项目异常处理方案:

  1. 业务异常
  • 发送对应信息传递给用户,提醒用户进行修改。
  1. 系统异常
  • 记录日志,排查问题,修复bug,安抚用户。
  1. 其他异常
  • 记录日志,排查问题,修复bug,安抚用户。

拦截器

  拦截器(Interceptor)是SpringMVC中的一个重要组件,它允许我们在请求处理的不同阶段对HTTP请求进行拦截和处理。通过使用拦截器,我们可以实现一些通用的功能,如权限校验、日志记录、性能监控等,而无需在每个控制器方法中重复编写这些逻辑。

  其实感觉就是validator的升级版,validator只能在方法参数上进行校验,而拦截器可以在请求处理的不同阶段进行拦截和处理,功能更加强大和灵活。

拦截器和过滤器

  1. 拦截器(Interceptor):是SpringMVC中的组件,专门用于拦截HTTP请求并在请求处理的不同阶段进行处理。它可以访问请求和响应对象,并且可以控制请求的继续执行或终止。拦截器通常用于权限校验、日志记录、性能监控等功能。仅针对SpringMVC的访问进行增强。
  2. 过滤器(Filter):是Java Servlet规范中的组件,用于在请求到达Servlet之前或响应离开Servlet之后对请求和响应进行过滤和处理。过滤器可以用于请求日志记录、编码设置、安全检查等功能。过滤器在整个Web应用程序范围内生效,而拦截器只在SpringMVC的控制器范围内生效。

  虽然它们的功能有些重叠,但它们的应用场景和实现方式是不同的。拦截器是SpringMVC特有的组件,专门用于处理HTTP请求,而过滤器是Java Servlet规范的一部分,可以在任何Java Web应用程序中使用。

  Interceptor可以放在controller/interceptor包里,专门用来放拦截器类的。它需要实现HandlerInterceptor接口,并重写其中的方法来定义拦截逻辑。例如:

java
public class MyInterceptor implements HandlerInterceptor {

    // 在控制器方法执行之前调用
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("Pre Handle method is Calling");
        return true; // 返回true表示继续执行后续的拦截器和控制器方法,返回false表示终止请求处理。
    }

    // 在控制器方法执行之后调用,但在视图渲染之前调用
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("Post Handle method is Calling");
    }

    // 在整个请求处理完成之后调用,通常用于资源清理等操作
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("Request and Response is completed");
    }
}

  然后在SpringMVC的配置类中注册这个拦截器:

java
@Configuration
public class MyWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册自定义的拦截器,并指定拦截的路径模式
        // /** 表示拦截所有请求,你也可以根据需要指定更具体的路径模式,例如 /api/** 只拦截以 /api/ 开头的请求。 
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**");
    }
}

  在Interceptor的参数中:

  • Object handler:这个handler就是被拦截的那个控制器方法的代理对象,我们可以通过它来获取被拦截的方法的信息,例如方法名、参数类型等。这样就可以在拦截器中进行一些基于方法信息的处理,例如权限校验、日志记录等。
  • ModelAndView modelAndView:这个已经没有任何意义了,因为我们现在都是做接口开发,根本不需要视图解析了,所以这个参数就没什么用处了。
  • Exception ex:这个参数表示在请求处理过程中抛出的异常,如果没有异常发生,则为null。我们可以通过这个参数来进行一些基于异常信息的处理,例如日志记录、错误响应等。
  • 对于response和request参数,我们也可以通过它们来获取请求和响应的信息,例如请求的URL、请求方法、请求头等,以及设置响应的状态码、响应头等。这些信息对于我们在拦截器中进行权限校验、日志记录、性能监控等功能非常有用。可以利用反射强行修改其内容。

多个拦截器的执行顺序

  当我们在SpringMVC中注册了多个拦截器时,它们的执行顺序是按照它们在配置类中注册的顺序来决定的。具体来说:

  1. preHandle方法的执行顺序:按照注册的顺序依次执行,即第一个注册的拦截器的preHandle方法最先执行,最后一个注册的拦截器的preHandle方法最后执行。
  2. postHandle方法的执行顺序:按照注册的逆序依次执行,即最后一个注册的拦截器的postHandle方法最先执行,第一个注册的拦截器的postHandle方法最后执行。
  3. afterCompletion方法的执行顺序:按照注册的逆序依次执行,即最后一个注册的拦截器的afterCompletion方法最先执行,第一个注册的拦截器的afterCompletion方法最后执行。

  例如,如果我们注册了三个拦截器A、B、C,按照顺序注册,那么它们的执行顺序如下:

  1. preHandle方法的执行顺序:A -> B -> C
  2. postHandle方法的执行顺序:C -> B -> A
  3. afterCompletion方法的执行顺序:C -> B -> A

  需要注意的是,如果在preHandle方法中返回了false,那么后续的拦截器和控制器方法将不会被执行,整个请求处理将被终止。因此,在设计拦截器时,我们需要根据实际需求来决定是否需要终止请求处理,以及如何处理这种情况。

SpringBoot

  SpringBoot是Spring框架的一个子项目,旨在简化Spring应用程序的开发和部署。它通过提供一系列的自动配置和约定优于配置的原则,使得开发者可以快速地创建独立、生产级别的Spring应用程序,而无需进行繁琐的配置和部署步骤。

  SpringBoot这么好用主要依赖于pom.xml中使用parent标签来继承spring-boot-starter-parent,这个父级POM文件中已经预定义了很多常用的依赖和插件的版本,这样我们在子项目中就不需要再去指定这些版本了,SpringBoot会自动帮我们管理这些依赖的版本,确保它们之间的兼容性和稳定性。

  去细细研究一下,它帮你搭配好了巨多的包,并且保证版本之间不会有冲突。

打包部署

  SpringBoot应用程序可以打包成一个可执行的JAR文件,包含了所有的依赖和资源,这样就可以直接运行这个JAR文件来启动应用程序,而不需要安装和配置一个独立的Servlet容器(如Tomcat)。SpringBoot内置了一个嵌入式的Servlet容器(如Tomcat、Jetty等),当我们运行这个JAR文件时,SpringBoot会自动启动这个嵌入式的Servlet容器,并将我们的应用程序部署到其中。

  打包部署的步骤如下:

  1. 使用maven或gradle构建工具来构建项目,并生成一个可执行的JAR文件。例如,使用maven可以运行以下命令:
bash
mvn clean package
  1. 执行启动指令
bash
java -jar target/myapp.jar

配置文件

  SpringBoot支持多种格式的配置文件,如application.propertiesapplication.yml等。我们可以在这些配置文件中定义应用程序的各种配置项,例如数据库连接信息、服务器端口号、日志级别等。当我们运行SpringBoot应用程序时,SpringBoot会自动加载这些配置文件,并将其中的配置项注入到应用程序中,使得我们可以通过配置文件来灵活地调整应用程序的行为和性能。

  yml文件示例:

yml
spring:
  application:
    name: springboot

server:
  port: 9099

# 可以在这里定义更多的配置项,例如数据库连接信息、日志级别等
# 直接在JAVA里面使用@Value("${server.port}")来获取配置项的值
lesson: SpringBoot

enterprise:
  name: itcast
  subject:
    - Java
    - Spring

  如果三个配置文件都有,SpringBoot会按照以下优先级来加载它们:

  1. application.properties:这是SpringBoot默认的配置文件,如果存在,它会被优先加载。
  2. application.yml:如果application.properties不存在,SpringBoot会尝试加载application.yml文件。(推荐这个)
  3. application.yaml:如果application.propertiesapplication.yml都不存在,SpringBoot会尝试加载application.yaml文件。

  需要注意的是,虽然application.ymlapplication.yaml文件的扩展名不同,但它们的内容格式是相同的,都是使用YAML格式来定义配置项的。因此,无论是使用application.yml还是application.yaml,我们都可以按照相同的方式来编写配置项,并且SpringBoot会正确地解析和加载这些配置项。

yaml的文件格式

  1. yaml不允许使用制表符(Tab)进行缩进,必须使用空格来进行缩进。通常建议使用2个空格或4个空格来表示一个缩进级别。
  2. yaml中的键值对使用冒号(:)来分隔,冒号后面必须跟一个空格,然后是对应的值。例如:

  对于配置文件,在代码中可以如下使用:

java
@RestController
public class MyController {

    @Value("${lesson}") // 这个注解表示从配置文件中获取名为 lesson 的配置项的值,并将其注入到这个字段中。
    private String lesson;

    @Value("${enterprise.name}")
    private String enterpriseName;

    @Value("${enterprise.subject[0]}") // 获取 enterprise.subject 列表中的第一个元素
    private String firstSubject;

    @Autowired
    private Environment env; // 这个是 Spring 提供的一个环境对象,可以用来获取配置项的值,例如 env.getProperty("lesson") 就可以获取到 lesson 配置项的值。

    @GetMapping("/config")
    public String getConfig() {
        return "Lesson: " + lesson + ", Enterprise Name: " + enterpriseName + ", First Subject: " + firstSubject;
    }
}

  比较常用的是我们根据配置定义一个新的类来接收这些配置项的值,这样就可以将相关的配置项进行分组和封装,方便我们在代码中使用和管理。例如:

java
@Component
@ConfigurationProperties(prefix = "enterprise") // 这个注解表示将配置文件中以 enterprise 开头的配置项注入到这个类的字段中。
public class EnterpriseConfig {

    private String name;
    private List<String> subject;

    // getters and setters
}

多环境开发

  在实际项目开发中,我们通常会有多个环境,例如开发环境、测试环境、生产环境等。每个环境可能需要不同的配置项,例如数据库连接信息、服务器端口号等。SpringBoot提供了一种非常方便的方式来支持多环境开发,即通过使用不同的配置文件来定义不同环境的配置项。

  我们可以在一个yml中定义多个环境的配置项,例如:

yml
spring:
  profiles:
    active: dev # 这个配置项表示当前激活的环境是 dev 环境

---
spring:
  profiles: dev # 这个配置项表示这是 dev 环境的配置项
server:
  port: 8080
---
spring:
  profiles: test # 这个配置项表示这是 test 环境的配置项
server:
  port: 8081
---
spring:
  profiles: prod # 这个配置项表示这是 prod 环境的配置项
server:
  port: 8082

  如果打包部署的时候没有指定环境,那么SpringBoot会默认使用spring.profiles.active配置项中指定的环境(在上面的例子中是dev环境)。如果我们想要在打包部署的时候指定使用哪个环境,可以通过以下方式来实现:

bash
java -jar myapp.jar --spring.profiles.active=test

整合JUnit

  原本在Spring中,正常的运行方式如下:

java
@RunWith(SpringRunner.class) // 这个注解表示使用 SpringRunner 来运行这个测试类,SpringRunner 是 Spring 提供的一个测试运行器,它可以帮助我们在测试中加载 Spring 的应用上下文,并且支持依赖注入等功能。
@ContextConfiguration(classes = AppConfig.class) // 这个注解表示加载 AppConfig 这个配置类来构建 Spring 的应用上下文,AppConfig 是我们自己定义的一个配置类,它包含了我们需要的 Bean 定义和其他配置。
public class MyServiceTest {

    @Autowired
    private MyService myService; // 这个注解表示将 MyService 这个 Bean 注入到这个字段中,MyService 是我们自己定义的一个服务类,它包含了我们需要测试的业务逻辑。

    @Test
    public void testMyService() {
        // 在这里编写测试代码,例如调用 myService 的方法并断言结果
        String result = myService.doSomething();
        assertEquals("expected result", result);
    }
}

  但是在SpringBoot中,我们可以使用@SpringBootTest注解来简化这个过程,这个注解会自动加载SpringBoot的应用上下文,并且支持依赖注入等功能。具体的使用方式如下:

java
@SpringBootTest // 这个注解表示这是一个 SpringBoot 的测试类,SpringBoot 会自动加载应用上下文,并且支持依赖注入等功能。
public class MyServiceTest {

    @Autowired
    private MyService myService; // 这个注解表示将 MyService 这个 Bean 注入到这个字段中,MyService 是我们自己定义的一个服务类,它包含了我们需要测试的业务逻辑。

    @Test
    public void testMyService() {
        // 在这里编写测试代码,例如调用 myService 的方法并断言结果
        String result = myService.doSomething();
        assertEquals("expected result", result);
    }
}

整合MyBatis

  1. 修改application.yml配置文件,添加数据库连接信息:
yml
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: password
  1. 在dao层加入@Mapper注解:
java
@Mapper
public interface UserMapper {
    // 定义数据库操作方法,例如:
    @Select("SELECT * FROM users WHERE id = #{id}")
    User findById(Integer id);
}