SpringBootV12

image-20230624230313370

ssm

spring --> applicationContext.xml配置文件

springmvc --> springmvc.xml配置文件

mybatis —> mybatis-config.xml配置文件

—> 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的配置文件,不推荐使用xml,后期会采用.java的文件去编写配置信息。
  • SpringBoot工程在部署时,采用的是jar包的方式,内部自动依赖Tomcat容器,提供了多环境的配置。
  • 后期要学习的微服务框架SpringCloud需要建立在SpringBoot的基础上。

三、SpringBoot快速入门


3.1 快速构建SpringBoot

3.1.1 选择构建项目的类型

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

注意:根据网络状况,可能会提示无法连接。如果不能连接,使用http://start.springboot.io(或者https://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 导入依赖

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

 <dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
     <optional>true</optional>
 </dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-test</artifactId>
     <scope>test</scope>
 </dependency>

3.1.4 编写了Controller

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

@Controller
public class TestController {

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

3.1.5 编写页面

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

image-20230817202540837

3.1.6 测试

主类点击启动项目

image-20230624233626353

效果
image-20230624233656863image-20230817202613225

3.2 SpringBoot的目录结构

3.2.1 pom.xml文件

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

        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.7.2</version>
            <relativePath/> 
        </parent>
    
  • 项目的元数据:包名,项目名,版本号。

       <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   				      # 只是为了测试用的

四、Spring

介绍…balabala

spring的核心功能

  • IOC,DI
  • AOP

4.1 IOC+DI[重点]

4.1.0 引言

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

public class LoginServlet extends HttpServlet{
    AdminService service = new AdminServiceImpl();
    void doGet(){
        service.findAdminByLogin(username,password);
    }
}

public interface AdminService{
    Admin findAdminByLogin(String username,String password);
}
public class AdminServiceImpl implements AdminService {
    Admin findAdminByLogin(String username,String password) {
     // ...   
    }
}

以上这样写有缺点:

1 LoginServlet类还是需要和AdminService和AdminServiceImpl耦合

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


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

4.1.1 介绍

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

控制: 控制创建对象的能力

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


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

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


创建对象的注解

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

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

  • @Autowired

4.1.2 演示1

需求: 项目中控制层servlet需要使用到业务层对象来处理业务,例如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;

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

}

练习: 在AdminService中使用AdminDao对象

4.1.3 演示2

演示@Component注解创建对象

假如有个类User,现在需要该类对象,就可以在该类上加上@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指定哪些类型(目标类)需要增强,就会产生对应的代理对象,代理对象执行方法前后会先执行增 强的方法.

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

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 演示

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

开发步骤

  • 创建切面类,类上加注解

    • @Component ,加上该注解,springboot框架就会创建该类对象
    • @Aspect , 加上该注解,springboot框架内部就会知道该类是一个切面类
  • 设置切入点方法,并加注解

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

    • 注解解释
      @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.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;
    }

    @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() {
        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());
    }
}

启动项目,测试

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

image-20230819185329456

4.2.5 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;

日志注解文件

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 = "";
        // 获得目标方法名
        String methodName = joinPoint.getSignature( ).getName( );
        System.out.println("---------->" + methodName );// 目标方法名
        // 根据目标方法所在的对象得到该类的全部方法
        Method[] methods = joinPoint.getTarget().getClass().getDeclaredMethods();
        /*
         *  1.遍历所有方法
         *  2.找到与目标方法一样的方法
         *  3.找到该方法上的所有注解
         *  4.遍历所有注解,判断注解的类型是否是我们自定义的日志注解
         *  5.如果是,就获取该注解对象
         *  6.由注解对象得到注解值
         */

        // 1.遍历所有方法
        for (Method method : methods) {
            // 2.找到与目标方法一样的方法
            if (methodName.equals(method.getName())) {
                // 3.找到该方法上的所有注解
                Annotation[] annotations = method.getDeclaredAnnotations();
                // 4.遍历所有注解,判断注解的类型是否是我们自定义的日志注解
                for (Annotation annotation : annotations) {
                    // 5.如果是,就获取该注解对象
                    if (annotation.annotationType( ).equals(com.qf.util.Log.class)) {
                        // 6.由注解对象得到注解值
                        value = method.getAnnotation(com.qf.util.Log.class).value( );
                        return value;
                    }
                }
            }
        }
        return value;
    }
}

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

