SpringBootV12和mybatis全部知识点

框架: 快速开发项目的一个架子

image-20230624230313370

ssh

ssm

spring --> applicationContext.xml配置文件(spring不是业务层,是管理其他框架的)

springmvc --> springmvc.xml配置文件 (对应之前servlet)

mybatis —> mybatis-config.xml配置文件(对应之前jdbc)

—> springboot优化了之前的框架配置,思想是约定大于配置

一、引言


1.1 初始化配置

为了使用SSM框架去开发,准备SSM框架的模板配置。

1.2 整合第三方框架

为了Spring整合第三方框架,单独的去编写xml文件。

1.3 后期维护

后期SSM项目后期xml文件特别多,维护xml文件的成本是很高的

1.4 部署工程

SSM工程部署也是很麻烦,依赖第三方的容器

1.5 敏捷式开发

基于Java的SSM开发方式是很笨重,而现在的python,php,NodeJS的敏捷式开发已经盖过Java一头

二、SpringBoot介绍


SpringBoot是由Pivotal团队研发的,SpringBoot并不是一门新技术,只是将之前常用的Spring,SpringMVC,data-jpa等常用的框架封装到了一起,帮助你隐藏这些框架的整合细节,实现敏捷开发。 约定大于配置

SpringBoot就是一个工具集。

官网:https://spring.io/projects/spring-boot

SpringBoot特点:

  • SpringBoot项目不需要模板化的配置。
  • SpringBoot中整合第三方框架时,只需要导入相应的starter依赖包,就自动整合了。约定大于配置。
  • SpringBoot默认只有一个.properties或者.yml的配置文件,不推荐使用xml,后期会采用.java的文件去编写配置信息。
  • SpringBoot工程在部署时,采用的是jar包的方式,内部自动依赖Tomcat容器,提供了多环境的配置。
  • 后期要学习的微服务框架SpringCloud需要建立在SpringBoot的基础上。

三、SpringBoot快速入门


3.1 快速构建SpringBoot

3.1.1 选择构建项目的类型

选择构建项目的类型
image-20230624230840419

注意:根据网络状况,可能会提示无法连接。如果不能连接,使用http://start.springboot.io(或者http://start.aliyun.com)

3.1.2 指定SpringBoot版本和需要的依赖

指定SpringBoot版本和需要的依赖
image-20230624231046116

ps: 如果2.7.13版本创建完项目会报错,提示alimaven找不到依赖的情况,就手动将pom文件中版本改成2.7.2

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

3.1.3 导入依赖

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

3.1.4 编写了Controller

Controller就是之前的Servlet,用于接收请求做出响应

创建一个controller包,单独写controller类

@Controller
public class TestController {

    @RequestMapping("/test")
    public String test(){
        return "ok.html";
    }
}

3.1.5 编写页面

在resources/static下创建ok.html页面

ps: 默认的固定格式,只能在resources下,且只能是static

image-20230817202540837

3.1.6 测试

主类点击启动项目

image-20230624233626353

SpringBoot项目启动成功后,不会主动打开浏览器,需要自己手动打开浏览器测试

效果
image-20230624233656863image-20230817202613225手动访问/tset路径,看到页面跳转

3.2 SpringBoot的目录结构

