跳转至

Ssm

Spring :企业级一站式框架

一、Container

1. 组件和容器

​ 组件:具有一定功能的对象,组件的三大特性:(名字、类型)、对象、作用域,组件的名字是全局唯一的。在Spring中,组件的创建时机是容器启动过程中,单实例特性:每次获取直接拿就完事了。

​ 容器:管理组件(创建、获取、保存、销毁)

2. IOC 控制反转

​ IOC 是一种设计思想,不是具体的技术。它的核心理念是:将对象的创建和依赖管理交给外部容器控制,而不是由对象自己控制

传统方式下,一个对象主动去创建它所依赖的对象; 而在 IOC 模式下,对象被动地接收依赖对象由容器提供)。

​ “控制” 指的是对象依赖的创建与管理。“反转”指的是这种控制权从程序员手中反转给了外部容器(如 Spring 容器)。

3. DI 依赖注入

​ DI 是实现 IOC 的具体手段。 即:容器在运行时将对象所依赖的实例“注入”进去。

4. 注册组件的不同方法

4.1 @Bean

1. 概念

@Bean 是 Spring 中的一种 显式注册 Bean 的方式,用于告诉 IOC 容器:“我要手动定义并注册一个组件(Bean)到容器中进行管理。” 换句话说,使用 @Bean 标注的方法会由 Spring 调用其返回对象会被加入到 IOC 容器中,并由容器统一管理生命周期和依赖关系。

2. 主要作用

​ 1)注册 Bean 到容器:通过 @Bean 标注的方法返回的对象,会自动成为 Spring 管理的 Bean。

​ 2)代替 XML 配置:在注解驱动的配置类中,用 @Bean 方法代替 <bean> 标签进行配置。

​ 3)自定义第三方类的注入:当某个类不是由我们自己编写(例如外部库中的工具类),无法直接加上 @Component 注解时,就可以通过 @Bean 手动注册。

3. 获得组件的方法

spring3.png

// 按照组件名称获取组件对象
Student ltb = (Student) ioc.getBean("LiuTianBa");
System.out.println("ltb  " + ltb);

// 按照组件类型获取组件对象
Dog dog = ioc.getBean(Dog.class);
System.out.println("bean = " + dog);

// 按照名字加类型,就不需要强转了

4.2 @Conguration注解

@Configuration 是 Spring 中的一个注解,用于标记一个类为配置类。它告诉 Spring 容器:这个类包含一个或多个 @Bean 注解的方法,这些方法将返回需要被 Spring 容器管理的对象(即 Bean)。Spring 会处理这个类,执行其 @Bean 方法,并将返回的对象注册到 IoC 容器中,以便后续依赖注入和使用

​ 相当于传统的 XML 配置文件(如 applicationContext.xml),只不过现在用 Java 类来编写配置,类型安全、易于调试和重构。

4.3 MVC分层注解

​ 用 @Bean 手动注册组件太麻烦,所以 Spring 提供了一些特殊类的快速注册。注:分层注解能起作用的前提是:这些类必须在主程序所在的包及其子包的程序下。如果非要放到外部包,在主程序上加个ComponentScan实现批量扫描。(但最好还是老老实实的在主程序所在包下面玩)

1. @Controller

​ 用于标记控制层(Controller)的类,表示该类是 Spring MVC 中的控制器组件,负责处理 HTTP 请求。被此注解标记的类会被 Spring 容器自动扫描并注册为 Bean。

2. @Service

​ 用于标记业务逻辑层(Service)的类,表示该类是服务组件,通常包含核心业务逻辑。它是一种特殊的 @Component,语义更明确,便于分层管理。

3. @Repository

​ 用于标记数据访问层(DAO/Repository)的类,表示该类负责与数据库进行交互。该注解还具有自动翻译数据库异常的能力(如将 SQLException 转为 Spring 的数据访问异常),使数据层异常处理更统一。

4.4 注册第三方组件

​ 有两个方法,一是直接用@Bean注解去手动的new一个对象并返回,二是@Import注解去注册第三方类对象。

4.5 @Scope

@Scope 是 Spring 中的一个注解,用于指定由 @Component@Bean 等注解创建的 Bean 实例在 Spring 容器中的作用域(即生命周期和可见范围)。具体而言,它是用来控制一个 Bean 是单例的、每次请求都新建的,还是与 Web 中的请求、会话等绑定的。

常见取值
  • singleton(默认):在整个 Spring 容器中,该 Bean 只有一个共享的实例。
  • prototype:每次请求该 Bean 时(如注入或调用 getBean()),都会创建一个新的实例。
  • request:每个 HTTP 请求创建一个实例,仅适用于 Web 应用。
  • session:每个 HTTP Session 创建一个实例,仅适用于 Web 应用。
  • application:每个 ServletContext 生命周期内创建一个实例,仅适用于 Web 应用。

4.6 @Lazy

​ 它指示 Spring 容器在启动时不立即创建被标记的 Bean,而是等到该 Bean 第一次被实际使用(如注入、调用 getBean())时才进行初始化。

​ 默认情况下,Spring 容器在启动时会预初始化所有 singleton 作用域的 Bean。@Lazy 可用于类上(配合 @Component 等)或方法上(配合 @Bean)。

​ prototype作用域的 Bean 本身就是懒加载的,每次使用都会新建。懒加载的 Bean 在第一次使用时会有初始化开销,可能影响响应时间。

4.7 FactoryBean接口

FactoryBean 是 Spring 提供的一个接口,用于自定义复杂对象的创建过程

​ 当某个 Bean 的实例化过程比较复杂(例如需要动态代理、组合多个对象、读取配置生成等),无法通过简单的构造函数或工厂方法完成时,可以通过实现 FactoryBean 接口来精确控制 Bean 的创建逻辑。

- 实现 `FactoryBean` 的类本身也是一个 Bean,会被 Spring 容器管理。
- 从容器中获取该 Bean 时,默认返回的是 `getObject()` 方法的返回值,而不是 `FactoryBean` 实例本身。
- 若想获取 `FactoryBean` 本身,需在 Bean 名称前加 `&`,如 `context.getBean("&myFactoryBean")`。

4.8 @Conditional 条件注册

@Conditional 是 Spring 中的一个注解,用于实现条件化注册 Bean。它可以根据指定的条件来决定是否将某个 Bean 注册到 Spring 容器中。只有当条件满足时,对应的组件(类或方法)才会被加载和实例化。

1. 核心机制
  • @Conditional 接收一个或多个 Condition 接口的实现类
  • Spring 在解析配置时会先检查这些条件的 matches() 方法返回值。
  • 如果返回 true,则注册 Bean;否则跳过。
2. 衍生注解
  • @ConditionalOnClass:当类路径中存在指定类时才注册。
  • @ConditionalOnMissingBean:当容器中不存在指定类型的 Bean 时才注册。
  • @ConditionalOnProperty:当配置文件中存在特定属性且值符合条件时注册。
  • @ConditionalOnWebApplication:当应用是 Web 环境时注册。
  • @ConditionalOnExpression:根据 SpEL 表达式结果决定是否注册。

5. 注入组件的各种方式

1. @Autowired

@Autowired 是 Spring 提供的注解,用于自动装配 Bean。它可以标记在字段、构造函数、Setter 方法或普通方法上,Spring 容器会自动从 IoC 容器中查找匹配的 Bean 并注入。

装配流程:

​ 1)先按类型找,找到了就注入

​ 2)若按类型没找到或者找到了多个,就按组件的名字来找(就是把当前的对象名看作是组件名,然后去ioc容器找)

​ 3)若未找到匹配的 Bean,默认会抛出异常

匹配规则
- 默认按**类型**(byType)进行匹配。
- 如果同一类型存在多个 Bean,则结合 `@Qualifier` 注解按名称(byName)区分。
- 若未找到匹配的 Bean,默认会抛出异常;可设置 `required = false` 允许为 null。
# 可以自动装配很多
@ToString
@Controller
public class UserController {

    // 自动注入演示
    @Autowired
    UserService userService;


    @Autowired(required = false)
    Person bill;

    @Autowired
    List<Person> arr;

    @Autowired
    Map<String, Person> map;

    @Autowired
    ConfigurableApplicationContext ioc;
}

​ 小拓展:@Qualifier("beanname") 可以指定要注入的组件名称(新版本直接用变量名就可以了) @Primary 就是把某个 @Bean 标记的方法返回的组件标为一个特殊的Bean(默认组件),以后主动注入时如果有多个该类型的组件,就用这个特殊的来注入。

​ 特别的,当我们开启了默认组件后,在想用其他的,就得靠@Qualifier了,此时该属性名不能实现组件的切换了。

2. @Resource

@Resource 是 JSR-250 规范提供的注解,用于实现依赖注入。它并非 Spring 特有,但被 Spring 容器支持。

装配流程:

​ 1)默认按名称(byName)进行匹配。即使用字段名或方法名作为 Bean 的名称去查找。

​ 2)如果找不到对应名称的 Bean,则退回到按类型(byType)匹配。

​ 3)可通过 name 属性显式指定要注入的 Bean 名称:@Resource(name = "userService")

与 @Autowired 的区别