image-20230819190408990

image-20230819190421386

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

五、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 请求&响应【重点】

其实我们之前的那些案例中就已经使用了请求和响应

@GetMapping(“/ioc”)、@PostMapping(“/ioc”)等就是绑定映射路径和处理请求的方法的,返回值就是响应(跳转页面)

image-20230818200557052

练习: 类中再定义其他方法和请求路径,(与servlet做对比)

@RequestMapping注解使用

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="/map" 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="Map类型">
</form>
<h2>模糊查询-Map绑定</h2>
<form action="/map" method="get">
    address<input type="text" name="address"><br>
    floor<input type="text" name="floor"><br>
    deco<input type="text" name="deco"><br>
    <input type="submit" value="模糊-Map类型">
</form>
name就是map的key
输入框的值就是map的value

后台

    @GetMapping("/map")
    public String map(@RequestParam Map<String,Object> map){
        System.out.println("map = " + map);
        return "ok.html";
    }

5.3.6 路径参数@PathVariable

参考这个路径

https://blog.csdn.net/weixin_39641494/article/details/131625212

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

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

前端

<h2>路径参数绑定</h2>
<a href="/user/101">路径参数101</a>

后端

    @GetMapping("/user/{id}")
    public String path(@PathVariable int id){
        System.out.println("id = " + id); // id=101
        return "ok.html";
    }

ps: 能接收到请求中的id为101,但是响应回报错.因为使用@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";
    }

重定向

在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 会话[重点]

如果需要在控制层中使用session存储会话数据,比如登录的用户信息,就可以直接在方法的参数列表中定义HttpSession对象即可

    @GetMapping("/ts")
    public String testSession(HttpSession session) {
        session.setAttribute("aa","AA");
        System.out.println(session.getAttribute("aa"))
        return "ok.html";
    }

5.6 拦截器

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

  • 编写自定义拦截器类
  • 实现接口
  • 重写拦截方法
  • 配置拦截器
    • 这个不一样,以前是配置在web.xml中或者加上注解@WebFilter
    • 现在SpringBoot推荐使用java类的方式配置

自定义拦截器类

@Component
public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 System.out.println("my preHandle");
 // false  所有都拦截,排除在外的不拦截
 return false;
    }

}

拦截器配置类

@Configuration // !!!加注解!!!!配置类
public class MyConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
 // addPathPatterns 定义拦截的路径
 // excludePathPatterns 定义放行的路径
 // 这两个方法支持不定长参数,可以设置多个拦截/放行路径
 registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").excludePathPatterns("/test");
    }
}

5.7 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

5.8 文件上传

图片上传

上传tomcat服务器/上传本地磁盘

前端

<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>
    /**
     * 接收文件,上传文件到服务器
     * ----------------------
     * 方法的参数列表设置MultipartFile类型参数,
     * 参数名与前端name一致,即可封装
     */
    @PostMapping("/upload")
    @ResponseBody
    public R upload(MultipartFile img, HttpServletRequest request) throws IOException {
        // 1.获得上传的对象 参数img就是文件对象

        // 2. 获得最终上传的目的地路径(上传至服务器中当前项目下)
        // 通过servlet方法获得路径,即最终上传到Tomcat的/upload
        String realPath = request.getServletContext().getRealPath("/upload");
        System.out.println(realPath);
        // 2.1 将最终目的文件夹创建出来
        File file = new File(realPath);
        // day35/tomcat/upload/2342895429834.png
        // 判断该文件是否存在
        if(!file.exists()) {
            // 不存在则创建出
            file.mkdir();
        }
        // 2.2 获得文件名
        /*
         * 文件名重复时不能重复上传文件
         */
        String fileName = img.getOriginalFilename();
        System.out.println(fileName);
        /*
         * 根据.拆分字符串,获得文件后缀名
         */
        String[] split = fileName.split("\\.");
        System.out.println(Arrays.toString(split));
        String suffix = split[split.length-1];
        // 以当前毫秒值为文件名
        long prefix = new Date().getTime();
        // 组装文件名
        String newFileName = prefix+"."+suffix;
        System.out.println("新的文件名 : "+newFileName);
        // 2.3 确定上传路径
        File newFile = new File(file,newFileName);
        // 3. 用工具上传
        FileUtils.writeByteArrayToFile(newFile, img.getBytes());
        // 4 返回路径,测试使用,放查看是否上传成功
        // String path = "http://localhost:8081/upload/"+newFileName;
        return R.ok("http://localhost:8080/upload/"+newFileName);
    }

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

