本文最后更新于:3 个月前
破冰 写作目标 今天是开发壁纸网站 memory-icon 的第一天,本来没打算写什么开发文档的,太耗时间,整体实现思路也简单,就不写了
但是有必要记录一下:从零开发一个 SpringBoot 项目,至少应该要做些什么
这就算给定制自己的 SpringBoot 模板开个好头了(2023/08/01 午)
开发思路 从零开发一个 SpringBoot 要做什么?一定是做到以下几点:(2023/08/01 午)
新建模块
导入必要的依赖:MySQL、Mybatis、MybatisPlus、commons-lang3、lombok、Gson 等
新增必要 yaml 配置:项目名、启动端口、上下文路径、数据库地址
新增 banner.txt
新增数据库表,连接数据库,保存建表语句
新增 controller 层、service 层
利用 MybatisX-Generator 插件,快速生成 domain、mapper、service、XXXMaper.xml
新增 config 层:CorsConfig、MybatisPlusConfig、Knife4jConfig、JsonConfig 等
思维碰撞 着手开发 快速编写 Controller 层
什么是 Controller 层?(2023/08/14 早)
1 2 3 4 5 6 7 在Web开发中,Controller层是MVC(模型-视图-控制器)架构的一部分,用于接收并处理前端请求。 具体来说,Controller层负责以下几个任务: 1. 接收HTTP请求:Controller通过定义特定的路由(URL 映射)来接收前端发送的HTTP请求。 2. 处理请求:Controller中的方法将根据请求的类型、路径以及请求参数等信息,执行相应的逻辑操作。 3. 调用服务层:在Controller中,可以调用服务层(Service层、业务层)的方法来实现相关业务逻辑。 4. 返回响应:在完成对请求的处理后,Controller负责将数据或者视图响应返回给前端。 Controller层的目标是实现请求-响应的流程控制,将请求按照业务逻辑分配给对应的服务层处理,并将执行结果返回给前端。同时,Controller层还可以处理一些与业务逻辑相关的验证或者数据转换等操作。
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 2.7.14</version > </dependency >
这里贴个小技巧,alt + insert 快速添加依赖模板:
新增 controller 层,编写响应请求的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("/test") public class TestController { @GetMapping("/name") public String getName (String name) { return String.format("我的名字是: %s" , name); } }
编辑 application.yaml 配置文件:
1 2 3 4 server: port: 8088 servlet: context-path: /api
1 GET http://localhost:8088/api/test/name?name=邓啊呀
全局异常处理 MybatisPlus
1 2 3 4 5 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.2</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: ASSIGN_ID logic-delete-field: isDelete logic-delete-value: 1 logic-not-delete-value: 0
1 2 3 4 5 6 @TableField(value = "isDelete") @TableLogic private Integer isDelete;
最后在 WebConfig 下配置分页插件:(2023/08/22 早)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration @Slf4j public class WebConfig implements WebMvcConfigurer { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor (DbType.MYSQL)); return interceptor; } }
对象映射器(消息转换器)
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 public class JacksonObjectMapper extends ObjectMapper { public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd" ; public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss" ; public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss" ; public JacksonObjectMapper () { super (); this .configure(FAIL_ON_UNKNOWN_PROPERTIES, false ); this .getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); SimpleModule simpleModule = new SimpleModule () .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer (DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addDeserializer(LocalDate.class, new LocalDateDeserializer (DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addDeserializer(LocalTime.class, new LocalTimeDeserializer (DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))) .addSerializer(BigInteger.class, ToStringSerializer.instance) .addSerializer(Long.class, ToStringSerializer.instance) .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer (DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addSerializer(LocalDate.class, new LocalDateSerializer (DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addSerializer(LocalTime.class, new LocalTimeSerializer (DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); this .registerModule(simpleModule); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Configuration @Slf4j public class WebConfig implements WebMvcConfigurer { @Override public void extendMessageConverters (List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器..." ); MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter (); messageConverter.setObjectMapper(new JacksonObjectMapper ()); converters.add(0 , messageConverter); } }
全局跨域处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Configuration @Slf4j public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings (CorsRegistry registry) { registry.addMapping("/**" ) .allowCredentials(true ) .allowedOrigins("http://localhost:7070" , "http://localhost:3000" ,"http://120.55.62.195:7071" ) .allowedMethods("GET" , "POST" , "DELETE" , "PUT" ) .maxAge(3600 ); } }
只要是前端浏览器页面,调用后端接口,都应该在前后端做好跨域处理 ,不然就会遇到这种鸟问题,都不知道怎么解决:(2023/09/09 晚)
Filter 全局过滤器
年后,我学习了第一个完整的 SpringBoot 项目:瑞吉外卖,下面给出编写过滤器的一段代码:(2023/08/22 早)
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 @WebFilter(filterName = "LoginCheckFilter") @Slf4j public class LoginCheckFilter implements Filter { public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher (); @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String requestURI = request.getRequestURI(); String[] urls = new String []{ "/employee/login" , "/employee/logout" , "/backend/**" , "/front/**" , "/user/sendMsg" , "/user/login" }; boolean check = check(urls, requestURI); if (check) { log.info("本次请求不需要处理..." ); filterChain.doFilter(request, response); return ; } Long empId; if ((empId = (Long) request.getSession().getAttribute("employee" )) != null ) { log.info("该员工已登录,id为{}" , empId); BaseContext.setCurrentId(empId); filterChain.doFilter(request, response); return ; } Long userId; if ((userId = (Long) request.getSession().getAttribute("user" )) != null ) { log.info("该用户已登录,id为{}" , userId); BaseContext.setCurrentId(userId); filterChain.doFilter(request, response); return ; } log.info("该用户未登录..." ); response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN" ))); } public boolean check (String[] urls, String requestURI) { for (String url : urls) { boolean match = PATH_MATCHER.match(url, requestURI); if (match) { return true ; } } return false ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public boolean check (String[] urls, String requestURI) { for (String url : urls) { boolean match = PATH_MATCHER.match(url, requestURI); if (match) { return true ; } } return false ; }
这段代码的亮点包括以下几点:
使用了@Log4j 注解,简化了日志记录的操作。通过这个注解,可以直接使用 log 变量来记录日志。
使用了 AntPathMatcher 来匹配请求路径,支持通配符。这样可以减少对每个请求进行完全匹配的操作,提高了效率。
定义了不需要处理的请求路径,使用数组来存储。这样可以在数组中添加需要排除的路径,只对数组中未包含的路径进行处理。
使用 check 方法来判断当前请求是否需要处理。check 方法会遍历请求路径数组,通过 AntPathMatcher 进行匹配,如果匹配成功则返回 true,否则返回 false。
如果当前请求需要处理,将继续调用 FilterChain 的 doFilter 方法,执行后续的过滤器或 Servlet 逻辑。如果不需要处理,则直接放行,不进行后续处理。
这样的设计可以在过滤器中对请求进行过滤,在需要处理的请求中执行相应的逻辑,而不需要处理的请求可以直接放行,提高了性能和效率。(2023/08/22 早)
ThreadLocal
1 2 3 4 5 6 7 8 9 Long empId; if ((empId = (Long) request.getSession().getAttribute("employee" )) != null ) { log.info("该员工已登录,id为{}" , empId); BaseContext.setCurrentId(empId); filterChain.doFilter(request, response); return ; }
这是将已登录员工的 id 封装在 ThreadLocal 中,为每一个使用到这个变量的线程创建了一个属于自己的副本
ThreadLocal 用作保存每个线程独享的对象 ,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保线程安全
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class BaseContext { private static ThreadLocal<Long> threadLocal = new ThreadLocal (); public static void setCurrentId (Long id) { threadLocal.set(id); } public static Long getCurrentId () { return threadLocal.get(); } }
公共字段自动填充 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 @Configuration @Slf4j public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill (MetaObject metaObject) { log.info("公共字段自动填充..." ); metaObject.setValue("createTime" , LocalDateTime.now()); metaObject.setValue("updateTime" , LocalDateTime.now()); metaObject.setValue("createUser" , BaseContext.getCurrentId()); metaObject.setValue("updateUser" , BaseContext.getCurrentId()); } @Override public void updateFill (MetaObject metaObject) { metaObject.setValue("updateTime" , LocalDateTime.now()); metaObject.setValue("updateUser" , BaseContext.getCurrentId()); } }
Lombok commons-lang3 Gson
基于基础类,开发一个 VO 类
在开发过程,我们经常中使用基础类来开发 VO 类,是因为:(2023/09/14 晚)
1 2 基础类可以作为扩展点,用于定义VO类的共同特性和行为。 你可以在基础类中定义一些通用的属性和方法,然后在VO类中添加自定义属性和方法,以实现更具体的功能
最近在开发 MemoryChat 项目的过程中,遇到了这样的业务问题:
查询用户已加入的队伍 / 已创建的队伍 ,我想要展示队伍的 username(队长昵称),在 team 表中只有 userId 字段(队长 Id)
这就需要多表联查,封装携带队伍信息以及队长 username 的对象,并返回给前端
那我们就开发一个 teamVO 类,用 username 字段代替 userId 字段,作为封装返回的对象:
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 @Data public class TeamVO { @TableId(type = IdType.ASSIGN_ID) private Long id; private String name; private String description; private Integer maxNum; private String userName; private String imgUrl; private Integer joinNum; ............................ }
根据相关查询条件,正确执行业务逻辑获取到 teamList 后,转换为 teamVOList
1 2 List<TeamVO> teamVOList = getTeamVOByTeam(teamList);
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 public List<TeamVO> getTeamVOByTeam (List<Team> teamList) { List<TeamVO> teamVOList = teamList.stream().map(team -> { Long userId = team.getUserId(); String userName = userService.getById(userId).getUsername(); TeamVO teamVO = new TeamVO (); teamVO.setId(team.getId()); teamVO.setName(team.getName()); teamVO.setDescription(team.getDescription()); teamVO.setMaxNum(team.getMaxNum()); teamVO.setUserName(userName); teamVO.setImgUrl(team.getImgUrl()); teamVO.setJoinNum(team.getJoinNum()); teamVO.setStatus(team.getStatus()); teamVO.setExpireTime(team.getExpireTime()); teamVO.setCreateTime(team.getCreateTime()); teamVO.setUpdateTime(team.getUpdateTime()); teamVO.setIsDelete(team.getIsDelete()); return teamVO; }).collect(Collectors.toList()); return teamVOList; }
踩坑记录
每当我兴致勃勃地启动项目,准备大展身手的时候,项目启动窗口总会弹出冰冷的报错信息
这里列举了:我遇到过的所有启动项目报错的解决办法
XXXMapper 包扫描不到
1 Unsatisfied dependency expressed through field 'baseMapper' ; nested exception is org.springframework.beans.factory .NoSuchBeanDefinitionException: No qualifying bean of type 'com.memory.memoryiconbackend.mapper.WallpaperMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org .springframework.beans.factory .annotation.Autowired(required =true )}
这个报错信息大致意思是,未扫描到你的 XXXMapper 包,项目启动失败
这个问题可谓最常见了,刚刚我就又被这个问题恶心到了,网上查了半天,感觉他们都是一知半解
那么我是怎么解决这个问题的呢?思路如下:
XXXMapper.xml 配置错误
检查 resource 下的 XXXMapper.xml 配置,检查实体类扫描和 mapper 扫描路径是否正确:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.memory.memoryiconbackend.mapper.WallpaperMapper" > <resultMap id ="BaseResultMap" type ="com.memory.memoryiconbackend.model.Wallpaper" > <result property ="id" column ="id" jdbcType ="VARCHAR" /> <result property ="name" column ="name" jdbcType ="VARCHAR" /> <result property ="url" column ="url" jdbcType ="VARCHAR" /> <result property ="type" column ="type" jdbcType ="VARCHAR" /> <result property ="tags" column ="tags" jdbcType ="VARCHAR" /> <result property ="createTime" column ="create_time" jdbcType ="VARCHAR" /> <result property ="updateTime" column ="update_time" jdbcType ="VARCHAR" /> <result property ="isDelete" column ="is_delete" jdbcType ="VARCHAR" /> <result property ="userId" column ="user_id" jdbcType ="VARCHAR" /> </resultMap > <sql id ="Base_Column_List" > id,name,url, type,tags,create_time, update_time,is_delete,user_id </sql > </mapper >
确保 XXXMapper 包的扫描路径正确后,再继续排查:
XXXMapper 上添加@Mapper 注解
检查 XXXMapper 上是否添加了@Mapper 注解:
1 2 3 4 @Mapper public interface WallpaperMapper extends BaseMapper <Wallpaper> { }
开启@MapperScan 注解
@MapperScan 注解是干嘛的呢?它是用来在项目启动后,扫描你的 XXXMapper 所在路径,用法如下:
1 2 3 4 5 6 7 8 @SpringBootApplication @MapperScan("com.memory.memoryiconbackend.mapper.WallpaperMapper") public class MemoryIconBackendApplication { public static void main (String[] args) { SpringApplication.run(MemoryIconBackendApplication.class, args); } }
那这个注解跟上面提到的@Mapper 注解,功能不是一样的吗?都是将 XXXMapper 标注为一个 Bean,交给 Spring 管理
没错,这两个注解的作用是可以说是一摸一样的,无非就是写的位置不一样
正是因为这两个注解作用是一样的,所以在开发过程中,这两个注解写一个就行,而且只能写一个,否则会报错
网上总会有蠢蛋,说在 XXXMapper 上,添加了@Mapper 注解之后,一定不要忘了在启动类上添加@MapperScan 注解
这种方法肯定解决不了问题,是大错特错的
所以,如果你已经在 XXXMapper 上添加了@Mapper 注解,一定记得删除启动类上的@MapperScan 注解
如果到这里,你已经按照上面的方法解决了问题,成功启动了项目,恭喜你;如果仍旧报错,请继续往下看:
MybatisPlusConfig 配置
我们在项目中,导入了 MybatisPlus 依赖之后,总会写一个 MybatisPlusConfig 的分页配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration @MapperScan("com.memory.memoryiconbackend.mapper.WallpaperMapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor (DbType.MYSQL)); return interceptor; } }
如果你的问题没有解决,一定是因为在这个配置类上边,写上了@MapperScan 注解:
而这个注解的作用,跟启动类上的@MapperScan 注解的作用是一模一样的,删除它就好了
总结
如果你已经在 XXXMapper 上添加了@Mapper 注解,请把启动类和 MybatisPlusConfig 配置类上的@MapperScan 注解删除
如果你已经在启动类和 MybatisPlusConfig 配置类上添加了@MapperScan 注解,请把 XXXMapper 上的@Mapper 注解删除
希望这篇文章对你有帮助,感谢您的支持!😁
类文件版本不匹配
如果在启动项目时,出现了这样的报错,你会怎么解决呢:
引起这样报错的原因只有一种:类文件版本不匹配,即项目里导入的依赖版本不兼容
Mybatis 版本 和 SpringBoot 版本不兼容
查看上方报错信息,很显然是 XXXMapper 包扫描失败了,这就是 Mybatis 版本 和 SpringBoot 版本不兼容的问题
如此配置:(2023/08/02 早)
1 2 3 4 5 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 2.3.0</version > </dependency >
1 2 3 4 5 6 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.7.14</version > <relativePath /> </parent >
Jdk 版本 和 SpringBoot 版本不兼容
这是由于 Jdk 版本 和 SpringBoot 版本不兼容导致的
JDK 版本不兼容,还并且可能导致其他配置构建失败:
1 2 3 4 5 6 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.7.9</version > <relativePath /> </parent >
按照提示,升级 JDK 版本到 1.8 以上即可解决问题
相关依赖未导入 MySQL 相关依赖未导入
今天在做 PicMemories 项目过程中,启动项目时,出现了以下报错:
1 org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'downloadServiceImpl' : Unsatisfied dependency expressed through field 'baseMapper' ; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'downloadMapper' defined in file [D:\Project\星球项目\PicMemories\target\classes\com\memory\picmemories\mapper\DownloadMapper.class ]: Cannot resolve reference to bean 'sqlSessionTemplate' while setting bean property 'sqlSessionTemplate' ; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'dataSourceScriptDatabaseInitializer' defined in class path resource [org/springframework/boot/autoconfigure/sql /init/DataSourceInitializationConfiguration.class ]: Unsatisfied dependency expressed through method 'dataSourceScriptDatabaseInitializer' parameter 0 ; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class ]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception ; nested exception is java.lang.IllegalStateException: Cannot load driver class : com.mysql.cj.jdbc.Driver
经排查,发现是 MySQL 相关依赖导入错了,我导入的错误依赖如下:
1 2 3 4 5 <dependency > <groupId > com.microsoft.sqlserver</groupId > <artifactId > mssql-jdbc</artifactId > <scope > runtime</scope > </dependency >
1 2 3 4 5 <dependency > <groupId > com.mysql</groupId > <artifactId > mysql-connector-j</artifactId > <scope > runtime</scope > </dependency >
为什么会出现这样的错误呢?是因为我在构建项目时,选择导入以下依赖中,发生了错误:
这里一定要选择 MySQL Driver,而不是 MySQL Server Driver,否则就会导入错误的依赖而引发报错(2023/08/02 午)
快速生成 Swagger
+ Knif4j
接口文档
按照官方文档,我们可以清楚地看到不同版本的 SpringBoot 导入 Swagger + Knif4j 接口文档的方式是不一样的
Spring Boot
版本在 2.4.0
~ 3.0.0
之间,以我为例,我的 Spring Boot 版本是 2.7.9
那么使用 Swagger + Kni4j 自动生成接口文档的步骤如下:
1 2 3 4 5 <dependency > <groupId > com.github.xiaoymin</groupId > <artifactId > knife4j-openapi2-spring-boot-starter</artifactId > <version > 4.0.0</version > </dependency >
在 appilcation.yaml 中导入配置:
1 2 3 4 5 6 7 8 9 10 11 12 knife4j: enable: true openapi: title: PicMemories 接口文档 description: PicMemories 壁纸分享小程序 concat: 3348407547 @qq.com url: https://deng-2022.gitee.io/blog/ version: 1.0 license: Apache 2.0 group: test1: group-name: 壁纸分享
执行 SQL 找不到字段
1 Unknown column 'user_id' in 'field list'
报错信息显示,找不到字段 user_id,解决思路如下:
首先,确定这个对应数据库中有这个字段 user_id
其次,确保实体类属性和表中字段映射无误
最后,检查数据库配置是否正确
我就栽在最后一步上了,数据库连接到别的表了,怪不得找不到相应字段,折磨了我一晚上(2023/08/03 早)
2024 年 8 月 31 日
栽他妈一晚上跟头
表记录/实体类 ID 自增长
如何实现:往数据库表中插入数据记录时,id 自增长?
1 `user_id` bigint auto_increment primary key comment '用户id' ,
1 2 3 4 5 @TableId(type = IdType.ASSIGN_ID) private Long userId;
1 2 3 4 5 6 7 8 User user = new User (); user.setUsername(username); user.setPassword(encryptPassword); user.setPassword(phone); user.setAvatar("http://ry2s7czdf.hd-bkt.clouddn.com/imgs/avatar/winter_nature_6-wallpaper-2560x1600.jpg" ); boolean save = this .save(user);
1 id = 1687297781782978562
测试类添加@SpringBootTest 注解
如果不加这个注解,导入的对象值为 null:(2023/08/05 午)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class MemoryClientTest { @Resource private MemoryClient memoryClient; @Test public void testMemorySdk () { if (memoryClient != null ){ System.out.println("成功了" ); memoryClient.getNameByGet("邓哈哈" ); memoryClient.getNameByPost("邓嘻嘻" ); User user = new User ("邓尼玛" ); memoryClient.getUserByPost(user); }else { System.out.println("失败!" ); } } }
还有一点要注意,测试类返回值必须为 void,返回其他值会报错:(2023/08/07 早)
Enum 类不能使用@DATA 注解
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 public enum WallpaperStatusEnum { REVIEW(0 , "审核中" ), PASS(1 , "已发布" ), NOPASS(2 , "不通过" ); private int value; private String text; public static WallpaperStatusEnum getEnumByValue (Integer value) { if (value == null ) { return null ; } WallpaperStatusEnum[] values = WallpaperStatusEnum.values(); for (WallpaperStatusEnum teamStatusEnum : values) { if (teamStatusEnum.getValue() == value) { return teamStatusEnum; } } return null ; } WallpaperStatusEnum(int value, String text) { this .value = value; this .text = text; } public int getValue () { return value; } public void setValue (int value) { this .value = value; } public String getText () { return text; } public void setText (String text) { this .text = text; } }
循环依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Service public class WallpaperServiceImpl extends ServiceImpl <WallpaperMapper, Wallpaper> implements WallpaperService { @Resource private WallpaperService wallpaperService; @Override public List<Wallpaper> getPageByType (Integer searchType) { if (WallpaperTypeEnum.getEnumByValue(searchType) == null ) { throw new BusinessException (ErrorCode.PARMS_ERROR, "没有这样的壁纸类型" ); } QueryWrapper<Wallpaper> type_wqw = new QueryWrapper <>(); type_wqw.eq("type" , searchType); return wallpaperService.list(type_wqw); } }
1 2 3 4 5 6 7 8 Description: The dependencies of some of the beans in the application context form a cycle: wallpaperController ┌─────┐ | wallpaperServiceImpl └─────┘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Service public class WallpaperServiceImpl extends ServiceImpl <WallpaperMapper, Wallpaper> implements WallpaperService { @Override public List<Wallpaper> getPageByType (Integer searchType) { if (WallpaperTypeEnum.getEnumByValue(searchType) == null ) { throw new BusinessException (ErrorCode.PARMS_ERROR, "没有这样的壁纸类型" ); } QueryWrapper<Wallpaper> type_wqw = new QueryWrapper <>(); type_wqw.eq("type" , searchType); return this .list(type_wqw); } }
时间戳格式问题 1 @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
MyBatis 逻辑删除 1 2 3 4 5 @TableLogic private Integer isDelete;
1 2 3 4 5 6 7 mybatis-plus: global-config: db-config: logic-delete-field: isDelete logic-delete-value: 1 logic-not-delete-value: 0
Redis 的引入和测试
快速实现 Redis 的引入,主要做到以下几点:(2023/08/07 早)
导入相关依赖坐标
作相关 yaml 配置
作测试
项目引入
导入依赖坐标:
1 2 3 4 5 6 <!-- https: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.6 .4 </version> </dependency>
1 2 3 4 5 redis: port: 6379 host: localhost database: 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @SpringBootTest @Slf4j class UserServiceImplTest { @Resource private RedisTemplate redisTemplate; @Resource private StringRedisTemplate stringRedisTemplate = new StringRedisTemplate (); @Test void loginTest () { String redisKey = "pic_memories:user:login:session_key" ; stringRedisTemplate.opsForValue().set("pic" , "memory" ); stringRedisTemplate.opsForValue().set(redisKey, "memory" , 20 , TimeUnit.HOURS); redisTemplate.opsForValue().set(redisKey + "2" , "memory2" ); } }
这里注意:
RedisTemplate 底层的序列化方式,会导致存入的序列化后的 value 值成为乱码
StringRedisTemplate 继承了 RedisTemplate 有效解决了这个问题,但只能存放<String,String>
综上,我们在使用 Redis 缓存技术时,可以自己自定义(封装一个)RedisTemplate
自定义 RedisTemplate<String, Object> (config/RedisTemplateConfig)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration public class RedisTemplateConfig { @Bean public RedisTemplate<String, Object> redisTemplate (RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate <>(); redisTemplate.setConnectionFactory(connectionFactory); redisTemplate.setKeySerializer(RedisSerializer.string()); GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer (); redisTemplate.setValueSerializer(jsonRedisSerializer); return redisTemplate; } }
解决 RedisTemplate 存入的序列化后的 value 值成为乱码的问题(2023/08/07 早)
接入阿里云对象存储服务
自定义 Banner
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 启动成功! ${AnsiColor.BRIGHT_GREEN} * ( ` )\))( ( ) ( ( ((_)()\ ))\ ( ( )( )\ ) (_()((_)/((_) )\ ' )\(()\(()/( | \/ (_)) _((_)) ((_)((_))(_)) | |\/| / -_) ' \() _ \ '_| || | |_| |_\___|_|_|_|\___/_| \_, | |__/ ${AnsiColor.BRIGHT_WHITE} 欢迎使用~ spring boot 版本为 ${spring-boot.version} 作者:@Memory 项目名:PicMemories 线上访问地址: 未完成
项目启动类处,可以控制是否选择加载 banner.txt 文件:(2023/08/12 早)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import org.springframework.boot.Banner;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication public class LaunchApplication { public static void main (String[] args) { SpringApplication app = new SpringApplication (LaunchApplication.class); app.setBannerMode(Banner.Mode.OFF); app.run(args); } }
主键自增长
今天使用 MybatisPlus 执行插入数据时,发现了这样的报错:(2023/08/15 午)
1 2 3 4 5 6 7 8 org.springframework.jdbc.BadSqlGrammarException: user_id, wallpaper_id ) VALUES ( 1691349539878477825, 168799521293' at line 1 ### The error may exist in com/memory/picmemories/mapper/LikeMapper.java (best guess) ### The error may involve com.memory.picmemories.mapper.LikeMapper.insert-Inline ### The error occurred while setting parameters
在上面的栏目《表记录/实体类 ID 自增长》中,已经实现了如何使记录中的 id 自增长,这次我就犯了这个错误:
1 2 3 4 5 6 7 8 9 10 create table `like` ( id bigint primary key comment '点赞id' , user_id bigint not null comment '点赞用户id' , wallpaper_id bigint not null comment '被点赞的壁纸id' , create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间' , update_time datetime default CURRENT_TIMESTAMP not null comment '更新时间' , is_delete varchar (256 ) default '0' not null comment '是否删除' ) comment '点赞信息' ;
1 2 3 4 5 @TableId(type = IdType.ASSIGN_ID) private Long id;
1 2 3 4 5 6 Like like = new Like (); like.setUserId(userId); like.setWallpaperId(wallpaperId); boolean save = likeService.save(like);
如上,很显然,我没有将 id 字段设置为自增长,所以才会出现这样的报错(2023/08/15 午)
修改表字段后要做的那些事
修改表字段后,应该做哪些事?
修改对应实体属性
修改 XXXmapper.xml 文件(mapper 路径、domain 路径)
使用插件 MybatisX-Generator 快速生成 mapper、domain、service(2023/08/16 午)
YAML 配置重复键
启动项目报了这样的错误:(2023/08/23 晚)
这个错误通常是由于在 YAML 配置文件中多次定义了相同的键,检查 yaml 配置即可
git 合并提交
在上午,我使用 git 进行合并提交项目代码后,下午运行项目,代码变成了这样:
Spring 项目起不来
准确的说,项目没有启动成为一个 Web 服务器后台,这是为什么呢?
妈的,原来是构建项目时,忘记导入相关依赖了:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency >
呐,导入以上依赖后,SpringBoot 成功运行起来了:(2023/08/29 早)
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency >
Spring 项目起不来(2)
这次是什么原因呢?看报错:(2023/09/06)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 *************************** APPLICATION FAILED TO START *************************** Description: Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. Reason: Failed to determine a suitable driver class Action: Consider the following: If you want an embedded database (H2, HSQL or Derby), please put it on the classpath . If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active). Process finished with exit code 1
这个就很清晰了,因为引入了数据库相关依赖,却没有作相关配置:
1 2 3 4 5 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 2.2.2</version > </dependency >
1 2 3 4 5 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.2</version > </dependency >
1 2 3 4 5 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.31</version > </dependency >
1 2 3 4 5 6 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/memory_api username: root password: Dw990831
mapper 注入为 null
但项目运行后,通过 debug 发现,该 interfaceInfoMapper 为 null,为什么注入不成功呢?我犯了一个错误:
如上,我 new 了一个 interfaceIdSource,这个问题被我忽视了:
1 类的实例化问题:确保你在使用 InterfaceIdSource 类时是通过 Spring 容器来获取实例,而不是通过 new 关键字手动创建对象。只有通过 Spring 容器管理的对象才会进行自动注入。
直接注入即 interfaceIdSource 可解决问题:(2023/09/07 晚)
MybatisPlus 踩坑记录
1 2 3 4 5 6 for (Friends friends : friendsList) { Long friendId = friends.getFriendId(); uqw.eq("id" , friendId); User one = userService.getOne(uqw); userList.add(one); }
1 2 3 4 5 6 for (Friends friends : friendsList) { Long friendId = friends.getFriendId(); User one = userService.getById(friendId); userList.add(one); }
日常犯傻
使用 Vue 的 ref() 语法时,容易忘记取.value
1 const currentUserId = currentUser.value.id
1 const socketUrl = `ws:// localhost:8081 /api/ websocket/${currentUserId} `;
导入 Excel 文件
Easyexcel 官网:(2023/10/05 午)
简单的测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 File file = null ; try { file = ResourceUtils.getFile("classpath:test_excel.xlsx" ); } catch (FileNotFoundException e) { e.printStackTrace(); } List<Map<Integer, String>> list = null ; list = EasyExcel.read(file) .excelType(ExcelTypeEnum.XLSX) .sheet() .headRowNumber(0 ) .doReadSync(); if (CollUtil.isEmpty(list)) { return "" ; }
这部分代码,简单地读取了 resourse 目录下 的 test_excel.xlsx 文件 ,并成功获取表格数据
附上原表格数据和解析效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @PostMapping("/generate") public BaseResponse<String> uploadFile (@RequestPart("file") MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) { String name = genChartByAiRequest.getName(); String goal = genChartByAiRequest.getGoal(); String chartType = genChartByAiRequest.getChartType(); ThrowUtils.throwIf(StringUtils.isBlank(goal), ErrorCode.PARAMS_ERROR, "目标为空" ); ThrowUtils.throwIf(StringUtils.isNotBlank(name) && name.length() > 100 , ErrorCode.PARAMS_ERROR, "名称过长" ); String result = ExcelUtils.excelToCsv(multipartFile); return ResultUtils.success(result);
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 public static String excelToCsv (MultipartFile multipartFile) { File file = null ; try { file = ResourceUtils.getFile("classpath:test_excel.xlsx" ); } catch (FileNotFoundException e) { e.printStackTrace(); } List<Map<Integer, String>> list = null ; list = EasyExcel.read(file) .excelType(ExcelTypeEnum.XLSX) .sheet() .headRowNumber(0 ) .doReadSync(); if (CollUtil.isEmpty(list)) { return "" ; } StringBuilder stringBuilder = new StringBuilder (); LinkedHashMap<Integer, String> headerMap = (LinkedHashMap) list.get(0 ); List<String> headerList = headerMap.values().stream().filter(ObjectUtils::isNotEmpty).collect(Collectors.toList()); stringBuilder.append(StringUtils.join(headerList, "," )).append("\n" ); for (int i = 1 ; i < list.size(); i++) { LinkedHashMap<Integer, String> dataMap = (LinkedHashMap) list.get(i); List<String> dataList = dataMap.values().stream().filter(ObjectUtils::isNotEmpty).collect(Collectors.toList()); stringBuilder.append(StringUtils.join(dataList, "," )).append("\n" ); } return stringBuilder.toString(); }
校验文件
1 2 3 4 5 6 7 8 9 10 long ONE_MB = 1024 * 1024L ; List<String> VALID_FILE_SUFFIX_LIST = Arrays.asList("xlsx" , "xls" );
1 2 3 4 5 6 7 8 9 long size = multipartFile.getSize(); ThrowUtils.throwIf(size > ONE_MB, ErrorCode.PARAMS_ERROR, "文件超过 1M" ); String originalFilename = multipartFile.getOriginalFilename(); String suffix = FileUtil.getSuffix(originalFilename); ThrowUtils.throwIf(!VALID_FILE_SUFFIX_LIST.contains(suffix), ErrorCode.PARAMS_ERROR, "文件后缀非法" );
限流
1 2 3 4 5 <dependency > <groupId > org.redisson</groupId > <artifactId > redisson</artifactId > <version > 3.21.0</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Configuration @ConfigurationProperties(prefix = "spring.redis") @Data public class RedissionConfig { private String host; private String port; private String password; private Integer database; @Bean public RedissonClient redissonClient () { Config config = new Config (); String redisAddress = String.format("redis://%s:%s" , host, port); config.useSingleServer().setAddress(redisAddress).setDatabase(database).setPassword(password); return Redisson.create(config); } }
1 2 3 4 redis: port: 6379 host: localhost database: 1
限流实现(区别不同的限流器,每个用户都分别拥有对应的限流器)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void doRateLimit (String key) { RRateLimiter rateLimiter = redissonClient.getRateLimiter(key); rateLimiter.trySetRate(RateType.OVERALL, 2 , 1 , RateIntervalUnit.SECONDS); boolean canOp = rateLimiter.tryAcquire(1 ); ThrowUtils.throwIf(!canOp, ErrorCode.TOO_MANY_REQUEST); }
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test void doRateLimit () throws InterruptedException { String userId = "1" ; for (int i = 0 ; i < 2 ; i++) { redisLimiterManager.doRateLimit(userId); System.out.println("成功" ); } Thread.sleep(1000 ); for (int i = 0 ; i < 5 ; i++) { redisLimiterManager.doRateLimit(userId); System.out.println("成功" ); } }
业务中使用限流器,调用智能分析接口时,限制用户在单位时间内的调用次数 (2023/10/11 晚)
1 2 redisLimiterManager.doRateLimit("genCharByAi_" + loginUser.getId());
Slf4j 导入依赖错误
这是个很有意思的问题,起因是我计划实现一个延迟队列 Demo ,结果在运行时出现了这样的报错:
这些警告信息是由 SLF4J(Simple Logging Facade for Java)引发的,它是一个为 Java 程序提供日志功能的简单抽象层。
警告 No SLF4J providers were found
表示在类路径上未找到任何 SLF4J 提供者。这意味着没有配置合适的日志库实现,导致程序在运行时无法正常输出日志。
警告 Defaulting to no-operation (NOP) logger implementation
表示在缺少真正的日志库实现时,SLF4J 默认使用了一个空操作的日志记录器实现。这意味着代码在运行时不会输出任何日志。
警告 Class path contains SLF4J bindings targeting slf4j-api versions 1.7.x or earlier
表示在类路径上发现了与 SLF4J 绑定,但它们是针对 1.7.x 版本或更早版本的 slf4j-api。这可能导致不兼容或出现日志记录问题。
警告 Ignoring binding found at [jar:file:/D:/softWare/Maven/apache-maven-3.6.1/mvn_resp/ch/qos/logback/logback-classic/1.2.12/logback-classic-1.2.12.jar!/org/slf4j/impl/StaticLoggerBinder.class]
表示 SLF4J 发现了一个冲突的绑定,将被忽略。这意味着在类路径上有多个不同版本的 SLF4J 绑定或日志库实现。
解决这些警告的方法是确保正确配置了 SLF4J 绑定和日志库实现,以及它们的版本兼容性。
同时我也受到了 AI 的点拨,可以从以下几个方面排查问题:
1 2 3 4 5 1 、确保在项目的依赖管理中添加了正确的 SLF4J 绑定和日志库实现。2 、确保依赖的版本与所使用的 SLF4J 版本兼容。推荐使用 SLF4J 版本 1 .7 .x 或更高版本。3 、检查项目的构建路径,确保没有冲突的 SLF4J 绑定存在于类路径上。4 、如果问题仍然存在,可以尝试在项目中排除掉冲突的 SLF4J 绑定,或通过 Maven 或 Gradle 等构建工具来管理依赖关系。5 、如果仍然无法解决问题,可以参考 SLF4J 的文档和常见问题页面(https://www.slf4j.org/codes.html)寻找更详细的解决方案。
我检查了引入的相关依赖,发现除了 lombok 依赖外,还引入了一个 slf4j-api 依赖:
1 2 3 4 <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency >
1 2 3 4 5 <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-api</artifactId > <version > 2.0.5</version > </dependency >
这就是问题所在了,删除这个依赖即可解决问题 ,程序成功运行:(2023/10/19 早)
实现 updateTime 字段自动更新
什么意思呢?我们希望在修改完数据库表中的记录 后,该条记录对应的 uodateTime 字段 实现自动更新
实现方法很简单,在 IDEA 中,直接修改表的 updateTime 字段属性,如下:
1 updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间' ,
这样,我们更新记录 后,该记录 updateTime 字段会自动更新为最近修改时间 :(2023/11/07 晚)
实体类未序列化
我们在后端写接口时,会这样编写接收的参数:(2023/11/20 晚)
1 2 3 4 5 6 7 8 @PostMapping("/list/page/vo") public BaseResponse<Page<Picture>> listPictureByPage (@RequestBody Picture picture) throws IOException { String category = picture.getCategory(); Page<Picture> picturePage = pictureService.listPictureVOByPage(category); return ResultUtils.success(picturePage); }
1 2 om.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `memory.cloud.memoryclient.model.domain.Picture` (no Creators, like default constructor , exist): cannot deserialize from Object value (no delegate- or property-based Creator) at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]
排了一个小时的错,可算发现了:我编写的 Picture 不支持序列化,改写成如下这样即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Data public class Picture implements Serializable { public Picture () { } private String category; private String title; private String url; private static final long serialVersionUID = 1L ; }
平时这种实体类都是使用 MybatisX-Generator 快速生成的,没注意到实体类要支持序列化 (2023/11/20 晚)
存储数据库编码错误
终于解决了如何正确保存含 emoji
表情数据到数据库中的问题了
直接保存
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 String url = "https://juejin.cn/post/7313418992310976549" ; try { Document doc = Jsoup.connect(url) .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.81" ) .get(); Elements title = doc.select(".article-area .article-title" ); System.out.println("----------------博文标题----------------" ); System.out.println(title.text()); Elements content = doc.select(".article-viewer p" ); System.out.println("---------------博文正文------------------" ); byte [] contentBytes = content.toString().getBytes(StandardCharsets.UTF_8); System.out.println(contentBytes); Article article = new Article (); article.setId(Long.valueOf("7313418992310976549" )); article.setTitle(title.text()); article.setContent(content.toString()); article.setAuthorId(0L ); article.setView(0 ); article.setLikes(0 ); article.setComments("" ); article.setCollects(0 ); article.setTags("" ); String decodedContent = new String (contentBytes, StandardCharsets.UTF_8); System.out.println("-------------解码后--------------" ); System.out.println(decodedContent); } catch (IOException e) { throw new RuntimeException (e); } } article.setContent(contentBytes);
这里直接保存,会出现字符编码无法识别而转换错误,就是因为保存的数据记录中有 emoji 这样的小图标
这里我也查询了相关文章,解决这个问题,虽然最后没有解决,但仍可做参考:
🔥 推荐阅读:
1 Error updating database. Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x8D\x84 \xE5...' for column 'content' at row 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Document doc = Jsoup.connect(url) .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.81" ) .get();Elements title = doc.select(".article-area .article-title" ); System.out.println("----------------博文标题----------------" ); System.out.println(title.text());Elements content = doc.select(".article-viewer p" ); System.out.println("---------------博文正文------------------" );byte [] contentBytes = content.toString().getBytes(StandardCharsets.UTF_8);
1 2 3 String decodedContent = new String (contentBytes, StandardCharsets.UTF_8); System.out.println("-------------解码后--------------" ); System.out.println(decodedContent);
保存到数据库中的问题解决了,接下来就是保证正确从数据库中拿到数据并解码出原数据:
1 2 String contentStr = article.getContent();byte [] contentBytes = contentStr.getBytes(StandardCharsets.UTF_8);
1 2 3 4 5 6 7 8 9 String contentStrWithoutBrackets = contentStr.substring(1 , contentStr.length() - 1 ); String[] byteStrs = contentStrWithoutBrackets.split("," );byte [] contentBytes = new byte [byteStrs.length];for (int i = 0 ; i < byteStrs.length; i++) { contentBytes[i] = Byte.parseByte(byteStrs[i]); }
经过诸多尝试,仍无法正确解码
经过前面的测试发现,转码后保存 byte [] 可以解决编码错误,问题是出在保存数据库时
由于字段 content 为 text(varchar 也可以,可能会出现要保存的数据记录过长而导致溢出,就选择 text 了),所以我们在保存 byte [] 到数据库中时,是先转换成字符串再保存的
1 article.setContent(Arrays.toString(contentBytes));
而后才引发了解码失败的问题,因为对 byte [] 直接解码是可以获取原文内容 content 的,但是先转字符串存入,取出时就不好处理了
那就干脆直接保存 byte [] 到数据库中了,改变字段 content 属性为 blob:
1 2 3 4 private byte [] content;
接下来,我们选择直接保存 byte [] 到数据库中即可:
1 article.setContent(contentBytes);
这里也可以看出,将 byte [] 转字符串数组后保存和直接保存 byte [] 到数据库中的形式是很不一样的(如下图所示):
1 2 3 4 5 6 7 8 9 10 11 12 13 Article article = articleService.getById(7313418992310976549L );Long id = article.getId();String title = article.getTitle();byte [] content = article.getContent();String decodedContent = new String (content, StandardCharsets.UTF_8);Integer type = article.getType(); System.out.println(id); System.out.println(title); System.out.println(decodedContent); System.out.println(type);
至此,我们成功解决了如何正确保存数据记录到数据库中的问题,并成功解决了编码问题
汉字转拼音
1 2 3 4 Random random = new Random ();int randomPage = random.nextInt(5 ) + 1 ;String url = String.format("https://www.vcg.com/creative-image/%s/?page=%d" , category, randomPage);String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.81" ;
拼接 category
,只要是中文字符就不定时出现报错,尝试将中文转为拼音:
导入依赖:(2024/01/18 晚)
1 2 3 4 5 <dependency > <groupId > com.belerweb</groupId > <artifactId > pinyin4j</artifactId > <version > 2.5.1</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 StringBuilder output = new StringBuilder (); for (char c : name.toCharArray()) { try { String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c); if (pinyinArray != null && pinyinArray.length > 0 ) { output.append(pinyinArray[0 ]).append(" " ); } else { output.append(c).append(" " ); } } catch (Exception e) { output.append(c).append(" " ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat format = new net .sourceforge.pinyin4j.format.HanyuPinyinOutputFormat(); format.setToneType(net.sourceforge.pinyin4j.format.HanyuPinyinToneType.WITHOUT_TONE); StringBuilder output = new StringBuilder (); for (char c : name.toCharArray()) { try { String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c, format); if (pinyinArray != null && pinyinArray.length > 0 ) { output.append(pinyinArray[0 ]).append("" ); } else { output.append(c); } } catch (Exception e) { output.append(c).append("" ); } }
无法加载 Spring 的配置文件 启动测试类,发现这样的报错:(2024/02/13 早)
在 SpringBoot 中报错 java.lang.IllegalStateException: Failed to load ApplicationContext,无法加载 Spring 的配置文件
出现这种问题,无外于这几个方面:jar 包过于老旧、@Bean 注入有分歧、配置文件中的数据库连接失败、未扫描到 Mapper
1、jar 包有问题或者过老,换最新的试试。 2、 内部 Bean 配置有歧义,Spring 自身无法分辨 3、缺少某个依赖、或属性的配置 4、引用 外部属性文件的情况下,属性文件内有错误,无法加载。比如属性文件配置的数据库连接 有问题 5、在使用到 applicationContext 的 地方引用的路径不正确。
🍖 推荐阅读: java.lang.IllegalStateException: Failed to load ApplicationContext-CSDN 博客
我的问题解决了,确实是配置文件中数据库连接有问题。我这个项目配置了本地 Elasticsearch 的,启动 本地 ES 就好了
1 2 3 4 5 elasticsearch: uris: http://localhost:9200 username: root password: ******
@ControllerAdvice 拦截
2024 年 4 月 25 日
🍖 推荐阅读:@ControllerAdvice 的介绍及三种用法(转载)-CSDN 博客
在看单位的后端开发代码模板,看到了这个类:
这段代码是一个名为 ApiResultHandler 的类,它实现了 Spring 框架中的 ResponseBodyAdvice 接口。这个类的主要作用是对 RestController 的接口方法进行拦截,对返回的结果进行处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 @ControllerAdvice(annotations = {RestController.class}) public class ApiResultHandler implements ResponseBodyAdvice { private static final Class[] annos = { RequestMapping.class, GetMapping.class, PostMapping.class, DeleteMapping.class, PutMapping.class }; ............................... }
首先,定义了一个静态数组 annos,包含了 RequestMapping、GetMapping、PostMapping、DeleteMapping 和 PutMapping 这五个注解,用于判断一个方法是否使用了这些注解。
1 2 3 4 5 6 7 8 @Override public boolean supports (MethodParameter returnType, Class converterType) { AnnotatedElement element = returnType.getAnnotatedElement(); return Arrays.stream(annos).anyMatch(anno -> anno.isAnnotation() && element.isAnnotationPresent(anno)); }
重写了 supports 方法,用于判断当前拦截的方法是否使用了上述五个注解之一。如果使用了,返回 true,表示需要拦截;否则返回 false,表示不需要拦截。
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 @Override public Object beforeBodyWrite (@Nullable Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof RestfulResponse){ return body; } HttpHeaders headers = request.getHeaders(); List<String> list = headers.get(FeginClientConfig.KEEP_ORIGINAL); if (list != null && list.contains(FeginClientConfig.KEEP_ORIGINAL)) { return body; } if (body instanceof ResultCode) { return new RestfulResponse (((ResultCode) body).getCode(), ((ResultCode) body).getDesc()); } AnnotatedElement element = returnType.getAnnotatedElement(); if (element.isAnnotationPresent(SuccessfulMessage.class)) { boolean ignore = element.getAnnotation(SuccessfulMessage.class).ignore(); if (ignore) { return body; } } String msg = null ; if (element.isAnnotationPresent(SuccessfulMessage.class)) { msg = element.getAnnotation(SuccessfulMessage.class).value(); } if (msg == null ) { if (element.isAnnotationPresent(GetMapping.class)) { msg = "操作成功" ; } else if (element.isAnnotationPresent(PostMapping.class)) { msg = "操作成功" ; } else if (element.isAnnotationPresent(PutMapping.class)) { msg = "修改成功" ; } else if (element.isAnnotationPresent(DeleteMapping.class)) { msg = "删除成功" ; } else { msg = "请求成功" ; } } return RestfulResponse.success(msg, body); }
重写了 beforeBodyWrite 方法,这个方法会在 Controller 方法执行后,返回结果之前被调用。在这个方法中,对返回的结果进行了处理:
如果返回的结果已经是 RestfulResponse 类型,直接返回;
如果请求头中包含 FeginClientConfig.KEEP_ORIGINAL,表示是内部请求,不进行格式转换,直接返回;
如果返回的结果是 ResultCode 类型,将其转换为 RestfulResponse 类型并返回;
如果方法上有 SuccessfulMessage 注解,根据注解的属性决定是否忽略该次拦截;
根据方法上的注解(如 GetMapping、PostMapping 等)设置默认的成功消息;
最后,将成功消息和原始结果封装成 RestfulResponse 对象并返回。
定时任务实现
纯手写单线程循环 1 2 3 4 5 6 7 8 9 10 11 12 public static void timer1 () { new Thread (() -> { while (true ) { System.out.println("定时任务A 当前时间: " + LocalDateTime.now()); try { Thread.sleep(1000 ); } catch (InterruptedException e) { throw new RuntimeException (e); } } }).start(); }
Timer 和他的小伙伴 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void timer2 () { Timer timer = new Timer (); System.out.println("1秒后执行任务A,A完成后,等待1秒开始定时执行任务B,当前时间: " + LocalDateTime.now()); timer.schedule(new TimerTask () { @Override public void run () { System.out.println("定时任务A 当前时间: " + LocalDateTime.now()); } }, 1000 ); timer.schedule(new TimerTask () { @Override public void run () { System.out.println("定时任务B 当前时间: " + LocalDateTime.now()); } }, 1000 , 2000 ); }
ScheduledExecutorService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void timer4 () { ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); System.out.println("2秒后开始执行任务,此刻时间---" + LocalDateTime.now()); scheduledExecutorService.scheduleAtFixedRate(() -> { System.out.println("任务开始---" + LocalDateTime.now()); try { Thread.sleep(2000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务结束---" + LocalDateTime.now()); }, 2000 , 5000 , TimeUnit.MILLISECONDS); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void timer5 () { ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); System.out.println("2秒后开始执行任务,此刻时间---" + LocalDateTime.now()); scheduledExecutorService.scheduleWithFixedDelay(() -> { System.out.println("任务开始---" + LocalDateTime.now()); try { Thread.sleep(2000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务结束---" + LocalDateTime.now()); }, 2000 , 4000 , TimeUnit.MILLISECONDS); }
DelayQueue 延迟任务
DelayQueue 是 JDK 提供的 api,是一个延迟队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Slf4j public class DelayQueueDemo { public static void main (String[] args) { DelayQueue<SanYouTask> sanYouTaskDelayQueue = new DelayQueue <>(); new Thread (() -> { while (true ) { try { SanYouTask sanYouTask = sanYouTaskDelayQueue.take(); log.info("获取到延迟任务:{}" , sanYouTask.getTaskContent()); } catch (Exception e) { } } }).start(); log.info("提交延迟任务" ); sanYouTaskDelayQueue.offer(new SanYouTask ("三友的java日记5s" , 5L )); sanYouTaskDelayQueue.offer(new SanYouTask ("三友的java日记3s" , 3L )); sanYouTaskDelayQueue.offer(new SanYouTask ("三友的java日记8s" , 8L )); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Data public class SanYouTask implements Delayed { private final String taskContent; private final Long triggerTime; public SanYouTask (String taskContent, Long delayTime) { this .taskContent = taskContent; this .triggerTime = System.currentTimeMillis() + delayTime * 1000 ; } @Override public long getDelay (TimeUnit unit) { return unit.convert(triggerTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } @Override public int compareTo (Delayed o) { return this .triggerTime.compareTo(((SanYouTask) o).triggerTime); } }
getDelay
方法返回这个任务还剩多久时间可以执行 ,小于 0 的时候说明可以这个延迟任务到了执行的时间了。
compareTo
这个是对任务排序 的,保证最先到延迟时间的任务排到队列的头。
taskContent :延迟任务的具体的内容
delayTime :延迟时间,秒为单位
实现原理
🍻 offer
方法在提交任务的时候,会通过根据compareTo
的实现对任务进行排序,将最先需要被执行的任务放到队列头。
🍛take
方法获取任务的时候,会拿到队列头部的元素,也就是队列中最早需要被执行的任务,通过 getDelay 返回值判断任务是否需要被立刻执行,如果需要的话,就返回任务,如果不需要就会等待这个任务到延迟时间的剩余时间,当时间到了就会将任务返回。
Spring 提供定时任务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @EnableScheduling @Component public class Timer { @Scheduled(cron = "*/2 * * * * *") public void timer () { System.out.println("哈哈哈哈" ); } }
如果有多个定时任务类,可以考虑把@EnableScheduling 注解添加在启动类上
Cron 表达式
简单地介绍下如何使用吧:(2023/08/25 早)
代码合并
2024 年 5 月 22 日
今天学习:在团队协同开发工作中,如何使用 IDEA 整合的 git 功能实现本地代码提交至远程仓库 。
选择要提交的文件:
编写完毕 commit 信息后,选择好分支,拉取远程仓库最新代码:
直接 push 即可:
IDEA 连接 MySQL 失败
2024 年 5 月 29 日
经过排查,发现是从 maven 下载 MySQL 驱动失败了,
只好是手动指定本地 MySQL 驱动路径,直接在本机的 maven 目录下找就可以,我的在这:
1 D :\softWare\Maven\apache-maven-3 .6 .1 \mvn_resp\mysql
设置下时区,有两种设置方法:
解决:Server returns invalid timezone. Need to set ‘serverTimezone‘ property._server returns invalid timezone. need to set ‘serv-CSDN 博客
确实解决了问题:
1 2 3 4 5 6 DBMS : MySQL (ver. 5 .7 .19 -log)Case sensitivity: plain=lower, delimited=lowerDriver : MySQL Connector/J (ver. mysql-connector-java-8 .0 .21 (Revision: 33 f65445a1bcc544eb0120491926484da168f199), JDBC4.2 )Ping : 33 msSSL : no
但是连接上了 MySQL,却不显示数据库,显示为空是什么情况。
参考:【IDEA 连接 MySQL 服务后不显示本地数据库?——这样做!】_idea 的数据库内容没办法显示-CSDN 博客
那是因为我连接数据库时没有选定具体数据库,默认只有系统自带的。
可以如上图一样勾选,也可以在连接数据库时的配置中填充,就不会出现这样的问题:
Swagger 文档
2024 年 5 月 29 日
1 2 3 4 5 6 spring: server: address: localhost port: 8101 servlet: context-path: /api
在项目启动入口如此编写:统计启动时间,读取配置获取 ip、端口号和路径 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @SpringBootApplication(exclude = {RedisAutoConfiguration.class}) @MapperScan("com.yupi.springbootinit.mapper") @EnableScheduling @EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true) @Slf4j public class MainApplication { public static void main (String[] args) { ConfigurableApplicationContext ctx = SpringApplication.run(MainApplication.class, args); long start = System.currentTimeMillis(); long time = (System.currentTimeMillis() - start) / 1000 ; String info = "启动完成,耗时%d秒,swagger访问链接:http://%s:%s%s/doc.html" ; String address = ctx.getEnvironment().getProperty("server.address" ); String port = ctx.getEnvironment().getProperty("server.port" ); String path = ctx.getEnvironment().getProperty("server.servlet.context-path" ); log.info(String.format(info, time, address, port, path)); } }
这样启动项目后,很便捷地就能访问到 swagger 接口文档了:
2024 年 8 月 31 日
这个错误是由于Spring Data Redis在尝试从Redis中反序列化存储的对象时遇到了问题。具体来说,错误提示中提到了java.lang.ClassNotFoundException: com.memory.spring_0829.model.entity.User
,这意味着在尝试加载并实例化User
类时,JVM在类路径中找不到这个类。
这个问题通常有几个可能的原因:
类路径不一致 :存储到Redis中的对象是在一个类路径配置下序列化的,但是在尝试反序列化时,应用的类路径配置可能已经改变,导致JVM无法找到User
类。
部署问题 :如果你是在开发环境和生产环境之间迁移应用,确保所有必要的类和资源都正确地打包并部署到了生产环境。
序列化/反序列化配置 :你可能在序列化对象时使用了Java的默认序列化机制(如JdkSerializationRedisSerializer
),但是User
类可能不再存在于反序列化时的环境中,或者类的定义已经改变(比如类的包名或路径被修改了)。
YAML 格式有误
2024 年 6 月 1 日
今天上午遇到的报错:
1 2 09 :56 :32.110 [restartedMain] ERROR org.springframework .boot .SpringApplication - Application run failed org.yaml .snakeyaml .error .YAMLException : java.nio .charset .MalformedInputException : Input length = 1
这个错误日志显示你的 Spring Boot 应用程序在尝试启动时遇到了问题,特别是在解析 YAML 配置文件时。错误 org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 1
通常意味着 YAML 解析器在读取配置文件时遇到了字符编码问题。
具体来说,MalformedInputException
指的是输入数据(在这里是 YAML 配置文件)与期望的字符编码格式不匹配。这可能是因为:
YAML 文件可能包含无法用当前字符集(通常是 UTF-8)解码的字符。
YAML 文件可能已被保存为不同的字符编码格式(如 ISO-8859-1 或其他)。
在读取或处理 YAML 文件时,可能指定了错误的字符编码。
最后发现是因为不小心缩进了一下 application.yaml 配置文件,改回来就好了。
Spring Cloud 依赖版本
2024 年 6 月 7 日
🍖 推荐阅读:版本发布说明 | https://sca.aliyun.com
Nacos 未配置
2024 年 6 月 7 日
这个问题真的恶心,一直提醒我 yaml 配置文件有误,也不知道是格式有误还是少配置了些东西:
我把配置文件删得只剩端口号了,它才提示我检测到 nacos 依赖,却没有扫描到相关配置。
1 2 3 Add a spring.config.import =nacos: property to your configuration. If configuration is not required add spring.config.import =optional:nacos: instead. To disable this check, set spring.cloud.nacos.config.import-check.enabled =false .
要么配置不检测 nacos 相关配置,要么显示配置 nacos。
妈的就不能直接提示么。
还有一个问题,坑死我了。
yaml 配置文件里最好不要写中文注释,否则也会不停地提示你配置文件格式有误,你还找不到哪里有问题。
找不着主类
2024 年 6 月 7 日
怪了,我好像也没怎么改配置吧,突然就行了:
依赖配置心得
2024 年 6 月 7 日
有些公共依赖不要搞在父项目中,比如 Swagger、MybatisPlus 这些
微服务项目一定要注意版本兼容,Spring Boot,Spring Cloud Alibaba,Spring Cloud 这些。
日志输出
2024 年 6 月 7 日
1 2 3 4 5 6 7 logging: level: root: INFO org: springframework: INFO springframework.web: INFO springframework.boot.autoconfigure: WARN
搞这么个配置,启动项目后的日志打印就清晰多了:
编码格式
2024 年 8 月 6 日
配置文件里有中文字符出现编码格式错误导致项目起不来,这报错我受够了,今天解决:
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 17 :15 :56.384 [Thread-0] DEBUG org.springframework .boot .devtools .restart .classloader .RestartClassLoader - Created RestartClassLoader org.springframework .boot .devtools .restart .classloader .RestartClassLoader@47 e2d5ed17 :15 :56.715 [restartedMain] ERROR org.springframework .boot .SpringApplication - Application run failed org.yaml .snakeyaml .error .YAMLException : java.nio .charset .MalformedInputException : Input length = 1 at org.yaml .snakeyaml .reader .StreamReader .update (StreamReader.java :218 ) at org.yaml .snakeyaml .reader .StreamReader .ensureEnoughData (StreamReader.java :176 ) at org.yaml .snakeyaml .reader .StreamReader .ensureEnoughData (StreamReader.java :171 ) at org.yaml .snakeyaml .reader .StreamReader .peek (StreamReader.java :126 ) at org.yaml .snakeyaml .scanner .ScannerImpl .scanToNextToken (ScannerImpl.java :1218 ) at org.yaml .snakeyaml .scanner .ScannerImpl .fetchMoreTokens (ScannerImpl.java :329 ) at org.yaml .snakeyaml .scanner .ScannerImpl .checkToken (ScannerImpl.java :251 ) at org.yaml .snakeyaml .parser .ParserImpl$ParseImplicitDocumentStart .produce (ParserImpl.java :214 ) at org.yaml .snakeyaml .parser .ParserImpl .peekEvent (ParserImpl.java :166 ) at org.yaml .snakeyaml .parser .ParserImpl .checkEvent (ParserImpl.java :156 ) at org.yaml .snakeyaml .composer .Composer .checkNode (Composer.java :93 ) at org.yaml .snakeyaml .constructor .BaseConstructor .checkData (BaseConstructor.java :124 ) at org.yaml .snakeyaml .Yaml$1 .hasNext (Yaml.java :509 ) at org.springframework .beans .factory .config .YamlProcessor .process (YamlProcessor.java :198 ) at org.springframework .beans .factory .config .YamlProcessor .process (YamlProcessor.java :166 ) at org.springframework .boot .env .OriginTrackedYamlLoader .load (OriginTrackedYamlLoader.java :84 ) at org.springframework .boot .env .YamlPropertySourceLoader .load (YamlPropertySourceLoader.java :50 ) at org.springframework .boot .context .config .StandardConfigDataLoader .load (StandardConfigDataLoader.java :54 ) at org.springframework .boot .context .config .StandardConfigDataLoader .load (StandardConfigDataLoader.java :36 ) at org.springframework .boot .context .config .ConfigDataLoaders .load (ConfigDataLoaders.java :107 ) at org.springframework .boot .context .config .ConfigDataImporter .load (ConfigDataImporter.java :128 ) .............................................
原来是文件编码 File Encodings 下没有覆盖到 resources 路径下的文件,所以每次修改编码格式都不起作用的。
2024 年 9 月 25 日
今天总算真正解决了,先来看看 ChatGPT 的回答:
这个错误 java.nio.charset.MalformedInputException: Input length = 1
是因为你的 YAML 文件中的编码格式不匹配问题,通常是由于文件中的中文字符未使用正确的编码格式导致的。
解决步骤:
确认 YAML 文件的编码
首先,确保你的 YAML 文件是使用 UTF-8 编码保存的,因为 Spring Boot 默认使用 UTF-8 来解析配置文件。常见的编辑器中,可以检查并修改文件的编码。
在 IntelliJ IDEA 中:点击 File -> File Encoding
,选择 UTF-8
。
在 VSCode 中:点击右下角的编码格式区域,选择 Save with Encoding -> UTF-8
。
设置 JVM 文件编码参数
你可以通过设置 JVM 的启动参数,明确指定使用 UTF-8 作为默认编码。可以在 Spring Boot 应用的启动配置中添加以下 JVM 参数:
1 2 3 4 5 bash 复制代码 -Dfile.encoding =UTF-8
对于 Maven 项目,你可以在 pom.xml
中的 maven-surefire-plugin
配置中添加:
1 2 3 4 5 6 7 xml复制代码<plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-surefire-plugin</artifactId > <configuration > <argLine > -Dfile.encoding=UTF-8</argLine > </configuration > </plugin >
哟,这方法还挺好用的,已生效。
如果你使用 Gradle,添加如下内容到 build.gradle
:
1 2 3 groovy复制代码tasks.withType (JavaExec ) { jvmArgs = ['-Dfile.encoding=UTF-8' ] }
检查 Spring Boot 配置文件读取编码
如果你已经确认文件是 UTF-8 编码,但是仍然出现问题,可以在 application.properties
或 application.yml
中明确指定文件的读取编码:
1 2 3 4 5 properties 复制代码 spring.messages.encoding =UTF-8
或者在 application.yml
中:
1 2 3 yaml复制代码spring : messages : encoding : UTF-8
校验 YAML 文件内容
有时候,YAML 文件可能包含不可见的特殊字符(如 BOM 标记),这些字符可能会引起解析问题。可以使用文本编辑器中的显示不可见字符的功能,检查 YAML 文件头部是否有特殊字符存在。如果有,手动删除。
检查是否有非 UTF-8 字符
如果 YAML 文件中包含某些特殊字符或非 UTF-8 编码的字符(如中文乱码),可以手动确认或使用工具将其转为 UTF-8 编码格式。
重新加载项目
完成上述步骤后,重新启动项目,并确保配置文件被正确解析。
总结:
问题通常出在 YAML 文件的编码格式不正确。通过确认文件编码为 UTF-8、设置 JVM 启动参数、或在 Spring Boot 配置中指定编码,通常可以解决此问题。如果还是有问题,建议仔细检查文件中是否有不可见的特殊字符。
MySQL 连接失败
2024 年 8 月 14 日
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void init () { Properties params = new Properties (); String configFile = "src/main/resources/database.properties" ; try { FileInputStream in = new FileInputStream (configFile); params.load(in); } catch (IOException e) { e.printStackTrace(); } driver = params.getProperty("driver" ); url = params.getProperty("url" ); user = params.getProperty("user" ); password = params.getProperty("password" ); }
1 2 3 4 5 6 7 8 9 Hello world! java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader .loadClass(ClassLoaders.java:178) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522) at java.base/java.lang.Class.forName0(Native Method) at java.base/java.lang.Class.forName(Class.java:315) at com.memory.util.BaseDao.getConnection(BaseDao.java:50) at com.memory.Main.main(Main.java:20)
搭建 Spring
2024 年 8 月 19 日
如何搭建一个 Spring 项目?推荐阅读:【spring基础】如何搭建一个spring项目?-CSDN博客
今晚,我学会了如何搭建 Spring 项目,以及三种注入 Bean 的方式:基于 XML 文件,基于 XML 文件 + 注解,完全基于注解 。
代码已上传至:shixun_life: 实训生活,记录我的成长 (gitee.com)
基于 XML:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class UserDaoImpl1 implements UserDao { @Override public void sayHello () { System.out.println("你好啊!小一!" ); } @Override public void sayHi () { System.out.println("嗨嗨!小一!" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class UserServiceImpl implements UserService { @Autowired private UserDaoImpl1 userDaoImpl1; @Override public void sayHello () { userDaoImpl1.sayHello(); } public void setUserDaoImpl1 (UserDaoImpl1 userDaoImpl1) { this .userDaoImpl1 = userDaoImpl1; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd" > <bean class ="com.memory.dao.impl.UserDaoImpl1" name ="userDaoImpl1" > </bean > <bean class ="com.memory.dao.impl.UserDaoImpl2" name ="userDaoImpl2" > </bean > <bean class ="com.memory.service.impl.UserServiceImpl" name ="userService" > <property name ="userDaoImpl1" ref ="userDaoImpl1" > </property > </bean > </beans >
1 2 3 4 5 6 7 @Test public void classPathXmlTest () { ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext ("spring.xml" ); UserService bean = ioc.getBean(UserServiceImpl.class); bean.sayHello(); }
基于 XML 文件 + 注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Component public class UserDaoImpl1 implements UserDao { @Override public void sayHello () { System.out.println("你好啊!小一!" ); } @Override public void sayHi () { System.out.println("嗨嗨!小一!" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component public class UserServiceImpl implements UserService { @Autowired private UserDaoImpl1 userDaoImpl1; @Override public void sayHello () { userDaoImpl1.sayHello(); } public void setUserDaoImpl1 (UserDaoImpl1 userDaoImpl1) { this .userDaoImpl1 = userDaoImpl1; } }
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd" > <context:component-scan base-package ="com.memory" > </context:component-scan > </beans >
基于注解:
1 2 3 4 @Configuration @ComponentScan("com.memory") public class SpringConfig { }
1 2 3 4 5 6 7 8 @Test public void annotationConfigTest () { AnnotationConfigApplicationContext ioc = new AnnotationConfigApplicationContext (SpringConfig.class); UserServiceImpl bean = ioc.getBean(UserServiceImpl.class); bean.sayHello(); }
JWT 实践
2024 年 8 月 19 日
推荐阅读:什么是 JWT – JSON WEB TOKEN_jsonwebtoken-CSDN博客
这是昨天晚上看仿 B 站视频时,学习到的 JWT 在实际业务中的使用方式:
1、用户登录,以 uid 和 role 为键缓存用户信息;生成 token,以 uid 和 role 为键缓存 token 信息;返回 token 值给前端。
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 @Override public CustomResponse login (String username, String password) { CustomResponse customResponse = new CustomResponse (); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken (username, password); Authentication authenticate; try { authenticate = authenticationProvider.authenticate(authenticationToken); } catch (Exception e) { customResponse.setCode(403 ); customResponse.setMessage("账号或密码不正确" ); return customResponse; } UserDetailsImpl loginUser = (UserDetailsImpl) authenticate.getPrincipal(); User user = loginUser.getUser(); redisUtil.setExObjectValue("user:" + user.getUid(), user); if (user.getState() == 1 ) { customResponse.setCode(403 ); customResponse.setMessage("账号异常,封禁中" ); return customResponse; } String token = jwtUtil.createToken(user.getUid().toString(), "user" ); final_map.put("token" , token); final_map.put("user" , userDTO); customResponse.setMessage("登录成功" ); customResponse.setData(final_map); return customResponse; }
2、根据加密主体,加密算法和密钥,生成 Token,设置其他属性并封装载荷(即声明)。
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 public String createToken (String uid, String role) { String uuid = getUUID(); SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = getTokenSecret(); long nowMillis = System.currentTimeMillis(); Date now = new Date (nowMillis); long expMillis = nowMillis + JwtUtil.JWT_TTL; Date expDate = new Date (expMillis); String token = Jwts.builder() .setId(uuid) .setSubject(uid) .claim("role" , role) .setIssuer("https://api.teriteri.fun" ) .setAudience("https://www.teriteri.fun" ) .signWith(secretKey, signatureAlgorithm) .setIssuedAt(now) .setExpiration(expDate) .compact(); try { redisUtil.setExValue("token:" + role + ":" + uid, token, JwtUtil.JWT_TTL, TimeUnit.MILLISECONDS); } catch (Exception e) { log.error("存储redis数据异常" , e); } return token; }
3、生成 Token 密钥:
1 2 3 4 5 6 7 8 public static SecretKey getTokenSecret () { byte [] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); return new SecretKeySpec (encodeKey, 0 , encodeKey.length, "HmacSHA256" ); }
4、校验 Token,由传入的 Token 获取 Claim 信息,再根据 Claim 中的 uid 和 role 获取缓存的 Token,比较是否相同。
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 public boolean verifyToken (String token) { Claims claims = getAllClaimsFromToken(token); if (null == claims) { return false ; } String uid = claims.getSubject(); String role; if (claims.containsKey("role" )) { role = claims.get("role" ).toString(); } else { role = "" ; } String cacheToken; try { cacheToken = String.valueOf(redisUtil.getValue("token:" + role + ":" + uid)); } catch (Exception e) { cacheToken = null ; log.error("获取不到缓存的token" , e); } return StringUtils.equals(token, cacheToken); }
5、根据密钥和 Token 值即可获取所有 Claim 信息。
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 public static Claims getAllClaimsFromToken (String token) { if (StringUtils.isEmpty(token)) { return null ; } Claims claims; try { claims = Jwts.parserBuilder() .setSigningKey(getTokenSecret()) .build() .parseClaimsJws(token) .getBody(); } catch (ExpiredJwtException eje) { claims = null ; } catch (Exception e) { claims = null ; } return claims; }
6、Token 验证失败,则说明 Token 已过期需重新登录;根据 uid 和 role 查询到的用户信息不匹配,则会登录失败。
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 @Override protected void doFilterInternal (HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader("Authorization" ); if (!StringUtils.hasText(token) || !token.startsWith("Bearer " )) { filterChain.doFilter(request, response); return ; } token = token.substring(7 ); boolean verifyToken = jwtUtil.verifyToken(token); if (!verifyToken) { response.addHeader("message" , "not login" ); response.setStatus(403 ); return ; } String userId = JwtUtil.getSubjectFromToken(token); String role = JwtUtil.getClaimFromToken(token, "role" ); User user = redisUtil.getObject("security:" + role + ":" + userId, User.class); if (user == null ) { response.addHeader("message" , "not login" ); response.setStatus(403 ); return ; } UserDetailsImpl loginUser = new UserDetailsImpl (user); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken (loginUser, null , null ); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request, response); }
8、总而言之,使用 JWT 是在校验用户身份前,先校验是否携带了正确的 Token,再校验用户身份是否存在。
构建 Spring Boot 失败
2024 年 8 月 20 日
Initialization failed for ‘https:start.spring.io’ Please check URL, network and proxy settings. - 小洋ssr - 博客园 (cnblogs.com)
我也不知道怎么搞的,总之把那个 Server URL 改成阿里云的以后又改回来,莫名其妙就行了。
Spring Boot 初始框架
2024 年 8 月 20 日
今晚完善了自己的 Spring Boot 初始框架:memory-springboot-init: Spring Boot 初始框架 (gitee.com)
以后会慢慢学习和完善。
我想要做一个练习:前后端不分离与前后端分离的Java Web开发对比介绍_java 前后端不分离-CSDN博客
快速地构建一个 Spring Boot 项目,确保导入以下依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency >
做好相关配置 appication.xml:
1 2 3 4 5 6 7 spring: application: name: spring-demo server: port: 8080 thymeleaf: cache: false
新增 Controller 层,处理 Web 请求:
1 2 3 4 5 6 7 8 @Controller public class HelloController { @GetMapping("/hello") public String sayHello (Model model) { model.addAttribute("message" , "Hello, World!" ); return "hello" ; } }
新增 hello.html:
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <h1 th:text ="${message}" > </h1 > </body > </html >
最终项目结构:
启动项目,访问:Title ,得到以下结果:
二维码生成
2024 年 8 月 27 日
使用JAVA代码实现生成二维码_java 生成二维码-CSDN博客
导入依赖:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > com.google.zxing</groupId > <artifactId > core</artifactId > <version > 3.4.1</version > </dependency > <dependency > <groupId > com.google.zxing</groupId > <artifactId > javase</artifactId > <version > 3.4.1</version > </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 public class QRCodeGenerator { public static void generateQRCodeImage (String text, int width, int height, String filePath) { try { QRCodeWriter qrCodeWriter = new QRCodeWriter (); Map<EncodeHintType, Object> hints = new HashMap <>(); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height, hints); BufferedImage bufferedImage = new BufferedImage (width, height, BufferedImage.TYPE_INT_RGB); bufferedImage.createGraphics(); Graphics2D graphics = (Graphics2D) bufferedImage.getGraphics(); graphics.setColor(java.awt.Color.WHITE); graphics.fillRect(0 , 0 , width, height); graphics.setColor(java.awt.Color.BLACK); for (int i = 0 ; i < width; i++) { for (int j = 0 ; j < height; j++) { if (bitMatrix.get(i, j)) { graphics.fillRect(i, j, 1 , 1 ); } } } ImageIO.write(bufferedImage, "png" , new File (filePath)); } catch (WriterException | IOException e) { e.printStackTrace(); } } }
新增一个 Controller 控制器处理请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Slf4j @RestController @RequestMapping("/api/common") public class CommonController { @Value("${backend.path}") private String path; @GetMapping("/QRCode") public String save () { try { String QR_CODE_IMAGE_PATH = path + "MyQRCode.png" ; String QR_CODE_TEXT = "https://baidu.com" ; QRCodeGenerator.generateQRCodeImage(QR_CODE_TEXT, 350 , 350 , QR_CODE_IMAGE_PATH); return "创建成功" ; } catch (Exception e) { log.error(e.toString()); return "接口异常" ; } } }
打开 Postman 或者 IDEA 自带的 http 请求调试工具,构建请求并发送:
如上,请求处理完成,成功生成二维码:
手机扫码,即可跳转至百度首页:
EasyExcel
2024 年 9 月 2 日
alibaba/easyexcel: 快速、简洁、解决大文件内存溢出的java处理Excel工具 (github.com)
读Excel | Easy Excel 官网 (alibaba.com)
一文带你彻底玩转EasyExcel:导入导出excel数据起飞_easyexcel 导入-CSDN博客
我十分看好这个博客,介绍得相当清晰条理,介绍了 Excel 提供的监听器、注解、数据导入导出以及格式转换等内容,很值得学习。
2024 年 9 月 5 日
历时三天,总算有时间能聊聊如何使用 EasyExcel实现表格的导入导出功能,Demo 代码已经写完了,顺便把曹老板也教会了。
导出功能实现,Controller 层新增接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @PostMapping("/excel/write") public BaseResponse<Boolean> writeExcel (@RequestParam String path) { ThrowUtils.throwIf(StringUtils.isAnyBlank(path), ErrorCode.PARAMS_ERROR); Boolean result = userService.writeExcel(path); return ResultUtils.success(result); }
导出表格,业务逻辑:
1 2 3 4 5 6 Boolean writeExcel (String path) ;
1 2 3 4 5 6 7 8 9 10 11 12 @Override public Boolean writeExcel (String path) { String fullPath = DOWNLOAD_PATH + USER_TABLE_NAME; this .isExist(fullPath); List<UserVO> userList = this .listUserByPage(); EasyExcel.write(fullPath, User.class).sheet(USER_SHEET_NAME).doWrite(userList); return true ; }
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 public void isExist (String fullPath) { File file = new File (fullPath); if (file.exists()) { boolean isDeleted = file.delete(); if (!isDeleted) { throw new RuntimeException ("删除文件失败: " + fullPath); } System.out.println("文件已成功删除: " + fullPath); } File parentDir = file.getParentFile(); if (!parentDir.exists() && !parentDir.mkdirs()) { throw new RuntimeException ("创建父级目录失败: " + parentDir.getAbsolutePath()); } if (parentDir.exists()) { System.out.println("父级目录已存在或已成功创建: " + parentDir.getAbsolutePath()); } }
有个巨坑,常量 doc/ownload/user.xlsx
前面多写了个/
,变成 /doc/ownload/user.xlsx
后问题百出。。
这里支持用户选择导出文件的路径,导出文件名已经在配置文件中做好配置:
1 2 3 4 5 @Value("${file.path.userExcelTable}") String userExcelTable = "" ;
1 2 3 file: path: userExcelTable: "/user.xlsx"
再配置实体类,标记实体类和 Excel 表格列名的关系:
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 @TableName(value ="user") @Data public class User implements Serializable { @TableId(type = IdType.ASSIGN_ID) @ExcelProperty("id") private Long id; @ExcelProperty("登录名") private String userAccount; @ExcelProperty("角色名称") private String userRole; ............................................. @TableLogic @ExcelIgnore private Integer isDeleted; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @ExcelIgnore private Date createTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @ExcelIgnore private Date updateTime; @TableField(exist = false) private static final long serialVersionUID = 1L ; }
Controoler 层接受 @RequestParam 传参,尝试发送请求:
1 2 POST http://localhost:8101/api/user/excel/write?path=doc Content-Type: application/x-www-form-urlencoded
显然,导出表是成功的:
在指定导出目录下,可以找到已经成功导出的文件:
Postman
2024 年 9 月 5 日
下午尝试用 Postman 测试导入 Excel 文件功能时,却发现连基本的请求都发不出去”:
特么竟然是请求 url 不规范,就不应该在前面加什么 http://,已解决,太坑了。
导入功能实现,Controller 层新增接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @PostMapping("/excel/read") public BaseResponse<Boolean> readExcel (@RequestPart("file") MultipartFile multipartFile) { ThrowUtils.throwIf(multipartFile == null , ErrorCode.PARAMS_ERROR); Boolean result = userService.readExcel(multipartFile); return ResultUtils.success(result); }
注意接收参数,待会儿使用 Postman 测试接口时要注意,这个问题在上半年实习那会儿,导入导出表格的时候学到了。
导入表格的业务逻辑:
1 2 3 4 5 6 7 Boolean readExcel (MultipartFile multipartFile) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public Boolean readExcel (MultipartFile multipartFile) { InputStream inputStream; try { inputStream = multipartFile.getInputStream(); } catch (IOException e) { throw new RuntimeException (e); } EasyExcel.read(inputStream, User.class, new UserExcelListener ()).sheet().doRead(); boolean saveBatch = this .saveBatch(CACHED_DATA_LIST); ThrowUtils.throwIf(!saveBatch, ErrorCode.PARAMS_ERROR); return true ; }
执行读取的时候,当然需要有相应的监听器,做表格数据到实体类之间的转换:
实体类字段上的注释,还是这样的:
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 @TableName(value ="user") @Data public class User implements Serializable { @TableId(type = IdType.ASSIGN_ID) @ExcelProperty("id") private Long id; @ExcelProperty("登录名") private String userAccount; @ExcelProperty("角色名称") private String userRole; ............................................. @TableLogic @ExcelIgnore private Integer isDeleted; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @ExcelIgnore private Date createTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @ExcelIgnore private Date updateTime; @TableField(exist = false) private static final long serialVersionUID = 1L ; }
监听器:
1 2 3 4 5 6 7 8 9 private static final int BATCH_COUNT = 100 ;public static List<User> CACHED_DATA_LIST = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Override public void invoke (User user, AnalysisContext context) { log.info("解析到一条数据:{}" , user); CACHED_DATA_LIST.add(user); if (CACHED_DATA_LIST.size() >= BATCH_COUNT) { boolean saveBatch = userService.saveBatch(CACHED_DATA_LIST); ThrowUtils.throwIf(!saveBatch, ErrorCode.PARAMS_ERROR); CACHED_DATA_LIST.clear(); } }
这里仅仅把最核心的处理逻辑列出来,详细的监听器代码已上传。
CACHED_DATA_LIST
就是在监听器中处理表格中的数据后,收集到的用户信息集合,在业务代码中直接选择入库就可以。
顺便更新了 ExcelConstant 常量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public interface ExcelConstant { String DOWNLOAD_PATH = "doc/download/" ; String USER_TABLE_NAME = "user.xlsx" ; String USER_SHEET_NAME = "用户信息" ; }
然后用 Postman 这样发送请求,上传准备好的表格文件:
当然,数据导入是很成功的:
代码生成器
2024 年 9 月 7 日
这属于是给自己挖的坑了,实习期间只是稍微了解使用了下部门内部的代码生成器,结果简历上写了自己主导并参与开发,赶紧补救下:
SpringBoot日常:整合属于自己的代码生成器_springboot 代码生成-CSDN博客
代码生成器 | MyBatis-Plus (baomidou.com)
好好学一学咯。
消息状态
2024 年 9 月 7 日
简历上写道:设计人脉圈消息列表展示并标记消息状态(已读,未读),这个消息状态标记是怎么实现的呢,今晚借助 AI 硬生学会了。
我也不废话,直接给出精彩的思路,将来有时间了一定会付诸实践的:
问 :
在设计人脉圈消息列表时,我会将消息状态(如已读、未读、待处理等)作为消息实体的一个字段来存储。
在查询消息列表时我会根据这个字段的值来过滤和展示不同的消息状态。此外我还会在数据库中为消息状态设置索引以加快查询速度。
这样的功能怎么实现?
消息本身包含了发送人信息,接收人信息,状态等等,如何正确处理消息的已读状态?
就是说人脉圈里的消息大家都能看见,怎么样才能维护到每个用户看到的未读信息呢?
回答 :
实现人脉圈消息系统中消息状态的正确管理,特别是已读状态的维护,需要综合考虑数据存储、查询效率以及用户体验。以下是一个详细的实现方案:
1. 数据库设计
首先,确保你的消息表(假设为messages
)包含以下关键字段:
id
:消息的唯一标识符。
sender_id
:发送人的ID。
content
:消息内容。
status
:消息状态,如unread
(未读)、read
(已读)、pending
(待处理)等。
timestamp
:消息发送的时间戳。
此外,为了高效处理消息的已读状态,可以引入一个额外的表(如message_reads
)来记录哪些用户已经阅读了哪些消息。这个表可能包含以下字段:
user_id
:阅读消息的用户的ID。
message_id
:被阅读的消息的ID。
read_timestamp
:用户阅读消息的时间戳(可选,用于更细粒度的分析)。
2. 已读状态的处理逻辑
2.1 发送消息
当一条新消息被发送时,它默认被标记为unread
状态,并存储在messages
表中。此时,message_reads
表中不会有关于这条消息的任何记录,因为它还没有被任何人阅读。
2.2 读取消息
当某个用户打开消息列表或查看特定消息时,系统需要执行以下操作:
2.3 更新消息状态(可选)
在某些情况下,你可能想要在messages
表中直接更新消息的status
字段,以便快速过滤出所有未读消息。然而,这种方法在多人可见的人脉圈中可能不适用,因为你需要为每个用户维护独立的消息状态。不过,如果你只是想要快速获取所有用户的总体未读消息数,可以在messages
表中维护一个总未读计数,并在用户阅读消息时更新这个计数(但这通常不是最佳实践,因为它需要处理并发更新和回滚等复杂问题)。
3. 性能优化
为messages
表和message_reads
表的关键字段(如user_id
、message_id
)设置索引,以加快查询速度。
使用缓存来存储用户最近的未读消息列表,以减少数据库查询次数。
考虑使用消息队列或事件驱动架构来处理消息状态的更新,以提高系统的响应性和可扩展性。
4. 用户体验
在UI层面,清晰地区分已读和未读消息,如使用不同的颜色或图标。
提供“标记为已读”的功能,允许用户一键将所有未读消息标记为已读。
在用户进入消息列表时,自动加载并更新未读消息的状态。
通过上述方案,你可以有效地实现人脉圈消息系统中的已读状态管理,同时保证系统的性能和用户体验。
问 :
我大概明白了你的意思,但还是有漏洞的。
如果用户读到了消息,怎么处理才能保证当前读到消息的用户,他看到的消息变成已读状态了,而不影响其他用户看到消息的状态,总不能直接修改 message 的 status 值吧。
答 :
您提出的问题非常关键,确实需要确保在用户阅读消息时,仅将该消息对于当前用户的状态标记为已读,而不影响其他用户对该消息的状态。在之前描述的方案中,我们并没有直接修改messages
表中的status
字段来标记消息为已读,而是依赖于message_reads
表来跟踪每个用户阅读消息的情况。
这里是一个更详细的处理流程,以确保只有当前用户看到的消息被标记为已读:
用户请求消息列表:
当用户请求查看消息列表时,系统从messages
表中检索出所有与该用户相关的消息(可能是通过某种关联表或字段来确定哪些消息是“与该用户相关的”,比如消息是发送给该用户的,或者该用户是消息所属人脉圈的一员)。
对于每一条检索出的消息,系统同时查询message_reads
表,以确定该用户是否已经阅读过这条消息。
根据查询结果,系统构建消息列表,其中每条消息的显示状态(如未读、已读)根据message_reads
表中的记录来确定。
用户阅读特定消息:
当用户点击或以其他方式选择阅读某条消息时,系统执行以下操作:
首先,检查message_reads
表,看是否已经存在该用户阅读该消息的记录。
如果不存在记录,说明该消息对于当前用户来说是未读的,系统应在message_reads
表中插入一条新记录,记录该用户阅读了哪条消息以及阅读的时间。
插入记录后,无需修改messages
表中的status
字段,因为该字段可能用于表示消息在系统中的整体状态(如是否已被删除、是否等待审核等),而不是针对单个用户的阅读状态。
用户的阅读行为仅影响message_reads
表,因此不会影响其他用户对该消息的阅读状态。
更新UI:
在用户阅读消息后,系统应更新用户界面,以反映该消息对于当前用户来说已经是已读状态。这通常涉及到修改前端页面上的消息显示状态,比如将未读消息的标记(如红点或粗体)移除。
考虑并发和性能:
在高并发的场景下,需要确保对message_reads
表的读写操作是原子性的,以避免数据不一致的问题。这可以通过使用数据库事务、乐观锁或悲观锁等机制来实现。
同时,为了提高性能,可以考虑使用缓存来存储用户最近的阅读记录,减少对数据库的查询次数。但是,缓存的使用需要谨慎处理缓存失效和更新的问题,以确保数据的准确性。
通过上述流程,我们可以确保在用户阅读消息时,仅将该消息对于当前用户的状态标记为已读,而不影响其他用户对该消息的状态。
问 :
高并发状态下确实需要保证原子性啊,所有用户都在查找维护更新自己的消息状态,那这块儿内容又该怎么实现呢?
答 :
在高并发的场景下,确保对message_reads
表的读写操作是原子性的,以防止数据不一致,是一个非常重要的任务。这里,我将详细解释如何通过数据库事务、乐观锁和悲观锁等机制来实现这一目标。
1. 数据库事务
数据库事务是一种机制,它确保数据库从一个一致的状态转变到另一个一致的状态。事务具有四个关键属性,即ACID特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
原子性 :事务是数据库操作的最小工作单位,一个事务中的所有操作要么全部完成,要么全部不完成,不可分割。
隔离性 :事务之间的操作是相互隔离的,即并发执行的事务之间不会互相干扰。
在message_reads
表的场景中,每当用户阅读一条消息时,可以将插入记录到message_reads
表的操作作为一个事务来处理。这样,即使在高并发的环境下,也能保证操作的原子性,从而避免数据不一致的问题。
2. 乐观锁
乐观锁是一种用于在并发控制中避免数据更新冲突的技术。它假设多用户并发的事务在处理时不会互相干扰,只有在更新数据时才会检查是否存在冲突。
在message_reads
表中使用乐观锁,通常的做法是在表中添加一个版本号(version)或时间戳(timestamp)字段。每次读取记录时,都获取该字段的值。在更新记录时,检查版本号或时间戳是否发生了变化(即检查在读取记录之后是否有其他事务修改了该记录)。如果没有变化,则执行更新操作,并将版本号或时间戳加一;如果发生了变化,则说明存在冲突,此时可以根据业务需求选择重试或报错。
3. 悲观锁
与乐观锁不同,悲观锁假定在数据处理过程中最坏的情况,即数据在处理过程中很可能被其他事务并发修改。因此,悲观锁会在数据处理开始时就对数据加锁,以防止其他事务对数据进行修改。
在数据库层面,悲观锁通常通过SQL语句中的锁定语句(如SELECT ... FOR UPDATE
)来实现。当执行这类语句时,数据库会对被选中的数据进行加锁,直到当前事务结束(提交或回滚)。
在message_reads
表的场景中,如果需要保证极高的数据一致性,并且可以接受加锁带来的性能开销,那么可以使用悲观锁。例如,在用户阅读消息并准备更新message_reads
表之前,先执行一个SELECT ... FOR UPDATE
语句来锁定该消息对应的记录,然后再进行更新操作。
总结
在高并发的场景下,保证对message_reads
表的读写操作是原子性的,可以通过数据库事务、乐观锁或悲观锁等机制来实现。选择哪种机制取决于具体的业务需求、系统性能要求以及可接受的数据一致性级别。在实际应用中,往往需要根据实际情况灵活选择或组合使用这些机制。
日志打印
2024 年 10 月 5 日
1 2 import org.slf4j.Logger;import org.slf4j.LoggerFactory;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Component @ServerEndpoint("/chat/{userId}") public class TomcatWebsocketServer { private static Map<Long, Session> clients = new HashMap <>(); private Logger logger = LoggerFactory.getLogger(ServerEndpoint.class); private Long userId; ............................................. }
1 2 3 4 5 6 7 @OnClose public void onClose (Session session) { clients.remove(session); System.out.printf("连接关闭 ------------------> %s%n" , session.getId()); System.out.println("连接关闭 ------------------> " + session.getId()); logger.info(String.format("连接关闭 ------------------> %s%n" , session.getId())); }
消费/生产
2024 年 10 月 5 日
这段代码是一个简单的阻塞队列实现,它使用了Java的synchronized关键字来保证线程安全,以及wait()和notifyAll()方法来实现线程间的通信和同步。 下面是对代码的详细解释: 构造函数 (MyBlockingQueue(int size)): 初始化队列的最大容量 maxSize。 创建一个 LinkedList 类型的 storage 来存储队列元素。 同步方法 (synchronized void put() throws InterruptedException): 生产者线程调用此方法来向队列中添加元素。 使用 while 循环检查队列是否已满。如果队列已满,调用 wait() 方法使当前线程进入等待状态,直到有其他线程调用 notifyAll() 方法唤醒它。 当队列有空间时,添加一个新的元素,并调用 notifyAll() 方法通知所有等待的线程。 同步方法 (synchronized void take() throws InterruptedException): 消费者线程调用此方法从队列中获取元素。 使用 while 循环检查队列是否为空。如果队列为空,调用 wait() 方法使当前线程进入等待状态,直到有其他线程调用 notifyAll() 方法唤醒它。 当队列中有元素时,移除并返回第一个元素,并调用 notifyAll() 方法通知所有等待的线程。 操作过程 创建一个 MyBlockingQueue 对象,指定队列的最大容量。 生产者线程调用 put() 方法尝试向队列中添加元素。如果队列已满,生产者线程将被阻塞,直到队列中有空间。 消费者线程调用 take() 方法尝试从队列中获取元素。如果队列为空,消费者线程将被阻塞,直到队列中有元素。 当队列中有空间时,生产者线程被唤醒,添加元素到队列,并通知所有等待的消费者线程。 当队列中有元素时,消费者线程被唤醒,从队列中移除并返回元素,并通知所有等待的生产者线程。 为什么使用 while 而不是 if 使用 while 循环而不是 if 语句是为了防止虚假唤醒。在多线程环境中,一个线程可能会在没有其他线程调用 notifyAll() 的情况下被唤醒,这被称为虚假唤醒。使用 while 循环可以确保线程在被唤醒后再次检查条件,如果条件不满足,它会继续等待,而不是继续执行可能导致错误的操作。 总结 这段代码实现了一个简单的阻塞队列,它通过使用synchronized关键字、wait()和notifyAll()方法来保证线程安全和线程间的同步。使用while循环而不是if语句是为了防止虚假唤醒,确保线程在等待条件满足时能够正确地被阻塞,并且在条件满足后能够正确地被唤醒。
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 package com.memory.simplewebsocket.basic.produce;import java.util.LinkedList;class MyBlockingQueue { private int maxSize; private LinkedList<Object> storage; public MyBlockingQueue (int size) { this .maxSize = size; storage = new LinkedList <>(); } public synchronized void put () throws InterruptedException { while (storage.size() == maxSize) { wait(); } storage.add(new Object ()); notifyAll(); } public synchronized void take () throws InterruptedException { while (storage.size() == 0 ) { wait(); } System.out.println(storage.remove()); notifyAll(); } }
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 package com.memory.simplewebsocket.basic.produce;public class WaitStyle { public static void main (String[] args) { MyBlockingQueue myBlockingQueue = new MyBlockingQueue (10 ); Producer producer = new Producer (myBlockingQueue); Consumer consumer = new Consumer (myBlockingQueue); new Thread (producer).start(); new Thread (consumer).start(); } }class Producer implements Runnable { private MyBlockingQueue storage; public Producer (MyBlockingQueue storage) { this .storage = storage; } @Override public void run () { for (int i = 0 ; i < 100 ; i++) { try { storage.put(); } catch (InterruptedException e) { e.printStackTrace(); } } } }class Consumer implements Runnable { private MyBlockingQueue storage; public Consumer (MyBlockingQueue storage) { this .storage = storage; } @Override public void run () { for (int i = 0 ; i < 100 ; i++) { try { storage.take(); } catch (InterruptedException e) { e.printStackTrace(); } } } }
Maven 镜像失效
2024 年 12 月 18 日
Cannot resolve plugin org.springframework.boot:spring-boot-maven-plugin: 解决办法-CSDN博客
用内部 VPN 账号连着公司的网络,看来 Maven 镜像源改换成泰豪软件部门的镜像源就行了。
自启动
2024 年 12 月 19 日
Spring Boot应用启动时自动执行代码的五种方式_springboot启动后执行一段代码-CSDN博客
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @SpringBootApplication public class LicenseServerApplication implements ApplicationRunner { public static void main (String[] args) { SpringApplication.run(LicenseServerApplication.class, args); } @Override public void run (ApplicationArguments args) throws Exception { System.out.println("do something 55555555" ); System.out.println("ApplicationRunner启动" ); System.out.println("===============" ); } }
Harbor
2024 年 12 月 20 日
harbor: Harbor 是为企业用户设计的容器镜像仓库开源项目,包括了权限管理(RBAC)、LDAP、审计、安全漏洞扫描、镜像验真、管理界面、自我注册、HA 等企业必需的功能,同时针对中国用户的特点,设计镜像复制和中文支持等功能。 (gitee.com)
Harbor
Docker-harbor私有仓库_harbor和docker的关系-CSDN博客
harbor与docker关系 harbor docker_colddawn的技术博客_51CTO博客
部署Docker私有镜像仓库Harbor_Mingo的技术博客_51CTO博客
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 在企业项目中,常用的镜像仓库主要包括以下几种: 一、公有镜像仓库 Docker Hub 简介:Docker Hub是一个提供共享应用程序和自动化工作流工具的公共存储库。 功能:它允许开发者创建、测试、存储和共享Docker容器映像。用户可以使用Docker Hub来发现、共享和运行容器化应用程序。它还支持自动化构建、存储和部署,以及团队协作等功能。 二、私有镜像仓库(企业级镜像仓库) Harbor 简介:Harbor是VMware公司开源的一个企业级Docker Registry项目,项目地址为https://gi thub.com/goharbor/ harbor。 功能:Harbor提供了一个安全、可信赖的仓库来存储和管理Docker镜像。它实现了基于角色的访问控制机制,通过项目来对镜像进行组织和访问权限的控制。此外,Harbor还提供了图形化的管理界面,方便用户通过浏览器来浏览和检索当前Docker镜像仓库,管理项目和命名空间。Harbor支持镜像的多用户管理、权限控制、镜像复制、镜像扫描、自动同步等功能,非常适合用于企业内部的镜像管理。 搭建:Harbor的搭建过程包括下载离线安装包、修改配置文件、修改Docker守护进程配置文件、启动安装等步骤。安装完成后,用户可以通过IP和端口访问Harbor的Web页面,进行镜像的上传、下载和管理。 JFrog Artifactory 简介:JFrog是一家知名的DevOps工具提供商,其Artifactory产品可以用作镜像仓库管理系统。 功能:Artifactory支持多种软件包格式,包括Docker镜像。它提供了强大的安全性、可伸缩性和管理功能,可以帮助开发团队更好地管理镜像。 Nexus Repository Manager 简介:Nexus Repository Manager是Sonatype公司推出的一款开源的存储管理工具。 功能:它支持多种包管理格式,包括Docker。Nexus提供了丰富的API和插件,可以与各种自动化工具和持续集成系统集成,方便用户进行镜像的存储和管理。 三、混合镜像仓库 除了公有镜像仓库和私有镜像仓库外,还有一些镜像仓库管理系统支持混合模式,即同时支持公有和私有镜像的存储和管理。这种混合模式可以根据企业的实际需求进行灵活配置和使用。 综上所述,企业项目在选择镜像仓库时,可以根据项目的具体需求、安全性要求、团队协作需求以及成本预算等因素进行综合考虑。Docker Hub、Harbor、JFrog Artifactory和Nexus Repository Manager等都是不错的选择,它们各有特点和优势,能够满足不同企业的需求。
字符串
2024 年 12 月 20 日
优雅拼接字符串,有哪些方法呢?
直接拼接:
1 this .param.setLicensePath(System.getProperty("user.dir" ) + prop.getProperty("license.license-path" ));
设置分隔符:
1 this .param.setLicensePath(String.join("" , LicenseConstant.USER_DIR, prop.getProperty("license.license-path" )));
然而,更常见的情况是,如果privateKeysStorePath
是一个文件名,而你需要将其附加到userDir
后面形成一个完整的文件路径,那么你可能需要使用文件分隔符(在Windows上是\
,在Unix/Linux/Mac上是/
)。但Java的File
类提供了跨平台处理文件路径的方法,因此更推荐使用File.separator
或Paths
类来构建路径。
File:
1 this .param.setLicensePath(new File (LicenseConstant.USER_DIR, prop.getProperty("license.license-path" )).getPath());
Paths:
1 this .param.setLicensePath(Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path" )).toString());
这几种全部都测试过了,都能够正常拼接字符串,找到文件路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void setParam (LicenseCreatorParamDTO param) { Properties prop = new Properties (); try { prop.load(LicenseCreator.class.getResourceAsStream("/application.properties" )); } catch (IOException e) { throw new RuntimeException (e); } this .param.setPrivateAlias(prop.getProperty("license.private-alias" )); this .param.setKeyPass(prop.getProperty("license.key-pass" )); this .param.setStorePass(prop.getProperty("license.store-pass" )); this .param.setLicensePath(Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.license-path" )).toString()); this .param.setPrivateKeysStorePath(Paths.get(LicenseConstant.USER_DIR, prop.getProperty("license.private-keys-store-path" )).toString()); this .param.setConsumerType(prop.getProperty("license.consumer-type" )); this .param.setConsumerAmount(Integer.parseInt(prop.getProperty("license.consumer-amount" ))); LicenseCheckModel serverInfos = this .getServerInfos(System.getProperty("os.name" )); param.setLicenseCheckModel(serverInfos); }
文件上传
2024 年 12 月 20 日
很早之前做过这方面的研究,上传文件什么的也还是挺容易,就是时间长了就都统统给忘记了,这东西也没法找时间回顾。
只能是遇着问题了,再回过头来重拾解决经验吧。
springboot中文件上传到本地_springboot上传文件到本地-CSDN博客
1 2 3 4 5 6 7 @PostMapping("/upload") public boolean upload (MultipartFile image) throws Exception { log.info("文件上传成功 {}" , image); String originalFilename = image.getOriginalFilename(); image.transferTo(new File ("D:\\Project\\tellhow\\license-server\\license\\" + originalFilename)); return true ; }
Postman 发起请求,上传文件:
本机查看指定上传目录,当然上传成功了:
定时任务
2024 年 12 月 23 日
写一个定时任务,启动类上添加 @EnableScheduling 注解,表示支持定时任务:
1 2 3 4 5 6 7 8 9 10 11 12 13 @SpringBootApplication @Import(CorsConfig.class) @EnableConfigurationProperties(UserPasswordConfigInfo.class) @ServletComponentScan(basePackages = "edu.hpu.filter") @EnableScheduling @EnableFeignClients(basePackages = {"com.th.cloud.iois.feign.api"}) @EnableDiscoveryClient public class ThIoisInspectionApplication { public static void main (String[] args) { SpringApplication.run(ThIoisInspectionApplication.class, args); } }
编写定时任务:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Component public class LicenseVerifyJob { @Resource private LicenseVerify licenseVerify; @Scheduled(cron = "*/2 * * * * *") public void getArticleContent () { System.out.println("定时任务执行" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ))); } }
日期
2024 年 12 月 23 日
输出当前时间,并设置日期格式:
1 System.out.println("当前时间: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" )));
接口文档
2024 年 12 月 19 日
所有模块接口的文档都拿到了,网页均保存至本地集锦。
Springboot 系列(十六)你真的了解 Swagger 文档吗?-腾讯云开发者社区-腾讯云 (tencent.com.cn)
2024 年 12 月 24 日
一篇文章带你搞定 SpringBoot 整合 Swagger2_Java 开发学习最全合集-CSDN专栏
springboot整合swagger2_spring boot swagger2-CSDN博客
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > <version > ${swagger-version}</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > <version > ${swagger-version}</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api () { return new Docket (DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class)) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo () { return new ApiInfoBuilder () .title("许可证管理接口文档" ) .description("许可证管理接口文档" ) .version("1.0" ) .build(); } }
org.springframework.context.ApplicationContextException: Failed to start bean ‘documentationPluginsBootstrapper’; nested exception is java.lang.NullPointerException - 哩个啷个波 - 博客园 (cnblogs.com)
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 org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper' ; nested exception is java.lang.NullPointerException at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.24.jar:5.3.24] at org.springframework.context.support.DefaultLifecycleProcessor.access$200 (DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.24.jar:5.3.24] at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup .start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.24.jar:5.3.24] at java.lang.Iterable.forEach(Iterable.java:75) ~[na:1.8.0_211] at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.24.jar:5.3.24] at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.24.jar:5.3.24] at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) ~[spring-context-5.3.24.jar:5.3.24] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.24.jar:5.3.24] at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.7.jar:2.7.7] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) [spring-boot-2.7.7.jar:2.7.7] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) [spring-boot-2.7.7.jar:2.7.7] at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) [spring-boot-2.7.7.jar:2.7.7] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) [spring-boot-2.7.7.jar:2.7.7] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) [spring-boot-2.7.7.jar:2.7.7] at com.license.server.LicenseServerApplication.main(LicenseServerApplication.java:10) [classes/:na] Caused by: java.lang.NullPointerException: null at springfox.documentation.spi.service.contexts.Orderings$8 .compare(Orderings.java:112) ~[springfox-spi-2.9.2.jar:null] at springfox.documentation.spi.service.contexts.Orderings$8 .compare(Orderings.java:109) ~[springfox-spi-2.9.2.jar:null] at com.google.common.collect.ComparatorOrdering.compare(ComparatorOrdering.java:37) ~[guava-20.0.jar:na] at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) ~[na:1.8.0_211] at java.util.TimSort.sort (TimSort.java:220) ~[na:1.8.0_211] at java.util.Arrays.sort (Arrays.java:1438) ~[na:1.8.0_211] at com.google.common.collect.Ordering.sortedCopy(Ordering.java:855) ~[guava-20.0.jar:na] at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:57) ~[springfox-spring-web-2.9.2.jar:null] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper$2 .apply(DocumentationPluginsBootstrapper.java:138) ~[springfox-spring-web-2.9.2.jar:null] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper$2 .apply(DocumentationPluginsBootstrapper.java:135) ~[springfox-spring-web-2.9.2.jar:null] at com.google.common.collect.Iterators$7 .transform(Iterators.java:750) ~[guava-20.0.jar:na] at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[guava-20.0.jar:na] at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[guava-20.0.jar:na] at com.google.common.collect.MultitransformedIterator.hasNext(MultitransformedIterator.java:52) ~[guava-20.0.jar:na] at com.google.common.collect.MultitransformedIterator.hasNext(MultitransformedIterator.java:50) ~[guava-20.0.jar:na] at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:249) ~[guava-20.0.jar:na] at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:209) ~[guava-20.0.jar:na] at com.google.common.collect.FluentIterable.toList(FluentIterable.java:614) ~[guava-20.0.jar:na] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.defaultContextBuilder(DocumentationPluginsBootstrapper.java:111) ~[springfox-spring-web-2.9.2.jar:null] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.buildContext(DocumentationPluginsBootstrapper.java:96) ~[springfox-spring-web-2.9.2.jar:null] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:167) ~[springfox-spring-web-2.9.2.jar:null] at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178) ~[spring-context-5.3.24.jar:5.3.24] ... 14 common frames omitted
org.springframework.context.ApplicationContextException: Failed to start bean ‘documentationPluginsB-CSDN博客
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api () { return new Docket (DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class)) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo () { return new ApiInfoBuilder () .title("许可证管理接口文档" ) .description("许可证管理接口文档" ) .version("1.0" ) .build(); } }
2024 年 12 月 25 日
怪了,昨晚能成功启动项目了,但接口文档怎么还是访问不了呢,其他接口访问正常,看来还是整合 Swagger 失败了。
一篇文章带你搞定 SpringBoot 整合 Swagger2_Java 开发学习最全合集-CSDN专栏
解决springboot接入springfox-swagger2遇到的一些问题_java_脚本之家 (jb51.net)
就这个文档对症下药了,果然是 Spring Boot 较高版本(2.6以上)整合 Swagger2 就会出现报错,三种解决方案:
1、ShortVideoSwagger2的配置类需要集成WebMvcConfigurationSupport
2、ShortVideoSwagger2的配置类增加@EnableWebMvc注解
3、springboot的配置增加增加一下配置
1 spring.mvc.pathmatch.matching-strategy =ANT_PATH_MATCHER
通过试验得知,这三种接口都可以解决,但是前两种是有副总用的
@EnableWebMvc建议慎用,最后在非springboot项目中使用
前两种解决方案会破坏springboot对springwebmvc的自动装配,导致自定义的一些convertor或者ObjectMapper失效。
目前我的项目中是自定义的ObjectMapper失效。
所以最后使用第三种方式,后期的springboot版本的matching-strategy默认的改为了PATH_PATTERN_PARSER,把它改为ANT_PATH_MATCHER就可以了。
这下子项目确实能够正常启动了,并且 Swagger 文档还特么生效了,完美解决问题,总结下 SpringBoot 集成 Swagger2 的步骤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <properties > <swagger-version > 2.9.2</swagger-version > <swagger-ui.version > 1.9.6</swagger-ui.version > </properties > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > <version > ${swagger-version}</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > <version > ${swagger-version}</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Configuration @EnableSwagger2 @EnableSwaggerBootstrapUI public class SwaggerConfig { @Bean public Docket api () { return new Docket (DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class)) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo () { return new ApiInfoBuilder () .title("许可证管理接口文档" ) .description("许可证管理接口文档" ) .version("1.0" ) .build(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @RestController @Api(value = "License 授权校验", tags = "License 授权校验") @RequestMapping("/license") public class LicenseVerifyController { @Resource private LicenseVerifyService licenseVerifyService; @ApiOperation(value = "License 证书上传", notes = "License 证书上传") @PostMapping("/upload") public CommonResult<Boolean> upload (MultipartFile file) { ........................................ } ...................................... }
如果出现报错就按照上面提到的方案,配置文件里增加配置即可一步到位解决,现在接口文档总算可正常查看了。
1 spring.mvc.pathmatch.matching-strategy =ANT_PATH_MATCHER
总结