3.2.1 pom.xml文件

  • 指定了一个父工程: 指定当前工程为SpringBoot,帮助我们声明了starter依赖的版本。

       <!-- 没有这个的话,有dependencyManagement也行 --> 
       <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.7.2</version>
            <relativePath/> 
        </parent>
    
       <!-- 有这个dependencyManagement,就不需要上面parent --> 
       <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                    <version>${spring-boot.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
  • 项目的元数据:包名,项目名,版本号。

       <groupId>com.qf</groupId>
       <artifactId>test_springboot01</artifactId>
       <version>0.0.1-SNAPSHOT</version>
       <name>TestSpringboot01</name>
    
  • 指定了properties信息:指定了java的版本为1.8

        <properties>
            <java.version>1.8</java.version>
        </properties>
    
  • 导入依赖:按需导入(web,mysql等等)

  • 插件:spring-boot-maven-plugin (如果有报错,也可以不要)

     <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <version>2.7.2</version>
                </plugin>
            </plugins>
        </build>
    

3.2.2 .gitignore文件

默认帮我们忽略了一些文件和目录,避免提交到Git仓库中

3.2.3 目录结构

-src
  -main	  
    -java
      -包名
      启动类.java # 需要将controller类,放在启动类的子包中或者同级包下,否则需要使用@ComponentScan 注解,并指定扫描的包即可
    -resources  # resources下放除了java代码之外其他资源文件
      -static				  # 存放静态资源的,js,css,html
      -templates			  # 存储模板页面的,Thymeleaf,jsp,freemarker
      application.properties  # SpringBoot提供的配置文件,后缀支持2种:1.properties 2.yml(推荐),用来修改默认配置
  -test   				      # 只是为了测试用的

image-20240701114556614

3.3 补充

可以给主类加上如下代码即可启动成功后,自动打开浏览器

public static void main(String[] args) {
    int port = 8080;
    String portPrefix = "--server.port=";
    for (String arg : args) {
        if (arg.startsWith(portPrefix)) {
            port = Integer.parseInt(arg.substring(portPrefix.length()));
        }
    }
    SpringApplication.run(Day35Springboot01Application.class, args);
    try {
        Runtime.getRuntime().exec("cmd /c start http://localhost:" + port);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

四、Spring

介绍…balabala

spring的核心功能

Spring是一个容器,帮助管理对象生命周期(创建对象,使用对象,对象销毁等),
Spring框架中主要的功能有
- IOC,DI
- AOP
- MVC
- Dao(JDBCTemplate)等
  • IOC,DI
  • AOP

image-20240701144050050

4.1 IOC+DI[重点]

4.1.0 引言

以前写的[登录+查询全部功能]

// 控制层
public class LoginServlet extends HttpServlet{
    UserService service;
    void doGet(){
        service.findAdminByLogin(username,password);
    }
}
// 业务层接口
public interface UserService{
    Admin findUseryLogin(String username,String password);
}

// 业务层实现类
public class UserServiceImpl implements AdminService {
    Admin findUseryLogin(String username,String password) {
     // ...   
    }
}

public class UserServiceImpl2 implements AdminService {
    Admin findUseryLogin(String username,String password) {
     // ...   
    }
}

以上这样写有缺点:

1 LoginServlet类还是需要和UserService和UserServiceImpl耦合

2 扩展性不好,假如有新的实现类UserServiceImpl2,就需要改动代码


现在需要一种技术,降低耦合且还可以根据运行时状态给属性动态赋值

4.1.1 介绍

IOC是Spring框架的核心功能之一,IOC(inversion of control)控制反转

控制: 控制创建对象的能力(以前是自己需要什么对象,自己创建)

反转: 原来创建对象是自己做,反转就是将创建对象的能力交给Spring


IOC(控制反转): 将创建对象的能力反转给Spring,由Spring创建对象

DI(dependency injection) 依赖注入,即 属性赋值


spring的ioc,帮助我们创建对象,是通过注解实现的, 常用的创建对象的注解

  • @Controller 在控制层代码上使用
  • @Service 在业务层代码上使用
  • @Repository 在数据层代码上使用
  • @Component 在其他代码上使用

–> 以上四个注解都是创建对象用的,只不过建议是各层用对应的

属性赋值(依赖注入的注解)

  • @Autowired
  • @Resource

4.1.2 演示1

需求: 项目中控制层Controller中需要使用到业务层service对象来处理业务,例如AdminController中需要创建AdminService对象使用,使用IOC+DI完成

AdminService和AdminServiceImpl

public interface AdminService {
    void login();
}

@Service // 加上该注解,AdminServiceImpl类就会被spring容器创建对象
public class AdminServiceImpl implements AdminService{

    @Override
    public void login() {
        System.out.println("业务层执行..." );
    }
}

AdminController

@Controller // 创建对象
public class AdminController {

    // 在控制层中需要使用业务层对象
    // 不再主动new对象,而是从容器中拿
    // @Autowired注解就会从容器中找到该类型的对象赋值给该变量
    // 即这就是属性赋值,也就是依赖注入,即DI
    @Autowired // 属性赋值
    private AdminService adminService;

    @RequestMapping("/login")
    public String login() {
        adminService.login();
        return "ok.html";
    }
}

练习: 在AdminService中使用AdminDao对象

创建dao,和dao.impl包以及AdminDao,AdminDaoImpl

public interface AdminDao {
    void login();
}
@Repository // 创建Dao对象
public class AdminDaoImpl implements AdminDao {
    @Override
    public void login() {
        System.out.println("持久层执行" );
    }
}

业务层注入Dao层对象

@Service // 加上这个注解,spring容器就会创建该类对象
public class AdminServiceImpl implements AdminService {

    @Autowired // 注入Dao对象
    private AdminDao dao;

    @Override
    public void login() {
        System.out.println("业务层执行..." );
        dao.login();
    }
}

4.1.3 演示2-@Autowired 依赖注入(属性赋值)的原理

@Autowired给属性赋值,是如何绑定属性赋值的? 是按照类型注入的


但是如果spring容器中有多个同类型的对象时,@Autowired注入就会失败

image-20240407144830339

如果spring容器中有多个同类型的对象时 , 如何解决?
此时通过对象名来确定指定注入(赋值)哪个对象. @Qualifier("as2")

对象名是什么? 默认通过@Controller,@Service等这些创建的对象,对象名默认是类名首字母小写,
也可以通过在@Controller,@Service等这些注解后设置对象名,例如@Service(“as1”)

image-20240407145510331

image-20240701154924379

思考,课下扩展: @Autowired和@Resource什么异同

4.1.4 演示3

演示@Component注解创建对象

假如有个类Admin,现在需要该类对象,就可以在该类上加上@Component注解,并在其他地方使用@Autowired注入

image-20230823155730068

4.2 AOP

4.2.1 介绍

Spring中另外一个核心功能,AOP


AOP(Aspect Oriented Programming),即面向切面编程.

OOP(Object Oriented Programming ),即面向对象编程.

AOP面向切面编程,利用 一种称为"横切"的技术,剖开封装的对象内部,并将那些影响了 多个类的公共行为抽取出封装到一个可重用模块,并将其命名 为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

image-20230612114224473

面向切面编程的作用:就是将项目中与核心逻辑无关的代码横向抽取成切面类,通过织入作用到目标方法,以使目标方法执行前后达到增强的效果.

原理: AOP底层使用的就是动态代理,给AOP指定哪些类型(目标类)需要增强,就会产生对应的代理对象,代理对象执行方法前后会先执行增 强的方法.

好处:减少系统的重复代码,降低模块之间的耦合度,便于维护,可以只关注核心业务

补充:
AOP底层实现原理是代理设计模式,且是动态代理

静态代理
动态代理(jdk动态代理技术 / cglib动态代理技术)
-----
jdk代理技术,要求目标类必须是接口
cglib代理技术, 目标类可以是接口也可以是类

想详细了解的: 
https://www.bilibili.com/video/BV1uN411r7MB/?spm_id_from=333.999.0.0

4.2.2 AOP术语

连接点(Joinpoint):连接点是程序类中客观存在的方法,可被Spring拦截并切入内容。即每个方法在切入之前,都是连接点

切入点(Pointcut):被Spring切入连接点。即真正会增强的目标方法

通知、增强(Advice):可以为切入点添加额外功能,分为:前置通知、后置通知、异常通知、环绕通知等。

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

织入(Weaving):把通知应用到具体的类,进而创建新的代理类的过程。

代理(Proxy):被AOP织入通知后,产生的结代理类。

切面(Aspect):由切点和通知组成

4.2.3 应用场景

  • 事务管理
    • 后续spring管理事务用的AOP原理
  • 权限校验
    • 后期使用Spring Security注解开发时,其实利用了AOP思想
  • 日志记录
  • 性能检测
  • 等等

4.2.4 演示: 前置增强

需求: 实现业务层代码执行时,能出现一些增强的效果

开发步骤

  1. 创建切面类,类上加注解

    • @Component ,加上该注解,springboot框架就会创建该类对象

    • @Aspect , 加上该注解,springboot框架内部就会知道该类是一个切面类

    • 设置切入点方法,并加注解

      • @Pointcut , 用于定义要增强的目标方法路径
  2. 设置各种增强(或者叫通知)方法

  • 注解解释
    @Around环绕通知
    @Before前置通知
    @After后置通知
    @AfterReturning后置返回通知
    @AfterThrowing异常通知

pom.xml添加aop依赖

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

演示给业务层代码设置前置增强

package com.qf.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @date 2024/7/1
 * @desc 切面类,定义增强的方法
 */
@Component  // 由spring创建对象
@Aspect  // 让框架知道,这是一个切面
public class MyAspect {


    /**
     * 自己定义一个方法,用于增强
     * 例如: 权限校验
     * @Before注解,可以在目标方法执行前,执行这个myBefore方法
     * 注解内写的就是,目标方法路径
     *  第一个* ,是返回值任意
     *  com.qf.service.impl.* 是指impl包下的任意类
     *  .* 类中任意方法名
     *  (..) 方法参数任意
     * ------------------------
     * 参数 JoinPoint,就是目标对象
     */
    @Before("execution(* com.qf.service.impl.*.*(..))")
    public void myBefore(JoinPoint joinPoint) {
        System.out.println("方法执行前: 权限校验" );
        System.out.println("目标方法名:"+ joinPoint.getSignature( ).getName( ));

        // 权限校验,如果通过,无事发生,正常执行
        // 权限校验,如果不通过,可以抛出异常,让代码停止,目标方法不执行
        // throw new RuntimeException("权限校验失败");

    }
}

启动项目,测试

  • 正常发请求访问控制层,控制层调用业务层代码时,就会触发AOP的增强机制

image-20240701170011578

4.2.5 演示各种增强

package com.qf.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
@Component
@Aspect
public class MyAspect {
    /**
     * 将目标方法路径,提取成公共的路径,直接调用
     * 后续其他增强就不用每次都写
     */
    @Pointcut("execution(* com.qf.service.*.*(..))")
    public void myPointcut() {}
    
    /**
     * @Around 注解说明该方法是环绕通知的方法
     * 注解内写的目标方法的路径模板
     *  execution 固定关键词
     *  * 返回值任意
     *  com.qf.day33_springboot.service 包路径
     *  .* 该包下所有文件
     *  .* 该类下所有方法
     *  (..) 该方法所有参数
     *-------------------------
     * ProceedingJoinPoint 参数代表目标方法对象
     */
    @Around("myPointcut()")
    public Object myAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 目标方法前:
        System.out.println("开启事务/权限校验" );

        // 目标方法执行
        Object ret = joinPoint.proceed( );
        System.out.println("目标方法返回值---> " + ret );
        // 目标方法后:
        System.out.println("提交事务/日志记录" );
        return ret;
    }

    /**
    JoinPoint 是目标方法对象
    */
    @Before("myPointcut()")
    public void myBefore(JoinPoint joinPoint) {
        // 目标对象
        Object target = joinPoint.getTarget( );
        System.out.println("target = " + target);
        // 获得目标方法签名(方法名)
        Signature signature = joinPoint.getSignature( );
        System.out.println("signature = " + signature);

        System.out.println("前置通知--->权限校验--->OK" );

        // 假设权限校验没有通过,通过抛出异常让代码停下,不再执行目标方法
        // System.out.println("前置通知--->权限校验--->ERROR" );
        // throw new RuntimeException("权限校验--->ERROR");
    }

    @After("myPointcut()")
    public void myAfter() {
        // 获得ip
        // 获得时间
        // 获得人名
        // 获得日志描述信息
        System.out.println("后置通知--->记录日志,释放资源" );
    }

    /**
     * 后置返回增强,一般用于接收目标方法的返回值
     * --------------------------------
     * 当注解括号内参数只有一个,且参数名是value,那么可以省略value直接写值
     * 当多于1个参数,所有参数都需要写k=v
     * ---------------------------------
     * @AfterReturning 后置返回增强,用于目标方法的返回值
     *  参数value用于指定目标方法
     *  参数returning用于指定返回值,该返回值需要定义在本方法的参数上
     */
    @AfterReturning(value = "myPointcut()",returning = "ret")
    public Object myAfterRet(Object ret) {
        System.out.println("后置返回通知,接收到目标方法返回值--->" + ret);
        return ret;
    }

    @AfterThrowing(value = "myPointcut()",throwing = "e")
    public void myException(Exception e) {
        System.out.println("目标方法报的错---> " + e.getMessage());
    }
}

4.2.6 AOP实战-Log记录

暂时无法完成,还有部分知识没讲,等讲完mvc和mybatis后再来实战,暂时可以大概看一眼

实战需求参考若依系统首页 (ruoyi.vip)

image-20230819185633620

即将用户在系统中做的任何操作都记录到数据库,供后续查看

首先设计日志表,将来存储日志信息

CREATE TABLE `log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `log_time` datetime DEFAULT NULL,
  `log` varchar(255) DEFAULT NULL,
  `ip` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

# 别忘了,要在idea中设置实体类

日志注解文件

package com.qf.annotation;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
    String value();
}

使用注解

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 先登录,登录成功后将信息存入Session
     * 这样log日志记录时才能取出人名
     */
    @GetMapping("/login")
    public User login(String name, HttpSession session) {
        User user = userService.selectUserByUsername(name);

        if (user != null) {
            session.setAttribute("user",user);
        }
        return user;
    }


    @GetMapping("/list")
    @MyLog("查询全部") // 使用日志注解
    public List<User> selectAll() {
        return userService.selectAll();
    }
}

切面类

package com.qf.aspect;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Date;

import javax.servlet.http.HttpServletRequest;

import com.qf.annotation.MyLog;
import com.qf.model.Log;
import com.qf.model.User;
import com.qf.service.MyLogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.WebUtils;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
@Component
@Aspect
public class MyLogAspect {
    // 注入业务层对象
    @Autowired
    private MyLogService logService;

    // 切入的目标是注解
    @Before("@annotation(com.qf.annotation.MyLog)")
    public void after(JoinPoint joinPoint) {
        // 调用下方自定义方法,获得注解中的值
        String desc = getAnnoDesc(joinPoint);

        System.out.println("注解中的值:" + desc);
        // 以下代码需要使用Springmvc,既控制层也需要由Spring托管,才会获得ip
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes( )).getRequest( );
        // 能取出数据的前提是登录时将用户信息存储到session
        User user = (User) WebUtils.getSessionAttribute(request, "user");

        String name = "未登录";
        if (user != null) { // 防止出现空指针异常
            name = user.getName( );
        }

        //ip
        String ip = request.getRemoteAddr( );
        System.out.println("请求对象的ip:" + ip);
        // 封装数据
        Log log = new Log( );
        log.setName(name);
        log.setIp(ip);
        log.setLogTime(new Date());
        log.setLog(desc);
        // 调用业务层,执行插入
        logService.insertLog(log);
    }

    /*
     * 封装的方法,通过反射技术获得目标方法上的注解的值
     */
    public static String getAnnoDesc(JoinPoint joinPoint) {
        String value = "";
        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature( );
            // 获得目标方法对象
            Method method = signature.getMethod( );
            // 获得目标方法对象上的日志注解
            MyLog myLog = method.getDeclaredAnnotation(MyLog.class);
            // 获得日志注解上的值
            value = myLog.value( );
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return value;
    }

启动项目,浏览器访问测试

image-20230819190408990

image-20230819190421386

ps: ip是因为使用localhost访问,换成126.0.0.1来访问就会正常

总结步骤

1. 向创建准备存储日志的表
2. 创建表对应的实体类
3. 因为最终要对日志进行增删改查,所有创建对应的service和mapper
4. 创建日志注解
5. 创建切面类,对目标注解解析(获得ip,name,时间,注解信息,最后插入数据库)
6. 将这些日志注解加在业务层上

五、Springmvc

springmvc其实是spring框架中关于web,webmvc开发的一个技术

spring核心ioc,aop,web开发

5.1 MVC

MVC架构: 根据不同的事情由不同的类去处理,内部单一职责

  • Model: 模型类,例如封装数据的实体类,业务模型(Service),数据层(Dao)
  • View: 视图,展示数据的.HTML,JSP
  • Controller: 控制器,控制整个流程走向. 决定是否能接收请求,调用哪个业务,跳转哪个页面,Servlet

MVC框架特点

  • 封装了Servlet
  • 接收请求方便(一个类中,不同的方法就可以接收不同的请求)
  • 接收请求数据方便(自动封装)
  • 响应数据方便(自动响应json)

image-20230824104944830

5.2 搭建环境

创建springboot项目

导入spring-boot-starter-web这个依赖,即可开始开发webmvc的代码

5.2 接收请求(请求映射)【重点】

@Controller
public class Demo1Controller {

    /**
     * springmvc框架,支持一个方法就可以接收一个请求
     * 不用像以前servlet一样,每个类只能接收一个请求
     * -----------------------------------------
     * @RequestMapping 用来匹配前端发送的请求路径,可以理解为之前的@WebServlet
     * @RequestMapping 默认允许接收get和post请求
     * 也可以指定只接收一种请求,
     *   例如method = RequestMethod.POST,那就是只能接收前端发送的POST请求
     * -----------------------------------------
     * @RequestMapping(value="/req1",method = RequestMethod.POST)
     * @RequestMapping(value="/req1",method = RequestMethod.GET)
     *
     * 以上两个注解,可以简化成分别是
     * @GetMapping("/req1")
     * @PostMapping("/req1")
     */

    //@RequestMapping(value="/req1",method = RequestMethod.POST)
    @PostMapping("/req1")
    public String test1() {
        System.out.println("接收请求1" );
        return "ok.html";
    }

    // @RequestMapping(value = "/req2",method = RequestMethod.GET)
    @GetMapping("/req2")
    public String test2() {
        System.out.println("接收请求2" );
        return "ok.html";
    }
}

5.3 参数绑定(接收请求数据) 【重点】

所谓参数绑定,就是前端发请求中的数据,可以直接在Controller的方法参数中接收.即前端请求数据和后端方法参数绑定.

5.3.1 简单类型参数绑定[重点]

简单类型指,常用的几种类型: 基本类型+String+Date

前端页面

<h2>基本类型数据绑定</h2>
<a href="/base?id=1&username=张三&score=10.0&birthday=2020-01-01">请求携带数据-基本类型</a>
<hr>
<form action="/base" method="get">
    id<input type="text" name="id"><br>
    username<input type="text" name="username"><br>
    score<input type="text" name="score"><br>
    birthday<input type="date" name="birthday"><br>
    <input type="submit" value="基本类型">
</form>

后端接收

@Controller
public class DataController {

    /**
     * 基本类型自动封装
     * 要求是: 前端请求的参数名,后端方法的参数名要一致
     * 【特殊的】 日期,前端发送的日期格式如果是yyyy-MM-dd,springmvc无法默认绑定参数,
     *            springmvc默认支持yyyy/MM/dd
     * 两种方案解决:
     *  1. 前端改,发出的日期格式就是yyyy/MM/dd即可
     *  2. 后端改,给日期参数加@DateTimeFormat(pattern = "yyyy-MM-dd")
     */
    @GetMapping("/base")
    public String base(int id, String username, double score, @DateTimeFormat(pattern = "yyyy-MM-dd") Date birthday){
        System.out.println("id = " + id);
        System.out.println("username = " + username);
        System.out.println("score = " + score);
        System.out.println("birthday = " + birthday);
        return "ok.html";
    }

}

5.3.2 对象[重点]

场景: 注册/添加/更新

实体类

public class User {

    private int id;
    private String username;
    private String password;
    private double score;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthday;
    // setget
}

前端

<h2>对象数据绑定</h2>
<form action="/obj" method="get">
    id<input type="text" name="id"><br>
    username<input type="text" name="username"><br>
    password<input type="text" name="password"><br>
    score<input type="text" name="score"><br>
    birthday<input type="date" name="birthday"><br>
    <input type="submit" value="对象类型">
</form>

后端

    /**
     * 自动绑定: 要求前端的请求中的参数要和对象的属性名一致
     */
    @GetMapping("/obj")
    public String obj(User user){
        System.out.println("user = " + user);
        return "ok.html";
    }

5.3.3 数组

场景: 批量删除需要同时接收多个id, (前端是复选框的)

delete from tb_user where id in (1,2,3,4)

前端

<h2>数组绑定</h2>
<form action="/array" method="get">
     <input type="checkbox" name="ids" value="1">1
     <input type="checkbox" name="ids" value="2">2
     <input type="checkbox" name="ids" value="3">3
     <input type="checkbox" name="ids" value="4">4
    <input type="submit" value="数组类型">
</form>

后端

    /**
     * 自动绑定: 要求前端的请求中的参数要和方法参数名(数组名)一致
     */
    @GetMapping("/array")
    public String array(int[] ids){
        System.out.println("ids = " + Arrays.toString(ids));
        return "ok.html";
    }

5.3.4 List集合

List集合使用场景与数组是一样的

前端

<h2>List绑定</h2>
<form action="/list" method="get">
    <input type="checkbox" name="skill" value="Java">Java
    <input type="checkbox" name="skill" value="HTML">HTML
    <input type="checkbox" name="skill" value="Linux">Linux
    <input type="submit" value="List类型">
</form>

SpringMVC默认是不支持直接封装List的,解决方案:

  • 参数前加注解@RequestParam
    @GetMapping("/list")
    public String list(@RequestParam List<String> skill){
        System.out.println("skill = " + skill);
        return "ok.html";
    }

5.3.5 Map集合

Map是键值对,键和值一一映射.

跟Java对象很类似,属性和属性值一一对应.

所以什么时候需要/可以使用Map类型来接收参数呢?

  • 凡是可以用对象接收的都可以使用Map

SpringMVC默认不支持直接将参数封装进Map,需要使用@RequestParam

前端

<h2>演示: 接收Map类型的数据</h2>
<form action="/receiveMap">
    用户名<input type="text" name="username"><br>
    年龄<input type="text" name="age"><br>
    分数<input type="text" name="score"><br>
    生日<input type="date" name="birthday"><br>
    <button>提交</button>
</form>

后台

   /**
     * 用Map接收默认不支持,需要在参数前使用注解@RequestParam
     */
    @GetMapping("/receiveMap")
    public String receiveMap(@RequestParam HashMap<String,String> map) throws ParseException {
        System.out.println("map = " + map);
        /**
         * map中,键是前端的name,值是输入框的值
         * 值都是String类型,如果后续要用,还需要解析成对应的数据类型
         */
        String username = (String) map.get("username");
        int age = Integer.parseInt(map.get("age"));
        double score = Double.parseDouble(map.get("score"));
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date birthday = sdf.parse(map.get("birthday"));

        System.out.println("username = " + username);
        System.out.println("age = " + age);
        System.out.println("score = " + score);
        System.out.println("birthday = " + birthday);
        return "ok.html";
    }

5.3.6 路径参数@PathVariable

参考这个路径

https://www.baidu.com/s?tn=68018901_3_dg&ie=UTF-8&wd=RESTful
https://blog.csdn.net/article?uid=weixin_39641494&aid=131625212
RESTful风格如下
https://blog.csdn.net/weixin_39641494/article/details/131625212

这个路径中weixin_39641494是用户编号,131625212是文章id

@GetMapping(“/{userid}/article/details/{aid}”)

前端

<h2>演示: 接收路径中的数据</h2>
<a href="/blog/weixin_39641494/article/details/131625212">查看文章详情</a>

后端

    /**
     * 接收路径中的数据
     * 这种是RESTful风格,这种开发是需要返回JSON数据的
     * 即不能跳转/返回页面,需要返回JSON数据
     * 需要将返回值设置成json格式,设置注解@ResponseBody即可返回json数据
     */
    @GetMapping("/blog/{wxid}/article/details/{aid}")
    @ResponseBody
    public String receivePathVariable(@PathVariable String wxid, @PathVariable String aid){
        System.out.println("wxid = " + wxid);
        System.out.println("aid = " + aid);
        return "{code:200,msg:\"信息\"}";
    }

ps: 能接收到请求中的数据,但是响应会报错.因为使用@PathVariable要求返回的是json数据而不是页面,这个暂时先不管

5.4 响应:页面跳转[了解]

回顾之前学过的servlet中跳转页面的功能

  • 请求转发:forward
    • req.getDispatcherServlet().forward(req,resp)
    • 请求路径不变
    • 是服务器内部请求
    • 一次请求
    • 请求域的数据可以共享
  • 重定向:redirect
    • resp.sendRedirect();
    • 请求路径改变
    • 是浏览器行为
    • 两次请求
    • 请求域的不能共享

请求转发

注意: 现在我们一直都在使用请求转发,因为默认就是请求转发跳转页面

也可以手动显示的在Controller的方法的返回值中写forward:路径即可完成跳转

例如: forward:/ok.html forward:/test

注意: 跳转后的路径要写完整

   /**
     * 演示请求转发至其他页面
     * @return
     */
    @GetMapping("/forward")
    public String forward(){
        System.out.println("执行请求转发" );
        return "forward:/ok.html";// 跳转页面
    }

    /**
     * 演示请求转发至其他请求
     * @return
     */
    @GetMapping("/forward2")
    public String forward2(){
        System.out.println("执行请求转发" );
        return "forward:/test"; // 跳转其他请求路径
    }

	@GetMapping("/test")
    public String forward3(){
        return "ok.html";
    }

重定向

在Controller的方法的返回值中写redirect:路径即可完成跳转

例如: redirect:/ok.html redirect:/test

注意: 跳转后的路径要写完整

   /**
     * 演示重定向至其他页面
     * @return
     */
    @GetMapping("/redirect")
    public String redirect(){
        System.out.println("执行重定向" );
        return "redirect:/ok.html";
    }

    /**
     * 演示重定向至其他请求
     * @return
     */
    @GetMapping("/redirect2")
    public String redirect2(){
        System.out.println("执行重定向" );
        return "redirect:/test";
    }

其他的请求转发和重定向的特点和之前学习的servlet是一样的,复习.

5.5 请求域|会话域[重点]

请求域HttpServletRequest

会话域HttpSession

这两个对象现在想要使用它们,只需要在Controller层方法的参数列表中直接定义该变量即可使用

@Controller
public class Demo4Controller {

    /**
     * 演示请求域,会话域的使用
     * 方法的参数列表中直接定义该变量即可
     */
    @GetMapping("/reqse")
    public String test(HttpServletRequest req, HttpSession session) {

        req.setAttribute("reqKey","reqValue");
        session.setAttribute("seKey","seValue");

        // return "forward:/reqse2";
        return "redirect:/reqse2";
    }


    @GetMapping("/reqse2")
    public String test2(HttpServletRequest req, HttpSession session) {

        Object reqKey = req.getAttribute("reqKey");
        Object seKey = session.getAttribute("seKey");
        System.out.println("reqKey = " + reqKey);
        System.out.println("seKey = " + seKey);

        return "ok.html";
    }
}

一般来说,会话域HttpSession用的多, 常用于存储用户的登录信息

5.6 响应: 返回json数据【重点】

后续工作项目,都是前后端分离开发,前后端使用JSON数据交互

  • 前端发送json,使用axios技术(类似于ajax),vue中就使用axios发送请求
  • 后端接收json,然后响应给前端json

前端发送json等vue时候再演示

**现在演示响应JSON数据,非常简单,方法加上@ResponseBody**即可,就会将任何解析为json返回

package com.qf.controller;

import com.qf.model.User;
import com.qf.util.R;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.*;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc 演示响应json
 * 将后台的数据以json形式返回给前端
 */
@Controller
public class Demo4Controller {

    /**
     * 控制层代码要想返回/响应给前端json数据,只需要
     * 1) 设计方法的返回值为对应类型(String,对象,Map,List)
     * 2) 最后在方法上加上@ResponseBody
     * ---------------------------------------------
     * 真正写项目时,前后端交互的json格式要固定,一般
     * {
     *     code:200,
     *     msg:"xxx",
     *     data:{}
     * }
     * --- 那么就会在java项目中定义该类型的类R
     * --- 前端拿到json后,会获取其中数据
     * if(json.code == 200) {
     *     json.data
     * } else {
     *     alert(json.msg)
     * }
     */


    @GetMapping("/json")
    @ResponseBody // 该注解就会把响应的数据当json返回给前端
    public String testJson(){
        /**
         * json格式
         * {k:v,k:v}
         * {"id":1,"username":"zs"}
         */
        String jsonStr = "{\"id\":1,\"username\":\"zs\"}";
        return jsonStr;
    }

    @GetMapping("/json2")
    @ResponseBody 
    public User testJson2(){
        // 假设这是从数据库查出的数据,并且封装成对象
        User user = new User( );
        user.setId(2);
        user.setScore(1.1);
        user.setPassword("123456");
        user.setUsername("老王");
        user.setBirthday(new Date(  ));
        /**
         * {
         *     id:2,
         *     score:1.1,
         *     password:"123456",
         *     username:"老王",
         *     birthday:
         * }
         */
        return user;
    }

    @GetMapping("/json3")
    @ResponseBody 
    public Map<String, Object> testJson3(){
        HashMap<String, Object> map = new HashMap<>( );
        map.put("id",1);
        map.put("username","老李");
        map.put("password","123456");
        map.put("score",2.2);
        map.put("birthday",new Date(  ));
        return map;
    }


    @GetMapping("/json4")
    @ResponseBody 
    public List<User> testJson4(){
        ArrayList<User> list = new ArrayList<>( );
        list.add(new User());
        list.add(new User());
        list.add(new User());
        /**
         * [
         *  {},
         *  {},
         *  {}
         * ]
         */
        return list;
    }
}

以上这些响应的json,但是格式不统一,真正开发时,是团队协作开发,前后端交互的json数据的格式要统一! 格式一般如下

{
 code:20000,
 msg:"成功|失败",
 data:{}
}
  • code是响应的状态码,自己公司定义
    • 2000 成功
    • 4000 失败
    • 5000 连接超时
    • 3000 未登录
  • msg是响应的提示信息
  • data是后端返回给前端的数据

前后端交互,定义的类,用于统一返回封装数据返回JSON

package com.qf.util;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc R类是用于前后端交互,返回json时固定格式
 *
 * 命名为R,是随意的,只不过有几个常见命名
 * R,ResultObject,ReturnObject
 */
public class R {

    /**
     * code,指状态码,
     *     随意定,20000 是正确,40000 错误
     *     50000 请求超时
     *     60000 没有权限
     * msg,指信息描述
     * data,返回的数据
     */
    private int code;
    private String msg;
    private Object data;


    public static R ok(){
        R r = new R( );
        r.setCode(20000);
        r.setMsg("成功");
        return r;
    }

    public static R ok(Object data){
        R r = new R( );
        r.setCode(20000);
        r.setMsg("成功");
        r.setData(data);
        return r;
    }


    public static R fail(){
        R r = new R( );
        r.setCode(40000);
        r.setMsg("失败");
        return r;
    }


    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}
    /**
     * 演示5: 统一json格式,返回R
     */
    @GetMapping("/json5")
    @ResponseBody
    public R json5(){
        // 模拟登录成功,返回一个对象
        // R r = new R( );
        // r.setCode(2000);
        // r.setMsg("登录成功");
        // r.setData(new User());

        // 模拟查询全部,
        // R r = new R( );
        // r.setCode(2000);
        // r.setMsg("查询全部数据成功");
        // ArrayList<String> list = new ArrayList<String>( );
        // list.add("北京");
        // list.add("上海");
        // list.add("广州");
        // r.setData(list);

        // 现在发现,因为要同一格式返回,所以每次返回都需要设置R对象,以及其属下,很麻烦
        // 想办法简化! 提取成工具方法
        return R.ok(3000,"登录成功",new User());
    }

补充: 如果该类中所有方法都返回json,那就需要在每个方法上都要加@ResponseBody注解,有点麻烦,此时可以直接将@Controller换成@RestController, 以后方法默认返回json,就不需要加@ResponseBody

// @Controller
// @ResponseBody // 整个类中的所有方法,都返回json
// 以上两个注解,可以简写成一个
@RestController
public class Demo5Controller {
}

5.7 拦截器

使用步骤,与Servlet中的拦截器思路基本一致

  • 编写自定义拦截器类
  • 实现接口 HandlerInterceptor
  • 重写拦截方法 preHandler()
  • 配置拦截器
    • 这个不一样,以前是配置在web.xml中或者加上注解@WebFilter
    • 现在SpringBoot推荐使用java类的方式配置
      • 自己定义一个配置类,实现WebMvcConfigurer接口,重写方法addInterceptors
      • 另外,该配置类上加了一个非常重要的注解 @Configuration

自定义拦截器类

package com.qf.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc 拦截器,
 *   一般只会重写preHandle方法
 */
@Component
public class MyInterceptor implements HandlerInterceptor {

    /**
     * 前置拦截器(目标处理器前执行此方法)
     * 一般做前置校验/编码格式设置
     *
     * @param request 请求对象
     * @param response 响应对象
     * @param handler 目标处理器(目标Controller)
     * @return 布尔值,true是放行,false是不放行
     * @throws Exception 目标Controller的异常
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("--------preHandle执行--------" );
        // 可以通过Request对象获得session,进行登录身份信息验证
        System.out.println("目标Controller:" + handler );
        System.out.println("--------preHandle结束--------" );
        return true;
    }

    /**
     * 目标处理器执行完后执行此方法
     *
     * 是在目标方法正常执行后执行,目的是对目标方法返回的数据和页面进行再处理
     * 但是,现在前后端分离后,返回的JSON,没有ModelAndView,
     * 所以这个将来一般不用重写
     *
     * @param handler 目标Controller
     * @param modelAndView 目标Controller返回的数据
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("--------postHandle执行--------" );
        System.out.println("目标Controller:" + handler );
        System.out.println("modelAndView:" + modelAndView );
        System.out.println("--------postHandle结束--------" );
    }


    /**
     * 目标处理器执行完后执行此方法
     *
     * 目标方法无论有无报错,都会执行
     *  如果目标方法没有报错,参数ex是null
     *  如果目标方法报错,参数ex是就是异常信息
     * 因为这个方法afterCompletion,目标方法无论有无报错,都会执行,所以呢
     * 一般用于后续"打扫战场",类似于finally,处理一些资源关联等.....
     *
     * @param ex 目标方法如果有异常,这个就是异常对象
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("--------afterCompletion执行--------" );
        System.out.println("目标Controller:" + handler );
        System.out.println("目标Controller的异常:" + ex );

        System.out.println("--------afterCompletion结束--------" );
    }
}

拦截器配置类

package com.qf.interceptor;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
@Configuration // 这个注解,让springboot框架知道,以下的这个类是提供配置
public class MyWebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private MyInterceptor myInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(myInterceptor)
                // addPathPatterns 定义拦截的路径
                // excludePathPatterns 定义放行的路径
                //.addPathPatterns("/ljq");
                .addPathPatterns("/*")
                .excludePathPatterns("/req1","/req2","/receive*");
    }
}

发请求测试即可

5.8 全局异常处理

SpringBoot中有一个**@ControllerAdvice的注解,使用该注解表示开启了全局异常的捕获,我们只需在自定义一个方法使用@ExceptionHandler**注解然后定义捕获异常的类型即可对这些捕获的异常进行统一的处理。

package com.qf.util;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @date 2024/7/4
 * @desc 全局处理异常
 */
@ControllerAdvice // 对控制层异常的处理
public class MyExceptionHandler {

    /**
     * 自己定义方法,完成处理异常的逻辑
     * @ExceptionHandler这个注解,默认能处理(发现)任意类型异常
     *
     * 如果注解中,设置了异常类型,那就只处理指定异常
     *
     * 返回值可以是页面路径
     */
    // @ExceptionHandler(NullPointerException.class) // 只处理NullPointerException异常
    // @ExceptionHandler // 处理所有异常
    // public String handlerException(Exception ex){
    //     System.out.println("出错啦!" + ex.getMessage());
    //     return "404.html";
    // }

    // 也可以返回给前端JSON
    @ExceptionHandler
    @ResponseBody
    public R handlerException(Exception ex){
        System.out.println("出错啦!" + ex.getMessage());
        return R.fail();
    }
}

测试

发请求,在Controller中故意写错代码,测试即可

5.9 文件上传

图片上传

上传tomcat服务器(今天演示)/上传本地磁盘/专业的文件存储服务器(阿里OSS,腾讯OSS,七牛云存储)

前端

<h2>文件上传</h2>
<!--
    文件上传必须是post请求
    修改表单发送数据的类型为文件类型
-->
<form action="/upload" method="post" enctype="multipart/form-data">
    文件<input type="file" name="img"><br>
    <input type="submit" value="上传"><br>
</form>

后端: 上传到tomcat服务器上

        <!-- 使用工具类 -->
		<dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>
@PostMapping("/upload")
    public R upload(MultipartFile img, HttpServletRequest request) throws IOException {

        /**
         * img中有前端发的那个文件对象全部信息
         * 思路:
         * 获得原始文件名
         * 重命名文件名
         * 确定Tomcat位置,创建文件夹单独存储图片
         * 开始上传
         */
        // 1.获得上传的对象 参数img就是文件对象

        // 2. 获得最终上传的目的地路径(上传至服务器中当前项目下)
        // 通过servlet方法获得路径,即最终上传到Tomcat的/upload
        String realPath = request.getServletContext().getRealPath("/upload");
        System.out.println("服务器中存储图片的真实路径: "+realPath);
        // 2.1 将最终目的文件夹创建出来
        File destPath = new File(realPath);
        // 判断该文件是否存在
        if(!destPath.exists()) {
            // 不存在则创建出
            destPath.mkdir();
        }

        // 2.2 获得文件名
        // 获得原始文件名
        String fileName = img.getOriginalFilename();
        System.out.println("原始文件名:" + fileName);
        /*
         例如:  美女.jpg
         * 根据.拆分字符串,获得文件后缀名
         * ["美女","jpg"]
         */
        String[] split = fileName.split("\\.");
        System.out.println("拆分后的文件信息:" + Arrays.toString(split));
        String suffix = split[split.length - 1];
        // 以当前毫秒值为文件名
        long prefix = System.currentTimeMillis();
        // 组装文件名 123424235.jpg
        String newFileName = prefix+"."+suffix;
        System.out.println("新的文件名 : "+newFileName);

        // 2.3 确定上传路径
        //         // xxx/tomcat/upload/234234324.jpg
        File finalFile = new File(destPath,newFileName);

        // 3. 用工具上传
        FileUtils.writeByteArrayToFile(finalFile, img.getBytes());

        // 4 返回路径,测试使用,放查看是否上传成功
        // http://localhost:8080/upload/1720064896763.png
        String path = "http://localhost:8080/upload/"+newFileName;
        System.out.println(path);

        /**
         * 后续写项目时,还有第5步,即将图片路径存储到数据库
         * 将来查询时查出,前端根据路径展现图片
         */
        return R.ok(path);
    }

因为是上传到服务的,上传后可以通过网络访问到图片

image-20231127153009777

特殊的,上传有文件大小限制(默认是1MB),可以通过改变SpringBoot配置文件application.properties

spring.servlet.multipart.max-file-size=10Mb  
spring.servlet.multipart.max-request-size=10Mb

或者application.yml文件

spring:
    servlet:
       multipart:
         max-file-size: 10MB
         max-request-size: 10MB

六、Mybatis

6.1 之前的JDBC的使用缺点?

  • 大量的代码重复
  • 手动加载驱动,创建连接(Connection),关流
  • 封装数据麻烦(ORM)
  • 效率不高(没有缓存)

6.2 Mybatis的介绍

官网: mybatis – MyBatis 3 | Introduction

image-20221214102653736

MyBatis本是apache的一个开源项目iBatis,2010年这个项目由apache software foundation迁移到了google code,并且改名为MyBatis。2013年11月迁移到Github

iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层(Dao)框架。用于操作数据库。


MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。且有缓存机制,可以提高查询效率。


Mybatis是一个半ORM框架,可以消除JDBC的代码和步骤,让开发者只关注SQL本身。

ORM是对象关系映射,是指数据库表和java实体类一一对应.

半ORM框架,还是需要写SQL,由框架帮你完成映射

完全ORM框架,连SQL都不需要写,只需要遵循ORM的要求,就会自动生成SQL完成映射(Hibernate,JPA,MybatisPlus等)


6.3 xml方式整合Mybatis[重点]

xml方式在编写复杂SQL时,更适合

6.3.1 环境

mybatis和druid的springboot环境下的依赖(别忘了web依赖)

<!-- 小辣椒 -->
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<optional>true</optional>
</dependency>
<!-- mysql驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>

<!-- druid 数据库连接池 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>

<!-- mybatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>

安装小辣椒插件

image-20230821175201225

image-20230825145211290

准备数据库

CREATE TABLE `tb_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
  `username` varchar(10) DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  `phone` varchar(11) DEFAULT NULL COMMENT '手机号',
  `create_time` date DEFAULT NULL COMMENT '注册时间',
  `money` double(10,2) DEFAULT NULL COMMENT '账户余额',
  `sex` int(1) DEFAULT NULL COMMENT '性别 1男2女',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8

实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User {

    private int id;
    private String username;
    private String password;
    private String phone;
    private Date createTime;
    private double money;
    private int sex;
}

6.3.2 编写接口和映射文件

接口就是我们之前的Dao层接口,Mybatis习惯叫做Mapper,所以先创建mapper包,然后再在其中创建接口文件

public interface UserMapper {
   User findUserById(int id)
}

以前是写接口的实现类,现在mybatis的接口实现功能由xml文件来实现了

在resources/下创建mapper文件夹,在其内创建mapper映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace名称空间 -->
<!-- 用来关联,映射文件(XML)和接口文件,即接口的类路径 -->
<mapper namespace="com.qf.mapper.UserMapper">

    <!--
        select标签,用于执行查询语句
        id: 是接口中的方法名
        resultType: 返回的结果类型,是接口对应方法的返回值
        [现在只写User报错,要写完整类路径]
         ==========================
        代码执行时,调用接口方法findUserById时传入的参数,就会赋值给#{}内的id
        执行完,查询返回结果集会自动封装到resultType指定的对象中
     -->
    <select id="findUserById" resultType="com.qf.entity.User">
        <!-- #{id} 就相当于是之前的预处理的 ?,会自动给此处复制  -->
        <!-- 其实就是接口方法的参数列表的值,会传给 #{id} -->
        select * from tb_user where id = #{id}
    </select>
</mapper>

6.3.3 yml文件

# 连接数据库的信息
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://ip:3306/数据库名?serverTimezone=UTC
    username: root
    password: 123456
    # 数据库连接池
    type: com.alibaba.druid.pool.DruidDataSource
# mybatis配置
mybatis:
  # 扫描映射文件
  mapper-locations: classpath:mapper/*.xml
  # 配置别名扫描的包
  type-aliases-package: com.qf.entity
  configuration:
    # 开启驼峰映射配置
    map-underscore-to-camel-case: true    
# 打印执行过程的sql信息
logging:
  level:
    com.qf.mapper: DEBUG    

6.3.4 扫描mapper

主类扫描mapper

@SpringBootApplication
@MapperScan("com.qf.mapper") // 扫描mapper接口,创建代理对象
public class TestSpringbootApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestSpringbootApplication.class, args);
    }
}

6.3.5 测试

创建controller,service ,按以前三层架构,依次调用即可

// Controller

@RestController
public class TestMybatisController {

    // todo: 这里省略了service,也可以调用service,由service掉mapper
    // 别忘了,业务层加@Service
    @Autowired
    private UserService userService;

    // http://localhost:8080/find?id=1
     @GetMapping("/findUserById")
    public R findUserById(int id) {
        User user = userService.findUserById(id);
        return R.ok(user);
    }
}

// 业务层(接口+实现类)

public interface UserService {
    User findUserById(int id);
}


@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User findUserById(int id) {
        User user = userMapper.findUserById(id);
        return user;
    }
}

// mapper在上面写过了…

启动项目,发出请求localhost:8080/findUserById?id=12即可


完整项目结构

image-20240704161536518

6.4 CRUD[核心]

6.4.1 查询

6.4.1.1 单条件查询

略,详情看入门演示

【特别注意:ORM时字段名要和实体类属性一致,否则封装失败】

<select id="findUserById" resultType="com.qf.model.User">
    <!-- #{id} 就相当于是之前的预处理的 ?,会自动给此处赋值  -->
    <!-- 其实就是接口方法的参数列表的值,执行时会传给 #{id} -->
    <!-- 查询返回的结果集,会自动封装到resultType指定的对象!! -->
    <!-- 但是ORM能自动封装有个前提: 查询返回的列名和实体类的属性名要完全一致-->
    <!--  这个就不会封装成功,因为列名uid,uname,pwd,tel 和User类中属性名不一致
        select id uid,username uname,password pwd,phone tel,create_time,sex,money from tb_user where id = #{id}
-->
    select id,username,password,phone,create_time,sex,money from tb_user where id = #{id}
</select>
6.4.1.2 查询全部

设计查询接口方法

public interface UserMapper {
    User findUserById(int id)
    List<User> findAll();
}

映射文件

    <!-- 一个标签,就是一个SQL执行的语句 -->
    <!-- 【注意】虽然查询返回集合,但是返回类型此处还要写集合中存储的类型 -->
    <!-- 【或者这样理解】虽然返回集合,此处定义的是查询返回要封装的实体类类型 -->
    <select id="findAll" resultType="com.qf.model.User">
        select * from tb_user
    </select>
6.4.1.3 多参数查询

需求: 通过用户名和密码查询

接口方法

public interface UserMapper {
    User findUserByLogin(String username,String password);
}

映射文件

<select id="findUserByLogin" resultType="com.qf.model.User">
    <!-- 默认是不支持传多个参数,传入多个参数时,需要如下操作 -->
    <!--
            方案1: #{}内按顺序写param1,param2,....或者agr0,arg1
             但是此种方式不建议
        -->
    select * from tb_user 
    where username = #{param1} and password = #{param2}
</select>



接口方法(参数加注解)(解决方案2)(推荐)

public interface UserMapper {

    User findUserByLogin(@Param("username") String username, @Param("password") String password);

}

映射文件

    <select id="findUserByLogin" resultType="com.qf.model.User">
        <!-- 默认是不支持传多个参数,传入多个参数时,需要如下操作(2选1) -->
        <!--
            方案2: 1)在接口方法参数前添加注解@Param并设置注解参数
                  2)在#{}内写注解中的值
        -->
        select * from tb_user 
        where username = #{username} and password = #{password}
    </select>
6.4.1.4 Map参数查询

需求: 查询时,就要传递分页数据,又要传递模糊查询关键词,此时就可以使用Map来封装参数(即请求中参数有点多…)

接口方法

public interface UserMapper {
    User findUserByLoginMap(HashMap<String,Object> map);
}

映射文件

<select id="findUserByLoginMap" resultType="com.qf.model.User">
    <!-- 参数是Map,#{}内写的map的key -->
    select * from tb_user 
    where username = #{usernameKey} and password = #{passwordKey}
</select>

6.4.2 增加

需求: 如下场景

image-20240705092134649

注意: 前端发送的表单数据,后端Controller要使用对象接收!!!

页面(resources/static/index.html)

<h2>添加</h2>
<form action="/add" method="post">
    用户名<input type="text" name="username"><br>
    密码<input type="password" name="password"><br>
    手机号<input type="text" name="phone"><br>
    日期<input type="date" name="createTime"><br>
    余额<input type="text" name="money"><br>
    性别<input type="radio" name="sex" value="1"><input type="radio" name="sex" value="2"><br>
    <input type="submit" name="提交">
</form>

提示: 后端springmvc使用User对象封装数据时,日期属性createTime要加注解@DateTimeFormat

Mapper接口方法

public interface UserMapper {
    int addUser(User user);
}

Mapper映射文件

    <!--
        执行插入语句的标签是insert
        id 是方法名
        参数类型parameterType="" 可以省略
        没有指定返回类型,默认返回受影响行数
     -->
    <insert id="addUser">
        <!-- 对象参数,#{}内对象的属性名 -->
        insert into tb_user (username,password,phone,money,create_time)
        values (#{username},#{password},#{phone},#{money},#{createTime})
    </insert>

6.4.3 修改

需求场景: 如下

image-20240705094356160

更新用户,最终在sql语句是根据id更新,所以前端需要发到后端用户的id

但是id值,又不能随意更改,所以id值在前端隐藏的

前端页面

<h2>更新</h2>
<form action="/update">
  <!-- type属性指定hidden,即可将输入框隐藏 -->
  <input type="hidden" name="id" value="3"><br>
  用户名<input type="text" name="username"><br>
  密码<input type="password" name="password"><br>
  手机号<input type="text" name="phone"><br>
  余额<input type="text" name="money" > <br>
  时间<input type="date" name="createTime" > <br>
  <input type="submit" name="更新"><br>
</form>

接口方法

public interface UserMapper {
    int updateUser(User user); // 修改方法的参数是对象
}

映射文件

    <update id="updateUser">
        <!-- 对象参数,#{}内属性名 -->
        update tb_user set username=#{username},password=#{password},
        phone= #{phone},create_time=#{createTime},money=#{money},sex=#{sex}
        where id = #{id}
    </update>

6.4.4 删除

需求: 场景如下

image-20240705095431757

在前端,设置删除按钮,点击发出请求,请求中携带这条数据的id

页面

<h2>删除</h2>
<a href="/delete?id=8">删除id=8</a>

接口方法

    int deleteById(int id);

映射文件

    <delete id="deleteById">
        delete from tb_user where id = #{id}
    </delete>

6.5 ORM映射


6.5.1 MyBatis自动ORM失效

MyBatis只能自动维护库表”列名“与”属性名“相同时的一一对应关系,二者不同时,无法自动ORM。

自动ORM失效
image-20230601164912980

6.5.2 方案一:改动列名或者属性名

ORM映射失效原因就是因为,列名和属性名不一致,所以方案一,

  • 要么修改列名,让其与类属性一致
  • 或者列名取别名,让其与类属性一致
  • 再或者,改属性名与类名一致

6.5.3 方案二:结果映射(ResultMap - 查询结果的封装规则)

假设: 数据列也不修改,类属性也不修改,但是还要映射成功,怎么解决?

–> 手动进行列和属性的映射!!!

通过**< resultMap id=“” type=“” >映射**,匹配列名与属性名。

  <!--手动关联映射
        id属性是唯一标识,见名知意,type是最终要封装的对象
    -->
    <resultMap id="userResultMap" type="User">
        <!--id标签是映射主键列
            column是列名 , property是对象的属性名
        -->
        <id column="uid" property="id"/>
        <!--result标签是映射其他列-->
        <result column="uname" property="username"/>
        <result column="pwd" property="password"/>
        <result column="phone" property="phone"/>
        <result column="create_time" property="createTime"/>
        <result column="money" property="money"/>
        <result column="sex" property="sex"/>
    </resultMap>
    <!-- 使用手动映射,不再使用resultType,而是使用resultMap,其中输入的是上面的<resultMap>标签的id -->
    <select id="findUserByIdByResultMap" resultMap="userResultMap">
        select
            id uid,username uname,password pwd,phone,create_time,money,sex
        from
            tb_user
        where id = #{id}
    </select>

image-20240705105204313

总结

  • 当数据库的列和实体类属性不一致时,可以通过手动映射来完成
  • 手动关联映射,就不再使用resultType,而是使用resultMap,其中写resultMap标签的id

6.6 多表联查 【重点】

表关系: 一对一,一对多,多对多

  • 1vs1 丈夫表 --> 妻子表
  • 1 vs n 用户 --> 车辆/房产
  • n vs n 老师/商品 --> 学生/订单

多表联查的SQL

  • 内连接
    • select * from 表1 inner join 表2 on 表1.字段 = 表2.字段
    • select * from 表1, 表2 where 表1.字段 = 表2.字段
  • 外连接
    • select * from 表1 left join 表2 on 表1.字段 = 表2.字段
  • 子查询

6.6.1 OneToOne

需求: 实现一对一查询,查询订单以及对应的用户信息

数据: tb_user表, tb_order表

关系:

用户 —> 订单 (1 VS N) 一个用户有多个订单
订单 —> 用户 (1 VS 1) 一个订单只会属于一个人

tb_user表

CREATE TABLE `tb_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
  `username` varchar(10) DEFAULT NULL COMMENT '用户名',
  `password` varchar(10) DEFAULT NULL COMMENT '密码',
  `phone` varchar(11) DEFAULT NULL COMMENT '手机号',
  `money` double(10,2) DEFAULT NULL COMMENT '账户余额',
  `create_time` date DEFAULT NULL COMMENT '注册时间',
  `sex` int(11) DEFAULT 1 COMMENT '1男2女'
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8;

tb_order表

CREATE TABLE `tb_order` (
  `oid` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单编号',
  `order_time` datetime DEFAULT NULL COMMENT '订单时间',
  `order_desc` varchar(255) DEFAULT NULL COMMENT '订单详情',
  `uid` int(11) DEFAULT NULL COMMENT '关联用户id',
  PRIMARY KEY (`oid`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

INSERT INTO `tb_order` VALUES (1, '2022-11-17 15:06:29', '笔记本电脑', 1);
INSERT INTO `tb_order` VALUES (2, '2022-12-16 11:00:41', 'Cherry键盘', 1);
INSERT INTO `tb_order` VALUES (3, '2022-12-16 11:01:23', 'Logi鼠标', 2);

实体类

@Data
public class Order {

    private int oid;
    private Date orderTime;
    private String orderDesc;
    private int uid;
 	  
}

思考1: 查询订单以及关联的用户,sql怎么写?

select * from tb_order o,tb_user u where o.uid = u.id 

思考2: 这个sql结果如何映射封装到对象?

image-20231128162048547

**[重点]**但是上面的实体类(Order.java和User.java),只有订单信息或者只有用户信息,但是我们要查询的是既有订单又有用户! 上面的类就无法展现以及封装全部数据,所以需要扩展类(即包含Order又包含User)

@Data
public class OrderVO extends Order {
    private User user;
}

OrderMapper.java接口文件

public interface OrderMapper {
    OrderVO findOrderWithUserByOid(int oid);
}

OrderMapper.xml映射文件

    <resultMap id="orderWithUserResultMap" type="OrderVO">
        <!-- 封装查询主体Order: -->
        <id column="oid" property="oid"/>
        <result column="order_time" property="orderTime"/>
        <result column="order_desc" property="orderDesc"/>
        <result column="uid" property="uid"/>
        <!-- 一对一映射,需要封装关联的User对象 -->
        <!-- 一对一映射,需要特殊标签 association-->
        <!-- property="user" 是OrderVO类中的属性,javaType是user属性的类型 -->
        <association property="user" javaType="com.qf.entity.User">
            <!-- 下面正常的列和属性 一一映射 -->
            <id column="id" property="id"/>
            <result column="username" property="username"/>
            <result column="password" property="password"/>
            <result column="phone" property="phone"/>
            <result column="money" property="money"/>
            <result column="create_time" property="createTime"/>
            <result column="sex" property="sex"/>
        </association>
    </resultMap>

    <!-- 多表联查,直接返回resultType无法封装关联的那个对象,就需要使用     
     resultMap手动映射 -->
    <select id="findOrderWithUserById" resultMap="orderWithUserResultMap">
        SELECT
            o.*,
            u.*
        FROM
            tb_order o,
            tb_user u
        WHERE
            o.uid = u.id
        AND o.oid = #{oid}
    </select>

测试

OrderController 调用 OrderService
OrderService  调用 OrderMapper

6.6.2 OneToMore

需求: 一对多,查询用户关联查询出所有的订单

SELECT
	* 
FROM
	tb_user u
LEFT JOIN tb_order o ON u.id = o.uid 
WHERE
	u.id = 1

目的查询用户,以及关联多个订单,User类不够展现全部数据,那么就创建扩展类UserVO,UserVO类继承User就可以存储用户信息,还需要再UserVO类中添加Order类来存储信息,但是!!不是一个Order类,因为是一对多,一个用户关联多个订单,所有要设置List<Order>

User扩展实体类

@Data
public class UserVO extends User{

    private List<Order> orderList;

}

UserMapper.java接口

public interface UserMapper {
    UserVO findUserWithOrdersById(int id);
}

UserMapper.xml映射文件

    <!-- 一对多 -->
    <resultMap id="userWithOrdersResultMap" type="UserVO">
        <!-- 封装User对象 -->
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="phone" property="phone"/>
        <result column="create_time" property="createTime"/>
        <result column="money" property="money"/>
        <result column="sex" property="sex"/>
        <!-- 一对多关联映射使用collection标签 -->
        <!-- property是UserVO类中关联的属性 -->
        <!-- 不是javaType,是ofType,是指定集合中存储的数据类型 -->
        <collection property="orderList" ofType="com.qf.entity.Order">
            <id column="oid" property="oid"/>
            <result column="order_time" property="orderTime"/>
            <result column="order_desc" property="orderDesc"/>
            <result column="uid" property="uid"/>
        </collection>
    </resultMap>

    <!-- 多表联查,另外的属性不会自动封装,需要使用resultMap -->
    <select id="findUserWithOrdersById" resultMap="userWithOrdersResultMap">
        SELECT
        	*
        FROM
        	tb_user u
        LEFT JOIN tb_order o ON u.id = o.uid
        WHERE
        	u.id = #{id}
    </select>

6.6.3 关联查询总结

  • 实体类要设置扩展类以用于封装多表的数据

    • 一对一,类中套类

    • 一对多,类中套集合

  • 正常封装使用resultMap

    • 一对一封装使用association
    • 一对多封装使用collection

6.7 动态SQL【重点】

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。


自己话理解: 帮助我们拼接SQL

常见的动态SQL语法

  • SQL片段(官方不是在动态SQL章节)
  • where , if
  • set
  • foreach
  • trim
  • choose-when-otherwise

6.7.1 SQL片段

这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。

自己的话: 减少代码重复,主要用于抽取字段,表名等

    <!-- 将重复的SQL代码抽取成SQL片段,以供复用 -->
    <sql id="userFields">
        id,
        username,
        password,
        phone,
        create_time,
        money,
        sex
    </sql>

    <select id="findAll" resultType="User">
        select
            <!-- 引入片段 -->
            <include refid="userFields"/>
        from
            tb_user
    </select>

6.7.2 if

if就是用来判断,主要用于判断要不要拼接对应的条件语句

-- 需求:查询用户,条件是money=1000,如果密码不为空,也根据密码查
select * from tb_user where money = 1000
select * from tb_user where money = 1000 and password= '123456'

UserMapper.java接口方法

public interface UserMapper {
    /**
     * 演示if动态sql
     */
    List<User> findByMap(HashMap<String,Object> map);

}

UserMapper.xml

   <!--演示单独使用if-->
    <select id="selectByIf" resultType="com.qf.entity.User">
        select
            <include refid="userFields"/>
        from
            tb_user
        where
            money = #{money}
        <!-- 如果成立,就拼接内部sql
            password是map的key
            这里面的并列条件,写法是and单词
         -->
        <if test="password != null and password != ''">
            and password = #{password}
        </if>
    </select>

测试

	/**
     * 动态sql,单独演示if
     */
    @GetMapping("/if")
    public R showIf(@RequestParam HashMap<String,String> map) {
        System.out.println("showIf()中接收map" + map );
        List<User> list = userService.selectByIf(map);
        return R.ok(list);
    }

注意: 通过application.yml添加日志,显示sql. 通过打印的sql来判断是否拼接成功

6.7.3 where [重点]

如果说只有if,可能会出现这么一种情况,就是如果没有条件,where不知道如何拼接

SELECT * FROM tb_user WHERE

多出一个where关键词!!


所以我们需要一个智能的,有条件时帮我们拼接where关键词,没有条件查询时,不拼接where,并且去掉多余的and关键词,但是不会主动帮助拼接and

	<!--演示where+if-->
    <select id="selectByWhereIf" resultType="com.qf.entity.User">
        select
            <include refid="userFields"/>
        from
            tb_user
        <where>
            <if test="username != null and username != ''">
                and username = #{username}
            </if>
            <if test="password != null and password != ''">
                and password = #{password}
            </if>
            <if test="phone != null and phone != ''">
                and phone = #{phone}
            </if>
        </where>
    </select>

所以一般会where和if一起用

6.7.4 set

用于动态更新语句的类似解决方案叫做 setset 元素可以用于动态包含需要更新的列,忽略其它不更新的列

UserMapper.java接口方法

public interface UserMapper {
    int updateUserBySet(User user);
}

UserMapper.xml

    <!-- set完成动态更新 -->
    <update id="updateUserBySet">
        update tb_user
        <!-- set标签自动拼接SET关键词 -->
        <set>
            <!-- 会自动过滤最后一个, -->
            <!-- 特别注意,因为判断条件是!=null,基本不可能为null,所以将基本类型变为包装类 -->
            <if test="username != null and username != ''">
                username = #{username},
            </if>
            <if test="password != null and password != ''">
                password = #{password},
            </if>
            <if test="phone != null">
                phone = #{phone},
            </if>
            <if test="createTime != null">
                create_time = #{createTime},
            </if>
            <if test="money != 0.0">
                money = #{money},
            </if>
             <if test="sex != null">
                sex = #{sex},
            </if>
        </set>
        where id = #{id}
    </update>

测试


6.7.5 foreach

场景: 批量删除

delete from tb_user where id in (1,2,3,...);
int[] idsArr = [1,2,3,4,5];
String sql = "delete from tb_user where id in (";
int iMax = idsArr.length - 1;// 最大下标
for (int i = 0; i < idsArr.length; i++) {
  int id = idsArr[i];
  sql += id;
  if (i != iMax) {
      sql += ",";
  } else {
      sql += ")";
  }
}

UserMapper.java

public interface UserMapper {
    // 为了演示动态sql foreach
    int deleteBatch(List<Integer> ids);
}

UserMapper.xml

    <!-- 动态sql foreach -->
    <delete id="deleteBatch">
        delete from tb_user
        where id in
        <!--
           <foreach>开始循环,取出集合中的数据
           collection,要遍历的集合,此处必须写list
           item , 遍历得到结果,命名任意,但是下面#{}内的名字要和这里一致
        -->
        <foreach collection="list" item="id" open="(" separator="," close=")">
            #{id}  
        </foreach>
    </delete>

测试

    // 批量删除
    @GetMapping("/delete/batch")
    // http://localhost:8888/delete?id=1&id=2&id=3
    public R deleteBatch(@RequestParam List<Integer> list){
        int i = userService.deleteBatch(list);
        if (i > 0) {
            return R.ok(i);
        }
        return R.fail();
    }

6.8 SpringBoot整合分页助手

关于分页有些数据

  • 默认访问首页,即默认当前页是 pageNum= 1
  • 数据有总条数, total = select count(*)
  • 页面大小/每页展示多少条数据, pageSize = 10
  • 总页数 , pageCount = total / pageSize (需要注意除不尽情况)

-- total共7条
select count(*) from tb_user
-- 每页多少条数据: pageSize 3条
-- 总页数pageCount
pageCount = total % pageSize == 0? total/pageSize :(total/pageSize)+1
-- 当前页pageNum=1
-- 查询第1页
select * from tb_user limit 0,3
-- 查询第2页
select * from tb_user limit 3,3

-- 查询第pageNum页
select * from tb_user limit (pageNo-1)*pageSize,pageSize
-- 查询第3页面
select * from tb_user limit 6,3

现在使用的是分页助手-pagehelper

  • 原理拦截sql,帮助我们拼接limit

6.8.1 导入依赖

<!-- pageHelper依赖-->
<dependency>
	<groupId>com.github.pagehelper</groupId>
	<artifactId>pagehelper-spring-boot-starter</artifactId>
	<!-- 不能使用1.2.6版本,因为版本太低与springboot高版本不兼容,导致启动项目报错 -->
	<version>1.4.2</version>
</dependency>

SpringBoot会自动完成配置,我们直接写代码

6.8.2 测试使用

测试是在Controller中直接设置开启分页即可

特别注意!!! 无需改动sql,即不需要自己写limit,分页工具自己会拼接

    /**
     * 使用分页查全部
     * pageNum 当前页码
     * pageSize 页面大小
     * 这两个参数需要前端发请求带过来
     */
	@GetMapping("/m2")
    public R testMybatis2(int pageNum,int pageSize) {
        // 使用步骤
        // 1 先设置分页信息
        PageHelper.startPage(pageNum,pageSize);
        // 2 正常执行查询
        List<User> list = mapper.findAll( );
        // 3 通过查询返回的list创建出分页信息,PageInfo内包含所有分页数据,可以点入源码查看
        PageInfo<User> info = new PageInfo<>(list);
        System.out.println("当前面"+info.getPageNum());
        System.out.println("页面大小"+info.getPageSize() );
        System.out.println("总条数"+info.getTotal() );
        System.out.println("总页数"+info.getPages() );
        System.out.println("数据"+info.getList());
        return R.ok(info);
    }

注意,mapper层面还是之前的查询全部findAll,sql语句还是select * from tb_user,不需要写其他的

因为PageHelper插件会自动帮助拼接limit等关键词

6.9 事务

开启事务
执行sql
如果成功,提交事务
如果失败,回滚事务

方法加@Transactional 注解即可,一般加在业务层

    @Override
	@Transactional // 加上注解,该方法执行时就有事务控制
    public int deleteById(int id) {
        int i = mapper.deleteById(id);
        System.out.println(1/0 );
        // 无事务管理时,即使会报错抛异常,但是上面删除还会成功执行
        // 但是有事务管理时,如果有报错异常抛出,上面的删除会回滚回去
        return i;
    }

该注解加在业务层方法上,那么该方法被事务管理

如果加业务层类上,那么该类的所有方法被事务管理


补充: @Transactional注解的参数说明

事务的传播机制propagation

  • 传播机制: 并发访问时事务和事务之间的相互影响的机制
  1. REQUIRED(默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是最常见的传播行为,适用于大多数情况。
  2. SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式执行。适用于对事务无要求的情况,可与现有事务一起执行,也可独立执行。
  3. MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。用于要求调用方必须在事务中调用该方法。
  4. REQUIRES_NEW:每次都会创建一个新的事务,并且暂停当前事务(如果存在)。如果没有现有事务,则只是简单地创建一个新事务。用于要求每次调用都使用自己的独立事务。
  5. NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,则将其挂起。用于在不需要事务保证的情况下执行方法。
  6. NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。用于确保方法不在事务中执行。
  7. NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则创建一个新事务。嵌套事务是现有事务的一个真正子事务,可以独立提交或回滚,但是只有在外部事务提交时才能被提交。

隔离级别:

  • 读未提交 read_uncommitted
  • 读已提交 read_committed
  • 可重复读(mysql默认) repeatable_read
  • 串行化 serializable

image-20240709105605414

超时: 事务并发访问,如果有行锁,表锁等锁上数据,其他事务的等待时间

  • 默认以连接的数据源为准(mysql默认是50秒)

可以通过sql查询

show variables like ‘innodb_lock_wait_timeout’

只读:

指定异常回滚: 指定某个异常出现时才回滚

  • 默认的回滚异常是RuntimeException

6.10 缓存(cache)【面试】

缓存主要目的是为了提高查询效率.缓存其实就是一个内存空间,存储在程序的某个地方,存储查询结果数据.

mybatis支持缓存的,且有两级缓存

  • 一级缓存
  • 二级缓存
无缓存:用户在访问相同数据时,需要发起多次对数据库的直接访问,导致产生大量IO、读写硬盘的操作,效率低下
image-20230601165546620
有缓存:首次访问时,查询数据库,将数据存储到缓存中;再次访问时,直接访问缓存,减少IO、硬盘读写次数、提高效率
image-20230601165610966

6.10.1 一级缓存

MyBatis的一级缓存是默认的.无需配置,自动实现.

默认的一级缓存是SqlSession级别,是指同一个SqlSession发起的多次查询同一条数据,会使用缓存.

ps: SqlSession会话,就是一次正常的mybatis执行sql的过程

image-20221217112819746

ps: Mybatis内部存储缓存使用的是一个HashMap对象,key为 hashCode + sqlId + sql 语句。而value值就是从查询出来映射生成的java对象。

6.10.1.1 命中缓存
    @GetMapping("/user/{id}")
    @Transactional // 【重点】需要开启事务才会生效一级缓存,因为mysql默认每句话都是独立的事务,即每句话都是独立的SqlSession,那么就不符合一级缓存的要求
// 前端每次请求 到Mybatis都会重新创建SqlSession,即SqlSession不一样,不会命中缓存
    // 只有同一个请求中,查询多次会命中缓存
    public R findUserById(@PathVariable int id) {
        User user = userService.findUserById(id);// 第一次查
        System.out.println(user );
        System.out.println("-------------" );
        User user2 = userService.findUserById(id);// 第二次查
        System.out.println(user2 );
        return R.ok(user);
    }

image-20230822180407157

第二次直接从缓存取值,没有经过数据库查询,直接出结果

6.10.1.2 清空缓存

在更新(更新,删除,插入)数据后,会清空缓存,下次重新查最新的数据.避免脏读

    @GetMapping("/user/{id}")
    @Transactional
    public R findUserById(@PathVariable int id) {
        User user = userMapper.findUserById(id);// 查一次
        System.out.println(user );
        System.out.println("-------------------" );
        userMapper.deleteById(3);// 中间执行删除,会清空缓存
        System.out.println("-------------------" );
        User user2 = userMapper.findUserById(id);// 再查一次
        System.out.println(user2 );
        return R.ok(user);
    }

image-20230822180845189

6.10.2 二级缓存

二级缓存是Mapper级别,比SqlSession级别范围更大.

SqlSession级别是指,同一次的请求查询才会生效

Mapper级别是指,不论查询多少次,只要查询的是同一个Mapper中的同一个数据即可生效

  • 需要在mapper.xml中设置caceh即可

     <cache/>
    
    • 映射语句文件中的所有 select 语句的结果将会被缓存。
    • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
6.10.2.1 使用二级缓存
  • 1)需要在mapper中设置caceh即可

    image-20230822182357205

    <!-- 默认是指定下面类的,只不过可以省略-->
    <cache type="org.apache.ibatis.cache.impl.PerpetualCache"></cache>
                                      
    
  • 2)实体类需要系列化,实现Serializable接口,要不然会报错

    image-20230822182411220

   // 两次请求,查询的是同一个mapper下的同一个方法的同一个参数的同一条数据 
   @GetMapping("/one")
    public R findUserById(int id){
        User user = userService.findUserById(id);
        if (user != null) {
            return R.ok(user);
        }
        return R.fail();
    }

    @GetMapping("/one2")
    public R findUserById2(int id){
        User user = userService.findUserById(id);
        System.out.println(user );
        if (user != null) {
            return R.ok(user);
        }
        return R.fail();
    }

