Spring MVC 框架

标题目录

定义

MVC

MVC是一种软件架构的设计思想,目的是为了降低项目中各个模块间的耦合度

  • M:Model 模型
  • V:View 视图
  • C:Controller 控制器

模型层: 负责业务逻辑的处理和数据库的操作

视图层: 负责页面数据的最终展示

控制器层: 负责接收客户端的请求,并调用模型处理业务,最终返回响应

Spring MVC

不同的编程语言都有实现MVC这种设计思想的具体框架,Spring MVC是Java编程语言中的一个具体框架

  • Java: Spring MVC框架
  • Python: Django、Flask、Tornado框架

请求分类

静态请求

固定的统一的文件,比如:html文件、图片文件等,所有用户得到的响应相同

Spring MVC处理静态请求:

  1. 启动 Spring MVC 工程
  2. 将静态文件[html、图片等]放入 resources/static 目录下
  3. 右键 static 目录,选择 Rebuild static
  4. 浏览器中输入地址访问

动态请求

每个用户向同一个URL地址发请求,得到的结果都不一样

SpringMVC处理动态请求:

  1. 创建Controller的类,添加@Controller注解
  2. 创建控制器方法,添加@RequestMapping("")、@ResponseBody注解
  3. 重启工程
  4. 浏览器中访问地址测试

HTTP 协议

定义

  • 超文本传输协议
  • 在Spring MVC中我们发送请求使用的协议是 HTTP(Hypertext Transfer Protocol)协议,它构建于TCP/IP 协议之上,是 TCP/IP 协议中的应用层协议,我们称之为超文本传输协议,用于实现客户端浏览器与服务器进行数据通讯

工作原理

  • 客户端(通常是Web浏览器)发起一个 HTTP 请求给服务器。
  • 服务器处理该请求,并返回一个HTTP响应给客户端。
  • HTTP响应可以包含任何类型的数据,但最常见的数据格式是HTML文档,这些文档可以嵌入图像、视频等其他资源。

请求和响应

请求 Request

  • 请求行:对当前请求的基本信息的描述,包括请求方法、请求路径、HTTP协议版本
    • GET:在服务器端获取资源,比如:查看朋友圈、查看订单、查看购物车、查看文章...
    • POST:在服务器端新增资源,比如:发表朋友圈、创建订单、添加购物车、发表文章...
    • PUT:在服务器端更新资源,比如:编辑朋友圈、修改订单、修改购物车、编辑文章...
    • DELETE:在服务器端删除资源,比如:删除朋友圈、删除订单、删除购物车、删除文章...
  • 请求头:对当前请求进一步的解析和描述
  • 请求体:客户端传递给服务端的具体数据,比如注册信息、登录信息等

响应 Response

  • 响应行:对当前响应的基本描述,包括HTTP协议版本、响应状态码、附加信息
    • 1xx:表示请求已接收,继续处理
    • 2xx:表示请求已被服务端成功处理
    • 3xx:表示请求需更进一步的操作(重定向)
    • 4xx:客户端请求不正确
    • 5xx:服务器处理请求时错误
  • 响应头:对当前响应的进一步描述
  • 响应体:服务端返回给客户端的具体数据,比如订单数据、商品数据

http 和 https 的区别

  • 默认端口号:http-80,https-443
  • 安全性:
    • http 数据以明文的方式传输,不太安全
    • https 数据以密文的方式传输,相对安全
  • 传输效率
    • http 传输效率较高,https 传输效率较低
  • https 网站需要 ssl 证书

数据传递与接收

客户端传递数据到服务端

GET 请求

POST 请求

  • 发送请求方式:在 <form> 表单中发送

  • 传递数据:通过 <input> 表单控件的方式传递

    <form action="http://localhost:8080/v1/users/login" method="post"> </form>

查询参数传递

http://localhost:8080/v1/users/login?username=xxx\&password=yyy

请求体传递

复制代码
<form action="" method="">
    <input type="text" name="username">
    <input type="password" name="password">
    <input type="submit" value="登录">
</form>

请求路径 PATH 传递

http://localhost:8081/bmi/1.75/50 (其中1.75 和 50 是客户端传递给服务端的数据 )

服务端接收数据

原则:参数数量较少时,使用非封装参数的方式接收;较多时,使用封装参数方式接收

非封装参数[声明参数接收]

  • 在控制器方法中通过声明参数方式接收客户端传递的数据(注意参数名和类型)

    public String login(String username, String password){
    return username + ":" + password;
    }

封装参数[声明POJO类]

  • 定义DTO类:UserLoginDTO

    public class UserLoginDTO {
    //用户名,密码
    private String username;
    private String password;
    }

  • 控制器中接收数据:UserController

    @RequestMapping("/v1/user/login")
    @ResponseBody
    public String login(UserLoginDTO userLoginDTO){
    String username = userLoginDTO.getUsername();
    String password = userLoginDTO.getPassword();
    return username + ":" + password;
    }

路径 PATH 数据接收

@PathVariable注解

复制代码
@GetMapping("/bmi/{height}/{weight}")
@ResponseBody
public String bmi(@PathVariable Double height,@PathVariable Double weight){...}

POJO类

POJO 指的是普通的 Java 对象,不依赖于任何特殊框架或接口的简单 Java 类,通常只包含一些属性和 getter/setter 方法

实体类 entity

和数据表做一一映射的关系

比如微博实体类

复制代码
public class Weibo {
    private Integer id;
    private String content;
    private Date created;
    private Integer userId;
    //setter getter toString方法
}

ORM:(Object Relational Mapper)对象关系映射,允许开发人员以面向对象的方式操作数据库,简化了数据持久化的流程,一个类--一张表,一个属性--一个表字段,一个对象--一条表记录

VO 类

服务端返回给客户端的数据,使用 VO 类进行封装

比如微博详情功能,SELECT id,content,created FROM weibo WHERE id=1

复制代码
public class WeiboDetailVO {
    private Long id;
    private String content;
    private Date created;
    //setter getter toString方法
}

DTO 类

客户端传递给服务器端的数据,使用DTO类进行封装

比如注册功能,客户端需要把用户名、密码、手机号、邮箱传递给服务器

如果用于增删改操作,取名 XxxParam,比如:UserRegParam

复制代码
public class UserRegParam {
    private String username;
    private String password;
    private String phone;
    private String email;
    //setter getter toString方法
}

如果用于查询操作,取名 XxxQuery 比如 :UserSelectQuery

测试

  • 开发工程师常用的测试工具有: HttpClient、Knife4j[工作中常用]、Postman;

在 test 文件夹下创建 http 文件夹,然后在 http 文件夹下创建 .http 文件

点击 Add request 选择对应的测试方法

填写相应参数后点击左边绿色箭头即可进行测试

  • 既是分隔符,又可以作为注释

  • POST 请求的 Content-Type 行和参数行之间必须有一个空行

工程流程

创建项目

  1. 创建工程,勾选依赖:mysql、mybatis、spring web
  2. 搭建整体的工程结构:controller、mapper、pojo、xml、http
    • controller 记得添加 @Controller 注解
    • mapper 记得添加 @Mapper注解
    • pojo 有子包 entity、vo 和 dto
    • xml 创建在 resources 文件夹下的 mappers 文件夹里,记得填写 namespace
    • http 创建在 test 文件夹下的 http 文件夹里,创建的是 file 类型的文件
  3. 配置文件中相关配置:数据库连接信息、xml文件映射的位置和端口号设置
  4. 实体类Notice
  5. 启动工程,依据API文档依次实现每个功能