​ 1)@Resource 是 Java 标准注解,@Autowired 是 Spring 特有。

​ 2)@Resource 优先按名称注入,@Autowired 默认按类型注入。

​ 3)@Resource 不支持 required = false 语法。

3. 构造器注入(无注解)

概念:在 Spring 配置类或组件中,通过构造函数的参数来接收依赖对象的注入方式。当类中只有一个构造函数时,Spring 会自动将其参数作为依赖进行注入,无需添加 @Autowired 注解。

作用:实现依赖的强制注入,确保对象创建时所需的所有依赖都已提供,从而保证对象的完整性与不可变性。

特点:

​ 1)无需注解:Spring 4.3+ 版本开始,若类中只有一个构造函数,可省略 @Autowired

​ 2)推荐使用:构造器注入是 Spring 官方推荐的依赖注入方式,有利于实现不可变对象,便于单元测试。

​ 3)依赖明确:所有必需的依赖都在构造函数中声明,代码可读性强。

4. Setter 方法注入

​ 通过类的 setter 方法将依赖对象注入到 Bean 中。通常配合 @Autowired 注解使用,Spring 容器在实例化对象后,会自动调用对应的 setter 方法完成依赖注入。

5. AwareXXX 感知接口

​ Aware 接口是 Spring 提供的一系列以 Aware 结尾的接口,用于让 Bean 感知并获取 Spring 容器或其内部组件的底层资源

​ 作用:当一个 Bean 实现了某个 Aware 接口后,Spring 容器会在初始化该 Bean 时,自动调用其提供的回调方法,将对应的运行时对象注入进去,从而让 Bean 能够访问容器的内部服务

// 常见 Aware 接口
- `ApplicationContextAware`:获取 `ApplicationContext` 容器本身
- `BeanFactoryAware`:获取 `BeanFactory` 工厂实例
- `BeanNameAware`:获取当前 Bean 在容器中的名称
- `EnvironmentAware`:获取 `Environment` 对象用于读取配置环境 profileproperties)。
- `ResourceLoaderAware`:获取 `ResourceLoader`,用于加载资源文件
- `ApplicationEventPublisherAware`:获取事件发布器用于发布自定义事件

6. @Value

@Value 是 Spring 提供的一个注解,用于将外部值注入到 Bean 的字段(非组件的字段,组件可以通过Autowired自动注入)或方法参数中。

​ 作用:实现配置属性、字面量、SpEL 表达式的注入,使代码可以动态获取配置信息,而不必硬编码。

​ 注入方式:

​ 1)直接@Value(字面量)

​ 2)从配置文件中获取数据:@Value("${key}")

@Value("${app.name}")
private String appName;

​ 3)SpEL(Spring Expression Language)表达式

@Value("#{systemProperties['user.home']}")
private String userHome;

@Value("#{T(java.lang.Math).random() * 100}")
private double randomValue;

​ 综合演示:

public class Cat {
    @Value("Tom")
    private String name;
    @Value("12")
    private Integer age;
    @Value("${cat.color}")
    private String color;
    @Value("#{T(java.util.UUID).randomUUID().toString()}")
    private String id;

}

7. @PropertySource

@PropertySource 是 Spring 提供的一个注解,用于将外部的 properties 配置文件加载到 Spring 的 Environment 抽象中。

@PropertySource("classpath:app.properties")

​ 补充知识:类路径找资源方式

​ classpath: 代表自己当前项目的类路径(java以及resources)

​ classpath*: 从所有包的类路径下面找

​ 在Spring中,提供了资源获取工具类:ResouceUtils 来获取文

8. @Profile

​ 实际也是 @Conditional 的一种,场景:当我们有多个数据源,也就是开发、测试、生成这三套,如果我们用 @Primary 或者@Qualifier,则需要修改代码,但用 @Profile 可以输入一个环境表示,从而根据不同的环境表示来注册组件。

​ 具体使用:

​ 1)先定义环境表示,默认是 ”default“ :自定义方法就是去资源文件下面定义。 spring.profiles.active=生产环境

​ 2)使用 @Profile 来实现条件注册

6. 组件生命周期

​ Spring 容器中每个 Bean 都有其完整的生命周期,从创建、初始化、使用到销毁。Spring 提供了多种机制让开发者可以在特定生命周期阶段插入自定义逻辑。

​ 第一种:通过 @Bean 注解去指定 init 以及 destory。

​ 第二种:去实现 InitializingBean 以及 DisposableBean

​ 具体生命周期如下:

​ 1、实例化(Instantiation) :Spring 容器根据配置或注解创建 Bean 的实例(通过构造函数或工厂方法)。

​ 2、属性赋值(Populate Properties):容器将配置中定义的属性值或依赖对象注入到 Bean 中(如通过 @Autowired@Value 等)。

​ 3、初始化(Initialization)

   - 补充: 调用后置处理器的前置操作
   - 调用 `@PostConstruct` 注解的方法。
   - 调用 `InitializingBean` 的 `afterPropertiesSet()` 方法。
   - 调用自定义的初始化方法(通过 `@Bean(initMethod = "init")` 指定)。
   - 应用 BeanPostProcessor 的 `postProcessBeforeInitialization` 和 `postProcessAfterInitialization`。

​ 4、使用(In Use):Bean 已准备好,可以被其他组件使用或对外提供服务。

​ 5、销毁(Destruction)

   - 调用 `@PreDestroy` 注解的方法。
   - 调用 `DisposableBean` 的 `destroy()` 方法。
   - 调用自定义的销毁方法(通过 `@Bean(destroyMethod = "cleanup")` 指定)。
   - 仅适用于 singleton 作用域的 Bean,在容器关闭时执行。

7. BeanPostProcessor

​ 之前 Bean 的生命周期其实只是在人家初始化完,或者销毁前让你做点其他的,而不能影响这个过程。

​ 而 BeanPostProcessor 能够在生命周期过程中正在的修改这个 Bean。

二、AOP ⭐

​ 概念:Aspect Oriented Programming(AOP)是 Spring 框架的重要特性之一,它允许开发者将横切关注点(如日志、事务、安全、缓存等)与核心业务逻辑分离。

​ 本质:AOP 的底层实现原理确实是代理模式。Spring 在运行时为目标对象创建一个代理对象,当调用目标方法时,实际执行的是代理对象的方法。代理对象可以在目标方法执行的前后,甚至环绕执行自定义逻辑,从而实现功能增强。

1. AOP 编码实现

​ 1)导入 AOP 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

​ 2)编写切面 Aspect 注:记得用 @Aspect 来标注切面类

​ 3)编写通知方式

​ 4)指明这些通知方法何时何地运行(切入点表达式) 注:可以将这些表达式抽离出来

    // 切入点表达式抽离
    @Pointcut("execution(* person.liutianba.spring02Aop.calculator.MathCal.*(..))")
    void pointCut(){}

    @Before("pointCut()")
    public void logStart(JoinPoint joinPoint){
        System.out.println("【切面 - 日志】开始... 方法名:" + joinPoint.getSignature().getName() + "参数为:" + Arrays.asList(joinPoint.getArgs()).toString());
    }

​ 5)测试AOP动态织入

@Aspect  // 切面类
@Component
public class LogAspect {

    /**
     * 告诉 Spring,以下通知方法何时何地运行
     * 何时:
     * @Before,
     * @After(不管正常异常,都运行),
     * @AfterReturning(正常返回结果),
     * @AfterThrowing(出现异常),
     * @Around
     *
     * 何地:切入点表达式
     *   execution(方法的全签名)
     *      全写法 :public int 全类名.方法名(参数列表) throws 异常
     *      省略写法:int 方法名(参数列表) int *(int, int)
     *      * 表示任意长度字符串
     *
     */

    @Before("execution(int *(int,int))")
    public void logStart(){
        System.out.println("【切面 - 日志】开始...");
    }
    @After("execution(int *(int,int))")
    public void logEnd(){
        System.out.println("【切面 - 日志】结束...");
    }

    @AfterThrowing("execution(int*(int, int))")
    public void logException(){
        System.out.println("【切面 - 日志】异常...");
    }


    @AfterReturning(pointcut = "execution(int *(int, int))", returning = "result")
    public void logReturn(Object result){
        System.out.println("【切面 - 日志】结果:" + result);
    }
}

2. AOP 切入点表示式写法

​ 切入点(Pointcut)用于定义 哪些方法需要被 AOP 增强。它是通过一个表达式来匹配类和方法的,只有匹配成功的方法才会执行通知(Advice)。

1. excution(方法签名)

​ 切入某个接口下的所有方法:execution(* person.liutianba.spring02Aop.calculator.MathCal. ***(..))

​ 注:只要有了切面类再切入,那么这个类的实例在容器中就不再是原本的类型了,而是CGLIB产生的一个代理对象。

 /**
     * 告诉 Spring,以下通知方法何时何地运行
     * 何时:
     * @Before,
     * @After(不管正常异常,都运行),
     * @AfterReturning(正常返回结果),
     * @AfterThrowing(出现异常),
     * @Around
     *
     * 何地:切入点表达式
     *   execution(方法的全签名)
     *      全写法 :public int 全类名.方法名(参数列表) throws 异常
     *      省略写法:int 方法名(参数列表) int *(int, int)
     *      *:表示任意长度字符串
     *      ..:
     *       1)参数位置:表示多个参数,任意类型
     *       2)类型位置:表示多个层级  

     *   @annotation(com.spzx.common.security.annotation.RequiresLogin) || "  
        + "@annotation(com.spzx.common.security.annotation.RequiresPermissions)
     *  
     *  
     *  
*/  
2. args