image-20230822182319152

6.10.2.2 清空缓存
  @GetMapping("/user/{id}")
    public R findUserById(@PathVariable int id) {
        User user = userMapper.findUserById(id);
        System.out.println(user );
        System.out.println("-------------------" );
        userMapper.deleteById(2);// 删除会清空缓存
        System.out.println("-------------------" );
        User user2 = userMapper.findUserById(id);
        System.out.println(user2 );
        return R.ok(user);
    }
什么是缓存?有什么好处?
mybatis有没有缓存?
一级 二级什么区别?
一级二级是如何设置缓存?

6.11 注解方式整合Mybatis[了解]

注解方式在编写配置简单,简单SQL推荐使用

create table `emp`  (
  `id` int auto_increment,
  `name` varchar(255),
  `age` int ,
  `birthday` date ,
  primary key (`id`)
) ;
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class Emp {
    private int id;
    private String name;
    private int age;
    private Date birthday;
}

6.11.1 Mapper接口

public interface EmpMapper {

    @Select("select * from emp")
    List<Emp> findAll();

}

6.11.2 添加Mybatis注解

针对增删改查:@Insert,@Delete,@Update,@Select

还是需要在启动类中添加@MapperScan注解

public interface EmpMapper {
    @Select("select * from emp where id = #{id}")
    public Emp getEmpById(int id); // 查一个