实现功能

  1. 确认是否需要 VO 和 DTO 类
  2. 完成 Controller 中的方法
  3. 完成 Mapper 中的方法
  4. 在 xml 文件中配置 SQL 语句
  5. 重启工程测试

整体流程

  1. 创建工程,勾选:mybatis、mysql、spring web 依赖

  2. 整体工程环境:
    a. controller.XxxController (接口上添加 @Controller 注解)
    b. mapper.XxxMapper (接口上添加 @Mapper 注解)
    c. pojo.entity.Xxx pojo.vo pojo.dto
    d. resources/mappers/Xxxx.xml (勿忘指定命名空间 namespace 属性值)
    e. test/http/XxxTests.http

  3. 配置文件做相关配置:

    server.port=8080

    spring.datasource.url= //千万不要忘记改库名
    spring.datasource.username=
    spring.datasource.password=

    mybatis.mapper-locations=classpath:mappers/*.xml

    mcm: 驼峰命名自动映射

    mybatis.configuration.map-underscore-to-camel-case=true

  4. 按照API文档依次实现功能
    a. 确定是否需要VO和DTO类
    b. 完成Controller中的方法逻辑 自动装配
    c. 完成XxxMapper中方法创建
    d. 完成xml文件中的SQL (id的属性值一定是接口方法的名字,要复制,不要手敲)
    e. 重启工程,测试功能

MySQL数据类型和Java类型映射对应关系

| 数据类型分类 | MySQL数据类型 || Java-Pojo类属性 ||

数据类型分类 类型名称 说明 类属性类型 说明
整型 TINYINT 微整型 Integer 对应Java中的Integer类型
整型 SMALLINT 小整型 Integer 对应Java中的Integer类型
整型 INT 标准整型 Integer 对应Java中的Integer类型
整型 MEDIUMINT 中等整型 Integer 对应Java中的Integer类型
整型 BIGINT 大整型 Long 对应Java中的Long类型
浮点型 FLOAT 单精度浮点型 Float 对应Java中的Float类型
浮点型 DOUBLE 双精度浮点型 Double 对应Java中的Double类型
浮点型 DECIMAL 高精度十进制数 Double 对应Java中的Double类型
字符类型 CHAR 固定长度字符串 String 对应Java中的String类型
字符类型 VARCHAR 可变长度字符串 String 对应Java中的String类型
字符类型 TEXT 长文本 String 对应Java中的String类型
字符类型 LONGTEXT 超长文本 String 对应Java中的String类型
日期时间类型 DATE 日期(YYYY-MM-DD) java.util.Date 或 java.time.LocalDateTime 对应Java中的日期时间类型
日期时间类型 TIME 时间(HH:MM:SS) java.util.Date 或 java.time.LocalDateTime 对应Java中的日期时间类型
日期时间类型 DATETIME 日期时间(YYYY-MM-DD HH:MM:SS) java.util.Date 或 java.time.LocalDateTime 对应Java中的日期时间类型
日期时间类型 TIMESTAMP 时间戳(自动记录更新时间) java.util.Date 或 java.time.LocalDateTime 对应Java中的日期时间类型

有用的方法

BeanUtils.copyProperties

BeanUtils.copyProperties 可以将源对象中同名属性的值复制到目标对象的对应属性中

  • 类型自动转换:如果源属性和目标属性的类型不同,会尝试进行类型转换(如 String 转 Integer)
  • 忽略空值:部分实现支持忽略源对象中的空值属性,只复制非空值
  • 支持嵌套属性:可以处理嵌套对象的属性复制

注意:

  • 属性名称必须相同:只有源对象和目标对象中名称相同的属性才会被复制
  • 访问限制:目标对象的属性必须有对应的 Setter 方法,否则无法赋值

示例:

复制代码
//将数据插入到数据库
Notice notice = new Notice();

notice.setTitle(noticeAddParam.getTitle());
notice.setContent(noticeAddParam.getContent());
notice.setType(noticeAddParam.getType());
notice.setStatus(noticeAddParam.getStatus());

//复制属性
BeanUtils.copyProperties(noticeAddParam,notice);

CONCAT

CONCAT() 是 SQL 中用于字符串拼接的核心函数,在模糊查询、动态 SQL 构建等场景中非常常用

  • 语法:CONCAT(str1, str2, ..., strN)
  • 参数:接受 2 个或以上的字符串参数(不同数据库可能有差异,如 Oracle 仅支持 2 个参数)
  • 返回值:拼接后的新字符串

错误写法:(SQL 注入风险)

复制代码
<!-- 危险!直接拼接用户输入 -->
LIKE '%${title}%'

正确写法:

复制代码
LIKE CONCAT('%', #{title}, '%')

原理:

  • MyBatis 将 #{title} 解析为参数占位符 ?。
  • SQL 被预编译为 WHERE title LIKE ?。
  • 参数值 "%用户输入值%" 由 JDBC 安全处理,避免注入。

注解

Spring Framework

组件扫描与注册

注解 作用
@Component 通用组件标记,被 Spring 自动扫描并注册为 Bean
@Controller 标记 MVC 控制器,处理 HTTP 请求(等价于 @Component + 特殊语义)
@Service 标记业务逻辑层组件(等价于 @Component + 业务逻辑语义)
@Repository 标记数据访问层组件,提供数据库操作(自动处理数据库异常)
@ComponentScan 指定 Spring 扫描组件的包路径(如 @ComponentScan("com.example"))

依赖注入

注解 作用
@Autowired 自动注入依赖 Bean(按类型匹配,支持构造器 / 字段 / 方法注入)
@Qualifier 配合 @Autowired 使用,通过 Bean 名称指定注入具体实现类
@Value 注入配置文件中的属性值(如 @Value("${app.name}"))

生命周期与作用域

注解 作用
@Scope 指定 Bean 的作用域(如 @Scope("prototype") 创建多例)
@PostConstruct 标记 Bean 初始化后执行的方法(等价于 InitializingBean 接口)
@PreDestroy 标记 Bean 销毁前执行的方法(等价于 DisposableBean 接口)

配置管理

注解 作用
@Configuration 标记一个类,表明这个类是 Spring 应用程序的配置类
@Bean 在 Java 配置类里,使用 @Bean 注解标记的方法会返回一个对象,该对象会被注册到 Spring 容器中,成为一个可被管理的 Bean
@PropertySource 加载自定义配置文件(如 @PropertySource("classpath:app.properties"))

MyBatis

注解 作用
@Mapper 标记接口为 MyBatis Mapper,由 Spring 自动扫描并创建实现类(替代 XML 配置)
@Insert 声明 INSERT SQL 语句,用于插入数据(如 @Insert("INSERT INTO users VALUES (...)"))
@Delete 声明 DELETE SQL 语句,用于删除数据(如 @Delete("DELETE FROM users WHERE id=#{id}"))
@Update 声明 UPDATE SQL 语句,用于更新数据(如 @Update("UPDATE users SET name=#{name} WHERE id=#{id}"))
@Select 声明 SELECT SQL 语句,用于查询数据(如 @Select("SELECT * FROM users"))
@MapperScan 指定需要扫描的 Mapper 接口所在包路径,自动注册为 Spring Bean,无需每个接口添加 @Mapper 注解

Spring MVC

注解 作用
@Controller 标记类为 MVC 控制器,处理 HTTP 请求(需配合 @ResponseBody 返回数据)
@ResponseBody 将返回值序列化为 JSON/XML 响应体(@RestController 已包含此功能)
@RequestBody 将 HTTP 请求的内容体(如 JSON、XML 等)自动反序列化为 Java 对象
@RestController 等价于 @Controller + @ResponseBody,直接返回 JSON/XML 数据,用于 RESTful API
@RequestMapping 通用请求映射,可指定路径、方法、请求头等属性(推荐使用专用方法注解)
@GetMapping 处理 HTTP GET 请求,等价于 @RequestMapping(method = RequestMethod.GET)
@PostMapping 处理 HTTP POST 请求,用于创建资源
@PutMapping 处理 HTTP PUT 请求,用于全量更新资源
@DeleteMapping 处理 HTTP DELETE 请求,用于删除资源
@PathVariable 从 URL 路径中获取参数(如 /api/{version} → @PathVariable String version)

其它

@JsonFormat

@JsonFormat 主要用于控制 Java 对象属性在序列化为 JSON 时的格式

当你需要自定义日期时间、数字或者枚举类型的输出格式时就可以用到 @JsonFormat

参数 :

  • pattern:用于指定日期时间的格式,使用标准的 Java 日期格式模式
    示例:"yyyy-MM-dd"、"HH:mm:ss"、"yyyy-MM-dd HH:mm:ss"

  • timezone:用于指定时区,避免出现时区转换错误
    示例:"Asia/Shanghai"、"UTC"、"GMT+8"

  • shape:用于指定序列化后的类型,通常用于枚举类型
    示例:JsonFormat.Shape.STRING、JsonFormat.Shape.NUMBER

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

JSON 格式

  • JSON格式数据是一种轻量级的通用的数据交换格式,用来存储和传输数据的一种文本字符串形式,用于不同的软件程序之间的数据传递
  • 使用SpringMVC框架时
    • 需要将 Java 对象转换成 JSON 格式返回给 Web 前端,注解为 @ResponseBody
    • 需要将 js 对象转换为 JSON 格式的字符串给到框架,注解为 @RequestBody

客户端通过 JSON 格式发送数据

复制代码
<html>
<script>
    const data = {username:"xxx", password:"yyy"};
    axios.post("服务端地址", data).then(response=>{});
</script>
</html>

服务端接收时需要在参数前添加 @RequestBody 注解

复制代码
public String login(@RequestBody UserLoginParam userLoginParam){}

使用 http 测试时,请求体类型: Content-Type: application/json

选择 Post Text Body

复制代码
POST http://localhost:8081/v1/user/login
Content-Type: application/json

{
  "username":"admin",
  "password":"123456"
}

Spring MVC 执行流程

Spring MVC中的组件

  • DispatherServlet前端控制器
  • HandlerMapping处理器映射器
  • HandlerAdapter处理器适配器
  • HandlerController处理器控制器
  • ViewResolver视图解析器

执行流程图

执行流程

  1. 客户端发送请求至前端控制器 DispatcherServlet
  2. DispatcherServlet 收到请求后,调用处理器映射器 HandlerMapping
  3. HandlerMapping 根据请求URL找到具体的 Controller
  4. 通过处理器适配器 HandlerAdapter 适配具体执行该 Controller 的方式
  5. Controller 处理请求,并返回 ModelAndView
  6. DispatcherServlet 通过 ViewReslover(视图解析器)确定负责显示数据的具体 View
  7. DispatcherServlet 对 View 进行渲染视图(即将Model填充至视图组件中),并将完整的视图响应到客户端

响应数据封装

在前后端分离架构中,前端团队和后端团队,在编码时是并行开发的,此时就需要有统一的API规范设计,统一的响应数据格式,如果对于每个接口都单独进行处理的话,不仅逻辑复杂,而且容易出现疏漏,进而增加前端调用的难度,所以在服务端设计时通常会对返回的数据进行标准格式的封装。

服务端返回给客户端的信息:

  • 响应状态码 state:
    200 404 405 500;
    自定义内部状态码, 比如10100登录成功、10101用户名错误、10102用户名被占用...
  • 提示消息 message:
    比如登录成功、用户名错误、用户名被占用、未登录...
  • 响应数据 data:
    没有具体数据的控制器方法: 登录、注册、发微博、发评论
    有具体数据的控制器方法: 首页、详情页、列表页

示例:

  • 登录功能:{"status":200,"message":"登录成功","data":null}
  • 查询订单:{"status":200,"message":"操作成功","data":List<OrdersVO>}

自定义枚举状态码

HTTP定义了很多状态码,比如200表示成功,404表示资源未找到,使用HTTP状态码虽然能够大致表示请求的处理结果,但是无法精准地表示某些特定的业务情况。

自定义状态码优点

  • 状态回馈更加精准
    自定义状态码可以针对性地表示某种业务的特定情况,更加精准地反映请求的处理结果

    • 服务端返回 JsonResult

      情况1: {"state": 10100, "message": "用户名或密码错误", "data": null}
      情况2: {"state": 10101, "message": "操作成功", "data": null}

    • 客户端代码

      <script> axios.post("/v1/user/login", data).then(response){ if (response.data.state==10100){ alert(response.data.message); } else if(response.data.state==10101){ //记录登录状态; //重定向到网站首页; } else{ //服务器繁忙,请稍后再试 } } </script>
  • 处理错误更加方便
    开发者可以针对每个状态码特定的处理,以便于更好地处理不同的错误情况

  • 易于扩展
    自定义状态码能够更好地应对业务的变化,可以根据业务需要自由地添加、移除或修改状态码来适应新的业务场景

统一响应结果返回

优点

  • 简化前端开发
    项目开发中,前端需要与后端进行数据交互,通过统一响应结果返回,前端不需要每次对不同格式的响应结果进行解析,只需处理统一的数据结构。
  • 提高代码可读性和可维护性
    通过统一响应结果返回,可以将响应结果的处理逻辑集中在一个地方,提高代码的可读性和可维护性。
  • 统一处理状态码
    统一响应结果返回中可以定义不同的状态码来标识不同的响应结果,如成功、失败等。通过统一的状态码,可以快速判断响应结果的状态,方便进行后续的逻辑处理。
  • 统一处理异常
    在Spring MVC中,异常处理常常需要自定义异常处理方法,并在异常处理器中进行统一处理。通过统一响应结果返回,可以将异常信息统一封装成一个固定格式的数据结构返回给前端,方便异常的处理和展示。

编码流程

  • 第1步:自定义枚举类 base.response.StatusCode(3个注解,2个属性)

    复制代码
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    public enum StatusCode {
        //所有枚举类的实例都在最上面;
        OPERATION_SUCCESS(2000, "操作成功"),
        OPERATION_FAILED(2001, "操作失败");
        private Integer state;
        private String message;
    }
  • 第2步:创建统一响应结果返回的类 base.response.JsonResult(4个注解,3个属性,2个构造,2个静态)

    复制代码
    @Setter
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public class JsonResult {
        //状态码、提示消息、具体数据
        private Integer state;
        private String message;
        private Object data;
    
        /**
         * 构造方法:针对于没有具体数据返回的控制器方法;
         * 比如:添加资讯、删除资讯、更新资讯、登录、注册;
         */
        public JsonResult(StatusCode statusCode) {
            this.state = statusCode.getState();
            this.message = statusCode.getMessage();
            this.data = null;
        }
    
        /**
         * 构造方法:针对于有具体数据返回的控制器方法;
         * 比如:资讯列表、资讯详情、查询订单、查询购物车;
         */
        public JsonResult(StatusCode statusCode, Object data) {
            this.state = statusCode.getState();
            this.message = statusCode.getMessage();
            this.data = data;
        }
    
        /**
         * 封装2个静态方法:
         * 1.针对于操作成功的没有具体数据返回的控制器方法;
         * 2.针对于操作成功的有具体数据返回的控制器方法;
         */
        public static JsonResult ok(){
            return new JsonResult(StatusCode.OPERATION_SUCCESS);
        }
    
        public static JsonResult ok(Object data){
            return new JsonResult(StatusCode.OPERATION_SUCCESS, data);
        }
    }
  • 第3步:在 Controller 中使用.

    复制代码
    public JsonResult xxx(){
        return JsonResult.ok();
    }