​ 匹配那些参数列表一直的所有方法

3. @args

​ 匹配参数列表有特定注解的方法

3. 获取 AOP 的连接点的信息

​ 直接传一个连接点,然后通过它获得信息。

​ 一般就是通过getSignature()获得方法签名,然后通过方法签名就可以获得该方法的详细信息。

@Before("execution(* person.liutianba.spring02Aop.calculator.MathCal.*(..))")
public void logStart(JoinPoint joinPoint){
    System.out.println("【切面 - 日志】开始... 方法名:" + joinPoint.getSignature().getName());
}

4. 多切面执行执行顺序

​ 我的理解:单个切面实际就是把目标方法做了个代理,当有其他切面时,它会把代理后的方法看成是一个要代理的目标方法,然后去代理,所以,多个切面的执行顺序是由谁先代理,谁后代理决定的。

​ 注:默认代理顺序是按类名自然排序的,但也可以通过 @Order 注解手动指定。@Order 值越小,优先级越高,在代理链中位于外层。

image-20251024100006963

5. 环绕通知 @Around 重点!!!

@Before@After 等通知属于“感知型”增强,只能在方法执行前后执行额外逻辑,无法控制目标方法的执行流程(如跳过方法、修改参数、改变返回值)。而 @Around 是最强大的通知类型,它完全包裹目标方法,可以:

  • 控制是否执行目标方法
  • 修改方法参数
  • 修改返回值
  • 捕获并处理异常 (这里建议抛出异常,如果不抛出的话,出了异常别人都感知不到)
  • 短路返回(不调用目标方法)
5.1 固定写法
@Aspect
@Component
public class LogAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法签名
        Signature signature = joinPoint.getSignature();
        String methodName = signature.getName();

        // 获取方法参数
        Object[] args = joinPoint.getArgs();

        System.out.println("【环绕前置】方法 " + methodName + " 开始执行,参数:" + Arrays.toString(args));

        Object result = null;
        try {
            // -----------------------------
            // 核心决定是否继续执行目标方法
            // 可在此处修改参数
            // args[0] = "modifiedArg";
            result = joinPoint.proceed(args); // 执行目标方法可传入修改后的参数
            // -----------------------------

            System.out.println("【环绕返回】方法 " + methodName + " 执行成功,返回值:" + result);
            return result; // 可在此处修改返回值
        } catch (Exception e) {
            System.out.println("【环绕异常】方法 " + methodName + " 执行出错:" + e.getMessage());
            throw e; // 可以处理或包装异常
        } finally {
            System.out.println("【环绕后置】方法 " + methodName + " 执行结束");
        }
    }
}

6. AOP 的应用场景

pass(懒得写了)

三、事务⭐

​ Spring 中的事务是对数据库事务的抽象和管理机制,其核心目标是:让开发者无需手动管理 commit 和 rollback,就能确保一组数据库操作具备 ACID 特性

1、Spring 连接数据库的方法

​ 1)导入依赖:数据库驱动、spring-boot-starter- data-jdbc

​ 2)配置数据库连接信息:在 xxx.properties 中的 spring.datasource

​ 3)可以直接使用 DataSource、 JDBCTemplate

2、开启事务管理

​ 1)在启动类上加一个注解:@EnableTransactionManagement 指明开启事务管理

​ 2)之后在需要事务管理的方法上添加一个 @Transactional 即可进行事务管理

3、事务管理的原理

​ Spring 的事务管理是基于 AOP(面向切面编程) + 拦截器模式 实现的,其核心原理是通过代理机制,在目标方法执行前后自动控制数据库事务的开启、提交或回滚。

3.1 动态代理

当一个方法被 @Transactional 注解标记时:

  • Spring 容器会为该 Bean 创建一个代理对象(Proxy)。
  • 原始对象的方法调用会被代理拦截。
  • 代理在方法执行前开启事务,执行后根据结果决定提交或回滚
3.2 关键组件
组件 作用
PlatformTransactionManager 事务管理器接口,负责实际的事务操作(如 DataSourceTransactionManager)。
@Transactional 声明式事务注解,定义事务属性(传播行为、隔离级别等)。
TransactionInterceptor 事务拦截器,AOP 的通知逻辑,真正执行“开启 → 执行 → 提交/回滚”的流程。
TransactionSynchronizationManager 绑定当前线程与事务资源(如 Connection),保证同一个事务中使用同一个数据库连接。
3.3 执行流程
1. 调用 @Transactional 方法
2. 代理拦截(TransactionInterceptor)
3. 获取事务管理器(PlatformTransactionManager)
4. 开启事务(doBegin) → 获取 Connection 并绑定到当前线程
5. 执行业务方法(所有 DAO 操作共享同一 Connection)
6. 方法正常结束 → 提交事务
7. 异常抛出 → 回滚事务(默认 RuntimeException 和 Error)
8. 清理资源(解绑 Connection)

4、@Transactional

@Transactional 是 Spring 框架提供的声明式事务管理注解,开发者只需在方法或类上添加该注解,即可自动获得事务支持,无需手动编写 commitrollback 代码。

4.1 该注解的属性

​ timeout:当事务超过这个事件后,就会回滚。

​ readOnly:只读优化(优化操作,就不需要开启一堆事务管理相关的东西了)

​ rollbackFor:Defines zero (0) or more exception types, which must be subclasses of Throwable, indicating which exception types must cause a transaction rollback.

​ 注:运行时异常都回滚,而编译时异常不回滚。而rollbackFor就是用来指定那些异常需要回滚

​ 下面,重点属性单独介绍

4.2 隔离级别 isolation

隔离性(Isolation) 是事务 ACID 特性中的 "I",它控制的是:多个事务并发执行时,彼此之间的可见性和影响程度。如果完全不隔离 → 数据混乱。如果完全隔离 → 性能极差,几乎串行执行。所以,数据库提供了 不同的隔离级别,在数据一致性并发性能之间做权衡。

一些并发问题介绍:

问题 定义 示例
脏读(Dirty Read) 一个事务读取了另一个事务尚未提交的数据。 事务 A 修改了数据但未提交,事务 B 读到了这个修改;如果 A 回滚,B 读到的就是“脏”数据。
不可重复读(Non-repeatable Read) 在同一个事务中两次读取同一行数据,结果不同 事务 A 第一次读 id=1 的记录为 name='张三';事务 B 将其改为 '李四' 并提交;A 再次读取,变成了 '李四'。
幻读(Phantom Read) 在同一个事务中两次执行相同查询,返回的行数不同(出现了“幻影”行)。 事务 A 查询 age > 18 有 3 条记录;事务 B 插入一条 age=20 的记录并提交;A 再次查询,变成 4 条。

隔离级别:

隔离级别 脏读 不可重复读 幻读 说明
READ UNCOMMITTED(读未提交) ✅ 允许 ✅ 允许 ✅ 允许 最低级别,性能最好,但数据最不安全。
READ COMMITTED(读已提交) ❌ 禁止 ✅ 允许 ✅ 允许 大多数数据库默认级别(如 Oracle、SQL Server)。只能读已提交的数据。
REPEATABLE READ(可重复读) ❌ 禁止 ❌ 禁止 ✅ 允许(部分数据库不允许) MySQL 默认级别。保证事务内多次读取同一数据结果一致。
SERIALIZABLE(串行化) ❌ 禁止 ❌ 禁止 ❌ 禁止 最高级别,事务串行执行,避免所有并发问题,但性能最差。

4.3 传播行为 propagation

传播行为(Propagation Behavior) 是指:当一个事务方法被另一个事务方法调用时,事务该如何进行传播或处理。换句话说: "我已经在一个事务里了,现在又调用了另一个标有 @Transactional 的方法,那这个新方法是加入当前事务?新建一个事务?还是不使用事务?”

​ 这就是 事务的传播行为 要解决的问题。

类比理解

想象你正在做一顿饭(外层事务),过程中需要洗菜、切菜、炒菜。

- 如果“洗菜”也是一个独立的任务(内层方法),但它必须和做饭在同一个流程中完成 → 那它就**加入当前事务**。
- 如果“接个电话”是一个操作,它不影响做饭 → 它可以**不参与事务**。
- 如果“尝一口咸淡”失败了,你想回滚整个做饭过程 → 那它就和外层事务**绑定在一起**。
- 如果“烧一壶水”是另一个独立任务,即使它失败也不影响做饭 → 它应该**开启一个新事务**。

这就是不同传播行为的现实映射。


Spring 中的 7 种传播行为