    @Insert("insert into emp (name,age,birthday) values (#{name},#{age},#{birthday})")
    public int insertEmp(Emp emp);// 增

    @Delete("delete from emp where id = #{id}")
    public int deleteEmpById(int id);//删

    @Update("update emp set name=#{name},age=#{age},birthday=#{birthday} where id=#{id}")
    public int updateEmpById(Emp emp);//改

}

6.11.4 测试,查看日志

@SpringBootApplication
@MapperScan("com.taotie.testspringboot.mapper")
public class TestSpringbootApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestSpringbootApplication.class, args);
    }
}
// ============================================
@RestController
public class TestMybatisController {
   
    @Autowired
    private EmpMapper empMapper;

    @GetMapping("/m2")
    public R testMybatis2() {
        List<Emp> list = empMapper.findAll( );
        return R.ok(list);
    }
}

6.12 #{}和${}

#{},是jdbc中的预处理语句,会将#{}地方变成?,然后再给?赋值,如果是数字直接赋值,如果是字符串会自动拼接引号

image-20240709092503204

${} 是jdbc中处理语句,是直接取值拼接,无论是数字还是字符串都是直接取值拼接,如下

image-20240709092746350


模糊查询,$的一个应用场景

<select id="findUserByLikeName" resultType="com.qf.entity.User">

    select
        <include refid="userFields"/>
    from
        tb_user
    <!--此时这样写,预处理语句会自动拼接引号,导致出现 '%'admin'%' 这样的情况,报错-->
    <!--where username like '%#{username}%'-->

    <!--方案1: 使用${},直接取值-->
    <!--where username like '%${name}%'-->

    <!--方案2: 使用#{}-->
    where username like concat('%',#{name},'%')
</select>

七、SpringBoot常用配置【重点


7.1 SpringBoot的配置文件格式

springboot是约定大于配置,几乎是不需要配置文件就可以帮助完成配置,

但是有些默认不好用的时候,也可以通过配置文件改变配置

比如: 端口,访问路径,文件上传大小,jdbc连接信息

SpringBoot的配置文件,文件名必须是application,格式支持propertiesyml

更推荐使用yml文件格式:

  1. yml文件,会根据换行和缩进帮助咱们管理配置文件所在位置

  2. yml文件,相比properties更轻量级一些

  3. K: V 表示一对键值对(冒号: 后一定有一个空格)

  4. 严格以空格的缩进来控制层级关系;只要是左对齐的都是属于一个层级的数据

  5. 属性和值大小写敏感.

yml文件的劣势:

  1. 严格遵循换行和缩进

  2. 在填写value时,一定要在: 后面跟上空格

配置文件的作用

  • 修改SpringBoot的配置的默认值:

    ​ 比如默认启动的Tomcat的端口是8080,可以修改为8081

propertiesyml
image-20230625220509355image-20230625220524979

image-20230823113117827

配置文件的位置:

  • 一般默认都是放在resources/下
  • 也有其他位置的,暂且不讨论

7.2 多环境配置[了解]

实际开发中,有三种环境:

1.开发环境dev-程序员日常开发所需(本地)

2.测试环境test-项目的集成测试

3.生产环境prod-最终项目部署的环境,真实环境

SpringBoot支持多环境的配置。只需要根据环境需要,编写多个配置文件,通过配置属性选择使用哪个环境

使用步骤:

1.多环境的配置文件命名:application-环境名.yml

2.在总文件application.yml中通过属性:spring.profiles.active: 环境名

image-20230625221539596

ps:也可在部署工程时,通过 java -jar jar文件 --spring.profiles.active=环境

7.3 获取配置文件数据

场景: 1) 加密盐值 2) 秘钥