Lombok

作用

  • 通过 @Setter @Getter @ToString @Data @AllArgsConstructor @NoArgsConstructor注解生成模板代码,简化开发,提高开发效率
  • 提供了日志功能

日志功能

日志级别【5种】

一旦在工程中设置了某一个日志级别,则只会打印该日志级别和比该级别严重的日志信息

TRACE < DEBUG < INFO < WARN < ERROR

  • TRACE: 追踪级别,显示特别详细的日志,一般不使用,因为非常消耗资源
  • DEBUG: 调试级别,显示比较详细的日志,一般开发过程中使用该级别,用于调试程序;
  • INFO: 运行级别,是默认的日志级别,一般项目上线后使用该级别
  • WARN: 警告级别,一般不影响程序的执行,但是需要重点关注
  • ERROR: 错误级别,立刻解决

日志使用流程

  • 第1步:添加依赖,刷新Maven[可以通过勾选]

    复制代码
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.30</version>
                        </path>
                    </annotationProcessorPaths>
                    <source>16</source>
                    <target>16</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
  • 第2步:配置文件中配置日志级别信息

    复制代码
    # 全局设置为info
    logging.level.root=info
    
    # 工程目录设置:开发过程设置为debug,上线后设置为info
    logging.level.工程目录=debug
  • 第3步:控制器中使用

    • 在 Controller 类上添加 @Slf4j 注解

    • 在方法中使用日志

      log.trace("追踪信息");
      log.debug("调试信息"); //开发过程使用
      log.info("运行信息");
      log.warn("警告信息");
      log.error("错误信息"); //在全局异常处理器中使用