| 传播行为 | 含义 |
|--------|------|
| `REQUIRED`(默认) | 如果当前有事务,就加入;没有就新建一个。 |
| `REQUIRES_NEW` | 无论当前是否有事务,都**挂起当前事务,新建一个新事务**。 |
| `SUPPORTS` | 支持当前事务,但如果当前没有事务,也不创建新事务(以非事务方式运行)。 |
| `NOT_SUPPORTED` | 不支持事务,**以非事务方式执行**,如果当前有事务则将其挂起。 |
| `MANDATORY` | 必须在事务中运行,如果当前没有事务,则抛出异常。 |
| `NEVER` | 不能在事务中运行,如果当前有事务,则抛出异常。 |
| `NESTED` | 如果当前有事务,则在嵌套事务中执行;否则新建一个事务。(类似 `REQUIRED`,但可独立回滚) 

四、SpringMvc⭐⭐

1. RequestMapping

​ 路径映射:负责映射方法与请求路径。

​ 路径位置通配符:*匹配任意多字符 ** 匹配任意多层路径 ?匹配单个字符 如果有多个方法都能匹配上请求路径,则按着精确度来处理,也就是越精确,越优先。

请求限定:

​ 1)method

​ 2)params

​ 3)headers

​ 4)consumes

​ 5)produces

2. SpringMvc 中的请求处理

1、接收普通请求参数(?key=value

​ 适用于 GET 请求的查询参数 或 POST 请求的表单数据。直接绑定或者用 @RequestParam 注解

2、接受路径变量(RESTful 风格)

@GetMapping("/user/{id}")
public String getUserById(@PathVariable Long id) {
    System.out.println("用户ID:" + id);
    return "user";
}               

3、接收请求体(JSON/XML)数据

@PostMapping("/user")
public String createUser(@RequestBody User user) {
    System.out.println("创建用户:" + user.getName());
    return "created";
}
  • 客户端设置 Content-Type: application/json
  • 后端有 Jackson/Gson 等 JSON 转换器(Spring Boot 默认集成)

4、接收文件上传

@PostMapping("/upload")
public String uploadFile(@RequestPart("file") MultipartFile file,
                         @RequestParam String description) {
    System.out.println("文件名:" + file.getOriginalFilename());
    System.out.println("描述:" + description);
    return "uploaded";
}

5、自动绑定对象(表单或 JSON)

public class User {
    private String name;
    private int age;
    // getter/setter
}

@PostMapping("/user/form")
public String createUserForm(User user) {
    System.out.println("用户:" + user.getName());
    return "success";
}
  • 表单提交 → 使用 User user
  • JSON 提交 → 必须加 @RequestBody

6、接收请求头(Header)信息

@GetMapping("/info")
public String getInfo(@RequestHeader("User-Agent") String userAgent,
                      @RequestHeader("Authorization") String token) {
    System.out.println("浏览器:" + userAgent);
    System.out.println("Token:" + token);
    return "info";
}

7. 接收 Cookies

@GetMapping("/cookie")
public String getCookie(@CookieValue("JSESSIONID") String sessionId) {
    System.out.println("Session ID:" + sessionId);
    return "cookie";
}

8、获取整个http请求

    /**
     *
     * @param httpEntity 它的泛型就是要把 http 请求体转为这个泛型
     * @return
     */
    @PostMapping("/test03")
    public String test03(HttpEntity<User> httpEntity){
        // 获得请求头
        httpEntity.getHeaders().forEach((n, v) -> System.out.println(n + ":" + v));
        // 获得请求参数
        System.out.println("user = " +  httpEntity.getBody());
        return "ok_test03";
    }

3. SpringMvc 中的响应处理

1. 返回json

​ 注解 @ResponseBody 能够将方法的返回值自动的封装为json,现在都用组合注解 @RestController

2. 文件下载(返回的数据是让浏览器下载的)

spring8.png

  @RequestMapping("/test03")
    public ResponseEntity<byte[]> download() throws Exception{
        FileInputStream fileInputStream = new FileInputStream("E:\\school_work\\glories\\头像\\2.jpg");

        byte[] alls = fileInputStream.readAllBytes();

        return ResponseEntity.ok().contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(alls.length)
                .header("Content-Disposition", "attachment;filename=2.jpg")
                .body(alls);
    }

问题1:文件名如果是中文会乱码

问题2:文件太大会出现oom

@RequestMapping("/test03")
    public ResponseEntity<InputStreamResource> download() throws Exception{

        FileInputStream fileInputStream = new FileInputStream("E:\\school_work\\glories\\头像\\2.jpg");
        // 一口气读会异常
//        byte[] alls = fileInputStream.readAllBytes();
        InputStreamResource resource = new InputStreamResource(fileInputStream)
        // 解决中文乱码:把中文转码成utf-8
        String name = URLEncoder.encode("哈哈.jpg", "utf-8");

        return ResponseEntity.ok().contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(fileInputStream.available())
                .header("Content-Disposition", "attachment;filename="+ name)
                .body(resource);
    }

五、Spring Mvc 的最佳实践!!!⭐⭐⭐

1. RESTful风格⭐⭐⭐

​ REST 就是把任何web请求看作是对资源的操作,对同一资源的不同操作,他们的请求路径是一样的,具体是通过不同的动作来区分不同处理方法的。

2. 拦截器⭐⭐

1. 概念

拦截器(Interceptor) 是 Spring MVC 提供的一种机制,用于在 请求处理的前后 对请求进行预处理或后处理,而无需修改每个控制器的代码。它类似于 Servlet 中的 Filter(过滤器),但更贴近 Spring MVC 的执行流程,主要用于:

  • 权限校验(如登录检查)
  • 日志记录
  • 性能监控(统计请求耗时)
  • 请求参数预处理
  • 防止重复提交
  • 编码设置等
2. 执行时机(与请求生命周期的关系)

​ 请求 → preHandle() → (目标方法 execute) → postHandle() → (视图渲染) → afterCompletion()

3. 三个核心方法
方法 触发时机 返回值含义
boolean preHandle(HttpServletRequest, HttpServletResponse, Object handler) 控制器方法执行前 true:放行,继续执行;false:中断,不再执行后续操作
void postHandle(HttpServletRequest, HttpServletResponse, Object handler, ModelAndView modelAndView) 控制器方法执行后,视图渲染前 通常用于修改 Model 或 View
void afterCompletion(HttpServletRequest, HttpServletResponse, Object handler, Exception ex) 整个请求完成之后(视图渲染完毕) 用于资源清理、异常处理等

⚠️ 注意: - postHandleafterCompletion 只有在 preHandle 返回 true 时才会执行。 - 如果 preHandle 返回 false,则后续都不会执行。


4. 实现方式

​ Spring MVC 中实现拦截器有两种主要方式:第二种过时了,就不记录了。

✅ 方式一:实现 HandlerInterceptor 接口(推荐),

import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 模拟登录检查
        Object user = request.getSession().getAttribute("user");
        if (user == null) {
            response.sendRedirect("/login"); // 未登录,跳转到登录页
            return false; // 中断请求
        }
        return true; // 放行
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 可以修改 modelAndView,比如添加公共数据
        System.out.println("postHandle: 控制器执行完毕");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 请求结束,可用于清理资源
        System.out.println("afterCompletion: 请求已完成");
    }
}