image-20231127153009777

特殊的,上传有文件大小限制,可以通过改变SpringBoot配置文件application.properties或者application.yml文件

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

5.9 异常处理

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

package com.qf.common;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.io.IOException;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc 对请求响应过程中的异常进行处理
 */
@ControllerAdvice
public class MyGlobalExceptionHandler {

    /**
     * 处理异常的注解,不指定的话,默认处理所有异常
     * 也可以指定
     * @param e
     * @return
     */
    // @ExceptionHandler(value = {ArithmeticException.class, IOException.class} )
    @ExceptionHandler(Exception.class)
    public String handlerException(Exception e) {
        System.out.println("全局异常处理打印中...." );
        // 接收到异常信息
        e.printStackTrace();
        // 真实项目中应该记录异常日志或者将异常记录数据库
        return "404.html";
    }
}

测试

@RestController
public class TestController {
    @GetMapping("/test")
    public String test(){
 System.out.println(1/0 );
 return "Hello SpringBoot!";
    }
}

六、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等)


6.3 xml方式整合Mybatis[重点]

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

6.3.1 环境

mybatis和druid的springboot环境下的依赖

<!-- 小辣椒 -->
<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 primary key auto_increment,
  username varchar(50),
  password varchar(50),
  phone varchar(50),
  createTime date,
  money double
)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;
}

6.3.2 编写接口和映射文件

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