Knife4j

作用

  • 进行功能测试,服务端开发功能后, 可直接在 Knife4j 中进行测试
  • 生成 API 文档,开发者将 API 文档同步给前端, 降低了前后端之间沟通的成本

使用流程

  • 第1步:手动添加项目依赖
    注意:该依赖只能手动添加,不能在创建SpringBoot工程时勾选

    复制代码
    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
        <version>4.4.0</version>
    </dependency>
  • 第2步:在 application.properties 配置文件中添加如下配置

    复制代码
    # 启用Knife4j增强功能
    knife4j.enable=true
    # # 分组配置
    # 定义 API 分组的名称(组标识符)
    springdoc.group-configs[0].group=default
    # 指定该分组包含哪些路径的接口
    springdoc.group-configs[0].paths-to-match=/**
    # 明确扫描的控制器包路径
    springdoc.group-configs[0].packages-to-scan=工程目录中controller包路径
  • 第3步:启动项目测试
    启动项目,通过 http://localhost:8080/doc.html 即可访问在线API文档。

  • 第4步:【可以配置多个控制器的显示顺序】
    base.config.Knife4jConfig

    复制代码
    @Configuration
    public class Knife4jConfig {
        @Bean
        public OpenAPI customOpenAPI() {
            return new OpenAPI()
                    .tags(getTagsOrdered()); // 指定 Tag 顺序
        }
        // 定义 Tag 顺序
        private List<Tag> getTagsOrdered() {
            return Arrays.asList(
                    new Tag().name("01-XX模块").description("XX管理相关接口"),
                    new Tag().name("02-XX模块").description("XX管理相关接口"),
                    new Tag().name("03-XX模块").description("XX管理相关接口")
            );
        }
    }

常用注解

  • 控制器类名在文档中显示名称
    @Tag(name="")
  • 控制器方法名在文档中显示名称
    @Operation(summary="")
  • 控制器方法名排序
    @ApiOperationSupport(order=数字)
  • 封装类参数描述
    @Schema(description="", required=true, example="")
  • 非封装参数描述【单个参数】
    @Parameter(name="参数名", description="参数描述", required=true, example="", hidden=true)
  • 非封装参数【多个参数】
    @Parameters(value={@Parameter(), @Parameter(), ...})

请求体数据类型

JSON 格式

服务器的控制器方法参数前添加了 @RequestBody 注解

表单形式

服务器的控制器方法参数前没有添加 @RequestBody 注解

  1. 先在参数前添加 @RequestBody 注解,重启工程,到 knife4j 文档中测试

  2. 去掉RequestBody 注解,在方法上添加如下注解

    复制代码
    @Parameters(value ={
            @Parameter(name = "title",description = "资讯标题",required = true),
            @Parameter(name = "content",description = "资讯内容",required = true),
            @Parameter(name = "type",description = "资讯类型",required = true),
            @Parameter(name = "status",description = "资讯状态",required = true),
            @Parameter(name = "noticeAddParam",hidden = true)
    })
    public JsonResult add(NoticeAddParam noticeAddParam){}
  3. 重启工程,在 knife4j 文档中测试

常用配置

复制代码
# 1.应用服务WEB访问端口
server.port=8080

# 2.数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/库名?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=root

# 3.MyBatis中xml文件映射的位置
mybatis.mapper-locations=classpath:mappers/*.xml

# 4.mcm: 驼峰命名自动映射
# 自动识别表字段名和Java属性名命名方式不一致的问题,比如:create_time -> createTime
mybatis.configuration.map-underscore-to-camel-case=true

# 5.设置全局日志级别:info
logging.level.root=info
# 6.设置局部日志级别:debug
logging.level.工程目录=debug

# 7.启用Knife4j配置
knife4j.enable=true

# 分组配置
# 定义 API 分组的名称(组标识符)
springdoc.group-configs[0].group=default
# 指定该分组包含哪些路径的接口
springdoc.group-configs[0].paths-to-match=/**
# 明确扫描的控制器包路径
springdoc.group-configs[0].packages-to-scan=controller包的路径

会话

http协议是一种无状态的协议,客户端和服务器端的会话仅仅存在于一次请求和一次响应之间,所以必须使用第三方的会话保持技术来维持客户端和服务器端的会话

常见的会话保持技术有3种: Cookie、Session、jwt

  • Cookie:不太安全,容易被伪造,现在不用了!
  • Session:比较安全,但是比较消耗服务器端的资源[存储资源和内存资源],现在用的较少!
  • Jwt:很安全,资源消耗较少,现在工程中常用的会话保持技术!
  • 将用户的标识存入浏览器的Cookie中,利用Cookie的同域自动提交,来实现客户端和服务端的会话;
  • 用户标识容易被伪造, 所以不安全, 工程中很少使用它做会话保持;
    • 服务端
      • base64("5916_lucy")
      • {"lucy": 5916, "fcq": 6666}
    • 客户端
      • Cookie: username=NTkxNl9sdWN5
      • 解码:username=5916_lucy
      • 伪造其他用户身份:username=1234_fcq【MTIzNF9mY3E=】
        (需要知道其他用户前面四位随机数,但是也不安全)

Session

  • 将会话标识保存到服务器端的数据库中,生成唯一的用户标识session key,并将session key存入浏览器的Cookie中,来实现客户端和服务端的会话;
  • 优点
    session key是加密字符串,很难被伪造,所以相对来说比较安全,是之前企业中常用的会话保持技术;
  • 缺点
    服务端需要存储大量的用户标识,浪费存储资源
    需要经常删除过期的用户标识,消耗计算机资源

JWT

  • 企业项目中最常用的会话保持技术
  • 安全,也不浪费资源

具体内容后面再将

Spring MVC 支持

通过 HttpSession 参数来实现会话

复制代码
public JsonResult login(@RequestBody UserLoginParam userLoginParam, HttpSession session){}

以下三个方法都是在服务器端的内存中操作

  • 设置会话标识: session.setAttribute(key, value)
  • 获取会话标识: session.getAttribute(key)
  • 删除会话标识: session.removeAttribute(key)

全局异常处理器

GlobalExceptionHandler

说明

  • 全局异常处理器,是 Spring MVC 提供的一种异常处理机制,统一处理由控制器抛出的异常
  • 使用全局异常处理器的原因是 Spring MVC 默认的异常处理机制对用户端非常不友好,所以才会使用全局异常处理器手动处理由控制器抛出的异常

常用注解

  • @ControllerAdvice
    • 定义全局异常处理器,处理 Controller 中抛出的异常。
  • @RestControllerAdvice
    • 组合注解,是@ControllerAdvice注解和@ResponseBody注解的组合;
    • 用于捕获 Controller 中抛出的异常并对异常进行统一的处理,还可以对返回的数据进行处理
  • @ExceptionHandler
    • 用于捕获 Controller 处理请求时抛出的异常,并进行统一的处理

使用流程

  • 第1步:创建全局异常处理器的类,添加 @RestControllerAdvice 注解
    base.exception.GlobalExceptionHandler

  • 第2步:创建对应的异常处理方法,添加 @ExceptionHandler 注解

    复制代码
    @ExceptionHandler
    public JonsResult doHandleRuntimeException(RuntimeException ex){}

异常处理顺序

当控制器抛出异常后,异常处理的步骤:

  • 第1步:抛出异常后,首先检查是否定义了全局异常处理器
  • 第2步:如果未定义,则使用默认的异常处理机制
  • 第3步:如果已定义,则会在全局异常处理器中定位处理该异常的处理方法
  • 第4步:如果未找到对应的异常处理方法,则找该异常父类异常的处理方法
  • 第5步:如果也未找到父类异常的处理方法,则找能处理所有异常的Throwable处理方法
  • 第6步:如果也未找到 Throwable 方法,则使用默认的异常处理机制[不友好]

全局异常处理器代码示例

复制代码
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    //ExceptionHandler注解:标识该方法为异常处理方法;
    @ExceptionHandler
    public JsonResult doHandleIllegalArgumentException(IllegalArgumentException ex){
        //ex.getMessage():获取异常提示消息
        String data =  ex.getMessage();
        log.error("IllegalArgumentException:" + data);

        //{"state":3000,"message":"操作失败","data":"车辆id不能小于0"}
        return new JsonResult(StatusCode.OPERATION_FAILED, data);
    }

    @ExceptionHandler
    public JsonResult doHandleRuntimeException(RuntimeException ex){
        String data = ex.getMessage();
        log.error("RuntimeException:" + data);
        return new JsonResult(StatusCode.OPERATION_FAILED, data);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public JsonResult doHandleMethodArgumentNotValidException(MethodArgumentNotValidException ex){
        String data = ex.getFieldError().getDefaultMessage();
        log.error("MethodArgumentNotValidException:" + data);
        return new JsonResult(StatusCode.VALIDATED_ERROR,data);
    }


    @ExceptionHandler(ConstraintViolationException.class)
    public JsonResult doHandleConstraintViolationException(ConstraintViolationException ex){
        String data = ex.getMessage().split(":")[1].trim();
        log.error("ConstraintViolationException:" + data);
        return new JsonResult(StatusCode.VALIDATED_ERROR,data);
    }

    /**
     * 能够处理所有异常的异常处理方法,一般最后添加
     */
    @ExceptionHandler
    public JsonResult doHandleThrowable(Throwable ex){
        String data = ex.getMessage();
        log.error("未知异常:" + data);
        return new JsonResult(StatusCode.THROWABLE_ERROR, data);
    }
}