​ 当写完自定义的拦截器后,要把它注册到 Spring MVC 的拦截器链中。(通过 WebMvcConfigurer

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/admin/**")        // 拦截哪些路径
                .excludePathPatterns("/login", "/css/**", "/js/**"); // 放行哪些路径
    }
}   
5. Interceptor 的执行顺序

​ preHandle顺序执行,postHandle, afterCompletation 倒序执行,有一个拦截器不放行,所有 postHandle 都不能执行, 那些放行的拦截器的 afterCompletation 可以执行。

6. Interceptor 与 Fliter 的区别

Interceptor(拦截器)和 Filter(过滤器)都用于对请求进行预处理,但 Filter 是 Servlet 规范的一部分,作用于整个 Web 请求生命周期,能拦截所有请求(包括静态资源),且不依赖 Spring 容器;而 Interceptor 是 Spring MVC 提供的机制,只拦截控制器(Controller)相关的请求,可以方便地注入 Spring Bean,执行时机更精细(如 preHandle、postHandle),更适合在业务逻辑层进行权限校验、日志记录等操作

3. 异常处理⭐⭐⭐

​ 异常处理分为两类:编程式(如 try-catch)和 声明式。在 Spring 中,推荐使用 声明式异常处理,因为它更简洁、可维护性强,能实现全局统一处理。

1. 实现方式

Spring MVC 提供了两个核心注解来实现声明式异常处理:

方式一:@ExceptionHandler —— 处理单个 Controller 的异常

适用于某个控制器内部的异常处理。

@RestController
public class UserController {

    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id) {
        if (id == -1) {
            throw new UserNotFoundException("用户不存在");
        }
        return userService.findById(id);
    }

    // 使用 @ExceptionHandler 处理该 Controller 内部抛出的异常
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body("用户未找到:" + ex.getMessage());
    }
}
  • 只作用于当前 Controller
  • 可以处理特定类型的异常
  • 不影响其他 Controller
方式二:@ControllerAdvice —— 全局异常处理

用于集中处理 所有 Controller 的异常,是实现“全局统一异常处理”的最佳方式。

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body("用户未找到:" + ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGenericException(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("服务器内部错误:" + ex.getMessage());
    }
}
  • 使用 @ControllerAdvice 注解标记类
  • 所有 Controller 都会自动应用该异常处理器
  • 可以处理任意类型异常(如 Exception.class
  • 支持返回 JSON、视图页面等

2. 项目中最佳实践

​ 在实际项目开发中,异常种类繁多,无法穷尽所有情况。因此,后端应聚焦于核心业务逻辑的正确实现,当遇到非法状态、数据校验失败、资源不存在等异常情况时,无需手动处理每一个异常,而是通过主动抛出有意义的异常,提前终止当前业务流程。

​ 这些异常由全局异常处理器(如 @ControllerAdvice + @ExceptionHandler)统一捕获,转换为标准的错误响应格式(如 JSON 错误码、消息提示),返回给前端。这样既保证了代码的简洁性和可维护性,又能让前端清晰感知到异常类型和原因,进而做出相应的用户提示或重试操作。

实现方式:

​ 1)先自定义一个业务异常类,属性有code,mes

​ 2)再去用枚举类列出系统中所有可能的业务错误

​ 3)当出现业务错误时,直接抛出业务异常即可

​ 4)之后就可以交给全局异常处理器或者是本类下的异常处理器处理了

4. 数据校验⭐⭐⭐

​ 1) 导入场景启动器

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

​ 2)编写校验注解 @NotNull, @Max, @Min, @Email ...

​ 3)告诉 spring MVC 开启校验 @valid

 public R add(@RequestBody @Valid Employee employee, BindingResult bindingResult)

​ 4)获得校验结果:BindResult

 public R add(@RequestBody @Valid Employee employee, BindingResult bindingResult){
        if (!bindingResult.hasErrors()){
            return R.success();
        }
        HashMap<String, String> errorMap = new HashMap<>();
        bindingResult.getFieldErrors().forEach(fieldError -> {
            errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
        });

        return R.error(500, "校验失败", errorMap);
    }
1. BindResult

​ 它是用来去获得校验结果的。可以获得校验的字段以及校验的结果。

2. 全局异常处理校验失败
 // 处理校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        // 获得校验结果
        BindingResult bindingResult = e.getBindingResult();
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        HashMap<String, String>data = new HashMap<>();
        fieldErrors.forEach(fieldError -> {
            data.put(fieldError.getField(), fieldError.getDefaultMessage());
        });

        return R.error(500, "校验失败", data);
    }
3. 自定义校验器

​ 1)正则表达式

@Pattern(regexp="^男|女&" , message = "性别只能是男或女")

​ 2)自定义注解(写一个自定义注解,然后绑定一个自定义校验器)

==========================================================================================
1自定义校验注解抄源码即可
@Documented
@Constraint(
        validatedBy = {GenderValidator.class}
)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)

public @interface Gender {
    String message() default "{jakarta.validation.constraints.Email.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};


}
===================================================================================
2自定义校验器实现ConstraintValidator接口
public class GenderValidator implements ConstraintValidator<Gender, String> {
    /**
     *
     * @param s 前端传来的属性
     * @param constraintValidatorContext 校验上下文
     * @return
     */
    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        return s.equals("男") || s.equals("女");
    }
}
===================================================================================
3应用自己的校验器
@Constraint(
        validatedBy = {GenderValidator.class}
)
4 错误信息提示(国际化,其实没啥用)

​ 国际化是通过配置文件来完成的

​ 1)配置提示信息的编码方式以及位置(默认是messages)

spring.messages.encoding=UTF-8
spring.messages.basename=i18n/messages_en_US

​ 2)直接在不同国家的配置文件下写就完事了

# messages_en_US.properties下
gender.message=gender is allowed only for male or female
# messages_zh_CH.properties下
gender.message=性别只能为男或女
# messages
默认环境

5. 各种o的分层模型

​ 在传统的 Java Web 开发中,很多开发者习惯于使用同一个 POJO 类(如 User)既作为数据库实体,又作为接口返回给前端的数据对象。这种做法虽然简单,但存在严重的安全隐患和设计缺陷。例如,数据库实体中通常包含敏感字段,如用户密码、手机号、身份证号等,如果直接将这个对象序列化为 JSON 返回给前端,就可能导致敏感信息泄露。即使你在字段上加了 @JsonIgnore,这种方式也属于“打补丁式”编程,难以维护,且容易遗漏。

​ 更合理的设计是采用分层模型,将不同的职责分离。具体来说,应该为不同场景定义不同的 Java Bean:使用 Entity 类与数据库表映射,用于持久层操作;使用 DTO(Data Transfer Object)类专门用于向前端返回数据;使用 VOForm 类接收前端传来的请求参数。这样,每个类只承担单一职责,系统更加清晰、安全、可维护。

​ 特别是在数据脱敏场景下,这种分层设计的优势尤为明显。比如,用户手机号 13812345678 在返回前端时应显示为 138****5678,身份证号也应部分隐藏。如果使用统一的 POJO,你需要在每个 Controller 中手动处理脱敏逻辑,代码重复且容易出错。而通过 DTO 类,你可以在其静态方法或构造函数中统一完成脱敏转换,例如提供一个 fromEntity() 方法,将 UserEntity 转换为已脱敏的 UserDTO,从而确保所有接口返回的数据都经过统一处理。

​ 此外,使用 DTO 还能有效解耦数据库结构与接口协议。当数据库表结构发生变化时(如字段重命名、拆分),只需调整 Entity 到 DTO 的映射逻辑,而无需修改对外接口,避免影响前端代码。同样,前端需求变化时,也可以灵活调整 DTO 字段,而不必改动数据库设计。这种松耦合结构大大提升了系统的可扩展性和团队协作效率。

​ 为了进一步简化 DTO 与 Entity 之间的转换,可以引入 MapStruct 等映射框架。它通过注解在编译期生成高效的映射代码,避免了手动编写大量 setter 和 getter 的繁琐工作,同时保持高性能。相比反射工具(如 BeanUtils),MapStruct 更安全、更快速。

​ 总之,避免将数据库实体类直接暴露给前端,是构建安全、健壮 Web 应用的基本原则。通过引入 DTO 模式,不仅能实现敏感数据的自动脱敏,还能提升代码的可读性、可维护性和可扩展性。这是一种从“能用”到“好用”的工程思维升级,也是企业级开发中的最佳实践。

6. 日期处理⭐

1. @JsonFormat 的作用

​ 当我们把 Java 对象转换成 JSON 字符串(比如接口返回给前端)时,日期字段默认可能以时间戳(如 1672531200000)的形式输出,或者格式不统一(如 "2023-01-01T00:00:00"),前端难以直接展示。同样,前端传来的日期字符串也可能因为格式不匹配导致解析失败。

@JsonFormat 就是用来指定某个字段在转成 JSON 或从 JSON 解析时所使用的日期格式,确保前后端对日期的理解一致。

2. 基本用法
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;

public class User {

    private String name;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;

    // getter 和 setter
}
3. 全局配置

​ 如果不使用全局配置,那么每一个日期字段都需要去写 @JsonFormat,太麻烦了,尤其是采用 JavaBean 分层架构。此时,就可以用配置文件来实现全局配置。

# 日期处理配置
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

7. 接口文档

8. DispathcerServlet

DispatcherServlet 是 Spring MVC 框架的核心组件,可以把它理解为整个 Web 请求处理流程的“总指挥”或“前端控制器”。当你的浏览器发起一个 HTTP 请求(比如访问 /user/list),这个请求首先到达的就是 DispatcherServlet

1. 九大组件
 @Nullable
    private MultipartResolver multipartResolver;
    @Nullable
    private LocaleResolver localeResolver;
    /** @deprecated */
    @Deprecated
    @Nullable
    private ThemeResolver themeResolver;
    @Nullable
    private List<HandlerMapping> handlerMappings; // 存放请求路径和处理器的映射关系
    @Nullable
    private List<HandlerAdapter> handlerAdapters;  // 处理器的一些反射增强
    @Nullable
    private List<HandlerExceptionResolver> handlerExceptionResolvers;  // 异常处理解析器
    @Nullable
    private RequestToViewNameTranslator viewNameTranslator;
    @Nullable
    private FlashMapManager flashMapManager;
    @Nullable
    private List<ViewResolver> viewResolvers;
2. 运行流程

  1. 客户端(浏览器)发送请求 DispatcherServlet拦截请求.

  2. DispatcherServlet 根据请求消息调用 HandlerMapping 会根据本次请求的 url 找到对应的 handler,并会将请求涉及到的拦截器和 Handler 一起封装返回。

  3. DispatcherServlet 调用 HandlerAdapter适配器执行 Handler

  4. Handler 完成对用户请求的处理后,会返回一个 ModelAndView 对象给DispatcherServletModelAndView 顾名思义,包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View

  5. DispatcherServletModelAndView传给ViewReslover视图解析器

  6. ViewReslover解析后返回具体View

  7. DispaterServlet 把返回的 Model 传给 View(视图渲染)

  8. DispaterServlet 会将视图响应给客户端

六、Mybatis⭐

持久层框架 是用于简化数据库操作、管理数据持久化的软件框架。它的核心作用是 让 Java 对象与数据库中的记录之间进行方便、高效、安全的转换和操作,而不需要开发者手动编写大量重复的 JDBC 代码。