解释: 将yml配置的值,赋值给对应的类

方案:

  • 方案一: @ConfigurationProperties
  • 方案二: @Value

方案一: @ConfigurationProperties [了解]

# 示例
aliyun:
 accessKey: ATYSBD23B1N44
 accessSecret: 123456
@Data
@Component
@ConfigurationProperties(prefix = "aliyun") // yml中的前缀
public class AliyunProperties {
    // yml中的key
    private String accessKey; 
    private String accessSecret;

}
// java代码中使用AliyunProperties对象即可获得数据
@RestController
public class TestController {

    @Autowired
    private AliyunProperties aliyun;


    @GetMapping("/yml")
    public AliyunProperties testGetYmlValue(){
 return aliyun;
    }

}

image-20230625222751082

方案二: @Value [推荐]

# 示例
aliyun:
 accessKey: ATYSBD23B1N44
 accessSecret: 123456
// 哪里需要获得yml中的数据,哪里只需要加@Value注解取值即可
@RestController
public class UserController {

    @Value("${aliyun.accessKey}")
    private String accessKey;
    
     @GetMapping("/user")
    public R findUserById(){
        System.out.println(accessKey );
        return R.ok();
    }

7.4 热加载

热加载/热部署 : 实现不停机更新代码

7.4.1 导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>

新版idea(2022版以后)在设置里找File->Settings->Advanced Settings中找到Compiler并勾选Allow auto-make to start even if developed application is currently running

测试,以Debug方式启动项目,改动代码按ctrl+F9 ,就会自动重启项目刷新

7.5 接口测试工具

前言: springboot项目一般都是前后端分离开发,即我们idea中只有后台代码,那么现在写完成controller接收请求,调用业务层处理业务,调用dao层操作完数据库之后如何测试?