Validation 验证框架

数据校验谁来做

  • Web前端工程师一定做[JavaScript];
  • Java开发工程师一定做[Validation];

校验流程

首先添加依赖,刷新Maven,可以勾选依赖项

复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

封装参数流程[DTO类]

  • 第1步:在POJO类参数前添加 @Validated 注解

    复制代码
    public JsonResult reg(@Validated UserRegParam userRegParam) {}
  • 第2步:在POJO类属性上添加校验注解
    @NotNull、@NotEmpty、@NotBlank、@Size、@Range、@Email、@Pattern

    复制代码
    @NotBlank(message = "用户名不能为空值和空字符串和空白串")
    @Size(min = 2,max = 10,message = "用户名长度必须在2-10之间")
    private String username;
  • 第3步:定义异常处理方法,处理由 Spring Validation 抛出的异常.
    异常:MethodArgumentNotValidException

    复制代码
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public JsonResult doHandleMethodArgumentNotValidException(MethodArgumentNotValidException ex){
        String data = ex.getFieldError().getDefaultMessage();
        log.error("MethodArgumentNotValidException:" + data);
        return new JsonResult(StatusCode.VALIDATED_ERROR,data);
    }

非封装参数流程[声明参数]

  • 第1步:控制器类上添加 @Validated 注解

    复制代码
    @Validated
    public class UserController {}
  • 第2步:方法参数前添加数据校验的注解;
    @NotNull @NotEmpty @NotBlank @Size @Range @Email @Pattern

    复制代码
    public JsonResult weibo(@Range() @PathVariable Long id){}
  • 第3步: 全局异常处理器中定义异常处理方法
    ConstraintViolationException

    复制代码
    @ExceptionHandler
    public JsonResult doHandleConstraintViolationException(ConstraintViolationException ex){
        //获取异常提示消息,即:校验注解中的message参数的值;
        String data = ex.getMessage().split(":")[1].trim();
        log.error("ConstraintViolationException:" + data);
    
        return new JsonResult(StatusCode.VALIDATED_ERROR, data);
    }

Validation 常用注解

  • @Validated
    • 可以添加在类上,也可以添加在参数上,参数验证注解
  • @NotNull(message="提示消息")
    • 要求不能为null
  • @NotEmpty(message="提示消息")
    • 要求不能为空字符串,同时不能为 null
  • @NotBlank(message="提示消息")
    • 要求不能为空白串,同时不能为空字符串,也不能为 null
  • @Size(min=?, max=?, message="提示消息")
    • 作用于字符串类型
    • 限定字符串长度范围:@Size(min=x, max=x, message=x)
  • @Range(min=?, max=?, message="提示消息")
    • 作用于数值类型
    • 限定数值类型的范围:@Range(min=x, max=x, message=x)
  • @Email
    • 校验邮箱的合法性
  • @Pattern(regexp="正则表达式", message="提示消息")
    • 使用正则表达式进行验证:@Pattern(regexp='正则表达式', message='提示消息')