1. mybatis的使用方法

1)导入mybatis依赖

2)配置数据源信息

3)写一个 javaBean,也就是 PO( Persist Object

4)写持久层接口,也就是Mapper层(注意标 @Mapper)

5)去写 Mapper 层方法对应的 xml 配置文件,去映射这些接口怎么执行,执行那些sql。

6)注意数据库字段名下划线一般在 Po 种要写成驼峰式。

7)最后,在配置文件中告诉 Mybatis xml 文件的位置。

2. 参数处理

1. #{} 与 ${} 的区别

在 MyBatis 中,#{}${} 都可以用于传递参数,但它们的底层机制和使用场景有着本质区别。

1)首先,#{} 是 MyBatis 中的预编译占位符,相当于 JDBC 中的 ?。在执行 SQL 语句时,MyBatis 会先将 #{} 替换为一个问号占位符,然后通过 PreparedStatement 逐个设置参数值。这种方式能够有效防止 SQL 注入,因为参数值不会直接拼接到 SQL 字符串中,而是由数据库在执行阶段安全地绑定。此外,#{} 还能自动根据参数类型添加合适的引号,例如当传入字符串时会自动加上单引号,非常适合用于插入、更新或查询时的条件参数赋值。

2)与之相对,${} 则是 MyBatis 的字符串拼接方式,它在 SQL 语句解析阶段就会被直接替换为实际的参数内容,相当于直接将变量值插入到 SQL 字符串中。这意味着如果参数值来自用户输入,可能会造成 SQL 注入风险。因此,${} 一般不用于值的传递,而是用于那些不能使用 #{} 的场景,比如动态指定表名、列名、排序字段或关键字等位置。需要注意的是,${} 不会自动加引号,若拼接的是字符串类型参数,必须手动添加单引号,否则会导致 SQL 语法错误。

2. 参数取值方法

最佳实践:即使只有一个参数,也用 Param 直接参数名

spring14

3. 返回值

在 MyBatis 中,每一个 Mapper 接口的方法都需要在对应的 XML 映射文件中明确指定查询结果的封装类型,即通过 resultTyperesultMap 属性来定义返回值类型。虽然 MyBatis 内置了一些常用类型的别名(如 intstringmaplist 等),但在实际开发中仍建议使用完整限定类名(即全类名),这样能避免命名冲突,也让 XML 文件更直观、可维护。

1)指定返回类型

每一个 Mapper 层接口对应的 XML 语句块中,都必须指明查询结果要封装成的 Java 类型。通常使用 resultType 属性指定类型名称,例如:

<select id="findUserById" resultType="com.example.pojo.User">
    SELECT * FROM user WHERE id = #{id}
</select>

2)集合类型的返回值

当方法的返回值是集合类型(例如 ListSetMap),在 MyBatis 的配置中只需指定集合中元素的类型即可。MyBatis 会自动将查询结果的多行记录封装为集合。例如:

 // 将结果封装为 Map (集合,set,list同理)
    @MapKey("id")
    Map<Integer, Emp> getMap();

<select id="getMap" resultType="person.liutianba.mybatis.bean.Emp">
        select * from t_emp
</select>

3)复杂返回结果的映射

如果查询结果中字段名与实体类属性名不完全对应,或需要多表联合查询时,则可使用 resultMap 进行更灵活的映射配置,通过 <result><association> 等标签自定义字段与属性之间的对应关系。

<resultMap id="rme" type="person.liutianba.mybatis.bean.Emp">
        <!--主键映射用id-->
        <id column="id" property="id"></id>
        <!--普通属性映射用 result-->
        <result column="age" property="age"></result>
        <result column="emp_name" property="empName"></result>
        <result column="emp_salary" property="empSalary"></result>
    </resultMap>

    <select id="getEmpById" resultMap="rme">
        select * from t_emp where id = #{id}
    </select>

下面是多表查询的一些语法

一对一关系封装:

<resultMap id="orderWithCustomer" type="person.liutianba.mybatis.bean.Order">
        <id property="id" column="id"/>
        <result property="address" column="address"/>
        <result property="amount" column="amount"/>
        <result property="customerId" column="customer_id"/>
        <association property="customer" javaType="person.liutianba.mybatis.bean.Customer">
            <id property="id" column="c_id"/> 
            <result property="customerName" column="customer_name"/>
            <result property="phone" column="phone"/>
        </association>
    </resultMap>

image-20251030110448677

一对多关系封装:(collection)

 <!--演示collection-->
    <resultMap id="CustomerWithOrdersRM" type="person.liutianba.mybatis.bean.Customer">
        <id column="c_id" property="id"></id>
        <result column="customer_name" property="customerName"></result>
        <result column="phone" property="phone"></result>
        <collection property="orders" ofType="person.liutianba.mybatis.bean.Order">
            <id column="id" property="id"></id>
            <result column="address" property="address"></result>
            <result column="amount" property="amount"></result>
            <result column="c_id" property="customerId"></result>
        </collection>
    </resultMap>

3. 分步查询

​ 在需要链表查询的场景下,不仅可以按常规写法,也可以写分布查询,如下面的场景:查询具体订单以及它对应的客户的信息:

​ 1)查询订单

​ 2)根据客户id查询id

<resultMap id="CustomerWithOrdersStepRM" type="person.liutianba.mybatis.bean.Customer">
        <id column="id" property="id"></id>
        <result column="customer_name" property="customerName"></result>
        <result column="phone" property="phone"></result>
        <collection property="orders" select="getOrdersByCustomerId" column="id"></collection>
    </resultMap>


    <select id="getOrdersByCustomerId" resultType="person.liutianba.mybatis.bean.Order">
        select * from t_orders
        where customer_id=#{id}
    </select>

    <select id="getCustomerByIdWithOrders" resultMap="CustomerWithOrdersStepRM">
        SELECT *
        FROM t_customer
        WHERE id=#{id}
    </select>

​ 在分页查询的情况下,我们可以开启延期加载,也就是如果没用到需要调用其他查询方法才能查到的数据,就不发这个查询。

mybatis.configuration.lazy-loading-enabled=true
mybatis.configuration.aggressive-lazy-loading=false

4. 动态sql

4.1 if 标签

用于判断某个条件是否成立,决定是否拼接某段 SQL。

<if test="条件表达式">
    SQL 片段
</if>
4.2 <where> 标签

自动处理 WHERE 关键字,并智能去除多余的 ANDOR

<select id="findEmp" parameterType="EmpPO" resultType="EmpPO">
    SELECT * FROM t_emp
    <where>
        <if test="id != null">
            AND id = #{id}
        </if>
        <if test="empName != null and empName != ''">
            AND emp_name = #{empName}
        </if>
        <if test="age != null">
            AND age >= #{age}
        </if>
    </where>
</select>
4.3 <set>标签

用于 UPDATE 语句中,自动处理 SET 关键字,并去除最后一个多余的逗号。

<update id="updateEmp" parameterType="EmpPO">
    UPDATE t_emp
    <set>
        <if test="empName != null and empName != ''">
            emp_name = #{empName},
        </if>
        <if test="age != null">
            age = #{age},
        </if>
        <if test="empSalary != null">
            emp_salary = #{empSalary}
        </if>
    </set>
    WHERE id = #{id}
</update>
4.4 <trim> 实现 <where> 的功能

<trim> 是更灵活的标签,可以自定义前后缀处理。当标签不为空,就会在这个sql片段前添加一个前缀 prefix 以及后缀 suffix, 同时如果这个sql片段以指定的前缀开始,就覆盖为空串。

<trim prefix="前缀" prefixOverrides="要去除的前缀字符串"
      suffix="后缀" suffixOverrides="要去除的后缀字符串">
    SQL 片段
</trim>


<select id="findEmp" parameterType="EmpPO" resultType="EmpPO">
    SELECT * FROM t_emp
    <trim prefix="WHERE" prefixOverrides="AND |OR ">
        <if test="id != null">
            AND id = #{id}
        </if>
        <if test="empName != null and empName != ''">
            AND emp_name = #{empName}
        </if>
        <if test="age != null">
            AND age >= #{age}
        </if>
    </trim>
</select>
  • prefix="WHERE":如果有内容,加 WHERE 前缀
  • prefixOverrides="AND |OR ":去除第一个多余的 ANDOR
  • 效果和 <where> 完全一样,但更灵活
4.5 <trim> 实现 <set> 的功能
<update id="updateEmp" parameterType="EmpPO">
    UPDATE t_emp
    <trim prefix="SET" suffixOverrides=",">
        <if test="empName != null and empName != ''">
            emp_name = #{empName},
        </if>
        <if test="age != null">
            age = #{age},
        </if>
        <if test="empSalary != null">
            emp_salary = #{empSalary},
        </if>
    </trim>
    WHERE id = #{id}
</update>
4.6 <choose> <when> <otherwise>

​ 当需要从多个条件中选择一个来拼接 SQL 时使用。它保证只执行第一个匹配的条件,其余忽略,常用于“搜索优先级”或“单选条件”。

<choose>
    <when test="条件1">
        SQL 片段1
    </when>
    <when test="条件2">
        SQL 片段2
    </when>
    <otherwise>
        默认 SQL 片段(可选)
    </otherwise>
