体验SpringBoot
创建项目
所有的SpringBoot依赖都是以starter的形式命名的,之后我们需要导入其他模块也是导入 spring boot-starter-xxxx 这种名称格式的依赖。
SpringBoot为我们提供了包含内置Tomcat服务器的Web模块,我们只需要导入依赖就能直接运行服务器
把原来的
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>
|
换成
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
|

点击启动后可以看到已经启动了内置的 tomcat
在浏览器上打开 localhost:8080 可以访问

可以看到成功响应了404页面,相比之前的大量配置,可以说方便了很多,我们到目前为止仅仅是导入了一个依赖,就可以做到直接启动我们的Web服务器并正常访问
SpringBoot支持自动包妇描,我们不需要编写任何配置,直接在任意路径(但是不能跑到主类所在包外面去了)下创建的组件(如Controller、Service、Component、Configuration等)都可以生效
比如创建一个Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.example.demo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class TestController { @ResponseBody @GetMapping("/") public String index() { return "Hello World!"; } }
|
然后我们不需要扫描,直接启动

包括一个对象现在也可以直接以JSON形式返回给客户端,无需任何配置,如果启动不成功或者页面没有显示,请检查Lombok是否有正确启动(我用的时候就是没有找到构造方法和getter/setter,推测原因是Lombok)
1 2 3 4 5 6 7 8 9
| @Controller public class TestController { @ResponseBody @GetMapping("/") public Student index() { return new Student(1, "呵帅", "男"); } }
|

常用模块整合
spring-boot-starter-web包含了以下依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>3.4.2</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> <version>3.4.2</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <version>3.4.2</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>6.2.2</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>6.2.2</version> <scope>compile</scope> </dependency> </dependencies>
|
里面包含了以下内容
- spring-boot-starter 基础依赖starter
- spring-boot-starter-json 配置JSON转换的starter
- spring-boot-starter-tomcat 内置Tomcat服务器
- spring-web、spring-webmvc 之前mvc的内容
如果需要像之前一样添加WebMvc配置类,方法是一样的,直接创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package com.example.demo.config; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration public class WebConfiguration implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new HandlerInterceptor() { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("请求被拦截"); return HandlerInterceptor.super.preHandle(request, response, handler); } }); } }
|
同样security也是一样的,首先添加依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
|
导入完依赖直接启动

我们没有进行任何配置,而是对应的Starter帮助我们完成了默认的配置,并且在启动时,就已经帮助我们配置了一个随机密码的用户可以直接登录使用(Username是user)

如果需要额外进行配置,只需要添加配置类即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> { auth.anyRequest().authenticated(); }) .formLogin(conf -> { conf.loginPage("/login"); conf.loginProcessingUrl("/doLogin"); conf.defaultSuccessUrl("/"); conf.permitAll(); }) .build(); } }
|
同样的,我们也可以快速整合之前使用的模版引擎,比如Thymeleaf框架,直接上对应的Starter即可
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
|
自定义运行器
在项目中,可能会遇到这样一个问题:我们需要在项目启动完成之后,紧接着执行一段代码,我们可以编写自定义的ApplicationRunner,会在项目启动完成后执行
1 2 3 4 5 6 7
| @Component public class TestRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { System.out.println("自定义执行"); } }
|
当然也可以使用CommandLineRunner,它也支持使用@Order或是实现Ordered接口来支持优先级执行
配置文件
虽然SpringBoot快捷开发确实很方便,不过我们发现有些东西还是需要我们自己来编写配置才可以,不然SpringBoot项目无法正常启动。我们可以直接在application.properties 中进行配置编写,它是整个SpringBoot的配置文件,比如要修改服务器的默认端口:

这些配置其实都是各种Starter提供的,部分配置在Starter中具有默认值,我们即使不配置也会使用默认值,比如这里的8080就是我们服务器的默认端口,我们也可以手动修改它,来变成我们需要的。
除了配置已经存在的选项,我们也可以添加自定义的配置,来方便我们程序中使用,比如我们这里创建一个测试数据:
1 2 3 4 5 6 7 8 9 10 11 12
| @Controller public class TestController { @Value("${test.hello}") String hello; @GetMapping("/login") public String login() { System.out.println(hello); return "login"; } }
|
配置文件除了使用 properties 格式以外,还有一种叫做 yaml 格式,它的语法如下
1 2 3 4 5 6 7 8
| 一级目录: 二级目录: 三级目录1:值 三级目录2:值 三级目录List: - 元素1 - 元素2 - 元素3
|
我们可以看到,每一级目录都是通过缩进(不能使用Tab,只能使用空格)区分,并且键和值之间需要添加冒号+空格来表示
例如:
1 2 3 4 5 6 7 8
| server: port: 8080 Spring: datasource: url: jdbc:mysql://localhost:3306/test username: root password: Abc123 driver-class-name: com.mysql.cj.jdbc.Driver
|
注意如果你的数据库密码是以0开头或者0x开头会被默认转义为八进制和十六进制,要用引号把密码引起来
常见的配置项
SpringSecurity和SpringBootMvc配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| spring: mvc: static-path-pattern: /static/** security: filter: order: -100 user: name: 'admin' password: '123456' roles: - admin - user
|
mybatis
整合Mybatis需要把本身的驱动加上
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>
|
1 2 3 4 5 6 7 8
| server: port: 8080 Spring: datasource: url: jdbc:mysql://localhost:3306/test username: root password: Abc123 driver-class-name: com.mysql.cj.jdbc.Driver
|
这里我们接续来测试一下MyBatis的配置,想要在SpringBoot中使用Mybatis也很简单,不需要进行任何配置,我们直接编写Mapper即可
直接为需要注册为Mapper的接口添加 @Mapper 注解,来表示这个接口作为Mapper使用
1 2 3 4 5
| @Mapper public interface UserMapper { @Select("select * from test where id = #{id}") User findUserById(int id); }
|
测试:
1 2 3 4 5 6 7 8
| @Resource UserMapper mapper;
@ResponseBody @GetMapping("/test") public User test() { return mapper.findUserById(1); }
|
注:如果运行失败,检查是否是maven依赖版本不兼容
打包运行
我们可以使用maven工具

然后在当前目录下的命令行中输入:java -jar 包名.jar
GraalVM
我们SpringBoot项目除了打包为传统的Jar包基于JVM运行之外,我们也可以将其直接编译为操作系统原生的程序来进行使用(这样会大幅提升程序的运行效率,但是由于编译为操作系统原生程序,这将无法支持跨平台)
具体内容:十分钟带你了解 Oracle 最新的 JVM 技術——GraalVM - 知乎
还未广泛运用,了解为主
日志系统
我们在之前学习SSM时,如果不配置日志,就会报错,但是到了SpringBoot阶段之后日志打印得也非常统一,不会出现这个问题
日志门面和日志实现
日志门面,如Slf4j,是把不同的日志系统的实现进行了具体的抽象化,只提供了统一的日志使用接口,使用时只需要按照其提供的接口方法进行调用即可,由于它只是一个接口,并不是一个具体的可以直接单独使用的日志框架,所以最终日志的格式、记录级别、输出方式等都要通过接口绑定的具体的日志系统来实现,这些具体的日志系统就有lqg4j、logback、java.util.logging等,它们才实现了具体的日志系统的功能。
日志门面和日志实现就像JDBC和数据库驱动一样,一个是画大饼的,一个是真的去做饼的。
SpringBoot为了统一日志框架的使用,做了这些事情
- 直接将其他依赖以前的日志框架剔除
- 导入对应日志框架的Slf4i中间包
- 导入自己官方指定的日志实现,并作为Slf4j的日志实现层
日志级别从低到高分为TRACE<DEBUG<INFO<WARN<ERROR<FATAL, SpringBoot默认只会打印INFO以上级别的信息
如果想要输出日志信息像之前JUL一样就行了
1 2 3 4 5 6 7 8
| @Slf4j @Controller public class TestController { @PostConstruct public void init() { log.info("日志信息"); } }
|
配置 logback日志
和JUL一样,Logback也能实现定制化,我们可以编写对应的配置文件,SpringBoot推荐将配置文件名称命名为表示这是SpringBoot下Logback专用的配置,可以使用SpringBoot 的高级Profile功能,它的内容类似于这样:logback-spring.xml
最外层由 configuration 包裹,一旦编写,那么就会替换默认的配置,所以如果内部什么都不写的话,那么会导致我们的SpringBoot项目没有配置任何日志输出方式,控制台也不会打印日志。
在org/springframework/boot/logging/logback/defaults.xml 中已经帮我们把日志的输出格式定义好了,我们只需要设置对应的appender 即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <configuration> <include resource="org/springframework/boot/logging/logback/defaults.xml" /> <property name="CONSOLE_LOG_PATTERN" value="%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd}}){faint} %clr([%X{reqId}]){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}){} %clr(${PID:-}){magenta} %clr(--- %esb(){APPLICATION_NAME}%esb{APPLICATION_GROUP}[%15.15t] ${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${CONSOLE_LOG_PATTERN}</pattern> <charset>${CONSOLE_LOG_CHARSET}</charset> </encoder> </appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <encoder> <pattern>${FILE_LOG_PATTERN}</pattern> <charset>${FILE_LOG_CHARSET}</charset> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <FileNamePattern>log/%d{yyyy-MM-dd}-spring-%i.log</FileNamePattern> <cleanHistoryOnStart>true</cleanHistoryOnStart> <maxHistory>7</maxHistory> <maxFileSize>10MB</maxFileSize> </rollingPolicy> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </root></configuration>
|
配置完,运行就会生成一个日志文件
比如我们现在需要记录是哪个用户访问我们网站的日志,只要是此用户访问我们网站,都会在日志中携带该用户的ID,我们希望每条日志中都携带这样一段信息文本,而官方提供的字段无法实现此功能,这时就需要使用MDC机制“Mapped Diagnostic Context”(映射诊断上下文):
1 2 3 4 5 6 7 8 9 10 11 12
| @Slf4j @Controller public class TestController { @GetMapping("/") @ResponseBody public String hello(HttpSession session) { MDC.put("reqId", session.getId()); log.info("访问了一次测试接口"); return "Hello World"; } }
|
自定义Banner展示
可以直接来配置文件所在目录下创建一个名为banner.txt的文本文档,内容随便
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ${AnsiColor.yellow} // _ooOoo_ // // o8888888o // // 88" . "88 // // (| ^_^ |) // // O\ = /O // // ____/`---'\____ // // .' \\| |// `. // // / \\||| : |||// \ // // / _||||| -:- |||||- \ // // | | \\\ - /// | | // // | \_| ''\---/'' | | // // \ .-\__ `-` ___/-. / // // ___`. .' /--.--\ `. . ___ // // ."" '< `.___\_<|>_/___.' >'"". // // | | : `- \`.;`\ _ /`;.`/ - ` : | | // // \ \ `-. \_ __\ /__ _/ .-` / / // // ========`-.____`-.___\_____/___.-`____.-'======== // // `=---=' // // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // // 佛祖保佑 永无BUG 永不修改 // ${AnsiColor.black}
|
可以使用在线生成网站进行生成自己的个性Banner:https://www.bootschool.net/ascii
切换颜色
1
| ${AnsiColor.BRIGHT_GREEN} //绿色
|
常用配置
1
| ${AnsiColor.YELLOW} 当前 Spring Boot 版本:${spring-boot.version}
|
多环境配置
在日常开发中,我们项目会有多个环境。不同的环境下,可能我们的配置文件也存在不同,但是我们不可能切换环境的时候又去重新写一次配置文件,所以我们可以将多个环境的配置文件提前写好,进行自由切换。
SpringBoot给我们提供了一种方式,在 application.yml下,我们可以通过配置文件指定
1 2 3
| spring: profiles: active: dev
|
我们分别创建两个环境的配置文件,application-dev.yml和application-prod.yml分别表示开发环境和生产环境的配置文件,比如开发环境我们使用的服务器端口为8080,而生产环境下可能就需要设置为80或是443端口,那么这个时候就需要不同环境下的配置文件进行区分:
SpringBoot自带的Logback日志系统也是支持多环境配置的,比如我们想在开发环境下输出日志到控制台,而生产环境下只需要输出到文件即可,这时就需要进行环境配置:
1 2 3 4 5 6 7 8 9 10 11 12
| <springProfile name="dev"> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </root> </springProfile>
<springProfile name="prod"> <root level="INFO"> <appender-ref ref="FILE"/> </root> </springProfile>
|
我们希望生产环境中不要打包开发环境下的配置文件呢,打包的问题就只能找Maven解决了,Maven也可以设置多环境:
在 pom.xml 中 project 里任意位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <profiles> <profile> <id>dev</id> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <environment>dev</environment> </properties> </profile> <profile> <id>prod</id> <activation> <activeByDefault>false</activeByDefault> </activation> <properties> <environment>prod</environment> </properties> </profile> </profiles>
|
在 pom.xml 中的 build 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <resources>
<resource> <directory>src/main/resources</directory> <excludes> <exclude>application*.yml</exclude> </excludes> </resource>
<resource> <directory>src/main/resources</directory> <filtering>true</filtering> <includes> <include>application.yml</include> <include>application-${environment}.yml</include> </includes> </resource> </resources>
|
接着,我们可以直接将Maven中的environment属性,传递给SpringBoot的配置文件,在构建时替换为对应的值:
1 2 3
| spring: profiles: active: '@environment@'
|
注意切换环境之后要重新加载一下Maven项目,不然不会生效!
常用框架
邮件发送模块
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
|
比较常用的协议有两种:
- SMTP协议(主要用于发送邮件 Simple Mail Transfer Protocol)
- POP3协议(主要用于接收邮件 Post Office Protocol 3)
1 2 3 4 5
| spring: mail: host: 去百度搜索该邮箱的smtp username: 邮箱 password: 密码(并非账号的密码)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @SpringBootTest class Demo1ApplicationTests { @Autowired JavaMailSender sender; @Test void contextLoads() { SimpleMailMessage message = new SimpleMailMessage(); message.setSubject("【广东白云学院教务处】关于近期学校对您的处分决定"); message.setText("同学您好,完美校园数据显示您在本学期以来在本校食堂消费超过503次,积累消费金额超过3000元,您已被评为“校园大饭桶”,特此通知予以嘉奖。奖品将在3月1日西校区一品堂门口发放,感谢您对学校餐厅工作的支持!"); message.setTo("邮箱@qq.com"); message.setFrom("发送的邮箱@qq.cn"); sender.send(message); } }
|
如果需要添加附件等更多功能,可以使用MimeMessageHelper来帮助我们完成
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Test void contextLoads() throws MessagingException { MimeMessage message = sender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true); helper.setSubject("Test"); helper.setText("lbwnb"); helper.setTo("你的QQ号@qq.com"); helper.setFrom("发送的邮箱@163.com"); sender.send(message); }
|
接口规则校验
所需依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
|
如下面代码所示,我们要将接收到的字符串作切割,但是如果接收的字符串长度不满足切割条件时,或者说用户没有按照我们所设想来传参,那么就会直接报错
1 2 3 4 5 6 7 8
| @ResponseBody @PostMapping("/submit") public String submit(String username, String password){ System.out.println(username.substring(3)); System.out.println(password.substring(2, 10)); return "请求成功!"; }
|
这是最简单的解决方法是做个判断:
1 2 3 4 5 6 7 8 9 10 11 12
| @ResponseBody @PostMapping("/submit") public String submit(String username, String password){ if(username.length() > 3 && password.length() > 10) { System.out.println(username.substring(3)); System.out.println(password.substring(2, 10)); return "请求成功!"; } else { return "请求失败"; } }
|
虽然这样就能直接解决问题,但是如果我们的每一个接口都需要这样去进行配置就太麻烦了,Springboot 为我们提供了很方便的接口校验框架,我们可以使用注解开发完成全部接口的校验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Slf4j @Validated @Controller public class TestController {
...
@ResponseBody @PostMapping("/submit") public String submit(@Length(min = 3) String username, //使用@Length注解一步到位 @Length(min = 10) String password){ System.out.println(username.substring(3)); System.out.println(password.substring(2, 10)); return "请求成功!"; } }
|
不过这样依然会抛出一个异常,我们可以稍微处理一下,这里我们可以直接使用之前在SSM阶段中学习的异常处理Controller来自行处理这类异常:
1 2 3 4 5 6 7 8 9
| @ControllerAdvice public class ValidationController {
@ResponseBody @ExceptionHandler(ConstraintViolationException.class) public String error(ValidationException e){ return e.getMessage(); } }
|
| 验证注解 |
验证的数据类型 |
说明 |
| @AssertFalse |
Boolean,boolean |
值必须是false |
| @AssertTrue |
Boolean,boolean |
值必须是true |
| @NotNull |
任意类型 |
值不能是null |
| @Null |
任意类型 |
值必须是null |
| @Min |
BigDecimal、BigInteger、byte、short、int、long、double 以及任何Number或CharSequence子类型 |
大于等于@Min指定的值 |
| @Max |
同上 |
小于等于@Max指定的值 |
| @DecimalMin |
同上 |
大于等于@DecimalMin指定的值(超高精度) |
| @DecimalMax |
同上 |
小于等于@DecimalMax指定的值(超高精度) |
| @Digits |
同上 |
限制整数位数和小数位数上限 |
| @Size |
字符串、Collection、Map、数组等 |
长度在指定区间之内,如字符串长度、集合大小等 |
| @Past |
如 java.util.Date, java.util.Calendar 等日期类型 |
值必须比当前时间早 |
| @Future |
同上 |
值必须比当前时间晚 |
| @NotBlank |
CharSequence及其子类 |
值不为空,在比较时会去除字符串的首位空格 |
| @Length |
CharSequence及其子类 |
字符串长度在指定区间内 |
| @NotEmpty |
CharSequence及其子类、Collection、Map、数组 |
值不为null且长度不为空(字符串长度不为0,集合大小不为0) |
| @Range |
BigDecimal、BigInteger、CharSequence、byte、short、int、long 以及原子类型和包装类型 |
值在指定区间内 |
| @Email |
CharSequence及其子类 |
值必须是邮件格式 |
| @Pattern |
CharSequence及其子类 |
值需要与指定的正则表达式匹配 |
| @Valid |
任何非原子类型 |
用于验证对象属性 |
此时接口是以对象形式接收前端发送的表单数据的,这个时候就没办法向上面一样编写对应的校验规则了
1 2 3 4 5 6 7
| @Data public class Account { @Length(min = 3) String username; @Length(min = 10) String password; }
|
1 2 3 4 5 6 7
| @ResponseBody @PostMapping("/submit") public String submit(@Valid Account account){ System.out.println(account.getUsername().substring(3)); System.out.println(account.getPassword().substring(2, 10)); return "请求成功!"; }
|
然后修改之前的错误处理,对于实体类接收参数的验证,会抛出MethodArgumentNotValidException 异常也加进去
1 2 3 4 5 6 7 8 9 10 11
| @ResponseBody @ExceptionHandler({ConstraintViolationException.class, MethodArgumentNotValidException.class}) public String error(Exception e){ if(e instanceof ConstraintViolationException exception) { return exception.getMessage(); } else if(e instanceof MethodArgumentNotValidException exception){ if (exception.getFieldError() == null) return "未知错误"; return exception.getFieldError().getDefaultMessage(); } return "未知错误"; }
|
接口文档生成(肥肠豪用)
所需依赖
1 2 3 4 5
| <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.1.0</version> </dependency>
|
Swagger的主要功能如下:
- 支持 API 自动生成同步的在线文档:使用 Swagger 后可以直接通过代码生成文档,不再需要自己手动编写接口文档了,对程序员来说非常方便,可以节约写文档的时间去学习新技术。
- 提供 Web 页面在线测试 API:光有文档还不够,Swagger 生成的文档还支持在线测试。参数和格式都定好了,直接在界面上输入参数对应的值即可在线测试接口。
结合Spring框架(Spring-doc,官网:https://springdoc.org/
Swagger可以很轻松地利用注解以及扫描机制,来快速生成在线文档,以实现当我们项目启动之后,前端开发人员就可以打开Swagger提供的前端页面,查看和测试接口。
项目启动之后,我们可以直接访问:http://localhost:8080/swagger-ui/index.html 就可以看到开发文档了
修改Swagger的UI界面
1 2 3 4 5 6 7 8 9 10 11 12
| @Configuration public class SwaggerConfiguration { @Bean public OpenAPI springDocOpenAPI() { return new OpenAPI().info(new Info() .title("图书管理系统 - 在线API接口文档") .description("这是一个图书管理系统的后端API文档,欢迎前端人员查阅!") .version("2.0") .license(new License().name("我的B站个人主页") .url("https://space.bilibili.com/288205652"))); } }
|
为Controller编写API描述信息
1 2 3 4 5
| @Tag(name = "账户验证相关", description = "包括用户登录、注册、验证码请求等操作。") public class TestController { ... }
|
我们可以直接在类名称上面添加@Tag注解,并填写相关信息,来为当前的Controller设置描述信息。接着我们可以为所有的请求映射配置描述信息:
1 2 3 4 5 6 7 8 9 10 11
| @ApiResponses({ @ApiResponse(responseCode = "200", description = "测试成功"), @ApiResponse(responseCode = "500", description = "测试失败") //不同返回状态码描述 }) @Operation(summary = "请求用户数据测试接口") @ResponseBody @GetMapping("/hello")
public String hello(@Parameter(description = "测试文本数据", example = "KFCvivo50") @RequestParam String text) { return "Hello World"; }
|
对于那些不需要展示在文档中的接口,我们也可以将其忽略掉:
1 2 3 4 5 6
| @Hidden @ResponseBody @GetMapping("/hello") public String hello() { return "Hello World"; }
|
对于实体类,我们也可以编写对应的API接口文档:
1 2 3 4 5 6 7 8 9 10 11 12
| @Data @Schema(description = "用户信息实体类") public class User { @Schema(description = "用户编号") int id; @Schema(description = "用户名称") String name; @Schema(description = "用户邮箱") String email; @Schema(description = "用户密码") String password; }
|
不过,这种文档只适合在开发环境下生成,如果是生产环境,我们需要关闭文档:
1 2 3
| springdoc: api-docs: enabled: false
|
数据交互
JDBC交互框架
所需依赖
1 2 3 4 5 6 7 8 9
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency>
|
1 2 3 4 5 6
| spring: datasource: url: jdbc:mysql://localhost:3306/test username: root password: Abc123 driver-class-name: com.mysql.cj.jdbc.Driver
|
spring-jdbc 把所有的增删改查都加入到一个模板类里,并且已经注册好了
1 2
| @Resource JdbcTemplate template;
|
通过 template 调用已经封装好的方法,现在我们可以只写 SQL 语句了,相当于省略掉了 mapper
1 2 3 4 5 6
| @Test void contextLoads() { int id = 1; Map<String, Object> map = template.queryForMap("select * from user where id = ?", id); System.out.println(map); }
|
也可以自定义
1 2 3 4 5 6 7 8
| @Data @AllArgsConstructor public class User { int id; String name; String email; String password; }
|
1 2 3 4 5 6
| @Test void contextLoads() { User user = template.queryForObject("select * from user where id = ?", (r, i) -> new User(r.getInt(1), r.getString(2), r.getString(3), r.getString(4)), 1); System.out.println(user); }
|
简单封装
对于一些插入操作,Spring JDBC为我们提供了更方便的SimpleJdbcInsert工具,比如我们的表是采用自增的ID,那么它支持插入后返回自动生成的ID
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Configuration public class WebConfiguration {
@Resource DataSource source;
@Test void contextLoads() { SimpleJdbcInsert simple = new SimpleJdbcInsert(source) .withTableName("user") .usingGeneratedKeyColumns("id"); Map<String, Object> user = new HashMap<>(2); user.put("name", "bob"); user.put("email", "112233@qq.com"); user.put("password", "123456"); Number number = simple.executeAndReturnKey(user); System.out.println(number); } }
|
JPA框架
官网:https://spring.io/projects/spring-data-jpa
而实现JPA规范的框架一般最常用的就是Hibernate
所需依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
|
1 2 3 4
| <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency>
|
使用JPA快速上手
我们通过注解形式,在属性上添加数据库映射关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Data @Entity @Table(name = "account") public class Account {
@GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @Id int id;
@Column(name = "username") String username;
@Column(name = "password") String password; }
|
修改配置文件,把日志打印打开
1 2 3 4 5 6 7
| spring: jpa: show-sql: true hibernate: ddl-auto: update
|
ddl-auto属性用于设置自动表定义,可以实现自动在数据库中为我们创建一个表,表的结构会根据我们定义的实体类决定,它有以下几种:
none: 不执行任何操作,数据库表结构需要手动创建。
create: 框架在每次运行时都会删除所有表,并重新创建。
create-drop: 框架在每次运行时都会删除所有表,然后再创建,但在程序结束时会再次删除所有表。
update: 框架会检查数据库表结构,如果与实体类定义不匹配,则会做相应的修改,以保持它们的一致性。
validate: 框架会检查数据库表结构与实体类定义是否匹配,如果不匹配,则会抛出异常。
创建一个Repository接口来继承 JpaRepository 泛型,第一个参数是实体类,第二个是主键的类型
1 2 3
| @Repository public interface AccountRepository extends JpaRepository<Account, Integer> { }
|
直接调用方法即可
1 2 3 4 5 6 7 8 9 10
| @Resource AccountRepository repository;
@Test void contextLoads() { Account account = new Account(); account.setUsername("小红"); account.setPassword("1234567"); System.out.println(repository.save(account).getId()); }
|
方法名称拼接自定义SQL
超级帅,只需要在我们创建 AccountRepository 上通过方法名称的拼接来实现条件判断
| 关键字 |
方法名称示例 |
执行的语句 |
| Distinct |
findDistinctByLastnameAndFirstname |
select distinct … where x.lastname = ?1 and x.firstname = ?2 |
| And |
findByLastnameAndFirstname |
… where x.lastname = ?1 and x.firstname = ?2 |
| Or |
findByLastnameOrFirstname |
… where x.lastname = ?1 or x.firstname = ?2 |
| Is,Equals |
findByFirstname, findByFirstnameIs, findByFirstnameEquals |
… where x.firstname = ?1 |
| Between |
findByStartDateBetween |
… where x.startDate between ?1 and ?2 |
| LessThan |
findByAgeLessThan |
… where x.age < ?1 |
| LessThanEqual |
findByAgeLessThanEqual |
… where x.age <= ?1 |
| GreaterThan |
findByAgeGreaterThan |
… where x.age > ?1 |
| GreaterThanEqual |
findByAgeGreaterThanEqual |
… where x.age >= ?1 |
| After |
findByStartDateAfter |
… where x.startDate > ?1 |
| Before |
findByStartDateBefore |
… where x.startDate < ?1 |
| IsNull,Null |
findByAge(Is)Null |
… where x.age is null |
| IsNotNull,NotNull |
findByAge(Is)NotNull |
… where x.age not null |
| Like |
findByFirstnameLike |
… where x.firstname like ?1 |
| NotLike |
findByFirstnameNotLike |
… where x.firstname not like ?1 |
| StartingWith |
findByFirstnameStartingWith |
… where x.firstname like ?1(参数与附加%绑定) |
| EndingWith |
findByFirstnameEndingWith |
… where x.firstname like ?1(参数与前缀%绑定) |
| Containing |
findByFirstnameContaining |
… where x.firstname like ?1(参数绑定以%包装) |
| OrderBy |
findByAgeOrderByLastnameDesc |
… where x.age = ?1 order by x.lastname desc |
| Not |
findByLastnameNot |
… where x.lastname <> ?1 |
| In |
findByAgeIn(Collection ages) |
… where x.age in ?1 |
| NotIn |
findByAgeNotIn(Collection ages) |
… where x.age not in ?1 |
| True |
findByActiveTrue |
… where x.active = true |
| False |
findByActiveFalse |
… where x.active = false |
| IgnoreCase |
findByFirstnameIgnoreCase |
… where UPPER(x.firstname) = UPPER(?1) |
比如我们想要实现根据用户名模糊匹配查找用户、根据大于这个ID并且降序查询、通过用户名和ID查询、判断数据库中是否存在某个ID的用户:
1 2 3 4 5 6 7 8 9 10
| @Repository public interface AccountRepository extends JpaRepository<Account, Integer> { Account findAccountByUsernameLike(String str);
List<Account> findAccountsByIdGreaterThanOrderByIdDesc(int id);
Account findAccountByUsernameAndId(String username, int id);
boolean existsAccountById(int id); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @SpringBootTest class Demo2ApplicationTests {
@Resource AccountRepository repository;
@Test void contextLoads() { System.out.println(repository.findAccountByUsernameLike("%es%")); System.out.println("======================================"); System.out.println(repository.findAccountsByIdGreaterThanOrderByIdDesc(0)); System.out.println("======================================"); System.out.println(repository.findAccountByUsernameAndId("test", 1)); System.out.println("======================================"); System.out.println(repository.existsAccountById(1)); System.out.println(repository.existsAccountById(10)); System.out.println("======================================"); } }
|
注意自定义条件操作的方法名称一定要遵循规则,不然会出现异常:
1
| Caused by: org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract ...
|
关联查询
一对一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Data @Entity @Table(name = "users_detail") public class AccountDetail {
@Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) @Id int id;
@Column(name = "address") String address;
@Column(name = "email") String email;
@Column(name = "phone") String phone;
@Column(name = "real_name") String realName; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Data @Entity @Table(name = "users") public class Account {
@GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @Id int id;
@Column(name = "username") String username;
@Column(name = "password") String password;
@JoinColumn(name = "detail_id") @OneToOne AccountDetail detail; }
|
添加数据时,利用实体类之间的关联信息,一次性添加两张表的数据
1 2 3
| @JoinColumn(name = "detail_id") @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) AccountDetail detail;
|
- ALL:所有操作都进行关联操作
- PERSIST:插入操作时才进行关联操作
- REMOVE:删除操作时才进行关联操作
- MERGE:修改操作时才进行关联操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Test void addAccount(){ Account account = new Account(); account.setUsername("Nike"); account.setPassword("123456"); AccountDetail detail = new AccountDetail(); detail.setAddress("翻斗大街"); detail.setPhone("1234567890"); detail.setEmail("123456@qq.com"); detail.setRealName("张三"); account.setDetail(detail); account = repository.save(account); System.out.println("插入时,自动生成的主键ID为:"+account.getId()+",外键ID为:"+account.getDetail().getId()); }
|
一对多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Data @Entity @Table(name = "users_score") public class Score {
@GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @Id int id;
@OneToOne @JoinColumn(name = "cid") Subject subject;
@Column(name = "socre") double score;
@Column(name = "uid") int uid; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Data @Entity @Table(name = "subjects") public class Subject {
@GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "cid") @Id int cid;
@Column(name = "name") String name;
@Column(name = "teacher") String teacher;
@Column(name = "time") int time; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Data @Entity @Table(name = "account") public class Account { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) Integer id;
@Column(name = "username") String username;
@Column(name = "password") String password;
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @JoinColumn(name = "detail_id") AccountDetail detail;
@JoinColumn(name = "uid") @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) List<Score> scoreList; }
|
多对多
1 2 3 4 5 6
| @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "teach_relation", //多对多中间关联表 joinColumns = @JoinColumn(name = "cid"), //当前实体主键在关联表中的字段名称 inverseJoinColumns = @JoinColumn(name = "tid") //教师实体主键在关联表中的字段名称 ) List<Teacher> teacher;
|
JPQL自定义SQL语句
1 2 3 4 5 6 7 8
| @Repository public interface AccountRepository extends JpaRepository<Account, Integer> {
@Transactional @Modifying @Query("update Account set password = ?2 where id = ?1") int updatePasswordById(int id, String newPassword); }
|
或者
1 2 3 4 5
| @Transactional @Modifying @Query(value = "update users set password = :pwd where username = :name", nativeQuery = true) int updatePasswordByUsername(@Param("name") String username, //我们可以使用@Param指定名称 @Param("pwd") String newPassword);
|
MybatisPlus框架
所需依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.10.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>9.1.0</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-extension</artifactId> <version>3.5.5</version> </dependency>
|
数据源
1 2 3 4 5 6
| spring: datasource: url: jdbc:mysql://localhost:3306/test username: root password: Abc123 driver-class-name: com.mysql.cj.jdbc.Driver
|
开启日志打印
1 2 3
| mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
快速上手
实体类,可以直接映射到数据库中的表
1 2 3 4 5 6 7 8 9 10 11 12
| @Data @TableName("user") public class User { @TableId(type = IdType.AUTO) int id; @TableField("name") String name; @TableField("email") String email; @TableField("password") String password; }
|
跟 mybatis 一样,需要一个 mapper
1 2 3 4 5
| @Mapper public interface UserMapper extends BaseMapper<User> { }
|
1 2 3 4 5 6 7 8 9 10 11
| @SpringBootTest class DemoApplicationTests {
@Resource UserMapper mapper;
@Test void contextLoads() { System.out.println(mapper.selectById(1)); } }
|
条件构造器
1 2 3 4 5 6 7 8 9
| @Test void contextLoads() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper .select("id", "name", "email", "password") .ge("id", 2) .orderByDesc("id"); System.out.println(mapper.selectList(wrapper)); }
|
等同于
1
| select id,name,email,password from user where id >= 2 order by id desc
|
支持批处理操作,我们可以一次性删除多个指定ID的用户:
1 2 3 4
| @Test void contextLoads() { mapper.deleteByIds(List.of(1, 3)); }
|
更新:
1 2 3 4 5 6 7 8
| @Test void contextLoads() { UpdateWrapper<User> wrapper = new UpdateWrapper<>(); wrapper .set("name", "lbw") .eq("id", 1); System.out.println(mapper.update(null, wrapper)); }
|
分页查询
先配置
1 2 3 4 5 6 7 8 9 10
| @Configuration public class MybatisConfiguration { @Bean public MybatisPlusInterceptor paginationInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }
|
使用
1 2 3 4 5 6
| @Test void contextLoads() { Page<User> page = mapper.selectPage(Page.of(1, 2), Wrappers.emptyWrapper()); System.out.println(page.getRecords()); }
|
Lambda表达式
1 2 3 4 5 6 7 8
| @Test void contextLoads() { LambdaQueryWrapper<User> wrapper = Wrappers .<User>lambdaQuery() .eq(User::getId, 2) .select(User::getName, User::getId); System.out.println(mapper.selectOne(wrapper)); }
|
代码生成器
依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| package com.example;
import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.DataSourceConfig; import jakarta.annotation.Resource; import org.apache.ibatis.annotations.Mapper; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest;
import javax.sql.DataSource;
@SpringBootTest class Demo4ApplicationTests {
@Resource DataSource DataSource;
@Test void contextLoads() { FastAutoGenerator .create(new DataSourceConfig.Builder(DataSource)) .globalConfig(builder -> { builder.author("hs"); builder.commentDate("2024-01-01"); builder.outputDir("src/main/java"); }) .packageConfig(builder -> builder.parent("com.example")) .strategyConfig(builder -> { builder .mapperBuilder() .mapperAnnotation(Mapper.class) .build(); }) .execute(); } }
|
前后端分离
基于 session 的分离
前后端分离,需要前端与后端各占一个服务器
环境搭建
勾选上spring web

在resources里创建一个web目录,把所有web相关的文件塞进去,然后修改配置文件
1 2 3 4
| spring: web: resources: static-locations: classpath:/web
|
把右下角看到的依赖都勾选上

后端文件的resources中只需要保留一个配置文件即可,因为前端已经占用了8080端口,我们这里需要改成其他端口
1 2 3 4 5 6 7 8
| server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/test username: root password: Abc123 driver-class-name: com.mysql.cj.jdbc.Driver
|
实现登录授权和跨域处理
还是跟之前security一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Configuration public class SecurityConfiguration {
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(conf -> { conf.anyRequest().authenticated(); }) .formLogin(conf -> { conf.loginProcessingUrl("/api/auth/login"); conf.permitAll(); }) .csrf(AbstractHttpConfigurer::disable) .build(); } }
|
手动设置SuccessHandler和FailureHandler来实现让SpringSecurity在登录成功之后返回一个JSON数据给前端而不是默认的重定向:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http ... .formLogin(conf -> { conf.loginProcessingUrl("/api/auth/login"); conf.failureHandler(this::onAuthenticationFailure); conf.successHandler(this::onAuthenticationSuccess); conf.permitAll(); }) ... }
void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {
}
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { }
|
REST应答一般使用的格式为JSON,这里我们创建一个实体类来装载响应数据,两种写法:
- 1、基本写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Data public class RestBean<T> { int code; T data; String message;
private RestBean(int code, T data, String message) { this.code = code; this.data = data; this.message = message; }
public static <T> RestBean<T> success(T data) { return new RestBean<>(200, data, "success"); }
public static <T> RestBean<T> failure(int code, String message) { return new RestBean<>(code, null, message); }
public String asJsonString() { return JSONObject .from(this, JSONWriter.Feature.WriteNulls) .toString(); } }
|
或者
- 2、JDK的新特性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public record RestBean<T> (int code, T data, String message) { public static <T> RestBean<T> success(T data){ return new RestBean<>(200, data, "请求成功"); }
public static <T> RestBean<T> failure(int code, String message){ return new RestBean<>(code, null, message); }
public static <T> RestBean<T> failure(int code){ return failure(code, "请求失败"); } public String asJsonString() { return JSONObject.toJSONString(this, JSONWriter.Feature.WriteNulls); } }
|
把handler补充完
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { response.setContentType("application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write(RestBean.failure(401, exception.getMessage()).asJsonString()); }
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { response.setContentType("application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write(RestBean.success(authentication.getName()).asJsonString()); }
|
我们在发起登录请求时,前端得到了一个跨域请求错误,这是因为我们前端的站点和后端站点不一致导致的,我们只需要告诉浏览器那些站点发出的请求是安全的即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http ... .cors(conf -> { CorsConfiguration cors = new CorsConfiguration(); cors.addAllowedOrigin("http://localhost:8080"); cors.setAllowCredentials(true); cors.addAllowedHeader("*"); cors.addAllowedMethod("*"); cors.addExposedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", cors); conf.configurationSource(source); }) ... .build(); }
|
SecurityConfiguration 最终代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| package com.example.config;
import com.example.entity.RestBean; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.io.IOException; import java.io.PrintWriter;
@Configuration public class SecurityConfiguration {
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(conf -> { conf.anyRequest().authenticated(); }) .formLogin(conf -> { conf.loginProcessingUrl("/api/auth/login"); conf.successHandler(this::onAuthenticationSuccess); conf.failureHandler(this::onAuthenticationFailure); conf.permitAll(); }) .csrf(AbstractHttpConfigurer::disable) .cors(conf -> { CorsConfiguration cors = new CorsConfiguration(); cors.addAllowedOrigin("http://localhost:8080"); cors.setAllowCredentials(true); cors.addAllowedHeader("*"); cors.addAllowedMethod("*"); cors.addExposedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", cors); conf.configurationSource(source); }) .build(); } void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { response.setContentType("application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write(RestBean.failure(401, exception.getMessage()).asJsonString()); }
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { response.setContentType("application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write(RestBean.success(authentication.getName()).asJsonString()); } }
|
实体类最终代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package com.example.entity;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONWriter;
public record RestBean<T> (int code, T data, String message) { public static <T> RestBean<T> success(T data){ return new RestBean<>(200, data, "请求成功"); }
public static <T> RestBean<T> failure(int code, String message){ return new RestBean<>(code, null, message); }
public static <T> RestBean<T> failure(int code){ return failure(code, "请求失败"); } public String asJsonString() { return JSONObject.toJSONString(this, JSONWriter.Feature.WriteNulls); } }
|
JWT
一个JWT令牌由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。
依赖:
1 2 3 4 5
| <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.3.0</version> </dependency>
|
快速体验
生成一个JWT令牌
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Main { public static void main(String[] args) { String jwtKey = "abcdefghijklmn"; Algorithm algorithm = Algorithm.HMAC256(jwtKey); String jwtToken = JWT.create() .withClaim("id", 1) .withClaim("name", "lbw") .withClaim("role", "nb") .withExpiresAt(new Date(2024, Calendar.FEBRUARY, 1)) .withIssuedAt(new Date()) .sign(algorithm); System.out.println(jwtToken); } }
|
得到的令牌为:
1
| eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoibmIiLCJuYW1lIjoibGJ3IiwiaWQiOjEsImV4cCI6NjE2NjQ4NjA4MDAsImlhdCI6MTc0MDA2OTA0N30.1IJCxw3HUmOy2qbEw--6Qnz5fub6dbnIv-u2GmZQam0
|
还原代码:
1 2 3 4 5 6 7 8 9
| public static void main(String[] args) { String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoibmIiLCJuYW1lIjoibGJ3IiwiaWQiOjEsImV4cCI6NjE2NjQ4NjA4MDAsImlhdCI6MTY5MDEzMTQ3OH0.KUuGKM0OynL_DEUnRIETDBlmGjoqbt_5dP2r21ZDE1s"; String[] split = jwtToken.split("\\."); for (int i = 0; i < split.length - 1; i++) { String s = split[i]; byte[] decode = Base64.getDecoder().decode(s); System.out.println(new String(decode)); } }
|
解码后:
1 2
| {"typ":"JWT","alg":"HS256"} {"role":"nb","name":"lbw","id":1,"exp":61664860800,"iat":1740069366}
|
SpringSecurity 实现 JWT 校验
首先先创建处理JWT令牌的工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public class JwtUtils { private static final String key = "abcdefghijklmn";
public static String createJwt(UserDetails user){ Algorithm algorithm = Algorithm.HMAC256(key); Calendar calendar = Calendar.getInstance(); Date now = calendar.getTime(); calendar.add(Calendar.SECOND, 3600 * 24 * 7); return JWT.create() .withClaim("name", user.getUsername()) .withClaim("authorities", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList()) .withExpiresAt(calendar.getTime()) .withIssuedAt(now) .sign(algorithm); }
public static UserDetails resolveJwt(String token){ Algorithm algorithm = Algorithm.HMAC256(key); JWTVerifier jwtVerifier = JWT.require(algorithm).build(); try { DecodedJWT verify = jwtVerifier.verify(token); Map<String, Claim> claims = verify.getClaims(); if(new Date().after(claims.get("exp").asDate())) return null; else return User .withUsername(claims.get("name").asString()) .password("") .authorities(claims.get("authorities").asArray(String.class)) .build(); } catch (JWTVerificationException e) { return null; } } }
|
然后再编写 JwtAuthenticationFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authorization = request.getHeader("Authorization"); if (authorization != null && authorization.startsWith("Bearer ")) { String token = authorization.substring(7); UserDetails user = JwtUtils.resolveJwt(token); if(user != null) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } }
|
最后再修改 SecurityConfiguration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| package com.example.config; import com.example.entity.RestBean; import com.example.filter.JwtAuthenticationFilter; import com.example.util.JwtUtils; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.io.IOException; import java.io.PrintWriter; @Configuration public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(conf -> { conf.anyRequest().authenticated(); }) .formLogin(conf -> { conf.loginProcessingUrl("/api/auth/login"); conf.successHandler(this::handleProcess); conf.failureHandler(this::handleProcess); conf.permitAll(); }) .csrf(AbstractHttpConfigurer::disable) .cors(conf -> { CorsConfiguration cors = new CorsConfiguration(); cors.addAllowedOrigin("http://localhost:8080"); cors.setAllowCredentials(true); cors.addAllowedHeader("*"); cors.addAllowedMethod("*"); cors.addExposedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", cors); conf.configurationSource(source); }) .exceptionHandling(conf -> { conf.accessDeniedHandler(this::handleProcess); conf.authenticationEntryPoint(this::onAuthenticationFailure); }) .sessionManagement(conf -> conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .build(); } void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { response.setContentType("application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write(RestBean.failure(401, exception.getMessage()).asJsonString()); } void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { response.setContentType("application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write(RestBean.success(authentication.getName()).asJsonString()); } private void handleProcess(HttpServletRequest request, HttpServletResponse response, Object exceptionOrAuthentication) throws IOException { response.setContentType("application/json;charset=utf-8"); PrintWriter writer = response.getWriter(); if(exceptionOrAuthentication instanceof AccessDeniedException exception) { writer.write(RestBean.failure(403, exception.getMessage()).asJsonString()); } else if(exceptionOrAuthentication instanceof Exception exception) { writer.write(RestBean.failure(401, exception.getMessage()).asJsonString()); }else if(exceptionOrAuthentication instanceof Authentication authentication){ writer.write(RestBean.success(JwtUtils.createJwt((User) authentication.getPrincipal())).asJsonString()); } } }
|