拦截器 Interceptor

拦截器定义

拦截器是 SpringMVC 提供的一个组件,它允许我们在请求到达处理方法之前或之后,对请求拦截并进行预处理或后处理。拦截器可以帮助我们实现许多功能,如用户权限验证、记录日志、处理异常等

应用场景

  • 权限验证:如登录检测,进入处理器检测检测是否登录,如果没有直接返回到登录页面
  • 网站限流: 限制同一个IP地址的访问频率
  • 日志记录:记录请求信息的日志,以便进行信息监控、信息统计等
  • 性能监控:有时系统在某段时间莫名其妙负载很高,可通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间

拦截器特性

  • 拦截:将请求拦住,执行后续操作
  • 过滤:对拦截到的请求进行统一处理
  • 放行:处理完请求后,如果满足条件则放行,让请求继续访问下一步的资源

使用流程

  • 第1步:创建拦截器[类],实现 HandlerInterceptor,并重写3个方法
    preHandle、postHandle、afterCompletion

    public class MyInterceptor implements HandlerInterceptor {
    //注意此方法的返回值
    @Override
    public boolean preHandle(...){return true|false;}
    @Override
    public void postHandle(){}
    @Override
    public void afterCompletion(){}
    }

  • 第2步:注册拦截器并添加拦截规则
    创建配置类,实现 WebMvcConfigurer,并重写 addInterceptors 方法,添加拦截规则

    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(){
    registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**"); //拦截所有请求
    registry.addInterceptor(new MyInterceptor()).addPathPatterns("");
    registry.addInterceptor(new MyInterceptor()).addPathPatterns("").excludePathPatterns(""); //排除拦截路径
    }
    }

多个拦截器使用

多个拦截器执行顺序

  • preHandle 是按照注册拦截器的顺序依次执行
  • postHandle 和 afterCompletion 是按照注册拦截器相反的顺序执行

查看源码步骤

  1. Ctrl + n : 搜索 DispatcherServlet
  2. 点击 Idea 左侧工具栏中的:Structure
  3. 找到 DoDispatch() 方法
  4. 依次查看DoDispatch()的源码,注意几个关键字:preHandle、postHandle、afterCompletion

拦截器应用

限定访问时间

复制代码
@Slf4j
public class TimeAccessInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.debug("进入访问时间限定的拦截器...");
        /*
            1.获取当前时间[小时];
            2.判断当前时间是否在限定的时间段内06:00-23:00;
              2.1 在:返回true[放行];
              2.2 不在:
                  选择1: 抛出异常,由全局异常处理器处理该异常,返回响应给到客户端;
                  选择2: 首先创建error.html页面,然后重定向到error.html页面,最后返回false;
         */
        LocalTime now = LocalTime.now(); //时间对象
        int hour = now.getHour(); //获取小时
        if (hour < 6 || hour > 22){
            //方案1:手动抛出异常,交由全局异常处理器进行处理;
            //throw new RuntimeException("非常抱歉,只能在06:00-23:00之间进行访问");
            //方案2:重定向到error.html页面;
            //sendRedirect("/error.html"): http://localhost:8080/error.html
            response.sendRedirect("/error.html");
            return false;
        }
        //放行
        return true;
    }
}

登录状态校验

复制代码
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.debug("进入登录状态校验的拦截器...");
        /*
            1.获取会话HttpSession;
              HttpSession session = request.getSession();
            2.从HttpSession中获取当前用户信息;
            3.判断用户信息是否为空;
              3.1 为空: 未登录, 重定向到登录页面/login.html, return false;
                  response.sendRedirect("/login.html");
              3.2 不为空: 已登录, 放行, return true;
         */
        HttpSession session = request.getSession();
        UserVO userVO = (UserVO) session.getAttribute("user");
        if (userVO == null){//未登录
            response.sendRedirect("/login.html");
            return false;
        }
        //放行
        return true;
    }

网站限流

复制代码
@Slf4j
public class RateLimitInterceptor implements HandlerInterceptor {
    private static final int MAX_COUNT = 3;
    private static final int MAX_WINDOW_SIZE = 60;

    // 记录IP访问次数: {"192.168.1.11": 2, "192.168.1.12": 3, "192.168.1.13": 1}
    private final ConcurrentHashMap<String, Integer> clientCounts = new ConcurrentHashMap<>();
    //存储IP访问时间:{"192.168.1.11": "2025-07-11 00:00:00", "192.168.1.12":"2025-07-10:00:00:00", "192.168.1.13": "2020-01-01 00:00:00"}
    private final ConcurrentHashMap<String, LocalDateTime> lastResetTimes = new ConcurrentHashMap<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.debug("进入网站限流的拦截器...");
        String clientIp = request.getRemoteAddr();
        LocalDateTime lastResetTime = lastResetTimes.get(clientIp);
        /*关于lastResetTime的判断:
        * 1.该IP地址第1次访问:null; --------->  在两个HashMap中存放数据[计数器为0,设置时间];
        * 2.该IP地址1分钟之前访问过: 非空; ---->  在两个HashMap中更新数据[计数器为0,设置时间];
        * 3.该IP地址1分钟内访问过: 非空;   ---->  获取计数,+1,和3比较...;
        * */
        if (lastResetTime == null || LocalDateTime.now().isAfter(lastResetTime.plusSeconds(MAX_WINDOW_SIZE))){
            clientCounts.put(clientIp, 0); //计数器清零;
            lastResetTimes.put(clientIp, LocalDateTime.now()); //更新时间
        }
        //计数器+1
        Integer count = clientCounts.get(clientIp);
        count = count + 1;
        clientCounts.put(clientIp, count);
        //和3比较
        if (count > MAX_COUNT){
            throw new RuntimeException("访问过于频繁,请稍后再试...");
        }
        return true;//放行
    }
}

过滤器 Filter

定义

过滤器是Java Web应用程序中用于拦截请求和响应的组件,在请求处理之前或之后执行特定的任务,如权限校验、日志记录等

Spring MVC拦截器和过滤器区别

过滤器和 Spring MVC 拦截器在 Web 应用程序中都起到了处理请求和响应的作用,但它们之间存在一些不同

  • 归属不同
    • 拦截器是一个 Spring MVC 的组件,归 Spring 管理,配置在 Spring 文件中,因此能使用 Spring 的任何资源、对象
    • 过滤器不能直接使用Spring的资源、对象等
  • 执行顺序
    • 过滤器在前,拦截器在后
    • 对于需要更深度地处理请求或使用 Spring 资源的场景,使用拦截器可能更为合适。而如果仅需要进行一些基本的请求/响应处理,过滤器可能是一个更简单的选择
  • 作用深度
    拦截器能够深入到方法前后等,在使用上具有更大的弹性

过滤器常用方法

  • init()方法
    过滤器的初始化方法,在 Web 容器创建了过滤器实例后将调用这个方法进行一些初始化的操作
  • doFilter()方法
    过滤器核心方法,会执行实际的过滤操作,当用户访问与过滤器关联的URL时,Web容器会先调用过滤器的 doFilter 方法进行过滤
  • destory()方法
    Web容器在销毁过滤器实例前调用的方法,主要用来释放过滤器的资源等