  • 目前是使用浏览器,自己手动在地址栏输入请求路径和拼接参数;或者写一个html页面,其中写a标签或者form表单来设置数据和发送请求

  • 但是这样自己编写路径或者编写页面测试很麻烦! 那么接口测试工具(api测试工具)就是来做这个事情的,即帮助测试接口

在这里如何理解接口? 此处的接口是前后端对接的"接口",是一套完整接收请求处理请求返回结果的代码,即那就是controller-service-dao层

有哪些接口测试工具

  • postman
  • apipost
  • apifox
  • 以及各种插件

安装fast-request插件(不能用了)

  • 下载压缩包到本地
  • idea中plaguin从本地磁盘加载插件
  • image-20230828114155867

选择到本地那个zip文件

  • 点击右下角apply , 后再点ok

  • 此时idea中就可以使用

八、SpringBoot整合日志框架

8.0 日志

作用:

  1. 方便调试
  2. 记录运行信息
  3. 记录异常信息

现在如何实现记录日志的呢

  • sout 这个输出语句

弊端

  • 无论什么情况,只要到此处输出语句一定执行,不能有选择的可控输出
  • 只能输出到控制台
  • 信息不完整
  • 输出语句要删掉

8.1 日志框架

  • slf4j

slf4j 只是一个日志标准,并不是日志系统的具体实现。它用于提供日志操作的接口,提供获取日志对象的方法

