Spring Boot开发基础教程

Spring Boot开发入门终极详细教程,为Java开发者提供全面指南。本文篇幅较长,可分为两部分:1~5节主要介绍Maven与MVC入门、HTTP协议、请求响应、分层解耦;6~14节结合tlias智能学习辅助系统来介绍分页查询、文件上传、配置文件、登录认证、事务管理、AOP以及Spring Boot与Maven高级原理篇。

Maven官方仓库:https://mvnrepository.com/
Spring官网:https://spring.io/

SpringBoot打包部署到服务器(宝塔面板):CSDN博文

6~14节项目的接口文档:tlias智能学习辅助系统

Web流程
ssm

Hyplus目录

1 Maven

1.1 介绍&安装

Maven(官网:http://maven.apache.org/)是apache旗下的一个开源项目,是一款用于管理和构建java项目的工具,它基于项目对象模型(POM)的概念,通过一小段描述信息来管理项目的构建。

作用

  1. 依赖管理:方便快捷的管理项目依赖的资源(jar包),避免版本冲突问题
  2. 统一项目结构:提供标准、统一的项目结构

统一项目结构

  1. 标准化的项目构建流程:标准跨平台(Linux、Windows、MacOS)的自动化项目构建方式

标准化的项目构建流程

仓库:用于存储资源,管理各种jar包

  • 本地仓库:自己计算机上的一个目录
  • 中央仓库:由Maven团队维护,全球唯一。仓库地址:https://repo1.maven.org/maven2/
  • 远程仓库(私服):一般由公司团队搭建的私有仓库。

仓库

安装与配置

  1. 解压 apache-maven-3.6.1-bin.zip
  2. 配置本地仓库:修改apache-maven-3.6.1-bin/conf/settings.xml中的<localRepository>为一个指定目录
<!-- apache-maven-3.6.1-bin/conf/settings.xml -->
<localRepository>/Users/example/develop/apache-maven-3.9.5/mvn_repo</localRepository>
  1. 配置阿里云私服:修改apache-maven-3.6.1-bin/conf/settings.xml中的<mirrors>标签,为其添加如下子标签
<!-- apache-maven-3.6.1-bin/conf/settings.xml -->
<mirror>
    <id>alimaven</id>
    <name>aliyun maven</name>
    <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
    <mirrorOf>central</mirrorOf>
</mirror>
  1. 配置环境变量:MAVEN_HOME为maven的解压目录,并将其bin目录加入PATH环境变量

1.2 创建Maven项目

创建Maven项目1

创建Maven项目2

Maven坐标是资源的唯一标识,通过该坐标可以唯一定位资源位置。使用坐标来定义项目或引入项目中需要的依赖

  • groupId:定义当前Maven项目录属组织名称(通常是域名反写,例如:com.hyplus)
  • artifactId:定义当前Maven项目名称(通常是模块名称,例如order-service、goods-service)
  • version:定义当前项目版本号

Maven项目界面

1.3 依赖管理

依赖(Dependency)指当前项目运行所需要的jar包,一个项目中可以引入多个依赖。

1.3.1 配置依赖

直接去官方仓库搜索、复制依赖坐标

  1. 在 pom.xml 中编写<dependencies>标签
  2. <dependencies>标签中使用<dependency>引入坐标
  3. 定义坐标的groupldartifactldversion
  4. 点击刷新按钮,引入最新加入的坐标,之后可在右侧Maven面板
<!-- pom.xml -->
<dependencies>
    <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
</dependencies>

1.3.2 依赖传递

直接依赖:在当前项目中通过依赖配置建立的依赖关系

间接依赖:被依赖的资源如果依赖其他资源,当前项目间接依赖其他资源

依赖

右键选 Diagrams > Show Diagrams... 可直接以图表形式查看当前依赖信息

查看当前依赖信息

排除依赖:主动断开依赖的资源(<exclusionsl>),被排除的资源无需指定版本

排除依赖

<!-- pom.xml -->
<dependencies>
    <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.10</version>
        <exclusions>    <!-- 排除依赖(仅供示例) -->
            <exclusion>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

1.3.3 依赖范围

依赖的jar包,默认情况下可以在任何地方使用。可以通过<scope></scope>设置其作用范围:

  1. 主程序范围有效。(main文件夹范围内)
  2. 测试程序范围有效。(test文件夹范围内)
  3. 是否参与打包运行。(package指令范围内)
scope值 主程序 测试程序 打包(运行) 范例
compile(默认) Y Y Y log4j
test - Y - junit
provided Y Y - servlet-api
runtime - Y Y jdbc驱动

1.3.4 生命周期

Maven的生命周期:对所有的maven项目构建过程进行抽象和统一。有3套相互独立的生命周期:

  • clean: 清理工作。
  • default:核心工作,如:编译、测试、打包、安装、部署等。
  • site:生成报告、发布站点等。

每套生命周期包含一些阶段(phase),阶段是有顺序的,后面的阶段依赖于前面的阶段——在同一套生命周期中,当运行后面的阶段时,前面的阶段都会运行。(主要关注以下5个阶段)

生命周期

  1. clean:移除上一次构建生成的文件
  2. compile: 编译项目源代码
  3. test:使用合适的单元测试框架运行测试(junit
  4. package:将编译后的文件打包,如:jar、war等
  5. install:安装项目到本地仓库

生命周期2

生命周期2


2 SpringBootWeb入门

2.1 Spring概述

Spring(官网:https://spring.io)发展到今天已经形成了一种开发生态圈。Spring提供了若干个子项目,每个项目用于完成特定的功能。

Spring

Spring Boot可以非常快速地构建应用程序、简化开发、提高效率。

Spring Boot项目的静态资源(html、css、js等前端资源)默认存放目录为:src/main/resources下的staticpublic,或直接放于src/main/resources

2.2 创建SpringBoot项目

  1. 创建springboot工程,并勾选web开发相关依赖。
  2. 定义请求处理类RequestController, 添加方法hello,并添加注解
  3. 运行启动类SpringbootWebQuickstartApplication,测试
/* RequestController.class */
package com.hyplus.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

// 请求处理类
@RestController
public class RequestController {
    @RequestMapping("/hello")
    public String hello() {
        System.out.println("Hello World!");
        return "Hello World!";
    }
}

给类添加注释@Slf4j 自动创建日志对象成员,方法内用log.info("查询全部部门数据");输出日志信息。


3 HTTP协议

3.1 概述

HTTP

HTTP协议(Hyper Text Transfer Protocol,超文本传输协议)规定了浏览器和服务器之间数据传输的规则。

  1. 基于TCP协议:面向连接,安全。
  2. 基于请求-响应模型的:一次请求对应一次响应。
  3. 无状态的协议:对于事务处理没有记忆能力(每次请求-响应都是独立的)。缺点是多次请求间不能共享数据,优点是速度快。

协议

3.2 请求协议

request

请求格式:

  1. 请求行:请求数据第1行(请求方式、资源路径、协议)
  2. 请求头:第2行开始,格式key: value
  3. 请求体:最后一部分,POST请求存放请求参数

请求方式-GET:请求参数在请求行中,没有请求体,如:/brand/EindA11?name=OPO&status=1。GET请求大小是有限制的。
请求方式-POST:请求参数在请求体中。POST请求大小是没有限制的。

@RequestMapping对应于各种请求方式的衍生注解为@GetMapping@PostMapping ...

常见的请求头 含义
Host 请求的主机名
User-Agent 浏览器版本
Accept 表示浏览器能接收的资源类型,如text/*image/*,或者*/*表示所有
Accept-Language 表示浏览器偏好的语言,服务器可以据此返回不同语言的网页
Accept-Encoding 表示浏览器可以支持的压缩类型,例如gzip, deflate等
Content-Type 请求主体的数据类型
Content-Length 请求主体的大小(单位:字节)

3.3 响应协议

response

响应格式:

  1. 响应行:响应数据第1行(协议、状态码、描述)
  2. 响应头:第2行开始,格式key: value
  3. 响应体:最后一部分,存放响应数据(响应正文)

状态码大全:状态码大全

状态码分类 意义 说明
1xx 响应中 临时状态码,表示请求已经接收,告诉客户端应该继续请求或者如果它已经完成则忽略它
2xx 成功 表示请求已经被成功接收,处理已完成
200 OK:客户端请求成功,即处理成功,这是日常开发最想看到的状态码
3xx 重定向 重定向到其他地方;让客户端再发起一次请求以完成整个处理
4xx 客户端错误 处理发生错误,责任在客户端。如:请求了不存在的资源、客户端未被授权、禁止访问等
404 Not Found:请求资源不存在,一般是URL输入有误,或者网站资源被则除了
5xx 服务器错误 处理发生错误,贵任在服务端。如:程序抛出异常等
500 Internal Server Error:服务器发生不可预期的错误,服务器出异常了
常见的响应头 说明
Content-Type 表示该响应内容的类型,例如text/html、application/json
Content-Length 表示该响应内容的长度(字节数)
Content-Encoding 表示该响应压缩算法,例如gzip
Cache-Control 指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒
Set-Cookie 告诉浏览器为当前页面所在的域设置cookie

3.4 协议解析、Tomcat

web服务器是一个软件程序,对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作,让Web开发更加便捷。主要功能是“提供网上信息浏览服务”。

Tomcat(官网:https://tomcat.apache.org/)是Apache 软件基金会一个核心项目,是一个开源免费的轻量级Web服务器,支持Servlet、JSP等少量JavaEE规范,因此Tomcat也被称为Web容器、Servlet容器。Servlet程序需要依赖于Tomcat才能运行。

Java语言三大分支:JavaSE、JavaME、JavaEE
JavaEE(Java Enterprise Edition,Java企业版)指Java企业级开发的技术规范总和,包含13项技术规范:JDBC、JNDI、EJB、RMI.JSP、Servlet、XML、JMS、Java IDL、JTS、JTA、JavaMail、JAF

SpringBoot内嵌了Tomcat。

Tomcat

起步依赖

  • spring-boot-starter-web:包含了web应用开发所需要的常见依赖,包括tomcat
  • spring-boot-starter-test:包含了单元测试所需要的常见依赖

   
      org.springframework.boot
      spring-boot-starter-parent
      3.2.5
       

4 请求响应

Servlet

Servlet

4.1 架构概述

架构概述

请求(HttpServletRequest):获取请求效据
响应(HttpServletResponse):设置响应数据

BS架构(Browser/Server,浏览器/服务器架构模式):客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。维护方便,体验一般。
CS架构(Client/Server,客户端/服务器架构模式):需要单独安装客户端。开发、维护麻烦,体验不错。

4.2 接收请求

Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件,常用于进行接口测试。
同类工具:Apipost、Apifox

4.2.1 简单参数

http://localhost:8080/simpleParam?name=age为例——

原始方式:在原始的web程序中,获取请求参数,需要通过HttpServletRequest对象手动获取。

/*  RequestController.java */
@RequestMapping("/simpleParam")
public String simpleParam(HttpServletRequest request) {
  String name = request.getParameter("name");
  String ageStr = request.getParameter("age");
  int age = Integer.parseInt(ageStr);
  System.out.println(name + " : " + age);
  return "OK";
}

简单参数:参数名与形参变量名相同,定义形参即可接收参数。

/* RequestController.java */
@RequestMapping("/simpleParam")
public String simpleParam(String name, Integer age) {   // SpringBoot方式会自动进行类型转换
    System.out.println(name + " : " + age);
    return "OK";
}

如果方法形参名称与请求参数名称不匹配则默认接收到null
可以使用注解@RequestParam完成映射,其中的required属性默认为true,表示该请求参数必须传递。若该参数可选则可设为false,此时对应的形参默认赋null

/* RequestController.java */
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam(name = "name", required = false) String username, Integer age) {
    System.out.println(username + " : " + age);
    return "OK";
}

4.2.2 实体参数

简单实体对象:同简单参数,请求参数名与形参对象属性名相同,定义POJO(Plain Old Java Object)接收即可

/* RequestController.java */
@RequestMapping("/simplePojo")
public String simplePojo(User user) {
    System.out.println(user);   // 要打印成员的值需重写自定义类的toString()方法,此处略
    return "OK";
}
/* User.java */
public class User {     // POJO
    private String name;
    private Integer age;
}

复杂实体对象:请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套PQJO属性参数。
如下所示,GET请求可为GET http://localhost:8080/complexPojo?name=Akira&age=2024&address.province=浙江&address.city=杭州,POST请求的请求体同理。

/* RequestController.java */
@RequestMapping("/complexPojo")
public String complexPojo(User user) {
    System.out.println(user);
    return "OK";
}
/* User.java */
public class User {
    private String name;
    private Integer age;
    private Address address;
}
/* Address.java */
public class Address {
    private String province;
    private String city;
}

4.2.3 数组集合参数

http://localhost:8080/arrayParam?hobby=python&hobby=cpp为例——

数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数。

/* RequestController.java */
@RequestMapping("/arrayParam")
public String arrayParam(String[] hobby) {      // 或为可变参数(String... hobby)
    System.out.println(Arrays.toString(hobby));
    return "OK";
}

集合参数:请求参数名与形参集合名称相同且请求参数为多个,用@RequestParam绑定参数关系。

/* RequestController.java */
@RequestMapping("/listParam")
public String listParam(@RequestParam List<String> hobby) {
    System.out.println(hobby);
    return "OK";
}

4.2.4 日期时间参数

日期参数:使用@DateTimeFormat注解完成日期参数格式转换。

LocalDateTime类包含"年月日"、"时分秒",LocalDate类只包含"年月日"。

/* RequestController.java */
@RequestMapping("/dateParam")
public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")LocalDateTime updateTime) {
    System.out.println(updateTime);
    return "OK";
}

4.2.5 JSON参数

JSON数据需放在请求体中传输,故必须用POST请求。

JSON参数

JSON参数:键名与形参对象属性名相同,定义POJO类型形参即可接收参数,需要使用@RequestBody标识。

/* RequestController.java */
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user) {
    System.out.println(user);
    return "OK";
}
/* User.java */
public class User {
    private String name;
    private Integer age;
    private Address address;
}
/* Address.java */
public class Address {
    private String province;
    private String city;
}

4.2.6 路径参数

路径参数:通过请求URL直接传递参数(如http://localhost:8080/path/100)。使用{...}来标识该路径参数,需要使用@PathVariable获取路径参数

/* RequestController.java */
@RequestMapping("/path/{id}")
public String pathParam(@PathVariable Integer id) {
    System.out.println(id);
    return "OK";
}

// 传递多个参数
@RequestMapping("/path/{id}/{name}")
public String pathParam2(@PathVariable Integer id, @PathVariable String name) {
    System.out.println(id + " : " + name);
    return "OK";
}

4.3 响应数据

响应注解 @ResponseBody

  • 类型:方法注解、类注解
  • 位置:controller方法/类上
  • 作用:将方法返回值直接响应,如果返回值类型是实体对象/集合,将会转换为JSON格式响应
  • 说明:@RestController = @Controller + @ResponseBody
/* RestController.class 源码 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented     // 前3个为元注解(修饰注解的注解)
@Controller
@ResponseBody
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}

三种功能接口:


/* ResponseController.java */ 
@RestController
public class ResponseController {
    @RequestMapping("/hello")
    public String hello() {
        System.out.println("Hello World!");
        return "Hello World!";    // 直接传回字符串
    }

    @RequestMapping("/getAddr")
    public Address getAddr() {
        Address addr = new Address();
        addr.setProvince("浙江");
        addr.setCity("深圳");
        return addr;  // 传回JSON格式数据
    }

    @RequestMapping("/listAddr")
    public List
listAddr() { List
list = new ArrayList<>(); Address addr1 = new Address(); addr1.setProvince("浙江"); addr1.setCity("杭州"); Address addr2 = new Address(); addr2.setProvince("陕西"); addr2.setCity("西安"); Collections.addAll(list, addr1, addr2); return list; // 传回JSON格式数组 } } ```

统一响应结果,可以设置一个实体对象Result类进行接收

/* Result.java */
public class Result {
    private Integer code;   // 响应码:1代表成功,0代表失败
    private String msg;     // 提示信息
    private Object data;    // 返回的数据

    /* 省略Conductors、Getters and Setters、toString()重写 */

    // 响应成功
    public static Result success(Object data) {
        return new Result(1, "success", data);
    }
    // 响应成功(无需返回数据)
    public static Result success() {
        return new Result(1, "success", null);
    }
    // 响应失败
    public static Result error(String msg) {
        return new Result(0, msg, null);
    }
}

则上述功能接口可改写为:

/* ResponseController.java */
@RequestMapping("/hello")
public Result hello() {
    System.out.println("Hello World!");
    return Result.success("Hello World!");
}

@RequestMapping("/getAddr")
public Result getAddr() {
    Address addr = new Address();
    addr.setProvince("浙江");
    addr.setCity("深圳");
    return Result.success(addr);
}

@RequestMapping("/listAddr")
public Result listAddr() {
    List<Address> list = new ArrayList<>();

    Address addr1 = new Address();
    addr1.setProvince("浙江");
    addr1.setCity("杭州");

    Address addr2 = new Address();
    addr2.setProvince("陕西");
    addr2.setCity("西安");

    Collections.addAll(list, addr1, addr2);
    return Result.success(list);
}

案例:解析并处理xml中的数据
解析并处理emp.xml中的Emp类数据(仅供示例,稍后优化)


/* EmpController */
@RestController
public class EmpController {
  @RequestMapping("/listEmp")
  public Result list() {
      // 1. 数据访问:加载 emp.xml,并解析 emp.xml中的数据
      String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
      List empList = XmlParserUtils.parse(file, Emp.class);
      // 2. 逻辑处理:对员工信息中的gender,job字般进行处理
      empList.stream().forEach(emp -> {
          String gender= emp.getGender();     // gender: 1男,2女
          if ("1".equals(gender)) {
              emp.setGender("男");
          } else if ("2".equals(gender)) {
              emp.setGender("女");
          }
          String job = emp.getJob();          // job: 1讲师,2班主任,3就业指导
          if ("1".equals(job)) {
              emp.setJob("讲师");
          } else if ("2".equals(job)) {
              emp.setJob("班主任");
          } else if ("3".equals(job)) {
              emp.setJob("就业指导");
          }
      });
      // 3. 响应数据
      return Result.success(empList);```

5 分层解耦

传统未拆分的写法,复用性差,难以维护。解耦使得每一层职能单一,可以增强复用性,便于维护,利于拓展。

5.1 三层架构

  1. Controller(控制层):接收前端发送的请求,对请求进行处理,并响应数据
  2. Service(业务逻辑层):处理具体的业务逻辑
  3. Dao(Data Access Object,数据访问层,持久层):负责数据访问操作,包括数据的增、删、改、查

三层架构

根据三层架构初步改写4.3案例

Dao层:

/* dao.EmpDao.java */
public interface EmpDao {
    public List<Emp> listEmp();       // 获取员工列表数据
}

/* dao.impl.EmpDaoA.java */
public class EmpDaoA implements EmpDao {
    // 数据访问:加载 emp.xml,并解析 emp.xml中的数据
    @Override
    public List<Emp> listEmp() {
        String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
        List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
        return empList;
    }
}

Service层:

/* service.EmpService.java */
public interface EmpService {
    public List<Emp> listEmp();       // 获取员工列表数据
}
/* service.impl.EmpServiceA.java */
public class EmpServiceA implements EmpService {
    private EmpDao empDao = new EmpDaoA();      // 存在耦合

    // 逻辑处理:对员工信息中的gender,job字般进行处理
    @Override
    public List<Emp> listEmp() {
        List<Emp> empList = empDao.listEmp(); // 调用Dao层的listEmp()方法获取列表
        empList.stream().forEach(emp -> {
            String gender= emp.getGender();
            if ("1".equals(gender)) {
                emp.setGender("男");
            } else if ("2".equals(gender)) {
                emp.setGender("女");
            }
            String job = emp.getJob();
            if ("1".equals(job)) {
                emp.setJob("讲师");
            } else if ("2".equals(job)) {
                emp.setJob("班主任");
            } else if ("3".equals(job)) {
                emp.setJob("就业指导");
            }
        });
        return empList;
    }
}

Controller层:

/* RestController.java */
@RestController
public class EmpController {
    private EmpService empService = new EmpServiceA();  // 存在耦合

    @RequestMapping("/listEmp")
    public Result list() {
        List<Emp> empList = empService.listEmp(); // 调用Service,获取数据
        return Result.success(empList);     // 响应数据
    }
}

5.2 IOC & DI

5.2.1 解耦入门

内聚:软件中各个功能模块内部的功能联系。
耦合:衡量软件中各个层/模块之间的依赖、关联的程度。
软件设计原则:高内聚低耦合

要解除5.1代码中Controller层与Service层的耦合,可以设置一个容器用来存储Service层接口各种实现类的对象(控制反转),当Controller层运行时,Service层去容器中查找对应的实现类的对象(依赖注入)。同理也能实现Service层与Dao层解耦。

控制反转(Inversion of Control,IOC):对象的创建控制权由程序自身转移到外部(容器)
依赖注入(Dependency Injection,DI):容器为应用程序提供运行时,所依赖的资源,称之为依赖注入
Bean对象:IOC容器中创建、管理的对象

一般步骤

  1. IOC:Service层及Dao层的实现类,交给IOC容器管理——给实现类添加注释@Component或其衍生注解(详见5.2.2),使之成为IOC容器中的Bean
  2. DI:为Controller及Service注入运行时所需的依赖的对象——给变量添加注释@Autowired,使得程序运行时IOC容器会提供该类型的Bean,并赋值给该变量
  3. 运行测试

解耦后的5.1代码如下
Dao层:

/* dao.impl.EmpDaoA.java */
@Repository("daoA")
public class EmpDaoA implements EmpDao {
    // 数据访问:加载 emp.xml,并解析 emp.xml中的数据
    @Override
    public List<Emp> listEmp() {
        String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
        List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
        return empList;
    }
}

Service层:

/* service.impl.EmpServiceA.java */
@Service
public class EmpServiceA implements EmpService {
    @Autowired
    private EmpDao empDao;

    @Override
    public List<Emp> listEmp() {
        List<Emp> empList = empDao.listEmp();
        empList.stream().forEach(emp -> {
            String gender= emp.getGender();
            if ("1".equals(gender)) {
                emp.setGender("男");
            } else if ("2".equals(gender)) {
                emp.setGender("女");
            }
            String job = emp.getJob();
            if ("1".equals(job)) {
                emp.setJob("讲师");
            } else if ("2".equals(job)) {
                emp.setJob("班主任");
            } else if ("3".equals(job)) {
                emp.setJob("就业指导");
            }
        });
        return empList;
    }
}

Controller层:

/* RestController.java */
@RestController
public class EmpController {
    @Autowired
    private EmpService empService;

    @RequestMapping("/listEmp")
    public Result list() {
        List<Emp> empList = empService.listEmp();
        return Result.success(empList);
    }
}

若要切换实现类(例如从EmpServiceA改为EmpServiceB),则只需取消原来的实现类(EmpServiceA)的@Component注解,给切换后的实现类(EmpServiceB)打上该注解即可(双方同时带着该注解会报错,解决方法详见5.2.3),其他部分都无需任何变动。

5.2.2 IOC声明Bean

Bean的声明:要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一。Bean的名字默认为首字母小写的类名,可通过注解的value属性设定。

注解 说明 位置
@Component 声明Bean的基础注解 不属于以下三类时,用此注解
@Controller @Component的衍生注解 标注在控制器类(Controller)上(只能用该注解),已包括于@RestController
@Service @Component的衍生注解 标注在业务类(Service)上
@Repository @Component的衍生注解 标注在数据访问类(Dao)上(由于与mybatis整合,用的少)

Bean组件扫描:上述声明bean的四大注解,要想生效,还需要被组件扫描注解@ComponentScan扫描。该注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication中,默认扫猫的范围是启动类所在包及其子包。
因此为避免声明注解无法被扫描,建议将所有组件放在启动类所在包的子包下,规范编码。

5.2.3 DI自动装配

注解@Autowired(自动装配)默认按类型(接口)装配,若存在多个相同类型的Bean将会报错。通常有以下几种解决方法

  • @Primary:给实现类追加该注解,让带有该注解的Bean优先装配
@Primary
@Service
public class EmpServiceA implements EmpService { ... }
  • @Qualifier:配合@Autowired变量加上该注解,指定要注入的Bean的类型
@RestController
public class EmpController {
    @Autowired
    @Qualifier("empServiceA")
    private EmpService empService;
    // ...
}
  • @Resource:直接给变量加上该注解,指定要注入的Bean的名称
@RestController
public class EmpController {
    @Resource(name = "empServiceB")
    private EmpService empService;
    // ...
}

@Resource@Autowired的区别:

  1. @Autowiredspring框架提供的注解,而@ResourceJDK提供的注解。
  2. @Autowired默认是按照类型注入,而@Resource默认是按照名称注入。

6 项目准备:RESTful API简介、前后端联调

从本节起结合tlias智能学习辅助系统案例进行讲解,并使用MyBatis作为ORM(MyBatis-Plus留至微服务项目再运用)。

正式开始搭建项目浅先介绍相关预备知识——

准备阶段

REST(REpresentational State Transfer,表述性状态转换):一种软件架构风格

RESTful

/* Result.java */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    private Integer code;   // 响应码:1成功,0失败
    private String msg;     // 响应信息:描述字符串
    private Object data;    // 返回的数据

    /**
     * 增删改 成功响应
     */
    public static Result success() {
        return new Result(1, "success", null);
    }

    /**
     * 查询 成功响应
     */
    public static Result success(Object data) {
        return new Result(1, "success", data);
    }

    /**
     * 失败响应
     */
    public static Result error(String msg) {
        return new Result(0, msg, null);
    }
}

前后端联调mac电脑启动前端程序的方法

前后端联调


7 部门管理

  • 一个完整的请求路径 = 类上的@RequestMapping的value属性 + 方法上的 @RequestMapping的value属性
  • @RequestMapping的衍生注解:@GetMapping@PostMapping@DeleteMapping ...
  • 给类添加注解@Slf4j来自动生成日志变量
/* DeptController.java */
@Slf4j
@RestController
@RequestMapping("/depts")
public class DeptController {
    @Autowired
    private DeptService deptService;

    ...
}
/* DeptService.java */
@Service
public interface DeptService { ... }
/* DeptServiceImpl.java */
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;

    ...
}
/* DeptMapper.java */
@Mapper
public interface DeptMapper { ... }
/* Dept.java */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dept {
    private Integer id;
    private String name;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

7.1 查询部门

查询部门

/* DeptController.java */
@GetMapping
public Result list() {
    log.info("查询全部部门数据");
    List<Dept> deptList = deptService.list();
    return Result.success(deptList);
}
/* DeptService.java */
List<Dept> list();
/* DeptServiceImpl.java */
@Override
public List<Dept> list() {
    return deptMapper.list();
}
/* DeptMapper.java */
@Select("select * from dept")
public List<Dept> list();

7.2 删除部门

删除部门

/* DeptController.java */
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
    log.info("根据ID删除部门: {}", id);
    deptService.delete(id);
    return Result.success();
}
/* DeptService.java */
void delete(Integer id);
/* DeptServiceImpl.java */
@Override
public void delete(Integer id) {
    deptMapper.deleteById(id);
}
/* DeptMapper.java */
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);

稍后实现同时删除该部门下所有员工。

7.3 新增部门

新增部门

/* DeptController.java */
@PostMapping
public Result add(@RequestBody Dept dept) {
    log.info("新增部门: {}", dept);
    deptService.add(dept);
    return Result.success();
}
/* DeptService.java */
void add(Dept dept);
/* DeptServiceImpl.java */
@Override
public void add(Dept dept) {
    dept.setCreateTime(LocalDateTime.now());
    dept.setUpdateTime(LocalDateTime.now());

    deptMapper.insert(dept);
}
/* DeptMapper.java */
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);

7.4 修改部门

前端要求两个接口:根据ID查询、修改部门

/* DeptController.java */
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
    log.info("获取部门ID: {}", id);
    Dept dept = deptService.getById(id);
    return Result.success(dept);
}

@PutMapping
public Result update(@RequestBody Dept dept) {
    log.info("修改部门: {}", dept);
    deptService.update(dept);
    return Result.success();
}
/* DeptService.java */
Dept getById(Integer id);
void update(Dept dept);
/* DeptServiceImpl.java */
@Override
public Dept getById(Integer id) {
    return deptMapper.getById(id);
}

@Override
public void update(Dept dept) {
    dept.setUpdateTime(LocalDateTime.now());
    deptMapper.update(dept);
}
/* DeptMapper.java */
@Select("select * from dept where id = #{id}")
Dept getById(Integer id);

@Update("update dept set name = #{name}, update_time = #{updateTime} where id = #{id}")
void update(Dept dept);

8 员工管理

/* EmpController.java */
@Slf4j
@RestController
@RequestMapping("/emps")
public class EmpController {
    @Autowired
    private EmpService empService;

    ...
}
/* EmpService.java */
@Service
public interface EmpService { ... }
/* EmpServiceImpl.java */
@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;

    ...
}
/* EmpMapper.java */
@Mapper
public interface EmpMapper { ... }
/* Emp.java */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
    private Integer id;
    private String username;
    private String password;
    private String name;
    private Short gender;   // 1男,2女
    private String image;   // url
    private Short job;      // 1班主任,2讲师,3学工主管,4教研主管,5咨询师
    private LocalDate entrytime;
    private Integer deptId;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

8.1 分页查询

分页查询

分页查询

/* PageBean.java */
// 分页查询结果的封装类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean {
    private Long total;     // 总记录数
    private List rows;      // 当前页数据列表
}
/* EmpController.java */
// 分页查询
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
                   @RequestParam(defaultValue = "10") Integer pageSize) {    // 设置默认值
    log.info("分页查询,参数:{}, {}", page, pageSize);
    PageBean pageBean = empService.page(page, pageSize);
    return Result.success(pageBean);
}
/* EmpService.java */
// 分页查询
PageBean page(Integer page, Integer pageSize);

8.1.1 原始方法

/* EmpServiceImpl.java */
// 分页查询
@Override
public PageBean page(Integer page, Integer pageSize) {
    Long count = empMapper.count();

    Integer start = (page - 1) * pageSize;
    List<Emp> empList = empMapper.page(start, pageSize);

    PageBean pageBean = new PageBean(count, empList);
    return pageBean;
}
/* EmpMapper.java */
// 查询总记录数
@Select("select count(*) from emp")
public Long count();

// 分页查询获取列表数据
@Select("select * from emp limit #{start}, #{pageSize}")
public List<Emp> page(Integer start, Integer pageSize);

8.1.2 PageHelper分页插件

使用PageHelper分页插件,需先引入依赖pagehelper-spring-boot-starter,并指定版本为1.4.6以上。

/* EmpServiceImpl.java */
@Override
public PageBean page(Integer page, Integer pageSize) {
    // 设置分页参数
    PageHelper.startPage(page, pageSize);
    // 执行查询
    List<Emp> empList = empMapper.list();
    Page<Emp> p = (Page<Emp>) empList;      // 查询结果的封装类
    // 封装PageBean对象
    PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
    return pageBean;
}
/* EmpMapper.java */
// 员工信息查询
@Select("select * from emp")
public List<Emp> list();

8.2 条件分页查询

条件分页查询

改写3.1各层代码:条件查询(动态SQL-XML映射文件) + 分页查询(PageHelper分页插件)

/* EmpController.java */
// 条件查询
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
                   @RequestParam(defaultValue = "10") Integer pageSize,
                   String name, Short gender,
                   @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
                   @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {    // 设置默认值
    log.info("分页查询,参数:{}, {}, {}, {}, {}, {}", page, pageSize, name, gender, begin, end);
    PageBean pageBean = empService.page(page, pageSize, name, gender, begin, end);
    return Result.success(pageBean);
}
/* EmpService.java */
// 条件查询
PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end);
/* EmpServiceImpl.java */
@Override
public PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) {
    // 设置分页参数
    PageHelper.startPage(page, pageSize);
    // 执行查询
    List<Emp> empList = empMapper.list(name, gender, begin, end);
    Page<Emp> p = (Page<Emp>) empList;
    // 封装PageBean对象
    PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
    return pageBean;
}
/* EmpMapper.java */
// 员工信息条件查询
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
<!-- EmpMapper.xml -->
<mapper namespace="com.hyplus.tlias.mapper.EmpMapper">
    <!-- 条件查询 -->
    <select id="list" resultType="com.hyplus.tlias.pojo.Emp">
        select * from emp
        <where>
            <if test="name != null and name != ''">     <!-- name应既非null对象又非空串 -->
                name like concat('%', #{name}, '%')
            </if>
            <if test="gender != null">
                and gender = #{gender}
            </if>
            <if test="begin != null and end != null">
                and entrydate between #{begin} and #{end}
            </if>
        </where>
        order by update_time desc
    </select>
</mapper>

8.3 删除员工

删除员工

/* EmpController.java */
// 批量删除
@DeleteMapping("/{ids}")
public Result delete(@PathVariable List<Integer> ids) {
    log.info("批量删除操作 ids: {}", ids);
    empService.delete(ids);
    return Result.success();
}
/* EmpService.java */
// 批量删除
void delete(List<Integer> ids);
/* EmpServiceImpl.java */
@Override
public void delete(List<Integer> ids) {
    empMapper.delete(ids);
}
/* EmpMapper.java */
// 批量删除员工
void delete(List<Integer> ids);
<!-- EmpMapper.xml -->
<!-- 批量删除员工 -->
<delete id="delete">
    delete from emp
    where id in 
    <foreach collection="ids" item="id" separator="," open="(" close=")">
        #{id}
    </foreach>
</delete>

8.4 新增员工

新增员工

/* EmpController.java */
// 新增员工
@PostMapping
public Result save(@RequestBody Emp emp) {
    log.info("新增员工 emp: {}", emp);
    empService.save(emp);
    return Result.success();
}
/* EmpService.java */
// 新增员工
void save(Emp emp);
/* EmpServiceImpl.java */
@Override
public void save(Emp emp) {
    emp.setCreateTime(LocalDateTime.now());
    emp.setUpdateTime(LocalDateTime.now());
    empMapper.insert(emp);
}
/* EmpMapper.java */
@Insert("insert into emp (username, name, gender, image, job, entrydate, dept_id, create_time, update_time) " +
        "values (#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime})")
void insert(Emp emp);

8.5 文件上传

8.5.1 简介

文件上传是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,日常发微博、发微信朋友圈都用到了文件上传功能。

前端页面三要素:method="post"enctype="multipart/form-data"type="file"

<form action="/upload" method="post" enctype="multipart/form-data">
    姓名:<input type="text" name="username"><br>
    年龄:<input type="text" name="age"><br>
    头像:<input type="file" name="image"><br>
    <input type="submit" value="提交">
</form>

服务端接收文件:MultipartFile

@RestController
public class UploadController {
    @PostMapping("/upload")
    public Result upload(String username, Integer age, 
                         @RequestParam("image") MultipartFile file) { // 名称不一致时需加注解指定请求参数
        return Result.success();
    }
}

上传时会在本地端生成临时文件,上传过程结束后自动被删除。

8.5.2 本地存储

本地存储:在服务端,接收到上传上来的文件之后,将文件存储在本地服务器磁盘中。

@RestController
public class UploadController {
    @PostMapping("/upload")
    public Result upload(String username, Integer age, MultipartFile image) throws IOException {
        // 获取原始文件名
        String originalFilename = image.getOriginalFilename();
        // 构造唯一的文件名:UUID(通用唯一识别码)
        String newFileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf('.'));
        // 将文件保存在服务端目录下
        image.transferTo(new File("~/images/" + newFileName));
        return Result.success();
    }
}

在SpringBoot中,文件上传,默认单个文件允许最大大小为 1M。如果需要上传大文件,可以进行如下配置

# 配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB
# 配置单个请求最大上传大小(一次请求可上传多个文件)
spring.servlet.multipart.max-request-size=100MB
MultipartFile提供的方法 说明
String getOriginalFilename() 获取原始文件名
void transferTo(File dest) 将接收的文件转存到磁盘文件中
long getSize() 获取文件的大小,单位:字节
byte[] getBytes() 获取文件内容的字节数组
InputStream getInputStream() 获取接收到的文件内容的输入流

本地存储的缺点有无法直接访问、磁盘空间限制、磁盘损坏等,因此现在通常使用云存储

8.5.3 阿里云OSS

阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商。

阿里云对象存储OSS (Object Storage Service)是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

OSS

Bucket:存储空间,是用户用于存储对象(Object,文件)的容器,所有的对象都必须隶属于某个存储空间。

SDK(Software Development Kit 的缩写,软件开发工具包):辅助软件开发的依赖(jar包),代码示例等。

OSS流程

OSS流程2

OSS集成的步骤:

  • 引入阿里云OSS上传文件工具类(由官方的示例代码改造而来)
  • 上传图片接口开发
/* AliOSSUtils.java */
@Component
public class AliOSSUtils { /* 去官网复制工具类的示例代码 */ }
/* UploadCoontroller.java */
@RestController
public class UploadController {
    @Autowired
    private AliOSSUtils aliOSSUtils;

    @PostMapping("/upload")
    public Result upload(MultipartFile image) throws IOException {
        log.info("文件上传,文件名:{}", image.getOriginalFilename());
        String url = aliOSSUtils.upload(image); // 调用阿里云OSS工具类,将所上传的文件存入阿里云
        log.info("文件上传成功,文件访问的url: {}", url);
        return Result.success(url);     // 将图片上传完成后的url返回,用于浏览器回显展示
    }
}

8.6 查询回显

查询回显

/* EmpController.java */
// 根据ID查询员工
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
    log.info("根据ID查询员工信息 id: {}", id);
    Emp emp = empService.getById(id);
    return Result.success(emp);
}
/* EmpService.java */
// 根据ID查询员工
Emp getById(Integer id);
/* EmpServiceImpl.java */
@Override
public Emp getById(Integer id) {
    return empMapper.getById(id);
}
/* EmpMapper.java */
// 根据ID查询员工
@Select("select * from emp where id = #{id}")
Emp getById(Integer id);

8.7 修改员工

根据提供的接口文档,所有的修改操作在进行前均需先查询回显

修改员工

/* EmpController.java */
// 修改员工
@PutMapping
public Result update(@RequestBody Emp emp) {
    log.info("更新员工信息:{}", emp);
    empService.update(emp);
    return Result.success();
}
/* EmpService.java */
// 修改员工
void update(Emp emp);
/* EmpServiceImpl.java */
@Override
public void update(Emp emp) {
    emp.setUpdateTime(LocalDateTime.now());
    empMapper.update(emp);
}
/* EmpMapper.java */
// 修改员工
void update(Emp emp);
<!-- EmpMapper.xml -->
<!--更新员工信息 -->
<update id="update">
    update emp
    <set>
        <if test="username != null and username != ''">
            username = #{username},
        </if>
        <if test="password != null">
            password = #{password},
        </if>
        <if test="name != null and name != ''">
            name = #{name},
        </if>
        <if test="gender != null">
            gender = #{gender},
        </if>
        <if test="image != null and image != ''">
            image = #{image},
        </if>
        <if test="job != null">
            job = #{job},
        </if>
        <if test="entrydate != null">
            entrydate = #{entrydate},
        </if>
        <if test="deptId != null">
            dept_id = #{deptId},
        </if>
        <if test="updateTime != null">
            update_time = #{updateTime}
        </if>
    </set>
    where id = #{id}
</update>

9 配置文件

9.1 参数配置化

将Bean中的变量的值配置到配置文件中,再在变量上追加@Value注解指定名称。参数配置化便于参数的统一管理。

参数配置化

当Bean中所有变量在配置文件中前缀一致时还可用以下方式:给类加上注解@ConfigurationProperties并指明参数前缀,保证变量名与配置文件中的名称一致即可。需先引入依赖spring-boot-configuration-processor

@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
}

@Value@ConfigurationProperties的异同:

  • 相同点:都是用来注入外部配置的属性。
  • 不同点:
    • @Value注解只能一个一个的进行外部属性的注入。
    • @ConfigurationProperties可以批量的将外部的属性配置注入到Bean对象的属性中。

9.2 yml配置文件

SpringBoot提供了多种属性配置方式:

  1. application.properties文件
server.port=8080
server.address=127.0.0.1
  1. application.yml/application.yaml文件。基本语法:
    • 大小写敏感
    • 值前边必须有空格,作为分隔符
    • 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(IDEA中会自动将Tab转换为空格)
    • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
    • #表示注释,从这个字符一直到行尾,都会被解析器忽略
server:
 port: 8080
 address: 127.0.0.1
# 对象/Map集合
user:
 name: Akira
 age: 2024
 address: Hangzhou

# 数组/List/Set集合
hobby:
 - java
 - C++
 - python

10 登录认证

10.1 基础登陆功能

基础登陆功能

/* LoginController.java */
@Slf4j
@RestController
public class LoginController {
    @Autowired
    private EmpService empService;

    // 员工登录
    @PostMapping("/login")
    public Result login(@RequestBody Emp emp) {
        log.info("员工登陆:{}", emp);
        Emp e = empService.login(emp);
        return e != null ? Result.success() : Result.error("用户名或密码错误");
    }
}
/* EmpService.java */
// 员工登陆
Emp login(Emp emp);
/* EmpServiceImpl.java */
@Override
public Emp login(Emp emp) {
    return empMapper.getByUsernameAndPassword(emp);
}
/* EmpMapper.java */
// 根据用户名和密码查询用户信息
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);

10.2 登录校验

  • 登录标记:用户登录成功后,每一次请求中,都可以获取到该标记。(会话技术)
  • 统一拦截:过滤器Filter、拦截器Interceptor

登录校验

10.2.1 会话技术

会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。

会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据

会话跟踪方案:

  1. 客户端会话跟踪技术:Cookie

Cookie

  1. 服务端会话跟踪技术:Session

Session

  1. 令牌技术(主流)

令牌技术

10.2.2 JWT令牌

JWT(JSON Web Token,官网:https://jwt.io)定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

场景:登录认证

  1. 登录成功后,生成令牌
  2. 后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理
  • JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。
  • 如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效了,令牌非法。

组成:

  • 第一部分:Header(头),记录令牌类型、签名算法等。例如: {"alg":"HS256","type":"JWT"}(使用Base64编码,下同)
  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id":"1","username":"Akira37"}
  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

JWT

Base64:一种基于64个可打印字符(A-Z a-z 0-9+/)来表示二进制数据的编码方式。

要生成JWT令牌,需引入依赖jjwt

/* JwtUtils.java 工具类 */
public class JwtUtils {
    private static String signKey = "hyplus";
    private static Long expire = 43200000L;

    // 生成JWT令牌
    private static String generateJwt(Map<String, Object> claims) {
        String jwt = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, signKey)   // 签名算法
                .setClaims(claims)  // 自定义内容(载荷)
                .setExpiration(new Date(System.currentTimeMillis() + expire))  // 设置JWT令牌的有效期
                .compact();
        return jwt;
    }

    // 解析JWT令牌
    public static Claims parseJwt(String jwt) {
        Claims claims = Jwts.parser()
                .setSigningKey(signKey)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}

改进后的10.1中的登陆系统如下

/* LoginController.java */
// 员工登录
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
    log.info("员工登陆:{}", emp);
    Emp e = empService.login(emp);

    if (e != null) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", e.getId());
        claims.put("name", e.getName());
        claims.put("username", e.getUsername());

        String jwt = JwtUtils.generateJwt(claims);  // JWT中包含了当前登录员工的信息
        return Result.success(jwt);
    }

    return Result.error("用户名或密码错误");
}

10.2.3 Filter过滤器

过滤器(Filter)是Javaweb 三大组件(Servlet、Filter、Listener)之一,可以把对资源的请求拦截下来,从而实现一些特殊的功能。过滤器一般完成一些通用的操作,比如登录校验、统一编码处理、敏感字符处理等。

过滤器链:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链,各过滤器执行优先级按过滤器类名(字符串)的自然排序。

步骤:

  1. 定义过滤器:定义一个类,实现Filter接口,并重写其方法initdoFilterdestroy(其中initdestroy有默认实现)。
  2. 配置过滤器:Filter类上添加注解@WebFilter并在其中配置拦截资源的路径urlPatterns。启动类上加 @ServletComponentScan开启Servlet组件支持,让Spring可以扫描到。

根据需求配置拦截路径urlPatterns

拦截路径 urlPatterns值 含义
拦截具体路径 /login 只有访问 /login 路径时,才会被拦截
目录拦截 /emps/* 访问/emps下的所有资源都会被拦截
拦截所有 /* 访问所有资源都会被拦截

执行流程:请求-->放行前逻辑-->放行-->资源—>放行后逻辑

实现登录校验过滤器的流程:

实现登录校验过滤器的流程

/* LoginCheckFilter.java */
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("Initiating...");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse resp = (HttpServletResponse) servletResponse;
        // 1. 获取请求url
        String url = req.getRequestURL().toString();
        log.info(url);

        // 2. 判断请求url中是否包含login,若包含则为登录请求,放行
        if (url.contains("login")) {
            log.info("登录操作,放行...");
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }

        // 3. 非登录请求,则获取请求头中的令牌(token)
        String jwt = req.getHeader("token");

        // 4. 判断令牌是否存在,若不存在(无长度,为空串)则返回错误结果(未登录)
        if (!StringUtils.hasLength(jwt)) {
            log.info("请求头token为空,返回未登录信息");
            Result error = Result.error("NOT_LOGIN");
            // 手动将对象转换为json,写入响应
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return;
        }

        // 5. 解析token,若解析失败,返回错误结果(未登录)
        try {
            JwtUtils.parseJwt(jwt);
        } catch (Exception e) {     // JWT解析失败
            e.printStackTrace();
            log.info("解析令牌失败,返回未登录信息");
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return;
        }

        // 6. 放行
        log.info("令牌合法,放行");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        log.info("Destroying...");
    }
}

10.2.4 Interceptor拦截器

拦截器(Interceptor)是一种动态拦截方法调用的机制,类似于过滤器,为Spring框架所提供,用来动态拦截控制器方法的执行,在指定的方法调用前后,根据业务需要执行预先设定的代码。

步骤:

  1. 定义拦截器:实现HandlerInterceptor接口,并重写其所有方法preHandlepostHandleafterCompletion(均有默认实现)。
  2. 注册拦截器:实现WebMvcConfigurer接口并加上注解@Configuration,重写其方法addInterceptors,注入拦截器(使用@Autowired自动装Bean)

根据需要配置拦截路径,方法addPathPatterns配置需要拦截哪些资源,excludePathPatterns配置不需要拦截哪些资源

拦截路径 含义 举例
/* 一级路径 能匹配/depts/emps/login,不能匹配/depts/1
/** 任意级路径 能匹配/depts/depts/1/depts/1/2
/depts/* /depts下的一级路径 能匹配/depts/1,不能匹配/depts/1/2/depts
/depts/** /depts下的任意级路径 能匹配/depts/depts/1/depts/1/2,不能匹配/emps/1

过滤器Filter与拦截器Interceptor的区别:

  • 接口规范不同:过滤器需要实现Fiter接口,而拦截器需要实现Handlerlnterceptor接口。
  • 拦截范围不同:过滤器会拦截所有的资源,而拦截器只会拦截Spring环境中的资源。

实现登录校验拦截器的流程同过滤器。

/* LoginCheckInterceptor.java */
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    // 目标资源方法运行前运行,返回true:放行,放回false,不放行
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
        // 1. 获取请求url
        String url = req.getRequestURL().toString();
        log.info(url);
        // 2. 判断请求url中是否包含login,若包含则为登录请求,放行
        if (url.contains("login")) {
            log.info("登录操作,放行...");
            return true;
        }
        // 3. 非登录请求,则获取请求头中的令牌(token)
        String jwt = req.getHeader("token");
        // 4. 判断令牌是否存在,若不存在(无长度,为空串)则返回错误结果(未登录)
        if (!StringUtils.hasLength(jwt)) {
            log.info("请求头token为空,返回未登录信息");
            Result error = Result.error("NOT_LOGIN");
            // 手动将对象转换为json,写入响应
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return false;
        }
        // 5. 解析token,若解析失败,返回错误结果(未登录)
        try {
            JwtUtils.parseJwt(jwt);
        } catch (Exception e) {     // JWT解析失败
            e.printStackTrace();
            log.info("解析令牌失败,返回未登录信息");
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return false;
        }
        // 6. 放行
        log.info("令牌合法,放行");
        return true;
    }

    // 目标资源方法运行后运行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle...");
    }

    // 视图渲染完毕后运行,最后运行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion...");
    }
}
/* WebConfig.java */
@Configuration      // 配置类
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
    }
}

10.3 全局异常处理

出现异常时,默认返回的结果不符合规范。常规的异常处理方法为在每一个Controller的方法中进行try...catch处理,代码臃肿。

全局异常处理器:无需单独写try...catch,可全局捕获异常。想实现该处理器类需添加注解@RestControllerAdvice(= @ControllerAdvice + @ResponseBody),方法上添加注解@ExceptionHandler捕获异常。若请求处理正常,则从Controller输出;若请求处理异常,则走向全局异常处理器,统一封装错误信息。

全局异常处理

/* GlobalExceptionHandler.java */
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)  // 捕获所有异常
    public Result ex(Exception ex) {
        ex.printStackTrace();
        return Result.error("对不起,操作失败!请联系管理员!");
    }

11 事务管理

事务(Transaction)是一组操作的集合,是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败。

# 开启事务(一组操作开始前)
start transactions;
begin;

# 提交事务(这组操作全部成功后)
commit;

# 回滚事务(中间任何一个操作出现异常)
rollback;

11.1 Spring事务管理

根据事务的定义,调整本案例中解散部门的方法为删除部门同时删除该部门下的所有员工,并在Service层的方法添加注解@Transactional,将其交给spring进行事务管理。

方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务。

在Service层类、接口上使用该注解@Transactional时,表示其所属所有方法均进行事务管理。常加在Service层经常使用增删改操作的方法上。

/* DeptServiceImpl.java */
// 新增empMapper
@Autowired
private EmpMapper empMapper;

@Transactional      // 交给spring进行事务管理
@Override
public void delete(Integer id) {
    deptMapper.deleteById(id);  // 根据ID删除部门数据

    empMapper.deleteByDeptId(id);   // 根据ID删除该部门下的员工
}
/* EmpMapper.java */
// 根据部门ID删除该部门下的员工数据
@Delete("delete from emp where dept_id = #{deptId}")
void deleteByDeptId(Integer deptId);

在yml文件中配置spring事务日志开关:

# 开启事务管理日志
logging:
 level:
  org.springframework.jdbc.support.jdbcTransactionManager: debug

11.2 rollbackFor和propagation

rollbackFor属性

默认情况下,只有出现RuntimeException才回滚异常。可以设置rollbackFor属性控制出现何种异常类型时回滚事务,要想出现任何异常时都回滚可设置@Transactional(rollbackFor = Exception.class)

propagation属性

当一个事务方法被另一个事务方法调用时,该事务方法进行事务控制的方式称为事务传播行为。可以设置propagation属性来控制该方法运行时是否需要/支持事务。

@Transactional
public void a() {
    userService.b();
}

@Transactional(propagation = Propagation.REQUIRED)  // 该方法运行时需要事务,有则加入,无则新建
public void b() { ... }

常用的propagation属性值如下表所示:

propagation属性值 含义
REQUIRED 需要事务。有则加入,无则创建新事务【默认值】
REQUIRES_NEW 需要新事务。无论有无,总是创建新事务
SUPPORTS 支持事务。有则加入,无则在无事务状态中运行
NOT_SUPPORTED 不支持事务。在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY 必须有事务,否则抛异常
NEVER 必须无事务,否则抛异常
...

综上,进一步修改解散部门方法:要求解散部门时,无论是成功还是失败,都要记录操作日志。即新增记录日志到数据库表中。需要创建对应的接口及其实现类,具体如下所示:

/* DeptServiceImpl.java */
// 新增deptLogService
@Autowired
private DeptLogService deptLogService;

@Transactional(rollbackFor = Exception.class)   // 设为出现所有异常都进行回滚
@Override
public void delete(Integer id) {
    try {
        deptMapper.deleteById(id);

        empMapper.deleteByDeptId(id);
    } finally {     // 将记录日志部分的代码放在finally中
        DeptLog deptLog = new DeptLog();    // 部门日志表POJO,表参数有create_time、description
        deptLog.setCreateTime(LocalDateTime.now());
        deptLog.setDescription("执行了解散部门的操作,此次解散的是" + id + "号部门");
        deptLogService.insert(deptLog);
    }
}
/* DeptLogService.java */
public interface DeptLogService {
    void insert(DeptLog deptLog);
}
/* DeptLogServiceImpl.java */
public class DeptLogServiceImpl implements DeptLogService {
    @Autowired
    private DeptLogMapper deptLogMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)  // 设置为需要新事务,使得不被同时结束
    @Override
    public void insert(DeptLog deptLog) {
        deptLogMapper.insert(deptLog);
    }
}
  • REQUIRED:大部分情况下都是用该传播行为即可。
  • REQUIRES_NEW:当不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。

12 AOP

AOP(Aspect Oriented Programming,面向切面编程、面向方面编程)即为面向特定方法编程。

12.1 AOP快速入门

动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。

案例:统计各个业务层方法执行耗时——获取方法运行开始时间;运行原始方法;获取方法运行结束时间并计算执行耗时

  1. 导入依赖:在pom.xml中导入AOP的依赖
<!-- pom.xml -->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>3.2.1</version>
</dependency>
  1. 编写AOP程序:针对于特定方法根据业务需要进行编程。在类上添加注解@Aspect,在模板方法上添加通知注解,此处用@Around(详见7.3),并设置切入点表达式指定特定方法(详见7.5)
/* TimeAspect.java */
@Slf4j
@Component
@Aspect     //AOP类
public class TimeAspect {
    // 将切入点表达式进行抽取,提高复用性
    @Pointcut("execution(* com.hyplus.tlias.service.*.*())")  // 执行目录下所有接口/类的方法时都会调用!
    private void pt() {}

    // 统计方法运行耗时
    @Around("pt()")       // 引用抽取至pt方法的切入点表达式
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 记录开始时间
        long begin = System.currentTimeMillis();

        // 2. 调用原始方法
        Object result = joinPoint.proceed();

        // 3. 获取方法运行结束时间并计算执行耗时
        long end = System.currentTimeMillis();
        log.info(joinPoint.getSignature() + "方法执行耗时:" + (end - begin) + "ms");  // getSignature:获取方法签名

        return result;
    }
}

12.2 核心概念

  • 连接点(Join Point):可以被AOP控制的原始方法/目标方法(暗含方法执行时的相关信息,详见7.6)
  • 通知(Advice):指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
  • 切入点(Point Cut):匹配连接点的条件,通知仅会在切入点方法执行时被应用(即实际被AOP控制的方法)
  • 切面(Aspect):描述通知与切入点的对应关系(通知+切入点)
  • 目标对象(Target):通知所应用的对象

AOP执行流程

12.3 通知类型

5种常用的通知注解:

注解 类型 说明
@Before 前置通知 此注解标注的通知方法在目标方法前被执行
@Around 环绕通知 此注解标注的通知方法在目标方法前、后都被执行【最常用】
① 需有方法参数来传入目标方法,类型为ProceedingJoinPoint,并要求其调用方法proceed()来运行(更多信息调用详见7.6)
② 返回值必须指定为Object,来接收原始方法的返回值
@After 后置通知 此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行,又称最终通知
@AfterReturning 返回后通知 此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing 异常后通知 此注解标注的通知方法发生异常后执行

@PointCut:将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可("方法名()")。为private时仅能在当前类中被引用,为public时可在外部类中被引用。

12.4 通知顺序

当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。

  1. 不同切面类中,默认按照切面类的类名字母排序
    • 目标方法的通知方法:字母排名靠前的执行
    • 目标方法的通知方法:字母排名靠前的执行
  2. 在切面类上添加注解@Order(数字)来控制顺序
    • 目标方法的通知方法:数字小的执行
    • 目标方法的通知方法:数字小的执行

12.5 切入点表达式

切入点表达式:描述切入点方法的一种表达式,主要作用为决定项目中的哪些方法需要加入通知。

@Pointcut("切入点表达式")

12.5.1 execution

execution(...):根据方法签名(方法的返回值、包名、类名、方法名、方法参数等信息)来匹配

execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)

execution(public void com.hyplus.tilas.mapper.EmpMapper.*(java.lang.Integer))

?的表示可省略的部分:

  • 访问修饰符(比如public、protected等。建议省略)
  • 包名.类名(省略后匹配范围为整个项目,范围过大,因此不建议省略)
  • throws异常(注意是方法上声明抛出的异常,不是实际抛出的异常。通常不指定)

两种通配符

  1. *:一级通配符单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
// 例:通配如下所示包下所有以Service结尾的类中所有以delete开头的单参数方法
execution(* com.*.service.*Service.delete*(*))
  1. ..:多个连续的任意符号,可以通配任意层级的包,或任意类型任意个数的参数。
// 例1:通配com.hyplus目录下所有DeptService类的任意参数方法
execution(* com.hyplus..DeptService.*(..))
// 例2:通配任意方法(慎用)
execution(* *(..))

对于多个execution,可正常使用&&||!来连接:

@Pointcut("execution(* com..service.DeptService.list()) || " +
      "execution(* com..service.DeptService.delete(*))")

书写建议:

  1. 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是find开头,更新类方法都是update开头。
  2. 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
  3. 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用..,而是使用*匹配单个包。

12.5.2 @annotation

@annotation:用于匹配标识有特定注解的方法

@annotation(注解全类名)
// 例:匹配所有含有MyLog注解的方法
@Pointcut("@annotation(com.hyplus.aop.MyLog)")

若想某方法被匹配,只需给它加上相应的被检测注解即可。

12.6 连接点

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等,如7.2中所述。

  • 对于@Around通知,获取连接点信息只能用类ProceedingJoinPoint
/* MyAspectExamples.java */
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    log.info("MyAspectExamples around before ...");

    // 1. 获取目标对象的类名
    String className = joinPoint.getTarget().getClass().getName();
    log.info("目标对象的类名: {}", className);

    // 2. 获取目标方法的方法名
    String methodName = joinPoint.getSignature().getName();
    log.info("目标方法的方法名: {}", methodName);

    // 3. 获取目标方法运行时传入的参数
    Object[] args = joinPoint.getArgs();
    log.info("目标方法运行时传入的参数: {}", Arrays.toString(args));

    // 4. 放行:目标方法执行,并获取其返回值
    Object result = joinPoint.proceed();
    log.info("目标方法运行时的返回值: {}", result);

    log.info("MyAspectExamples around after ...");
    return result;
}
  • 对于其他4种通知,获取连接点信息只能用类JoinPoint,其为ProceedingJoinPoint的父类型。
    • 调用方法同上,但某些通知由于其作用时机,无法获得返回值。

12.7 例:记录接口操作日志至数据库表中

将前述案例中增、删、改相关接口的操作日志记录至数据库表中。

日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长。

思路分析:需要对所有业务类中的增、删、改方法添加统一功能,使用AOP技术最为方便——@Around环绕通知;由于增、删、改方法名没有规律,可以自定义@Log注解完成目标方法匹配。

准备
  • 在案例工程中引入AOP的起步依赖(详见7.1)
  • 导入资料中准备好的数据库表结构,并引入对应的实体类
/* com.hyplus.tlias.pojo.OperateLog.java 表结构对应的POJO */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
    private Integer id;     // ID
    private Integer operateUser;        // 操作人ID
    private LocalDateTime operateTime;  // 操作时间
    private String className;       // 操作类名
    private String methodName;  // 操作方法名
    private String methodParams;    // 操作方法参数
    private String returnValue;     // 操作方法返回值
    private Long costTime;      //操作耗时
}
/* com.hyplus.OperateLogMapper.java */
@Mapper
public interface OperateLogMapper {
    // 插入日志数据
    @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
            "values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime})")
    public void insert(OperateLog log);
}
编码
  • 自定义注解@Log
  • 定义切面类,完成记录操作日志的逻辑
/* com.hyplus.tlias.anno.Log.java */
@Retention(RetentionPolicy.RUNTIME)     // 设为运行时生效
@Target(ElementType.METHOD)             // 设为只能添加在方法上
public @interface Log {}

获取当前登录用户的方法:获取request对象,从请求头中获取JWT令牌,解析令牌获取当前用户的ID。详见前文JWT介绍。

/* com.hyplus.tlias.aop.LogAspect.java */
@Slf4j
@Component
@Aspect     // 切面类
public class LogAspect {
    // 直接注入一个Servlet请求Bean来获取JWT令牌(相关概念及工具类见前文JWT介绍)
    @Autowired
    private HttpServletRequest request;

    @Autowired
    private OperateLogMapper operateLogMapper;

    @Around("@annotation(com.hyplus.tlias.anno.Log)")
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取操作人ID(当前登录员工ID):从令牌获取
        // 获取请求头中的JWT令牌,解析令牌
        String jwt = request.getHeader("token");
        Claims claims = JwtUtils.parseJWT(jwt);
        Integer operateUser = (Integer) claims.get("id");

        // 获取操作时间
        LocalDateTime operateTime = LocalDateTime.now();

        // 获取操作类名
        String className = joinPoint.getTarget().getClass().getName();

        // 获取操作方法名
        String methodName = joinPoint.getSignature().getName();

        // 获取操作方法参数
        Object[] args = joinPoint.getArgs();
        String methodParams = Arrays.toString(args);

        // 调用原始方法运行
        long begin = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();

        // 获取方法返回值
        String returnValue = JSONObject.toJSONString(result);

        // 获取操作耗时
        long costTime = end - begin;

        // 记录操作日志
        OperateLog operateLog = new OperateLog(null, operateUser, operateTime, className, 
                                               methodName, methodParams, returnValue, costTime);
        operateLogMapper.insert(operateLog);
        log.info("AOP已记录操作日志: {}", operateLog);

        return result;
    }
}

再给所有需要的方法添加自定义注解@Log即可。


13 Spring原理篇

13.1 配置优先级

如4.2所述,SpringBoot中支持三种格式的配置文件,优先级:properties > yml > yaml

在项目开发时,推荐统一使用一种格式的配置(yml是主流)。

除了配置文件外,SpringBoot还支持Java系统属性-Dserver.port=9000命令行参数--server.port=10010的方式进行属性配置。

Idea中提供了可视化界面来配置:

Idea中提供了可视化界面来配置

打包后配置属性:

  1. 执行maven打包指令package生成jar包
  2. 执行java指令运行jar包,与此同时可执行上述属性配
java -Dserver.port=9000 -jar tlias-web-management-0.8.1-SNAPSHOT.jar --server.port=10019

13.2 Bean管理

13.2.1 获取Bean

默认情况下,Spring项目启动时会把bean都创建好放在IOC容器中。

会受到作用域及延迟初始化影响,本小节主要针对于默认的单例非延迟加载的bean而言。

若想要主动获取这些bean,可通过如下方法:

  1. 根据名称获取bean:Object getBean(String name)
  2. 根据类型获取bean:<T> T getBean(Class<T> requiredType)
  3. 根据名称和类型获取bean:<T> T getBean(String name, Class<T> requiredType)(自带类型转换)
/* 单元测试类 示例 */
// 需注入一个IOC容器对象
@Autowired
private ApplicationContext applicationContext;

@Test
public void testGetBean() {
    // 根据名称获取bean(名称默认为类名首字母小写)
    DeptController bean1 = (DeptController) applicationContext.getBean("deptController"); // 返回Object,需强转
    System.out.println(bean1);

    // 根据类型获取bean
    DeptController bean2 = applicationContext.getBean(DeptController.class);
    System.out.println(bean2);

    // 根据名称与类型获取bean
    DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);
    System.out.println(bean3);
}

13.2.2 Bean作用域

Spring支持五种作用域,后三种在web环境才生效:

作用域 说明
singleton 容器内同名称的bean只有一个实例(单例)【默认】
prototype 每次使用该bean时会创建新的实例(非单例
request 每个请求范围内会创建新的实例(web环境中,了解)
session 每个会话范围内会创建新的实例(web环境中,了解)
application 每个应用范围内会创建新的实例(web环境中,了解)

可以通过@Scope注解来配置作用域

@Scope("prototype")       // 设置该bean为非单例
@RestController
@RequestMapping("/depts")
public class DeptController { ... }

默认singleton的bean,会在容器启动时被创建。添加注解@Lazy可延迟初始化(延迟到其使用时才初始化)。
prototype的bean,每一次使用该bean的时候都会创建一个新的实例。
实际开发当中,绝大部分的Bean是单例的,也就是说绝大部分Bean不需要配置scope属性。

13.2.3 第三方Bean

如果要管理的Bean对象来自于第三方(不是自定义的),是无法用@Component及衍生注解声明Bean的。需要用到@Bean注解。

  1. 在启动类中定义方法,将方法返回值交给IOC容器管理,成为IOC容器的Bean对象(不建议)
@SpringBootApplication
public class SpringbootWebConfig2Application {
    // 声明第三方Bean
    @Bean(name = "method1")   // 将方法返回值交给IOC容器管理,成为IOC容器的Bean对象
    public SAXReader saxReader() {
        return new SAXReader();
    }
}
  1. 通过@Configuration注解声明一个配置类,对这些bean进行集中分类配置,其他同上
@Configuration
public class SpringbootWebConfig2Application {
    @Bean(value = "method2")
    public SAXReader saxReader() {
        return new SAXReader();
    }
}

通过@Bean注解的namevalue属性可以声明bean的名称,默认为方法名。

若第三方Bean需要依赖其它Bean对象,直接在Bean定义方法中设置形参即可,容器会根据类型自动装配。

13.3 SpringBoot原理1:起步依赖

Spring Boot两大原理:起步依赖、自动配置。

原始的Spring框架进行Web程序开发,需要引入大量依赖
原始的Spring框架

Spring Boot的起步依赖原理就是maven的依赖传递:

Spring Boot的起步依赖原理就是maven的依赖传递

13.4 SpringBoot原理2:自动配置

SpringBoot的自动配置就是当spring容器启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要手动声明,从而简化了开发,省去了繁琐的配置操作。

13.4.1 扫描方案

  1. @ComponentScan组件扫描(使用繁琐,性能低)
@ComponentScan({"com.example", "com.hyplus"})   // 设定组件扫描的包
@SpringBootApplication
public class SpringbootWebConfig2Application { ... }
  1. @Import导入,使用该注解导入的类会被Spring加载到IOC容器中。导入形式有:
    • 直接导入普通类
    • 直接导入配置类
    • 导入ImporterSelector接口实现类
/* ImporterSelector接口实现类 */
public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.example.HeaderConfig"};
    }
}
@Import({TokenParser.class, HeaderConfig.class, MyImportSelector.class})    // 导入
@SpringBootApplication
public class SpringbootWebConfig2Application { ... }
  1. 第三方包提供Enable开头的注解(形如@EnableXxxx),封装@Import注解(更方便、优雅,【被Spring Boot采用】)
/* 第三方包中提供的 @EnableXxxx 格式的注解 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)     // 封装导入注解@Import
public @interface EnableHeaderConfig {
}
@EnableHeaderConfig     // 添加该注解相当于直接使用上述Import
@SpringBootApplication
public class SpringbootWebConfig2Application { ... }

13.4.2 底层原理

@SpringBootApplication注解标识在SpringBoot工程引导类上,是SpringBoot中最最最重要的注解。由三个部分组成:

  1. @SpringBootConfiguration: 该注解与@Configuration注解作用相同,用来声明当前也是一个配置类。
  2. @ComponentScan:组件扫描,默认扫描当前引导类所在包及其子包。
  3. @EnableAutoConfiguration: SpringBoot实现自动化配置的核心注解。

自动化配置的核心注解

自动化配置

如上所示,并非全部装配为IOC容器的Bean,而是添加了@Conditional及其子注释来进行条件装配
@Conditional:在方法、类上使用,按照一定的条件进行判断,在满足给定条件后才会注册对应的bean对象到Spring IOC容器中。
其子注解如下图所示:

子注解

此处介绍常用的3个:

  • @ConditionalOnClass:判断环境中是否有对应字节码文件,才注册bean到IOC容器。需使用属性name指定类的全类名,或属性value直接指定字节码文件对象。
@Bean
@ConditionalOnClass(name = "io.jsonwebtoken.jwts")    //当前环境存在指定的这个类时,才声明该bean
public HeaderParser headerParser() { ... }
  • @ConditionalOnMissingBean:判断环境中没有对应的bean,才注册bean到IOC容器。可指定类型(value属性)或名称(name属性)。
@Bean
@ConditionalOnMissingBean   // 当不存在当前类型的bean时,才声明该bean
public HeaderParser headerParser() { ... }
  • @ConditionalOnProperty:判断配置文件中有对应属性name属性)和对应值havingValue属性),才注册bean到IOC容器。
    @Bean
    @ConditionalOnProperty(name = "name", havingValue = "hyplus")   // 配置文件中存在对应的属性和值,才注册bean到IOC容器。
    public HeaderParser headerParser() { ... }

13.4.3 自定义starter

在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用。而在SpringBoot的项目中,一般会将这些公共组件封装为SpringBoot的starter

自定义starter

案例需求:自定义aliyun-Oss-spring-boot-starter,完成阿里云OSS操作工具类AliyunOSSUtils的自动配置。
目标:引入起步依赖引入之后,要想使用阿里云OSS,注入AliyunOSSUtils直接使用即可。
步骤:

  1. 创建aliyun-oss-spring-boot-starter模块
  2. 创建aliyun-oss-spring-boot-autoconfigure模块,在starter中引入该模块
  3. 在aliyun-oss-spring-boot-autoconfigure模块中的定义自动配置功能,并定义自动配置文件META-INF/spring/xxxx.imports

自定义starter


14 Maven高级

14.1 分模块设计与开发

分模块设计即将项目按照功能拆分成若干个子模块
作用:方便项目的管理维护、扩展,也方便模块间的相互调用,资源共享。

注:分模块开发需要先针对模块功能进行设计,再进行编码。不会先将工程开发完毕,然后进行拆分。

分模块设计与开发

分模块设计与开发

14.2 继承

14.2.1 继承关系

继承描述的是两个工程间的关系,子工程可以继承父工程中的配置信息,常见于依赖关系的继承。与Java类继承相似,Maven工程继承只能单继承,但支持多重继承。

作用:简化依赖配置、统一管理依赖

实现:<parent> ... </parent>

继承关系

  1. 创建maven模块tlias-parent,该工程为父工程,设置打包方式为pom
    • jar:普通模块打包,springboot项目基本都是jar包(内嵌tomcat运行)【默认】
    • war:普通web程序打包,需要部署在外部的tomcat服务器中运行
    • pom:父工程或聚合工程,该模块不写代码,仅进行依赖管理

设置打包方式为pom

  1. 子工程的pom.xml文件中,配置继承关系
    • 配置了继承关系之后坐标中的groupId可省略,因为会自动继承父工程的。
    • <relativePath>指定父工程的pom文件的相对位置。若不指定,将从本地仓库/远程仓库查找该工程。

在子工程的pom.xml文件中,配置继承关系

  1. 在父工程中配置各个工程共有的依赖,如lombok、各种starter等(子工程会自动继承父工程的依赖)
    • 若父子工程都配置了同一个依赖的不同版本,以子工程的为准。

14.2.2 版本锁定

在maven中,可以在父工程的pom文件中通过<dependencyManagement>来统一管理依赖版本。子工程引入依赖时,无需指定<version>版本号,父工程统一管理。变更依赖版本,只需在父工程中统一变更。

<dependencies><dependencyManagement>的区别:

  • <dependencies>是直接依赖,在父工程配置了依赖,子工程会直接继承下来
  • <dependencyManagement>是统一管理依赖版本,不会直接依赖,还需要在子工程中引入所需依赖(无需指定版本)

版本锁定

自定义属性/引用属性:

自定义属性/引用属性

14.3 聚合

聚合指将多个模块组织成一个整体,同时进行项目的构建。聚合工程为一个不具有业务功能的“空”工程(有且仅有一个pom文件)。
作用:快速构建项目(无需根据依赖关系手动构建,直接在聚合工程上构建即可)

聚合

maven中可以通过<modules>设置当前聚合工程所包含的子模块名称。
聚合工程中所包含的模块,在构建时,会自动根据模块间的依赖关系设置构建顺序,与聚合工程中模块的配置书写位置无关。

通过modules标签设置当前聚合工程所包含的子模块名称

继承与聚合……

  • 作用
    • 聚合用于快速构建项目
    • 继承用于简化依赖配置、统一管理依赖
  • 相同点
    • 聚合与继承的pom.xml文件打包方式均为pom,可以将两种关系制作到同一个pom文件中
    • 聚合与继承均属于设计型模块,并无实际的模块内容
  • 不同点
    • 聚合是在聚合工程中配置关系,聚合可以感知到参与聚合的模块有哪些
    • 继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己

14.4 私服

私服是一种特殊的远程仓库,它是架设在局域网内的仓库服务,用来代理位于外部的中央仓库,用于解决团队内部的资源共享与资源同步问题。

私服在企业项目开发中,一个项目/公司只需1台即可(无需一般人自己搭建,会使用即可)。

依赖查找顺序:本地仓库 → 私服 → 中央仓库

私服

资源版本:

  • RELEASE发行版本):功能趋于稳定、当前更新停止,可以用于发行的版本,存储在私服中的RELEASE仓库中
  • SNAPSHOT快照版本):功能不稳定、尚处于开发中的版本,存储在私服的SNAPSHOT仓库中

资源上传与下载:

资源上传与下载

流程:

  1. 设置私服的访问用户名/密码(maven配置文件settings.xml中的<servers>中配置)

私服的访问用户名/密码

  1. 在IDEA的maven工程的pom文件中配置上传(发布)地址

在IDEA的maven工程的pom文件中配置上传(发布)地址

  1. 设置私服依赖下载的仓库组地址(maven配置文件settings.xml中的<mirrors><profiles>中配置)

设置私服依赖下载的仓库组地址

设置私服依赖下载的仓库组地址

《Spring Boot开发基础教程》有1条评论

发表评论