使用流程

  • 第1步:工程中创建过滤器的类,比如: base.filters.MyFilter.java ,实现 jakarta.servlet.Filter接口

  • 第2步:实现3个方法,常用的是: doFilter()方法;

    复制代码
    package cn.tedu._03vehicle.base.filters;
    
    import cn.tedu._03vehicle.pojo.vo.UserVO;
    import lombok.extern.slf4j.Slf4j;
    
    import jakarta.servlet.*;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import jakarta.servlet.http.HttpSession;
    import java.io.IOException;
    
    @Slf4j
    public class LoginCheckFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            log.debug("初始化...");
            Filter.super.init(filterConfig);
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            //doFilter是过滤器的核心方法:权限校验、网站限流、记录日志...
            //登录状态的校验;
            //HttpServletRequest是ServletRequest的子类,所以此处需要进行类型转换;
            HttpServletRequest request1 = (HttpServletRequest) request;
            HttpServletResponse response1 = (HttpServletResponse) response;
            //获取会话对象;
            HttpSession session = request1.getSession();
            UserVO userVO = (UserVO) session.getAttribute("user");
            if (userVO != null){//登录状态
                //有此行代码,则代表放行;
                //无此行代码,代表不放行;
                chain.doFilter(request1, response1);
            }else {//未登录
                response1.sendRedirect("/login.html");
            }
        }
    
        @Override
        public void destroy() {
            log.debug("销毁...");
            Filter.super.destroy();
        }
    }
  • 第3步:在类上添加 @WebFilter(filterName="", urlPatterns={""}) 注解配置过滤器名称及拦截路径

    (一个 * 号代表通配符)

    复制代码
    @WebFilter(filterName = "MyFilter", urlPatterns = {"/v1/vehicle/*"})
    public class MyFilter implements Filter {}
  • 第4步:通过在启动类上添加 @ServletComponentScan 注解注册过滤器

    复制代码
    /**
     * ServletComponentScan: 注册过滤器
     */
    @ServletComponentScan
    @SpringBootApplication
    public class Application {}
  • 第5步:重启工程测试

文件上传

相关配置

复制代码
# 配置静态文件路径: swrs
spring.web.resources.static-locations=file:d:/files,classpath:/static
# 配置单个文件的大小限制:ssmm
spring.servlet.multipart.max-file-size=100MB
# 配置多个文件的大小限制:ssmm
spring.servlet.multipart.max-request-size=500MB

代码流程

复制代码
@RestController
@RequestMapping("/v1/file/")
public class UploadController {
    @PostMapping("upload")
    public JsonResult upload(MultipartFile file) throws IOException {
        //1.处理文件名
        String filename = file.getOriginalFilename();
        String suffix = filename.substring(filename.lastIndexOf("."));
        filename = UUID.randomUUID() + suffix;

        //2.处理日期路径
        SimpleDateFormat sdf = new SimpleDateFormat("/yyyy/MM/dd/");
        String datePath = sdf.format(new Date());
        String dirPath = "D:/files";

        //3.创建存储目录
        File dirFile = new File(dirPath + datePath);
        if (!dirFile.exists()){
            dirFile.mkdirs();
        }
        
        //4.保存文件到服务器
        String filePath = dirPath + datePath + filename;
        file.transferTo(new File(filePath));

        //5.返回响应,响应中包含文件在服务器中的路径: /2025/07/14/xxx-xxx-xxx-xxx.jpg
        return JsonResult.ok(datePath + filename);
    }
}

AOP

定义

  • AOP 是面向切面编程,是 Spring Framework 的核心子模块
  • 是一种设计思想,在不修改源代码的情况下给程序统一植入扩展功能的一种技术
  • 提高代码的重用性,减少重复冗余的代码

作用

  • 代码重用:将一些通用的功能(比如日志记录、安全控制等)抽象出来,形成可重用的模块
  • 降低代码耦合度:将不同的关注点分离开来,这可以避免代码之间的紧耦合
  • 简化开发:使开发人员将关注点从业务逻辑中分离出来,使得开发更加简单明了
  • 提高系统可扩展性:在系统需求变化时,只需要修改AOP模块而不是修改业务逻辑,这可以使得系统更加易于扩展和维护