public interface Userapper {
   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.model.User">
        <!-- #{id} 就相当于是之前的预处理的 ?,会自动给此处复制  -->
        <!-- 其实就是接口方法的参数列表的值,会传给 #{id} -->
        select * from tb_user where id = #{id}
    </select>
</mapper>

6.3.3 yaml文件

# mybatis配置
mybatis:
  # 扫描映射文件
  mapper-locations: classpath:mapper/*.xml
  # 配置别名扫描的包
  type-aliases-package: com.qf.model
  configuration:
    # 开启驼峰映射配置
    map-underscore-to-camel-case: true
# 连接数据库的信息
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
# yml文件
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 测试

@RestController
public class TestMybatisController {

    // todo: 这里省略了service,也可以调用service,由service掉mapper
    @Autowired
    private UserMapper userMapper;

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

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,createTime,sex,money from tb_user where id = #{id}
-->
    select id,username,password,phone,createTime,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">
    <!-- 默认是不支持传多个参数,传入多个参数时,需要如下操作(2选1) -->
    <!--
            方案1: #{}内按顺序写param1,param2,....
        -->
    select * from tb_user where 
    username = #{param1} and password = #{param2}
</select>



接口方法(参数加注解)

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 增加

页面

<h2>添加</h2>
<form action="/add">
  用户名<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 addUser(User user);
}

映射文件

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

6.4.3 修改

前端页面

<h2>更新</h2>
<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},createTime=#{createTime},money=#{money}
        where id = #{id}
    </update>

6.4.4 删除

页面

<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 方案一:列的别名

在SQL中使用 as 为查询字段添加列别名,以匹配属性名。

通过取别名,让列的别名和实体类属性名一致即可!

<mapper namespace="com.qf.mapper.UserMapper">
    <select id="findUserById" resultType="User">
		select id as idd,username,password,phone,create_time,sex,money from tb_user where id = #{id}
    </select>
</mapper>

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

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

<mapper namespace="com.qf.mapper.UserMapper">

    <!--定义resultMap标签-->
    <resultMap id="findUserByIdResultMap" type="user">
      	<!--关联主键与列名-->
        <id property="idd" column="id" />
    </resultMap>
  
     <!--使用resultMap作为ORM映射依据-->
    <select id="findUserById" resultMap="findUserByIdResultMap">
        select id,username,password,phone,create_time,sex,money from tb_user where id = #{id}
    </select>
</mapper>

总结

  • 当数据库的列和实体类属性不一致时,可以通过手动映射来完成
  • 手动关联映射,就不再使用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 '账户余额',
   `createTime` date DEFAULT NULL COMMENT '注册时间'
  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);

实体类

public class Order {

    private int oid;
    private Date orderTime;
    private String orderDesc;
    private int uid;
 	// set get...   
}

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

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

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

image-20231128162048547

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

public class OrderVO extends Order {
    private User user;
    // set get
}

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.model.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="createTime" property="createTime"/>
        </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扩展实体类

public class UserVO extends User{

    private List<Order> orderList;
    // set get
}

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"/>
        <!-- 一对多关联映射使用collection标签 -->
        <!-- property是UserVO类中关联的属性 -->
        <!-- 不是javaType,是ofType,是指定集合中存储的数据类型 -->
        <collection property="orderList" ofType="com.qf.model.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
  • trim
  • foreach

6.7.1 SQL片段

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

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

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

    <select id="findAll" resultType="User">
        select
            <!-- 引入片段 -->
            <include refid="userField"/>
        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

    <select id="findByMap" resultMap="userResultMap">
        <!-- #{}写map的key -->
        select
            <include refid="userFields"/>
        from
            tb_user
		where 
        	1 = 1
        <!-- username是map的key -->
        <if test="username != null and username != ''">
            and username = #{username}
        </if>
        <if test="password != null and password != ''">
            and password = #{password}
        </if>
    </select>

测试

    @GetMapping("/map")
    @ResponseBody
    public R map(){
        HashMap<String, Object> map = new HashMap<>( );
        // map.put("password","123456");
        // map.put("username","Uzi");
        User user = userService.findByMap(map);
        if (user != null) {
            return R.ok(user);
        }
        return R.fail();
    }

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

6.7.3 where [重点]

如果说只有if,可能会出现这么一种情况

SELECT * FROM tb_user WHERE 1=1

多出一个where关键词!!


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

    <!-- 测试if的缺点 -->
    <select id="findUserByWhere" resultType="User">
        select
            <include refid="userField"/>
        from
            tb_user
       	<where>
            <if test="username != null and username != ''">
                and username = #{username}
            </if>
            <if test="password != null and password != ''">
                and password = #{password}
            </if>
         </where>
    </select>

所以一般会where和if一起用

6.7.4 set

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

UserMapper.java接口方法

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

UserMapper.xml

    <!-- set完成动态更新 -->
    <update id="updateUser">
        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>       
        </set>
        where id = #{id}
    </update>

测试


6.7.5 foreach

场景: 批量删除

delete from tb_user where id in (1,2,3,...);
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);
    }

6.9 事务

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

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

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

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

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

6.10 缓存(cache)【面试】

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

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

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

6.10.1 一级缓存

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

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

image-20221217112819746

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

6.10.1.1 命中缓存
    @GetMapping("/user/{id}")
    @Transactional // 【重点】需要开启事务才会生效一级缓存,因为mysql默认每句话都是独立的事务,即每句话都是独立的SqlSession,那么就不符合一级缓存的要求
    public R findUserById(@PathVariable int id) {
        User user = userMapper.findUserById(id);// 第一次查
        System.out.println(user );
        System.out.println("-------------" );
        User user2 = userMapper.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级别范围更大.

  • 需要在mapper中设置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接口

@Mapper
public interface EmpMapper {

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

}

6.11.2 添加Mybatis注解

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

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

@Mapper
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.3 添加日志配置

# yml文件
logging:
  level:
    com.taotie.testspringboot.mapper: DEBUG

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 DeptMapper deptMapper;

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

七、SpringBoot常用配置【重点


7.1 SpringBoot的配置文件格式

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 [推荐]

// yml配置文件内容是一样的
# 示例
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>

7.4.2 settings配置

修改settings中的配置
image-20230625223935285

7.4.3 设置

编辑界面同时 ctrl+shift+alt+/ 选择registry,勾选自动部署

勾选自动部署
image-20230625224058755

注意,要以**debug方式启动**,代码写完,光标离开idea 过1-3秒钟就会自动更新…

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

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-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">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout>
                <pattern>${pattern}</pattern>
            </PatternLayout>
        </Console>

        <!--debug级别日志文件输出-->
        <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>

        <!-- info级别日志文件输出 -->
        <RollingFile name="info_appender" fileName="${baseDir}/info.log"
                     filePattern="${baseDir}/info_%i.log.%d{yyyy-MM-dd}">
            <!-- 过滤器 -->
            <Filters>
                <!-- 限制日志级别在info及以上在error以下 -->
                <ThresholdFilter level="info"/>
                <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
            <!-- 日志格式 -->
            <PatternLayout>
                <pattern>${pattern}</pattern>
            </PatternLayout>
            <!-- 策略 -->
            <Policies>
                <!-- 每隔一天转存 -->
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <!-- 文件大小 -->
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
        </RollingFile>

        <!-- error级别日志文件输出 -->
        <RollingFile name="error_appender" fileName="${baseDir}/error.log"
                     filePattern="${baseDir}/error_%i.log.%d{yyyy-MM-dd}">
            <!-- 过滤器 -->
            <Filters>
                <!-- 限制日志级别在error及以上 -->
                <ThresholdFilter level="error"/>
            </Filters>
            <!-- 日志格式 -->
            <PatternLayout>
                <pattern>${pattern}</pattern>
            </PatternLayout>
            <Policies>
                <!-- 每隔一天转存 -->
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <!-- 文件大小 -->
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
        </RollingFile>
    </Appenders>
    <Loggers>
        <!-- 这是总的级别配置,优先级比Appenders中的要高 -->
        <Root level="ERROR">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="debug_appender"/>
            <AppenderRef ref="info_appender"/>
            <AppenderRef ref="error_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.warn("我是警告");
        logger.info("我是消息");
        logger.error("我是错误!");
        return R.ok( );
    }
}

image-20230830134613238

image-20230830134742030

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

九 总结

知识点重点

  • spring
    • ioc , 控制反转,创建对象
      • @Controller,@Service
    • di, 依赖注入,属性赋值
      • @Autowired
  • 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包/依赖
  • 熟系三层架构编码风格

其他东西都是锦上添花

  • aop

  • 会话,拦截器,文件上传,全局异常

  • 多表联查,缓存,事务,注解开发,分页

  • 多环境切换,热加载,接口工具

  • 日志记录
    }/info_%i.log.%d{yyyy-MM-dd}">








    ${pattern}








      <!-- error级别日志文件输出 -->
      <RollingFile name="error_appender" fileName="${baseDir}/error.log"
                   filePattern="${baseDir}/error_%i.log.%d{yyyy-MM-dd}">
          <!-- 过滤器 -->
          <Filters>
              <!-- 限制日志级别在error及以上 -->
              <ThresholdFilter level="error"/>
          </Filters>
          <!-- 日志格式 -->
          <PatternLayout>
              <pattern>${pattern}</pattern>
          </PatternLayout>
          <Policies>
              <!-- 每隔一天转存 -->
              <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
              <!-- 文件大小 -->
              <SizeBasedTriggeringPolicy size="100 MB"/>
          </Policies>
      </RollingFile>
    
```

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.warn("我是警告");
        logger.info("我是消息");
        logger.error("我是错误!");
        return R.ok( );
    }
}

[外链图片转存中…(img-iAG3w6Zi-1701265300108)]

[外链图片转存中…(img-zC1S6DYR-1701265300109)]

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

九 总结

知识点重点

  • spring
    • ioc , 控制反转,创建对象
      • @Controller,@Service
    • di, 依赖注入,属性赋值
      • @Autowired
  • 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包/依赖
  • 熟系三层架构编码风格

其他东西都是锦上添花

  • aop
  • 会话,拦截器,文件上传,全局异常
  • 多表联查,缓存,事务,注解开发,分页
  • 多环境切换,热加载,接口工具
  • 日志记录
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Su sir~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值