</choose>
5. foreach 标签

image-20251031092737559

1. foreach实现批量查询

​ 原生语法:where xxx in()

 <select id="getEmpByIds" resultType="person.liutianba.mybatis.bean.Emp">
        select * from t_emp where id in
           <if test="ids != null">
               <foreach collection="ids" item="id" open="(" close=")" separator=",">
                   #{id}
               </foreach>
           </if>
    </select>
2.)foreach实现批量插入

​ 原生语法:values (), (),

<insert id="addEmps">
        insert into t_emp (emp_name, age, emp_salary)
        values 
        <foreach collection="list" item="emp" separator=",">
            (#{emp.empName}, #{emp.age}, #{emp.empSalary})
        </foreach>
    </insert>
3. foreach实现批量修改

没有原生语法,所以需要在一个sql请求发送多个sql,注:需要在数据库连接时开始批处理。

spring.datasource.url=jdbc:mysql://localhost:3306/mybatis-example?allowMultiQueries=true
6. sql 标签

负责把常用的sql片段切出来,然后做一个复用。需要配合 include 标签来使用。

5. 特殊字符的转义

6. Mybatis 的缓存机制

MyBatis 提供了两级缓存机制:一级缓存二级缓存,用于提升数据库查询性能,避免重复执行相同的 SQL。

1. 一级缓存机制 (同一事务共享)

1)第一次执行 select 时,查询数据库,结果放入一级缓存

2)后续执行相同 SQL 和参数时,直接从缓存中取数据,不再查询数据库

3)如果执行了 insertupdatedeletesqlSession.clearCache(),缓存会被清空

2. 二级缓存机制 (所有事务共享)

1)作用范围Mapper 级别(命名空间级别)

2)生命周期:跨 SqlSession,甚至跨事务共享

3)默认关闭:必须手动配置才能启用

  1. 第一个 SqlSession 查询数据后,结果不仅存入一级缓存,还写入二级缓存
  2. 第二个 SqlSession 执行相同查询时,先查二级缓存,命中则直接返回
  3. 当任意 SqlSession 执行 commit() 时,二级缓存才会更新(未提交不更新)
// 第一个会话
SqlSession session1 = sqlSessionFactory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.findById(1);  // 查数据库 → 存入一级 + 二级缓存
session1.close(); // 二级缓存此时已更新(因为 commit)

// 第二个会话
SqlSession session2 = sqlSessionFactory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.findById(1);  // 直接从二级缓存读取!不查数据库
session2.close();

7. Mybatis的四大对象以及拦截器(先了解)

8. 分页插件

image-20251031100003920

使用步骤:

1)引入依赖:PageHelper

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>最新版本</version>
</dependency>

2)注册 PageHelper 插件,可以在注册过程中配置一些属性

@Configuration
public class PageHelperConfig {
    /*
     * 配置分页插件
     */

    @Bean
    public PageInterceptor pageHelper() {
        // 1. 创建插件对象
        PageInterceptor pageHelper = new PageInterceptor();

        // 2. 配置插件对象

        return pageHelper;
    }
}

3)在需要分页的方法之前开始分页

// 开启分页
PageHelper.startPage(pageNum, pageSize);

4)用 PageInfo 去包装分页查询结果,里面有前端要用到的很多数据,比如页面大小,是否是最后一页等等。

// 包装结果
PageInfo<Emp> pageInfo = new PageInfo<>(list);

七、Mybatis plus⭐⭐

MyBatis-Plus 是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

7.1 框架概述

MyBatis-Plus(简称 MP)是一个MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。其突出的特性如下:

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,提供了大量的通用的CRUD方法,因此可以省去大量手写sql的语句的工作。
  • 条件构造器:提供了强大的条件构造器,可以构造各种复杂的查询条件,以应对各种复杂查询。
  • 内置分页插件:配置好插件之后,写分页等同于普通 List 查询,无需关注分页逻辑。

下面通过一个简单案例快速熟悉MyBatis Plus的基本使用

7.2 数据库准备

首先在数据库中准备一张表,为后序的学习做准备。

  1. 创建数据库

在MySQL中创建一个数据库hello_mp

CREATE DATABASE hello_mp CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
  1. 创建表

hello-mp库中创建一个表user

DROP TABLE IF EXISTS user;
CREATE TABLE user
(
    id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT(11) NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
);
  1. 插入数据
INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');
7.3 与SpringBoot集成

Mybatis Plus与SpringBoot的集成十分简单,具体操作如下

  1. 引入Maven 依赖

提前创建好一个SpringBoot项目,然后在项目中引入MyBatis Plus依赖

 <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.6</version>
</dependency>

本案例完整的pom.xml文件如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>person.liutianba</groupId>
    <artifactId>springboot_03_mybatis-plus</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.7</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.6</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>
  1. 配置application.yml文件

配置数据库相关内容如下

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: liuqiang
    url: jdbc:mysql://192.168.10.101:3306/hello_mp?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2b8
7.4 创建实体类

创建与user表相对应的实体类,如下

@Data
@TableName("user")
public class User {

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField("name")
    private String name;

    @TableField("age")
    private Integer age;

    @TableField("email")
    private String email;
}

知识点

实体类中的三个注解的含义如下

  • @TableName:表名注解,用于标识实体类所对应的表
  • value:用于声明表名

  • @TableId:主键注解,用于标识主键字段

  • value:用于声明主键的字段名
  • type:用于声明主键的生成策略,常用的策略有AUTOASSIGN_UUIDINPUT等等

  • @TableField:普通字段注解,用于标识属性所对应的表字段

  • value:用于声明普通字段的字段名
7.5 通用Mapper

通用Mapper提供了通用的CRUD方法,使用它可以省去大量编写简单重复的SQL语句的工作,具体用法如下

  1. 创建Mapper接口

创建UserMapper接口,并继承由Mybatis Plus提供的BaseMapper<T>接口,如下

public interface UserMapper extends BaseMapper<User> {
}

知识点

若Mapper接口过多,可不用逐一配置@Mapper注解,而使用@MapperScan注解指定包扫描路径进行统一管理,例如

@SpringBootApplication
@MapperScan("com.atguigu.hellomp.mapper")
public class HelloMpApplication {

    public static void main(String[] args) {
        SpringApplication.run(HelloMpApplication.class, args);
    }
}
  1. 测试通用Mapper

创建userMapperTest测试类型,内容如下

@SpringBootTest
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testSelectList() {
        List<User> users = userMapper.selectList(null);
        users.forEach(System.out::println);
    }

    @Test
    public void testSelectById() {
        User user = userMapper.selectById(1);
        System.out.println(user);
    }

    @Test
    public void testInsert() {
        User user = new User();
        user.setName("zhangsan");
        user.setAge(11);
        user.setEmail("test@test.com");
        userMapper.insert(user);
    }

    @Test
    public void testUpdateById() {
        User user = userMapper.selectById(1);
        user.setName("xiaoming");
        userMapper.updateById(user);
    }

    @Test
    public void testDeleteById() {
        userMapper.deleteById(1);
    }
}
7.6 通用Service

通用Service进一步封装了通用Mapper的CRUD方法,并提供了例如saveOrUpdatesaveBatch等高级方法。

  1. 创建Service接口

创建UserService,内容如下

public interface UserService extends IService<User> {
}
  1. 创建Service实现类

创建UserServiceImpl,内容如下

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
  1. 测试通用Service

创建UserServiceImplTest测试类,内容如下

@SpringBootTest
class UserServiceImplTest {


    @Autowired
    private UserService userService;

    @Test
    public void testSaveOrUpdate() {
        User user1 = userService.getById(2);
        user1.setName("xiaohu");

        User user2 = new User();
        user2.setName("lisi");
        user2.setAge(27);
        user2.setEmail("lisi@email.com");
        userService.saveOrUpdate(user1);
        userService.saveOrUpdate(user2);
    }


    @Test
    public void testSaveBatch() {
        User user1 = new User();
        user1.setName("dongdong");
        user1.setAge(49);
        user1.setEmail("dongdong@email.com");

        User user2 = new User();
        user2.setName("nannan");
        user2.setAge(29);
        user2.setEmail("nannan@email.com");

        List<User> users = List.of(user1, user2);
        userService.saveBatch(users);
    }
}
7.7 条件构造器

MyBatis-Plus 提供了一套强大的条件构造器(Wrapper),用于构建复杂的数据库查询条件。Wrapper 类允许开发者以链式调用的方式构造查询条件,无需编写繁琐的 SQL 语句,从而提高开发效率并减少 SQL 注入的风险。