使用流程

  • 第1步:手动添加依赖,刷新 Maven

    <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
  • 第2步:创建切面类,添加 @Aspect 注解

    @Aspect
    @Component
    public class LogAspect {
    /**
    * Before注解: 前置通知,在目标方法执行之前执行;
    * value参数: 切入点表达式,指定哪些方法需要被植入增强逻辑;
    /
    @Before(value = "execution(public int cn.tedu._07springaop.aop.CalculatorImpl.
    (..))")
    public void beforeMethod(){
    //前置通知的增强逻辑
    }
    }

  • 第3步:编写切入点表达式,使用各种通知注解对目标函数进行功能增强

通知类型

  • 前置通知:@Before 在被代理的目标方法前执行
  • 返回通知:@AfterReturning 在被代理的目标方法成功结束后执行
  • 后置通知:@After 在被代理的目标方法最终结束后执行
  • 异常通知:@AfterThrowing 在被代理的目标方法异常结束后执行
  • 环绕通知:@Around 使用 try...catch...finally 结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

返回通知和后置通知的区别

  • 执行时机
    • 返回通知是在目标方法执行后返回结果时执行的通知,只有目标方法正常返回时才会执行,抛异常则不会执行
    • 后置通知则是在目标方法执行后执行的通知,无论目标方法是否抛出异常,后置通知都会执行
  • 访问权限
    • 返回通知可以获取并修改目标方法的返回值 [通过returnning参数]
    • 后置通知无法访问目标方法的返回值

相关注解

  • Aspect
    添加在切面类上,属于AOP的注解 用于告诉Spring容器将该类识别为一个切面,它定义了切入点(Pointcut)和通知(Advice), 切入点定义了在应用程序中哪些方法或代码块应该被拦截和执行额外的逻辑, 通知包括前置通知、返回通知、环绕通知、异常通知和最终通知

  • Before(value="")
    前置通知,添加在切面类方法上, 在目标方法执行之前执行的通知

  • After(value="")
    后置通知,添加在切面类方法上, 在目标方法最终执行之后执行的通知

  • AfterReturning(value="", returning="")
    返回通知,添加在切面类方法上, 在目标方法正常返回之后执行的通知

  • AfterThrowing(value="", throwing="")
    异常通知,添加在切面类方法上, 在目标方法抛出异常之后执行的通知

  • Around(value="")
    环绕通知,添加在切面类方法上, 将目标方法封装起来,在目标方法调用之前和之后执行自定义的行为,通常使用 try ... catch ... finally 包裹

  • Pointcut(value="")
    定义切入点表达式方法,添加在切入点表达式方法上

    复制代码
    @Pointcut(value="execution(public int cn.tedu._07springaop.aop.*.*(..))")
    public void doTime(){}
  • Order(10)
    指定切面优先级,数字越小,优先级越高

示例代码

复制代码
/**
 * Aspect注解:
 *   1.表示当前类为一个切面类;
 *   2.当调用目标方法[add、sub、mul、div]时,框架会先检查是否定义了切面类;
 *   3.当切面类中定义了切面方法,框架会检查目标方法[add、sub、mul、div]是否被指定了切点;
 */
@Aspect
@Component
public class LogAspect {

    /**
     * Pointcut注解: 定义切点表达式的方法,表示哪些方法需要被植入增强逻辑;
     */
    @Pointcut(value = "execution(public int cn.tedu._07springaop.aop.CalculatorImpl.*(..))")
    public void doTime(){}


    /**
     * Before注解: 前置通知,调用目标方法之前执行的方法;
     * value参数: 切入点表达式,表示哪些方法需要被植入增强逻辑;
     */
    @Before(value = "doTime()")
    public void beforeMethod(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName(); // 获取方法名称
        String args = Arrays.toString(joinPoint.getArgs()); // 获取方法参数

        System.out.println("[前置]" + name + "方法开始,参数为:" + args);
    }

    /**
     * After注解: 后置通知
     *   1.在目标方法彻底结束[正常结束|异常结束]之后执行;
     *   2.后置通知没有权限获取到目标方法的返回值;
     */
    @After(value = "doTime()")
    public void afterMethod(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        System.out.println("[后置]" + name + "方法结束");
    }

    /**
     * AfterReturning注解: 返回通知
     *   1.在目标方法正常结束之后执行;
     *   2.返回通知有权限获取到目标方法的返回值;
     */
    @AfterReturning(value = "doTime()", returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result){
        String name = joinPoint.getSignature().getName();
        System.out.println("[返回]" + name + "方法正常结束,返回值为:" + result);
    }

    /**
     * AfterThrowing注解: 异常通知
     *   1.只在目标方法抛出异常之后执行;
     *   2.异常通知有权限获取到目标方法抛出的异常对象;
     */
    @AfterThrowing(value = "doTime()", throwing = "ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
        String name = joinPoint.getSignature().getName();
        System.out.println("[异常]" + name + "方法抛出异常,异常信息为:" + ex.getMessage());
    }

    /**
     * Around注解: 环绕通知
     *   1.等价于 @Before + @After + @AfterReturning + @AfterThrowing 的组合;
     *   2.环绕通知有权限访问目标方法的返回值;
     *   3.环绕通知有权限获取异常对象[目标方法抛出异常时];
     *   4.通常使用 try ... catch ... finally ...结果包裹;
     */
    @Around(value = "doTime()")
    public Object aroundMethod(ProceedingJoinPoint joinPoint){
        Object result = null;

        try {
            System.out.println("[环绕通知-前置通知]");
            result = joinPoint.proceed();
            System.out.println("[环绕通知-返回通知]");
        } catch (Throwable e) {
            System.out.println("[环绕通知-异常通知]");
        } finally {
            System.out.println("[环绕通知-后置通知]");
        }

        return result;
    }
}

切入点表达式

定义

在 AOP 面向切面编程中,切入点表达式指定了哪些方法需要被植入增强逻辑。它是一个表达式,用于匹配目标对象中的方法

切入点表达式表示方式

  • 粗粒度

    • bean("IoC容器中Spring Bean对象的名称"):Bean中的所有方法都被植入增强逻辑,比如计算器类中:

      复制代码
      @Pointcut(value = "bean(calculatorImpl)")
      public void doTime(){}
    • within(包名.类名):指定包下的一个类或者所有类的方法植入增强逻辑,比如计算器类中:

      复制代码
      @Pointcut(value = "within(cn.tedu._06springaop.aop.CalculatorImpl)")
      public void doTime(){}
  • 细粒度

    • execution("切入点表达式"):指定类中的指定方法植入增强逻辑,比如计算器类中:

      复制代码
      @Pointcut(value = "execution(public int cn.tedu._06springaop.aop.CalculatorImpl.*(..))")
      public void doTime(){}
      
      //对指定的多个方法进行功能增强,使用表达式 或 ||
      @Pointcut(value = "execution(public int cn.tedu._06springaop.aop.CalculatorImpl.add(..)) || execution(public int cn.tedu._06springaop.aop.CalculatorImpl.sub(int,int)) || execution(public int cn.tedu._06springaop.aop.CalculatorImpl.mul(..))")
      public void doTime(){}

Spring Security

定义

Spring Security 是 Spring 提供的安全访问控制解决方案的安全框架,能够实现认证功能和鉴权功能

认证和鉴权

认证是"你是谁",鉴权是"你能做什么"

  • 认证:验证用户身份的真实性,通常通过凭证(密码)完成
  • 鉴权:在认证通过后,根据策略检查用户是否有权限执行特定操作或访问资源

核心关系:认证是鉴权的前提,两者构成访问控制的基础;

认证代码流程

  • 第1步:创建工程,勾选 Spring Security、Spring Web 依赖

  • 第2步:添加 Security 配置类,配置 http 的请求规则

    复制代码
    @Configuration
    public class SecurityConfig {
    
        //SecurityFilterChain 是 Spring Security 中的一个核心接口,它定义了一组安全过滤器以及这些过滤器应该应用的请求匹配规则
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
                    //配置http的请求规则
                    .authorizeHttpRequests(authorize -> authorize
                            // 对于公共API允许所有访问
                            .requestMatchers("/api/public/**").permitAll()
                            // 其他请求需要认证
                            .anyRequest().authenticated()
                    )
                    .formLogin(form -> form
                            // 允许所有用户访问登录相关URL(包括登录页面和登录处理URL)
                            .permitAll()
                    )
                    .logout(logout -> logout 
                            .permitAll()
                    );
            // 构建并返回配置好的SecurityFilterChain实例
            return http.build();
        }
    }
  • 第3步:启动测试

鉴权代码流程

  • 第1步:在SecurityConfig配置类上基于注解激活权限检查

    复制代码
    @EnableGlobalMethodSecurity(prePostEnabled = true)
  • 第2步:构建UserDetailsService的实现类并赋予权限

    构建UserDetailService接口的实现类service.UserDetailsServiceImpl,此类中基于用户名,密码等构建用户详情对象

    复制代码
    public class UserDetailServiceImpl implements UserDetailsService {
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            /*
                实现认证和鉴权的逻辑;
                1.用户在浏览器中输入:http://localhost:8080/api/private/hello;
                2.[我们完成配置类]根据SecurityConfig中的配置,该请求需要认证,则重定向到登陆页面;
                3.用户在登录页中输入用户名和密码,点击登录按钮,向服务端发送请求,由Security接收并处理;
                4.[Security框架完成]到了UserDetailsService接口中的loadUserByUsername方法中;
                5.[我们完成数据库查询]根据用户名到数据表中查询该用户对应的密码[数据库中的密码];
                6.[Security框架完成]将数据库中的密码与用户输入的密码进行比较;
                7.[我们完成数据库查询]根据用户名到数据表中查询该用户对应的权限;
                8.[我们返回]将用户名和密码以及权限封装成UserDetails对象并返回给Security框架;
             */
            return User.withUsername(username)
                    .password("{noop}123456") //SELECT password FROM user WHERE username=?
                    .authorities("sys:private:view", "sys:private:edit") //SELECT privileges FROM user WHERE username=?
                    .build();
        }
    }
  • 第3步:配置类中注册UserDetailsServiceImpl

    复制代码
    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig {
        @Bean
        public UserDetailsService userDetailsService(){
            return new UserDetailServiceImpl();
        }
    }
  • 第4步:资源方法标注

    对于需要有指定权限才可以方法的方法添加 @PreAuthorize 注解的描述

    复制代码
    @PreAuthorize("hasAuthority('sys:private:view')")//执行方法时需要sys:private:view权限
    @GetMapping("private/hello")
    public String helloPrivate() {
        return "Hello from a private endpoint! You need to be authenticated to see this.";
    }
  • 第5步:测试

    如果该用户没有权限访问相关资源,会报403 Forbidden状态码