  • log4j

apache 实现的一个开源日志组件

  • logback

相对于logback,有更好的特性,springboot默认使用logback

  • log4j2

是 log4j的升级版本,拥有更好的性能,支持异步日志

注意:slf4j属于日志接口,log4j、logback、log4j2属于日志实现

springboot默认使用logcak处理日志,本例中,使用log4j2处理日志

  • commons-logging

8.2 日志的等级

日志级别按照从低到高为:

ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF

程序会打印高于或等于所设置级别的日志,设置的**日志等级越高,打印出来的日志就越少**

All:最低等级,会输出所有日志记录

Trace:追踪,就是程序推进一下

Debug:调试日志

Info:消息日志,可用于输出应用程序的运行过程

Warn:输出警告级别的日志

Error:输出错误信息日志

Fatal:输出每个严重的错误日志.

OFF:最高等级的,用于关闭所有日志记录

8.3 整合Log4j2

8.3.0 依赖

Spring Boot默认使用LogBack,但是我们没有看到显示依赖的jar包,其实是因为所在的jar包spring-boot-starter-logging都是作为spring-boot-starter-web或者spring-boot-starter依赖的一部分。

如果这里要使用Log4j2,需要从spring-boot-starter-web中排除spring-boot-starter-logging依赖,同时显示声明使用Log4j2的依赖jar包,具体如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <!-- 去掉springboot默认配置 -->
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<!-- 去掉springboot默认配置 -->
	<exclusions>
		<exclusion>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-logging</artifactId>
		</exclusion>
	</exclusions>
</dependency>

再单独引入log4j2的依赖

<dependency> <!-- 引入log4j2依赖 -->
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

8.3.1 编写配置文件

在resources下面新建log4j2.xml,输入以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!-- status log4j2内部输出自身的日志信息的级别,可以不设置,没太大用 -->
<!-- configuration中主要包括有 Properties、Appenders、Loggers标签 -->
<Configuration status="fatal" monitorInterval="30">
    <!--打印在本地,根据具体存储地址填写 ./logs是当前项目名下的位置-->
    <Properties>
        <Property name="baseDir" value="./logs"/>
        <!--
			常见的配置如下:
        - %d{yyyy-MM-dd HH:mm:ss.SSS} : 日志生成时间,输出格式为“年-月-日 时:分:秒.毫秒”
        - %p : 日志输出格式
        - %c : logger的名称
        - %m : 日志内容,即 logger.info("message")
        - %n : 换行符
        - %T : 线程号
        - %L : 日志输出所在行数
        - %M : 日志输出所在方法名
        -->
        <Property name="pattern">%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p %c{1}:%L -%m%n</Property>
    </Properties>
    <!-- 输出源,常见的主要有Console、RollingFile、File 三种子节点
        Console:用于定义输出到控制台的Appender
        RollingFile:定义指定方式触发新的Appender
      -->
    <Appenders>
        <!-- 输出到控制台 -->
        <Console name="Console" target="SYSTEM_OUT">
          <PatternLayout>
                <pattern>${pattern}</pattern>
            </PatternLayout>
        </Console>

         <!-- 输出到本地磁盘,形成日志文件 -->
        <RollingFile name="debug_appender" fileName="${baseDir}/debug.log"
                     filePattern="${baseDir}/debug_%i.log.%d{yyyy-MM-dd}">
            <!-- 过滤器 -->
            <Filters>
                <!-- 限制日志级别在debug及以上在info以下 -->
                <ThresholdFilter level="debug"/>
                <ThresholdFilter level="info" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
            <!-- 日志格式 -->
            <PatternLayout>
                <pattern>${pattern}</pattern>
            </PatternLayout>
            <!-- 策略 -->
            <Policies>
                <!-- 每隔一天转存 -->
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <!-- 文件大小 -->
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
        </RollingFile>  
    </Appenders>
    <Loggers>
        <!-- 这个level可以控制输出的级别-->
        <Root level="ERROR">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="debug_appender"/>
        </Root>
    </Loggers>
</Configuration>

8.3.2 代码中使用日志

log4j2 的依赖包随着搭建springboot,自动导入,无需再导入

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
@RestController
public class TestLogController {
   // 导入包import org.apache.logging.log4j.LogManager;
   // 导入包import org.apache.logging.log4j.Logger;
    private Logger logger = LogManager.getLogger(TestLogController.class);

    @GetMapping("/log")
    public R log() {
        logger.debug("这是bug级别");
        logger.warn("我是警告");
        logger.info("我是消息");
        logger.error("我是错误!");
        return R.ok( );
    }
}

image-20230830134613238

image-20230830134742030

注意:实际开发中,不允许使用输出语句定位问题,需要采用debug要么就是日志

TODO: 特别注意!! 发现BUG 使用aliyun创建的SpringBoot按上面配置,无法完成这样的日志记录!!暂未解决

日志占位符

image-20231129195917686

九、SpringBoot整合Knife4j

9.1 接口文档

开发中一般写接口文档比较麻烦,需要定义访问路径,请求方式,请求参数,请求列表,请求参数实例,返回结果,返回结果类型,返回体结构,返回结构实例,返回参数列表,返回状态码列表等等等等等…

关键还会经常随着需求的更新而改变…

还有,就是这玩意儿真的手写起来太费劲,且不好分工,前端说这东西是后端写的,后端是这东西是前端写的…

这是一个手写的word版的接口文档…

接口文档1

9.2 接口文档工具

正是有这样那样的问题,才催生了这些接口文档工具,可以方便的生成接口文档,还可以当接口测试工具去测试,还可以导出成pdf,word,markdown, 太舒服了

常见的工具:

