4.0.0 com.glc.client glc-client 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent 3.1.5 21 21 UTF-8 UTF-8 true org.springframework.boot spring-boot-starter-web org.projectlombok lombok 1.18.30 provided
package com.glc.client; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GlcClientApplication { public static void main(String[] args) { SpringApplication.run(GlcClientApplication.class, args); } }
然后直接启动程序就可以了,启动成功的日志如下
. ____ _ __ _ _ /\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.1.0) 2023-11-12T15:19:22.634+08:00 INFO 13420 --- [ main] com.glc.client.GlcClientApplication : Starting GlcClientApplication using Java 21.0.1 with PID 13420 (/Users/cgl/codes/glc-client/target/classes started by cgl in /Users/cgl/codes/glc-client) 2023-11-12T15:19:22.635+08:00 INFO 13420 --- [ main] com.glc.client.GlcClientApplication : No active profile set, falling back to 1 default profile: "default" 2023-11-12T15:19:22.948+08:00 INFO 13420 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2023-11-12T15:19:22.952+08:00 INFO 13420 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2023-11-12T15:19:22.952+08:00 INFO 13420 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.8] 2023-11-12T15:19:22.988+08:00 INFO 13420 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2023-11-12T15:19:22.989+08:00 INFO 13420 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 333 ms 2023-11-12T15:19:23.116+08:00 INFO 13420 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2023-11-12T15:19:23.120+08:00 INFO 13420 --- [ main] com.glc.client.GlcClientApplication : Started GlcClientApplication in 0.622 seconds (process running for 0.858)
spring: application: name: glc-client profiles: active: dev server: port: 8077
The following 1 profile is active: "dev" Tomcat initialized with port(s): 8077 (http)
org.springframework.boot spring-boot-starter-test
2.在test.java.com.glc.client下面添加GlcClientApplicationTest文件,输入以下测试代码:
package com.glc.client; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class GlcClientApplicationTest { @Test void main() { System.out.println("test"); } }
package com.glc.client.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HomeController { @GetMapping("/") public String home(){ return "Hello World!"; } }
后面会用到StringUtils,这里先导入 apache commons lang依赖
org.apache.commons commons-lang3 3.13.0
mysql mysql-connector-java 8.0.33 runtime
关于这个依赖的解释:
这段代码是一个Maven依赖配置,用于指定项目依赖的MySQL JDBC驱动程序 mysql-connector-java。在这里,每个标签的含义如下:
简而言之,这段配置告诉Maven项目在运行时需要使用版本为8.0.33的MySQL JDBC驱动程序。
在application-dev.yml添加数据库信息,同时本项目使用springboot默认的连接池hikari:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/glc?useUnicode=true&allowMultiQueries=true&characterEncoding=utf8&serverTimezone=GMT%2B8 username: root password: 123456 hikari: minimum-idle: 5 idle-timeout: 600000 maximum-pool-size: 32 auto-commit: true pool-name: HikariCorePool max-lifetime: 1800000 connection-timeout: 30000 connection-test-query: select TABLE_NAME from information_schema.tables limit 1 connection-init-sql: SET NAMES utf8mb4
在做完下面的mybatis plus相关的引入之后启动项目并第一次测试数据库查询相关接口之后,可以看到HikariCorePool - Start completed.,说明数据库连接池启动成功。
com.baomidou mybatis-plus-boot-starter 3.5.4.1
创建config目录,在下面创建MybatisPlusConfig文件,写入以下内容:
package com.glc.client.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan("com.glc.client.mapper*") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor innerInterceptor = new PaginationInnerInterceptor(); innerInterceptor.setDbType(DbType.MYSQL); innerInterceptor.setMaxLimit(100000L); interceptor.addInnerInterceptor(innerInterceptor); return interceptor; } }
该插件主要用于一键生成表相关的实体类。
com.baomidou mybatis-plus-generator 3.5.4.1 org.apache.velocity velocity-engine-core 2.3
在com.glc.client下面创建MybatisPlusGenerator文件,写入以下内容:
package com.glc.client; import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.config.rules.DateType; import com.baomidou.mybatisplus.generator.config.rules.DbColumnType; import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine; import com.baomidou.mybatisplus.generator.fill.Column; import com.baomidou.mybatisplus.generator.fill.Property; import org.apache.commons.lang3.StringUtils; import java.sql.Types; import java.util.Collections; public class MybatisPlusGenerator { public static void main(String[] args) { String tableName = "user"; //需要生成的类对应的表名 IdType idType = IdType.ASSIGN_ID; //插入数据是默认的主键id的生成算法 String userDir = System.getProperty("user.dir"); String outputDir = StringUtils.join(userDir, "/src/main/java/"); String mapperXmlDir = StringUtils.join(userDir, "/src/main/resources/mapper"); //创建代码生成器对象 String url = "jdbc:mysql://127.0.0.1:3306/glc?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&autoReconnect=true&failOverReadOnly=false"; String username = "root"; String password = "123456"; FastAutoGenerator.create(url, username, password) .globalConfig(builder -> { builder.author("gulong") // 设置作者 .fileOverride() // 覆盖已生成文件 .commentDate("yyyy-MM-dd") // 注释里面的日期的格式 .dateType(DateType.ONLY_DATE) //定义生成的实体类中日期类型 DateType.ONLY_DATE 默认值: DateType.TIME_PACK .outputDir(outputDir); // 指定输出目录 }) .dataSourceConfig(builder -> { builder.schema("glc") .typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> { int typeCode = metaInfo.getJdbcType().TYPE_CODE; if (typeCode == Types.SMALLINT) { // 自定义类型转换 return DbColumnType.INTEGER; } return typeRegistry.getColumnType(metaInfo); }); }) .packageConfig(builder -> { builder.parent("com.glc.client") // 设置父包名 // .moduleName("") // 设置父包模块名 .controller("controller") .entity("entity") .service("service") .service("service.impl") .mapper("mapper") .pathInfo(Collections.singletonMap(OutputFile.xml, mapperXmlDir)); // 设置mapperXml生成路径 }) .strategyConfig(builder -> { builder.addInclude(tableName) // 设置需要生成的表名 // .addTablePrefix("t_") // 设置过滤表前缀 .serviceBuilder()//service策略配置 .formatServiceFileName("%sService") //去掉类名中默认的I前缀 .formatServiceImplFileName("%sServiceImpl") .entityBuilder()// 实体类策略配置 .idType(idType)//主键策略 这里AUTO是数据库自增ID,如果需要雪花算法生成的id可以改成ASSIGN_ID .addTableFills(new Column("create_time", FieldFill.INSERT)) // 自动填充配置 .addTableFills(new Property("update_time", FieldFill.INSERT_UPDATE)) .enableLombok() //开启lombok .logicDeleteColumnName("deleted")// 假删除字段 .enableTableFieldAnnotation()// 自动添加表字段的注解 .controllerBuilder() //controller 策略配置 .formatFileName("%sController") .enableRestStyle() // 开启RestController注解 .mapperBuilder()// mapper策略配置 .formatMapperFileName("%sMapper") .formatXmlFileName("%sMapper"); }) .templateEngine(new VelocityTemplateEngine()) .execute(); } }
在上述配置中,我们指定了表为user表,点击执行,看看生成结果:
这样mybatis的代码生成器就配置完成了,之后需要添加新的表只需要改一下MybatisPlusGenerator里的表名就行了,idType看实际需求选择AUTO(自增id)还是ASSIGN_ID(雪花算法生成的id)
为了方便调试接口,可以引入knife4j。在knife4j官方文档中,有如下说明:
由于springfox长久未更新,并且Swagger2规范在目前来看,一定程度上也并未升级,规范已经全部往OpenAPI3规范靠拢,因此,在Spring Boot 3.x版本中,开发者应该选择OpenAPI3规范来作为应用框架的开发首选方案。
com.github.xiaoymin knife4j-openapi3-jakarta-spring-boot-starter 4.3.0
在application.yml中,添加如下配置(点此查看官方关于权限控制的说明):
# springdoc-openapi项目配置 springdoc: swagger-ui: path: /swagger-ui.html tags-sorter: alpha operations-sorter: alpha api-docs: path: /v3/api-docs group-configs: - group: 'client' paths-to-match: '/**' packages-to-scan: com.glc.client # knife4j的增强配置,不需要增强可以不配 knife4j: enable: true setting: language: zh_cn production: false
首先在controller包里添加BodyController文件:
package com.glc.client.controller; import com.glc.client.model.FileResp; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("body") @Tag(name = "body参数") public class BodyController { @Operation(summary = "普通body请求") @PostMapping("/body") public ResponseEntitybody(@RequestBody FileResp fileResp) { return ResponseEntity.ok(fileResp); } @Operation(summary = "普通body请求+Param+Header+Path") @Parameters({ @Parameter(name = "id", description = "文件id", in = ParameterIn.PATH), @Parameter(name = "token", description = "请求token", required = true, in = ParameterIn.HEADER), @Parameter(name = "name", description = "文件名称", required = true, in = ParameterIn.QUERY) }) @PostMapping("/bodyParamHeaderPath/{id}") public ResponseEntity bodyParamHeaderPath(@PathVariable("id") String id, @RequestHeader("token") String token, @RequestParam("name") String name, @RequestBody FileResp fileResp) { fileResp.setName(fileResp.getName() + ",receiveName:" + name + ",token:" + token + ",pathID:" + id); return ResponseEntity.ok(fileResp); } }
其中FileResp如下:
package com.glc.client.model; import lombok.Data; @Data public class FileResp { private String name; private Long fileSize; private Boolean deleted; }
然后启动项目,访问:http://127.0.0.1:8077/doc.html, 看到如下界面, 说明成功了:
测试接口结果如下:
至此knife4j就配置完成了,下面来看看knife的增强配置。
首先看看官方说明:
在以前的版本中,开发者需要在配置文件中手动使用@EnableKnife4j来使用增强,自2.0.6版本后,只需要在配置文件中配置knife4j.enable=true即可不在使用注解
knife4j: # 开启增强配置 enable: true # 开启Swagger的Basic认证功能,默认是false basic: enable: true # Basic认证用户名 username: test # Basic认证密码 password: 123
目前暂时没有其他增强配置的需求。官方文档已经很详细了,有需要的话直接查阅官方文档。
下面我们将使用mybatis自带的分页功能进行分页查询
之前其实已经配置过了,就是在MybatisPlusConfig文件里面,具体配置如下所示:
package com.glc.client.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan("com.glc.client.mapper*") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor innerInterceptor = new PaginationInnerInterceptor(); innerInterceptor.setDbType(DbType.MYSQL); innerInterceptor.setMaxLimit(100000L); interceptor.addInnerInterceptor(innerInterceptor); return interceptor; } }
在userController中添加如下接口代码:
package com.glc.client.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.glc.client.entity.User; import com.glc.client.service.impl.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** ** 前端控制器 *
* * @author gulong * @since 2023-11-12 */ @RestController @RequestMapping("user") public class UserController { @Autowired private UserService userService; /** * 用户列表分页查询 * * @param current 当前页数 * @param size 每页条数 * @return ResponseEntity> */ @GetMapping("/page") public ResponseEntity > userPageQuery(@RequestParam("current") Integer current, @RequestParam("size") Integer size) { Page pageParam = new Page<>(current, size); LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); queryWrapper.select(User::getId, User::getCreateTime, User::getCreateTime) .ge(User::getCreateTime, "2023-11-12"); Page pageResult = userService.page(pageParam, queryWrapper); return ResponseEntity.ok(pageResult); } }
接下来我们使用之前配置好的knife4j来测试user/page接口,结果如下:
有时候我们可能对一些复杂的查询使用自定义的sql,我们可以用如下的方法进行分页查询。
UserController.java
/** * 分页查询当天注册的用户信息 * * @param current 当前页数 * @param size 每页条数 * @return user */ @GetMapping("/today") public ResponseEntity> todayUserPageQuery(@RequestParam("current") Integer current, @RequestParam("size") Integer size) { Page page = new Page<>(current, size); String todayDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); userMapper.selectPageVo(page, todayDate); return ResponseEntity.ok(page); }
UserMapper.java
package com.glc.client.mapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.glc.client.entity.User; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; /** ** Mapper 接口 *
* * @author gulong * @since 2023-11-12 */ public interface UserMapper extends BaseMapper{ Page selectPageVo(@Param("page") Page page, @Param("todayDate") String todayDate); }
UserMapper.xml
这样就实现了自定义的分页查询。
在上面的接口返回结果中,我们可以看到createTime的格式对前端不友好,我们统一个改成常用的yyyy-MM-dd HH:mm:ss格式。本项目暂时未引入fastjson,在springboot中默认使用的是jackjson对接口返回的数据进行序列化,所以我们可以在yml文件中配置全局默认时间序列化格式。
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8
如下图所示,可以看到已经按我们常用的格式返回了:
在上面的结果中,id的类型为long,这种类型的数据在返回给前端的时候如果数字过大,就会产生精度丢失的问题。
来看看AI智能助手对于这个问题的解释:
JavaScript中的Number类型仅能安全地表示-253到253之间的整数(即安全整数范围),而超出此范围的大整数在JavaScript中可能无法精确表示。
通常对此类问题的处理方法就是把这个long序列为string。
本项目默认使用jackjson序列化,所以可以使用如下注解来解决:
@JsonSerialize(using = ToStringSerializer.class) private Long id;
如果是使用fastjson序列化,则用如下注解:
@JSONField(serializeUsing = ToStringSerializer.class) private Long id;
web开发中,redis相关的操作是基本离不开的,下面将添加redis相关的一些依赖
经过对比,目前使用spring-boot-starter-data-redis是最合适的,下面开始引入。
拓展:点进依赖可以看到spring-boot-starter-data-redis是引入了spring-data-redis和lettuce-core,其默认使用lettuce-core客户端,线程安全
其中commons-pool2是为了使用连接池。许多Redis客户端库,如Jedis、Lettuce等,在实现其连接池功能时依赖于commons-pool2。这些库使用commons-pool2来管理Redis连接的创建、借用、返回和销毁。
org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2
在application-dev.yml配置redis信息:
spring: data: redis: host: localhost port: 6379 password: 123456 database: 0 connect-timeout: 30000 timeout: 10000 lettuce: pool: min-idle: 16 max-idle: 32 max-active: 128 max-wait: 30000
而且如果其他项目比如python项目设置了一个值,该项目将无法读取。
在test.java.com.glc.client下创建RedisTest文件,输入如下测试代码并执行
package com.glc.client; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.glc.client.model.FileResp; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.StringRedisTemplate; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @SpringBootTest public class RedisTest { @Autowired StringRedisTemplate stringRedisTemplate; @Test void testString() { //存储/读取字符串 stringRedisTemplate.opsForValue().set("test-string", "redis-value"); String value = stringRedisTemplate.opsForValue().get("test-string"); System.out.printf(value); stringRedisTemplate.delete("test-string"); } @Test void testJsonString() throws JsonProcessingException { FileResp afile = new FileResp("afile", 123l, true); FileResp bfile = new FileResp("bfile", 123l, false); Listlist = Arrays.asList(afile, bfile); stringRedisTemplate.opsForValue().set("test-json-string", new ObjectMapper().writeValueAsString(list)); String jsonValue = stringRedisTemplate.opsForValue().get("test-json-string"); System.out.printf(jsonValue); List fromRedis = new ObjectMapper().readValue(jsonValue, new TypeReference<>() { }); for (FileResp item : fromRedis) { System.out.println(item); } stringRedisTemplate.delete("test-json-string"); } @Test void testHash() throws JsonProcessingException { FileResp afile = new FileResp("afile", 123l, true); FileResp bfile = new FileResp("bfile", 123l, false); stringRedisTemplate.opsForHash().put("test-hash", "hash-1", new ObjectMapper().writeValueAsString(afile)); stringRedisTemplate.opsForHash().put("test-hash", "hash-2", new ObjectMapper().writeValueAsString(bfile)); String value = (String) stringRedisTemplate.opsForHash().get("test-hash", "hash-2"); System.out.println(value); FileResp fileResp = new ObjectMapper().readValue(value, FileResp.class); System.out.println(fileResp); stringRedisTemplate.delete("test-hash"); } @Test void testList() { FileResp afile = new FileResp("afile", 123l, true); FileResp bfile = new FileResp("bfile", 123l, false); List list = Arrays.asList(afile, bfile); //数组里面只能是string ObjectMapper objectMapper = new ObjectMapper(); List jsonStrings = list.stream() .map(fileResp -> { try { return objectMapper.writeValueAsString(fileResp); } catch (JsonProcessingException e) { throw new RuntimeException(e); } }) .collect(Collectors.toList()); stringRedisTemplate.opsForList().rightPushAll("test-list", jsonStrings); List range = stringRedisTemplate.opsForList().range("test-list", 0, -1); System.out.println(range); stringRedisTemplate.delete("test-list"); } }
测试结果发现序列化和反系列化都正常,至此,redis的普通操作都完成了。接下去引入redisson实现分布式锁
由于redisson提供了比较好的分布式锁解决方案,所以直接在项目中引入redisson
org.redisson redisson-spring-boot-starter 3.24.3
有时候我们需要请求第三方接口,我们有多种选择,具体选择哪个比较好,我们来看看AI智能助手的回答:
在Spring Boot中发送HTTP请求可以使用多种框架,每个框架都有其特点和适用场景。以下是几个常用的选项:
- Spring’s RestTemplate: Spring Boot内置支持,易于集成和使用。RestTemplate提供了丰富的方法来消费RESTful服务。它包括了多种便捷的方法来远程调用HTTP服务,并可以轻松地将响应绑定到POJOs上。但从Spring 5开始,RestTemplate被标记为不推荐使用(deprecated),Spring团队推荐使用WebClient。
- Spring’s WebClient: WebClient是Spring 5中引入的,它是一个更现代的、功能更全面的替代RestTemplate的选项。WebClient支持同步和异步请求,并且提供了更灵活的错误处理机制。WebClient是在Spring WebFlux中引入的,但也可以在Spring MVC应用程序中使用。
- OkHttp: OkHttp是一个高效的HTTP客户端,支持同步和异步请求,提供了快速的性能和广泛的功能,如连接池、GZIP压缩和响应缓存。OkHttp适用于需要精细控制HTTP通信的场景,但它不是Spring的一部分,需要单独添加依赖和配置。
- Apache HttpClient: 这是另一个流行的HTTP客户端库。它提供了丰富的功能,如连接管理、认证机制、缓存支持等。与OkHttp类似,Apache HttpClient不是Spring框架的一部分,需要额外配置。
选择哪个框架取决于您的具体需求:
- 如果您需要与Spring生态系统紧密集成,并且不介意使用反应式编程模型,可以选择WebClient。
- 如果您需要更精细的控制和高性能,并且不介意添加额外的依赖和配置,可以选择OkHttp或Apache HttpClient。
- 如果您的项目已经使用了Spring 5以下的版本,RestTemplate仍然是一个可行的选项,尽管它已被标记为不推荐使用。
总的来说,OkHttp是一个非常好的选择,特别是在需要高性能和定制HTTP请求的场景中。但如果您希望与Spring更紧密地集成,并利用Spring的反应式编程功能,那么WebClient可能是更好的选择。
基于上面的信息,本项目决定使用webflux,有以下几点考虑:
下面开始具体步骤:
org.springframework.boot spring-boot-starter-webflux
未完待续…