本文最后更新于:1 个月前
破冰 写作目标 今天是开发壁纸网站 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 快速添加依赖模板:
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; } }
对象映射器(消息转换器)
编写JacksonObjectMapper:(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 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); } }
全局跨域处理
在WebConfig下作如下配置:(2023/08/22早)
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 >
相关依赖未导入 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 >
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早)
表记录/实体类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# # #
在上面的栏目《表记录/实体类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早)
总结