  • Swagger (丝袜哥er)
  • Knife4j (乃夫 for j)

它们之间的关系是:

Swagger是一个开源框架,用于设计、构建和文档化API。它提供了一组工具和规范,可以生成具有交互式界面的API文档。Swagger可以通过注解或配置文件来定义API的元数据,包括请求和响应的数据结构、参数、路径等。它还支持自动生成客户端代码和执行API测试。

Knife4j是一个基于Swagger的增强工具,为Swagger文档提供了更直观、美观和易于使用的界面。它通过自定义样式和感知能力来改进Swagger生成的文档。Knife4j提供了一些额外的功能,如开发者友好的文档展示、在线测试工具、接口权限管理等。它可以轻松集成到Spring Boot等框架中,提供更好的API文档展示和管理体验。

因此,Knife4j可以看作是Swagger的一个扩展和增强工具,通过提供更好的UI和功能来改进Swagger生成的API文档。它使用Swagger的核心功能和规范,并在此基础上进行了定制和改进,提供更好的用户体验和开发者工具。

swagger的界面

s1

s2

Knife4j的界面

k1

k2

9.3 整合Knife4j

9.3.1 依赖

        <!--  接口文档 -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>

9.3.2 yml配置

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://ip:3306/test_springboot?serverTimezone=UTC&useSSL=false
    username: root
    password: ***
    type: com.alibaba.druid.pool.DruidDataSource
  mvc:
    pathmatch:    # Springfox使用的路径匹配是基于AntPathMatcher的
                    # 所以需要配置此参数
      matching-strategy: ant_path_matcher

9.3.3 Controller接口加注解

@RestController
@RequestMapping("/api/dept/")
@Api(tags = "部门接口")
public class TestKnife4jController {

    @Autowired
    private DeptMapper mapper;

    @GetMapping("/list")
    @ApiOperation(value = "查询所有部门")
    public ResultData test(){
        List<Dept> list = mapper.findAll( );
        return ResultData.ok( list );
    }

    @GetMapping("/{id}")
    @ApiOperation(value = "根据部门编号查询部门")
    public ResultData findById(@PathVariable String id){
        Dept dept = mapper.findById(id);
        return ResultData.ok( dept );
    }

    @PostMapping("/save1")
    @ApiOperation(value = "插入部门-表单")
    public ResultData save1(Dept dept){
        mapper.save(dept);
        return ResultData.ok( );
    }

    @PostMapping("/save2")
    @ApiOperation(value = "插入部门-json")
    public ResultData save2(@RequestBody Dept dept){
        mapper.save(dept);
        return ResultData.ok( );
    }

}

9.3.4 配置

package com.qf.config;

import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
@Configuration // 开启配置
@EnableSwagger2 // 启动Swagger2
public class Knife4jConfiguration {

    @Bean
    public Docket defaultApi2() {
        String groupName = "1.0版本";
        Docket docket = new Docket(DocumentationType.OAS_30)
                // 是否启用Swagger
                .enable(true)
                .apiInfo(new ApiInfoBuilder()
                        .title("这是Taotie-Test-knife4j API ")
                        .description("这是项目描述")
                        .termsOfServiceUrl("服务器URL")
                        .contact(new Contact("饕餮", null, "qiushiju0828@163.com"))
                        .version("1.0")
                        .build())
                //分组名称
                .groupName(groupName)
                .select()
                // 这里指定Controller扫描包路径,没有加注解的接口方法也会生成接口文档
                .apis(RequestHandlerSelectors.basePackage("com.qf.controller"))

                // 这里指定只有加了注解的才会生成接口文档
                //.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
}

image-20240709171822705

9.3.5 启动项目,访问接口文档

http://localhost:8080/doc.html

image-20230627201725175

image-20230627201814062

可以测试

image-20230627201921140

image-20230627202113129

9.4 其他注解

  • @Api:修饰整个类,描述Controller的作用

    @Api(tags ="用户管理API")
    public class UserController {}
    
  • @ApiOperation:描述一个类的一个方法,或者说一个接口

    @ApiOperation(value="获取用户详细信息", notes="根据id来获取用户详细信息")
    public ResultData<User> findUserById(Integer id){}
    
  • @ApiModel:用对象来接收参数 ,修饰类

  • @ApiModelProperty:用对象接收参数时,描述对象的一个字段

    例如:

    @ApiModel(description = "用户实体类")
    public class User {
        @ApiModelProperty(name="id", value="用户id")
        private Integer id;
        @ApiModelProperty(value="用户姓名")
        private String name;
    
  • @ApiResponse:HTTP响应其中1个描述

  • @ApiResponses:HTTP响应整体描述,一般描述错误的响应

    // 针对响应状态 修饰方法
    @ApiResponses({
                @ApiResponse(code=500, message = "服务器异常")
        })
    
  • @ApiIgnore:使用该注解忽略这个API

    @ApiError :发生错误返回的信息

    @ApiParam:单个参数描述,用在控制器的方法上

    @ApiImplicitParam:一个请求参数,用在方法上

    @ApiImplicitParam(name = "id", value = "用户ID", required = true, dataType = "Integer", paramType = "path")
    
    @ApiImplicitParams({
                @ApiImplicitParam(),
                @ApiImplicitParam()
        })
    
  • 针对返回值,使用泛型表示

    @ApiModel
    public class R {
    
        @ApiModelProperty(value = "返回数据状态",notes = "200成功 500失败")
        private int code;
        private String msg;
        @ApiModelProperty(value = "返回数据",notes = "可以是具体的对象,也可以是null")
        private Object data;
    

十 总结

知识点重点

  • Spring
    • ioc , 控制反转,创建对象
      • @Controller,@Service,@Component
    • di, 依赖注入,属性赋值
      • @Autowired,@Value
  • SpringMVC - web层,控制层框架,主要功能是请求响应相关的代码
    • 如何请求匹配路径 @GetMapping @PostMapping @RequestMapping
    • 如何接收请求数据的.前端name和后端参数名一致/后端对象属性名一致
      • 特殊类型数据(List,Map)需要@RequestParam
    • 如何响应
      • 默认是返回String,值是页面字符串 例如: return “ok.html”
      • 前后端分离开发时,后端响应json数据,就在方法上加@ResponseBody注解
        • 如果所有方法都加@ResponseBody注解,那么我们就可以在类上把@Controller改成@RestController即可
  • mybatis - jdbc - 用来操作数据库的
    • 知道ORM --> 表/字段;类/属性
    • 知道编码流程,熟练crud
      • 实体类
      • Mapper接口,定义crud方法
      • Mapper映射文件,定义crud标签
      • 配置yml文件
      • 主类配置@MapperScan
    • 动态sql
      • 场景: 条件查询 where+if
      • 场景: 条件更新 set+if
      • 场景: 批量删除/插入 foreach
    • 多表联查

编码上熟悉架构

  • 熟悉springboot整合ssm需要的jar包/依赖
  • 熟系三层架构编码风格

其他东西都是锦上添花

  • spring-aop
  • springmvc的会话,拦截器,文件上传,全局异常
  • mybatis缓存,事务,注解开发,分页
  • SpringBoot 多环境切换,获得配置文件数据,热加载,接口工具
  • 整合日志框架
  • aop+自定义注解实现日志记录
    ectors;
    import springfox.documentation.service.Contact;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**

  • — 天道酬勤 —

  • @author QiuShiju

  • @desc
    */
    @Configuration // 开启配置
    @EnableSwagger2 // 启动Swagger2
    public class Knife4jConfiguration {

    @Bean
    public Docket defaultApi2() {
    String groupName = “1.0版本”;
    Docket docket = new Docket(DocumentationType.OAS_30)
    // 是否启用Swagger
    .enable(true)
    .apiInfo(new ApiInfoBuilder()
    .title("这是Taotie-Test-knife4j API ")
    .description(“这是项目描述”)
    .termsOfServiceUrl(“服务器URL”)
    .contact(new Contact(“饕餮”, null, “qiushiju0828@163.com”))
    .version(“1.0”)
    .build())
    //分组名称
    .groupName(groupName)
    .select()
    // 这里指定Controller扫描包路径,没有加注解的接口方法也会生成接口文档
    .apis(RequestHandlerSelectors.basePackage(“com.qf.controller”))

             // 这里指定只有加了注解的才会生成接口文档
             //.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
             .paths(PathSelectors.any())
             .build();
     return docket;
    

    }
    }


[外链图片转存中...(img-bBV5Bj2f-1720519034666)]



### 9.3.5 启动项目,访问接口文档

http://localhost:8080/doc.html

[外链图片转存中...(img-D1WgZLgo-1720519034667)]

[外链图片转存中...(img-YZR3qNFx-1720519034667)]

> 可以测试

[外链图片转存中...(img-uvbKzPYh-1720519034668)]

[外链图片转存中...(img-0XayQ0xs-1720519034668)]



## 9.4 其他注解

- @Api:修饰整个类,描述Controller的作用

  ```java
  @Api(tags ="用户管理API")
  public class UserController {}
  • @ApiOperation:描述一个类的一个方法,或者说一个接口

    @ApiOperation(value="获取用户详细信息", notes="根据id来获取用户详细信息")
    public ResultData<User> findUserById(Integer id){}
    
  • @ApiModel:用对象来接收参数 ,修饰类

  • @ApiModelProperty:用对象接收参数时,描述对象的一个字段

    例如:

    @ApiModel(description = "用户实体类")
    public class User {
        @ApiModelProperty(name="id", value="用户id")
        private Integer id;
        @ApiModelProperty(value="用户姓名")
        private String name;
    
  • @ApiResponse:HTTP响应其中1个描述

  • @ApiResponses:HTTP响应整体描述,一般描述错误的响应

    // 针对响应状态 修饰方法
    @ApiResponses({
                @ApiResponse(code=500, message = "服务器异常")
        })
    
  • @ApiIgnore:使用该注解忽略这个API

    @ApiError :发生错误返回的信息

    @ApiParam:单个参数描述,用在控制器的方法上

    @ApiImplicitParam:一个请求参数,用在方法上

    @ApiImplicitParam(name = "id", value = "用户ID", required = true, dataType = "Integer", paramType = "path")
    
    @ApiImplicitParams({
                @ApiImplicitParam(),
                @ApiImplicitParam()
        })
    
  • 针对返回值,使用泛型表示

    @ApiModel
    public class R {
    
        @ApiModelProperty(value = "返回数据状态",notes = "200成功 500失败")
        private int code;
        private String msg;
        @ApiModelProperty(value = "返回数据",notes = "可以是具体的对象,也可以是null")
        private Object data;
    

十 总结

知识点重点

  • Spring
    • ioc , 控制反转,创建对象
      • @Controller,@Service,@Component
    • di, 依赖注入,属性赋值
      • @Autowired,@Value
  • SpringMVC - web层,控制层框架,主要功能是请求响应相关的代码
    • 如何请求匹配路径 @GetMapping @PostMapping @RequestMapping
    • 如何接收请求数据的.前端name和后端参数名一致/后端对象属性名一致
      • 特殊类型数据(List,Map)需要@RequestParam
    • 如何响应
      • 默认是返回String,值是页面字符串 例如: return “ok.html”
      • 前后端分离开发时,后端响应json数据,就在方法上加@ResponseBody注解
        • 如果所有方法都加@ResponseBody注解,那么我们就可以在类上把@Controller改成@RestController即可
  • mybatis - jdbc - 用来操作数据库的
    • 知道ORM --> 表/字段;类/属性
    • 知道编码流程,熟练crud
      • 实体类
      • Mapper接口,定义crud方法
      • Mapper映射文件,定义crud标签
      • 配置yml文件
      • 主类配置@MapperScan
    • 动态sql
      • 场景: 条件查询 where+if
      • 场景: 条件更新 set+if
      • 场景: 批量删除/插入 foreach
    • 多表联查

编码上熟悉架构

  • 熟悉springboot整合ssm需要的jar包/依赖
  • 熟系三层架构编码风格

其他东西都是锦上添花

  • spring-aop
  • springmvc的会话,拦截器,文件上传,全局异常
  • mybatis缓存,事务,注解开发,分页
  • SpringBoot 多环境切换,获得配置文件数据,热加载,接口工具
  • 整合日志框架
  • aop+自定义注解实现日志记录
  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值