在 MyBatis-Plus 中,Wrapper 类是构建查询和更新条件的核心工具。以下是主要的 Wrapper 类及其功能:

  • AbstractWrapper:这是一个抽象基类,提供了所有 Wrapper 类共有的方法和属性。它定义了条件构造的基本逻辑,包括字段(column)、值(value)、操作符(condition)等。所有的 QueryWrapper、UpdateWrapper、LambdaQueryWrapper 和 LambdaUpdateWrapper 都继承自 AbstractWrapper。
  • QueryWrapper:专门用于构造查询条件,支持基本的等于、不等于、大于、小于等各种常见操作。它允许你以链式调用的方式添加多个查询条件,并且可以组合使用 andor 逻辑。
  • UpdateWrapper:用于构造更新条件,可以在更新数据时指定条件。与 QueryWrapper 类似,它也支持链式调用和逻辑组合。使用 UpdateWrapper 可以在不创建实体对象的情况下,直接设置更新字段和条件。
  • LambdaQueryWrapper:这是一个基于 Lambda 表达式的查询条件构造器,它通过 Lambda 表达式来引用实体类的属性,从而避免了硬编码字段名。这种方式提高了代码的可读性和可维护性,尤其是在字段名可能发生变化的情况下。
  • LambdaUpdateWrapper:类似于 LambdaQueryWrapper,LambdaUpdateWrapper 是基于 Lambda 表达式的更新条件构造器。它允许你使用 Lambda 表达式来指定更新字段和条件,同样避免了硬编码字段名的问题。

  • 创建WrapperTest测试类,内容如下

@SpringBootTest
public class WrapperTest {

    @Autowired
    private UserService userService;

    @Test
    public void testQueryWrapper() {

        //查询name=Tom的所有用户
        QueryWrapper<User> queryWrapper1 = new QueryWrapper<>();
        queryWrapper1.eq("name", "Tom");

        //查询邮箱域名为baomidou.com的所有用户
        QueryWrapper<User> queryWrapper2 = new QueryWrapper<>();
        queryWrapper2.like("email", "baomidou.com");

        //查询所有用户信息并按照age字段降序排序
        QueryWrapper<User> queryWrapper3 = new QueryWrapper<>();
        queryWrapper3.orderByDesc("age");

        //查询age介于[20,30]的所有用户
        QueryWrapper<User> queryWrapper4 = new QueryWrapper<>();
        queryWrapper4.between("age", 20, 30);

        //查询age小于20或大于30的用户
        QueryWrapper<User> queryWrapper5 = new QueryWrapper<>();
        queryWrapper5.lt("age", 20).or().gt("age", 30);

        //邮箱域名为baomidou.com且年龄小于30或大于40且的用户
        QueryWrapper<User> queryWrapper6 = new QueryWrapper<>();
        queryWrapper6.like("email", "baomidou.com").and(wrapper -> wrapper.lt("age", 30).or().gt("age", 40));

        List<User> list = userService.list(queryWrapper6);
        list.forEach(System.out::println);
    }

    @Test
    public void testUpdateWrapper() {

        //将name=Tom的用户的email改为Tom@baobidou.com
        UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq("name", "Tom");
        updateWrapper.set("email", "Tom@baobidou.com");

        userService.update(updateWrapper);
    }
}
  1. 创建LambdaWrapperTest测试类,内容如下

上述的QueryWrapperUpdateWrapper均有一个Lambda版本,也就是LambdaQueryWrapperLambdaUpdateWrapperLambda版本的优势在于,可以省去字段名的硬编码,具体案例如下:

@SpringBootTest
public class LambdaWrapperTest {

    @Autowired
    private UserService userService;

    @Test
    public void testLambdaQueryWrapper() {
        //查询name=Tom的所有用户
        LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(User::getName, "Tom");

        List<User> list = userService.list(lambdaQueryWrapper);
        list.forEach(System.out::println);

    }

    @Test
    public void testLambdaUpdateWrapper() {
        //将name=Tom的用户的邮箱改为Tom@tom.com
        LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
        lambdaUpdateWrapper.eq(User::getName, "Tom");
        lambdaUpdateWrapper.set(User::getEmail, "Tom@Tom.com");

        userService.update(lambdaUpdateWrapper);
    }
}
7.8 逻辑删除

逻辑删除,可以方便地实现对数据库记录的逻辑删除而不是物理删除。逻辑删除是指通过更改记录的状态或添加标记字段来模拟删除操作,从而保留了删除前的数据,便于后续的数据分析和恢复。

  • 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据
  • 逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录

  • 数据库和实体类添加逻辑删除字段

  • 表添加逻辑删除字段

    可以是一个布尔类型、整数类型或枚举类型。

    ALTER TABLE USER ADD deleted INT DEFAULT 0 ;  # int 类型 1 逻辑删除 0 未逻辑删除
    
  • 实体类添加属性

    @Data
    public class User {
    
       // @TableId
        private Integer id;
        private String name;
        private Integer age;
        private String email;
    
        @TableLogic
        //逻辑删除字段 int mybatis-plus下,默认 逻辑删除值为1 未逻辑删除 0 
        private Integer deleted;
    }
    

2.指定逻辑删除字段和属性值

  1. 单一指定
@Data
public class User {

   // @TableId
    private Integer id;
    private String name;
    private Integer age;
    private String email;
     @TableLogic
    //逻辑删除字段 int mybatis-plus下,默认 逻辑删除值为1 未逻辑删除 0
    private Integer deleted;
}
  1. 全局配置
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted #列名
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

3.演示逻辑删除操作

删除代码:

//逻辑删除
@Test
public void testQuick5(){
    //逻辑删除
    userMapper.deleteById(5);
}

执行效果:

JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@5871a482] will not be managed by Spring

==> Preparing: UPDATE user SET deleted=1 WHERE id=? AND deleted=0 ==> Parameters: 5(Integer) <== Updates: 1 4. 测试查询数据

@Test
public void testQuick6(){
    //正常查询.默认查询非逻辑删除数据
    userMapper.selectList(null);
}

//SELECT id,name,age,email,deleted FROM user WHERE deleted=0
7.9 分页插件

分页查询是一个很常见的需求,故Mybatis-Plus提供了一个分页插件,使用它可以十分方便的完成分页查询。下面介绍Mybatis-Plus分页插件的用法,详细信息可参考官方文档

  • 配置分页插件

创建com.atguigu.hellomp.config.MPConfiguration配置类,增加如下内容

@Configuration
public class MPConfiguration {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
  • 分页插件使用说明

构造分页对象, 分页对象包含了分页的各项信息,其核心属性如下:

| 属性名  | 类型 | 默认值    | 描述                   |
| ------- | ---- | --------- | ---------------------- |
| records | List | emptyList | 查询数据列表           |
| total   | Long | 0         | 查询列表总记录数       |
| size    | Long | 10        | 每页显示条数,默认`10` |
| current | Long | 1         | 当前页                 |

分页对象既作为分页查询的参数,也作为分页查询的返回结果,当作为查询参数时,通常只需提供`current`和`size`属性,如下

```java
IPage<T> page = new Page<>(current, size);
```

注:`IPage`为分页接口,`Page`为`IPage`接口的一个实现类。
  • 分页查询

    Mybatis Plus的BaseMapperServiceImpl均提供了常用的分页查询的方法,例如:

    • BaseMapper的分页查询:
    IPage<T> selectPage(IPage<T> page,Wrapper<T> queryWrapper);
    
    • ServiceImpl的分页查询:
    // 无条件分页查询
    IPage<T> page(IPage<T> page);
    // 条件分页查询
    IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
    
    • 自定义Mapper

    对于自定义SQL,也可以十分方便的完成分页查询,如下

    Mapper接口:

    IPage<UserVo> selectPageVo(IPage<?> page, Integer state);
    

    Mapper.xml

    <select id="selectPageVo" resultType="xxx.xxx.xxx.UserVo">
        SELECT id,name FROM user WHERE state=#{state}
    </select>
    

    注意Mapper.xml中的SQL只需实现查询list的逻辑即可,无需关注分页的逻辑。

  • 案例实操

分页查询案例如下:

创建PageTest测试类,内容如下

@SpringBootTest
public class PageTest {

    @Autowired
    private UserService userService;

    @Autowired
    private UserMapper userMapper;

    //通用Service分页查询
    @Test
    public void testPageService() {
        Page<User> page = new Page<>(2, 3);
        Page<User> userPage = userService.page(page);
        userPage.getRecords().forEach(System.out::println);
    }

    //通用Mapper分页查询
    @Test
    public void testPageMapper() {
        IPage<User> page = new Page<>(2, 3);
        IPage<User> userPage = userMapper.selectPage(page, null);
        userPage.getRecords().forEach(System.out::println);
    }

    //自定义SQL分页查询
    @Test
    public void testCustomMapper() {
        IPage<User> page = new Page<>(2, 3);
        IPage<User> userPage = userMapper.selectUserPage(page);
        userPage.getRecords().forEach(System.out::println);
    }
}

在UserMapper中声明分页查询方法如下

IPage<User> selectUserPage(IPage<User> page);

创建resources/mapper/UserMapper.xml文件,内容如下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.hellomp.mapper.UserMapper">
    <select id="selectUserPage" resultType="com.atguigu.hellomp.entity.User">
        select *
        from user
    </select>
</mapper>

注意

Mybatis-Plus中Mapper.xml文件路径默认为:classpath*:/mapper/**/*.xml,可在application.yml中配置以下参数进行修改

 ```yml

mybatis-plus: mapper-locations: classpath:/mapper//.xml ```

7.10 MyBatisX插件

MyBatis Plus提供了一个IDEA插件——MybatisX,使用它可根据数据库快速生成EntityMapperMapper.xmlServiceServiceImpl等代码,使用户更专注于业务