数据库相关
MyBatisPlus代码生成器(旧版)
3.5.1(不含)以下的版本
MP代码生成器是模板+数据,MP有Velocity
(默认)、Freemarker
、Beetl
三个模板引擎。
- 新建一个SpringBoot项目。
- pom.xml导依赖。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- MP代码生成器,3.5.1(不含)之前为旧版 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!-- MP代码生成器模板 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<!-- swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
<exclusions>
<!--屏蔽旧版本的swagger-models-->
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--引入指定版本的swagger-annotations和swagger-models解决出现加载swagger时候For input string: ““问题-->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
<!--knife4j , swagger-ui增强-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.10</version>
</dependency>
- 在启动类同级目录下新建一个类,在该类的main方法中写自定义配置,主要改包名、类名命名方式和MySQL连接部分,以及生成表名。
package com.hisi;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.LikeTable;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.Scanner;
public class MPGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
// public static String scanner(String tip) {
// Scanner scanner = new Scanner(System.in);
// StringBuilder help = new StringBuilder();
// help.append("请输入" + tip + ":");
// System.out.println(help.toString());
// if (scanner.hasNext()) {
// String ipt = scanner.next();
// if (StringUtils.isNotBlank(ipt)) {
// return ipt;
// }
// }
// throw new MybatisPlusException("请输入正确的" + tip + "!");
// }
public static void main(String[] args){
//代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("FN");
// 代码生成是不是要打开文件夹
gc.setOpen(false);
gc.setSwagger2(true); //实体属性 Swagger2 注解
// 会在mapper.xml文件中生成一个基础的<resultmap>会映射所有的字段
gc.setBaseResultMap(true);
//覆盖掉原先生成的同文件
gc.setFileOverride(true);
gc.setDateType(DateType.TIME_PACK);
// 实体类名:直接用表名, %s=表名
gc.setEntityName("%s");
// mapper接口名
gc.setMapperName("%sMapper");
// mapper.xml文件名
gc.setXmlName("%sMapper");
// 业务逻辑接口名
gc.setServiceName("%sService");
// 业务逻辑实现类名
gc.setServiceImplName("%sServiceImpl");
// 将全局配置设置到AutoGenerator
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/hisi_pex?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
// 模块名
pc.setModuleName("hisi");
// 包名
pc.setParent("com");
// 完整的包名:com.example.quickstart.pms
mpg.setPackageInfo(pc);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
// 表名的生成策略:下划线转驼峰 pms_product -- PmsProduct
strategy.setNaming(NamingStrategy.underline_to_camel);
// 列名的生成策略: 下划线转驼峰 last_name -- lastName
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
// strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
strategy.setEntityLombokModel(true);
// 在controller类上是否生成RestController
strategy.setRestControllerStyle(true);
// 公共父类
// strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
// 写于父类中的公共字段
// strategy.setSuperEntityColumns("id");
// 要生成的表名,多个用逗号分隔
strategy.setInclude("hisi_note_order");
// 使用模糊前缀
// strategy.setLikeTable(new LikeTable("pms_"));
// strategy.setControllerMappingHyphenStyle(true);
// 设置 表的过滤替换前缀
// strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
// mpg.setTemplateEngine(new FreemarkerTemplateEngine());
// 进行生成
mpg.execute();
}
}
注意:
- 生成的mapper没有
@Mapper
注解,自行添加(@Service
这些有)mapper.xml
文件被自动生成在mapper目录下,自行更改到resources
文件夹下- 实体类中有MP自动填充时间的话,自行添加
@TableField(fill = FieldFill.INSERT_UPDATE)
- 以上代码生成效果如下:
参考链接:
Mybatis-Plus之代码生成器(模板,可直接套用超级好用,但是建议初学者少用)
参考视频
官方文档
MyBatisPlus代码生成器(新版)
MP 3.5.1及以上的版本
package com.hisi.fourk;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import java.util.Collections;
/**
* MyBatis-Plus 代码生成器模板
*/
public class CodeGenerator {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/hisi-fourk?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&serverTimezone=Asia/Shanghai";
String username = "root";
String password = "123456";
FastAutoGenerator.create(url, username, password)
.globalConfig(builder -> {
builder.author("FN") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
// .outputDir(System.getProperty("user.dir") + "\\src\\main\\java") // 指定输出目录
// 再加上当前模块名,在子工程时使用该配置
.outputDir(System.getProperty("user.dir") + "/hisi-4ksurveillance-task" + "\\src\\main\\java")
.commentDate("yyyy-MM-dd"); //设置时间格式(类注释作者下面那个时间)
})
.packageConfig(builder -> {
builder.parent("com.hisi") // 设置父包名
.moduleName("fourk") // 设置父包模块名
.entity("model") // 设置实体类包名
.service("service")
.controller("controller")
.mapper("mapper")
.xml("mapper")
// .pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "\\src\\main\\resources\\mapper")); // 设置mapperXml生成路径
.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/hisi-4ksurveillance-task" + "\\src\\main\\resources\\mapper")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("hisi_business_result") // 设置需要生成的表名
// .addTablePrefix("t_", "c_"); // 设置过滤表前缀,如hisi_test,生成的类名只需要test
.serviceBuilder() // service策略配置
.formatServiceFileName("%sService") // service类名,%s表示替换为表名
.formatServiceImplFileName("%sServiceImpl") // service实现类的类名
.entityBuilder() // 实体类策略配置
.enableLombok() // 开启Lombok
// .logicDeleteColumnName("deleted") // 说明逻辑删除是哪个字段
.enableTableFieldAnnotation() // 属性加上说明注解
.controllerBuilder() // controller策略配置
.formatFileName("%sController")
.enableRestStyle() // 开启@RestController
.mapperBuilder()
.superClass(BaseMapper.class) // 继承哪个父类
.formatMapperFileName("%sMapper")
.enableMapperAnnotation() // 加上@Mapper注解
.formatXmlFileName("%sMapper"); // xml名称
})
// .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
多数据源
springboot多数据源JdbcTemplate和SessionFactory两种方式学习
springboot多数据源配置-通过SqlSessionFactory指定的数据源来操作指定目录的XML文件的方式
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
MP乐观锁插件
- MP配置文件配置乐观锁插件。
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
- 在实体类字段上添加
@Version
注解,表示乐观锁字段(可以是Integer version 或者Localdatetime updateTime等字段)。 - 和以前一样直接写代码就行,在Update时自动会拦截SQL设置乐观锁。
boolean update = this.updateById(warehouseLeadBox);
return Result.success(update);
参考链接:
MyBatis-Plus 实现乐观锁
Mybatis Plus 乐观锁插件(手把手教学)
MySQL锁死处理
- 新建查询 , 查出当前的所有进程,找出那些卡住的进程
show full processlist;
- 将对应的进程杀掉
kill 1527
注:Time数值过大或者State含Lock、Warn等字符表示异常。
MySQL存储JSON
参考文章
场景:需要在一个表的某个字段存储对象(包括自定义类、List、Map等)。
思路:表中使用json类型字段存储数据,Java Entity取出时用转换器转换为指定对象。
以存储一个List<Double>
为例
- Json字段类型到Java类中指定为
List<Double>
;
然后使用MP@TableField
注解的typeHandler
属性指定为自定义的拦截器,拦截器用于json到指定对象类型之间的转换。
@Data
@EqualsAndHashCode(callSuper = true)
public class BaseDeviceManage extends TenantEntity {
@ApiModelProperty(value = "经纬度")
// ListDoubleTypeHandler是自定义的拦截器,用于Json到指定类型的转换
@TableField(value = "lng_and_lat", typeHandler = ListDoubleTypeHandler.class)
private List<Double> lngAndLat;
@ApiModelProperty(value = "图标")
private String icon;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "图片URI")
private String pic;
@ApiModelProperty(value = "是否标记,默认false")
private Boolean isMark;
}
- 自定义类型拦截器
ListDoubleTypeHandler
,用于Json到java对象的互转。
如果需要转换为其他Java对象,将类中的List<Double>
改为需要的对象类型就行。
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* @Author:
* @Date: 2024/5/28 13:54
* @Description:
* 数据库存储json,获取数据转为List<Double>
**/
public class ListDoubleTypeHandler extends BaseTypeHandler<List<Double>> {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<Double> parameter, JdbcType jdbcType)
throws SQLException {
try {
String jsonString = objectMapper.writeValueAsString(parameter);
ps.setString(i, jsonString);
} catch (JsonProcessingException e) {
throw new SQLException("Error converting List<Double> to JSON string", e);
}
}
@Override
public List<Double> getNullableResult(ResultSet rs, String columnName) throws SQLException {
String jsonString = rs.getString(columnName);
return parseJsonString(jsonString);
}
@Override
public List<Double> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String jsonString = rs.getString(columnIndex);
return parseJsonString(jsonString);
}
@Override
public List<Double> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String jsonString = cs.getString(columnIndex);
return parseJsonString(jsonString);
}
private List<Double> parseJsonString(String jsonString) throws SQLException {
if (StrUtil.isBlank(jsonString)) {
return null;
}
try {
return objectMapper.readValue(jsonString, new TypeReference<List<Double>>() {
});
} catch (JsonProcessingException e) {
throw new SQLException("Error converting JSON string to List<Double>", e);
}
}
}
- 如果是xml手写SQL,查询返回值需要使用
resultMap
指定json字段的typeHandler。
<resultMap id="deviceManageResultMap" type="com.dongfang.hope.manage.vo.DeviceManageVO">
<id property="id" column="id"/>
<result property="deviceName" column="device_name"/>
<result property="deviceCode" column="device_code"/>
<result property="deviceType" column="device_type"/>
<!-- lng_and_lat是需要json转对象的字段,指定typeHandler -->
<result property="lngAndLat" column="lng_and_lat"
typeHandler="com.dongfang.hope.manage.handler.ListDoubleTypeHandler"/>
<result property="icon" column="icon"/>
<result property="remark" column="remark"/>
<result property="pic" column="pic"/>
<result property="isMark" column="is_mark"/>
</resultMap>
<select id="pageDevice" resultMap="deviceManageResultMap">
SELECT id,
device_name,
device_code,
device_type,
lng_and_lat,
icon,
remark,
pic,
is_mark
FROM test
</select>
json字段存储高精度小数,导致精度丢失问题
- 环境描述:java中使用
List<List<List<BigDecimal>>>
存储地图区域坐标信息。
数据库使用json
类型进行存储。
使用mybatisplus的typeHandler
来进行两者的转换。
@ApiModelProperty(value = "地图-绘制区域")
@TableField(value = "draw_region", typeHandler = TwoListDoubleTypeHandler.class)
private List<List<List<BigDecimal>>> drawRegion;
存储到mysql后,格式是这样的:
[[[104.04460443658631,30.24296857808761],[104.0615093091583,30.2387778743828],[104.04709044725873,30.24296857808761],[104.04460443658631,30.24296857808761]]]
- 问题描述:接收前端的传入值没有问题。
使用mybatisplus的.saveOrUpdate()
方法,生成的SQL语句没有问题,注入的参数正常。
mysql执行sql语句后,因mysql的原因导致精度丢失。 - 问题摸排:mybatisplus生成的更新json字段sql是
set draw_region = "[[[省略内容]]]"
。
但是推荐的更新json字段语句是用函数更新draw_region = JSON_SET( draw_region, '$', '[[[。。。]]]' )
。
再但是,用函数进行更新后虽然精度没问题了,但存储在数据库的内容被""
引号包含了,导致mybatisplus在取出来准备解析成java类型时报错了。
所以需要在mybatisplus的typeHanlder中,插入/更新数据时将内容加上""
包含保证到mysql后不会精度丢失;查询数据时将""
去掉让MP能成功转换为java类型。 - 下面是具体代码
service:
@Override
public R upsertPlot(SmartPlantPlot smartPlantPlot) {
if (!CollectionUtils.isEmpty(smartPlantPlot.getDrawRegion())) {
// 如果在地图上绘制了区域,设置为已标记状态
smartPlantPlot.setIsMark(true);
}
// drawRegion在数据库中是二维数组json类型;
// 如果使用普通SET语句,二位数组json的新旧值比对会相同,所以必须使用SET JSON_SET(draw_region,'$' '内容')函数来更新json字段
// 此处也可以使用mybatisplus的sql拦截器解决(懒得研究,所以采用先set为null的方法)
if (smartPlantPlot.getId() != null) {
this.lambdaUpdate()
.set(SmartPlantPlot::getDrawRegion, null)
.eq(SmartPlantPlot::getId, smartPlantPlot.getId()).update();
}
boolean update = super.saveOrUpdate(smartPlantPlot);
return update ? R.success("新增/编辑成功") : R.fail("新增/编辑失败");
}
SmartPlantPlot实体类:
@ApiModelProperty(value = "地图-绘制区域")
@TableField(value = "draw_region", typeHandler = TwoListDoubleTypeHandler.class)
private List<List<List<BigDecimal>>> drawRegion;
类字段的typeHandler:
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* @Author:
* @Date: 2024/5/28 16:16
* @Description: 地图管理-地块二维经纬度
* 数据库存储json,获取数据转为List<List<List<Double>>>
**/
public class TwoListDoubleTypeHandler extends BaseTypeHandler<List<List<List<Double>>>> {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<List<List<Double>>> parameter, JdbcType jdbcType)
throws SQLException {
try {
String jsonString = objectMapper.writeValueAsString(parameter);
// mysql json存储高精度数字时,如果不用""包含去存储会导致精度丢失(数据库层面的)
if (StrUtil.isNotBlank(jsonString)) {
jsonString = "\"" + jsonString + "\"";
}
ps.setString(i, jsonString);
} catch (JsonProcessingException e) {
throw new SQLException("Error converting List<List<Double>> to JSON string", e);
}
}
@Override
public List<List<List<Double>>> getNullableResult(ResultSet rs, String columnName) throws SQLException {
String jsonString = rs.getString(columnName);
return parseJsonString(jsonString);
}
@Override
public List<List<List<Double>>> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String jsonString = rs.getString(columnIndex);
return parseJsonString(jsonString);
}
@Override
public List<List<List<Double>>> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String jsonString = cs.getString(columnIndex);
return parseJsonString(jsonString);
}
private List<List<List<Double>>> parseJsonString(String jsonString) throws SQLException {
if (StrUtil.isBlank(jsonString)) {
return null;
}
try {
// 取出给mybatisPlus解析时,去掉""
if (StrUtil.isNotBlank(jsonString) &&
jsonString.startsWith("\"") &&
jsonString.endsWith("\"")) {
jsonString = jsonString.substring(1, jsonString.length() - 1);
}
return objectMapper.readValue(jsonString, new TypeReference<List<List<List<Double>>>>() {
});
} catch (JsonProcessingException e) {
throw new SQLException("Error converting JSON string to List<List<List<Double>>>", e);
}
}
}
MP自动映射枚举类
定义一个接口,用于增强枚举类(可选)
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.IEnum;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
/**
* 枚举类型基类
*/
public interface DictionaryEnum<T extends Serializable> extends IEnum<T> {
/**
* 描述信息
*
* @return 描述
*/
String getDesc();
/**
* 获取枚举编码
*
* @return 编码
*/
default String getCode() {
return String.valueOf(this.getValue());
}
/**
* 枚举数组转集合
*
* @param dictionaries 枚举
* @return 集合
*/
static List<BaseDictionary> getList(DictionaryEnum<?>[] dictionaries) {
if (dictionaries == null) {
return null;
}
return Arrays.stream(dictionaries).map(dictionary -> BaseDictionary.builder()
.code(dictionary.getCode()).desc(dictionary.getDesc())
.build()).collect(Collectors.toList());
}
/**
* 获取指定类型枚举映射
*
* @param enumClass 枚举类
* @param type 类型
* @param <E> 包装类
* @return 枚举值
*/
static <E extends DictionaryEnum<?>> E of(Class<E> enumClass, Serializable type) {
E[] enumConstants = enumClass.getEnumConstants();
for (E e : enumConstants) {
final Serializable value = e.getValue();
if (value == type) {
return e;
}
}
return null;
}
char SEPARATOR = ',';
/**
* 转换成字符串
*
* @param dictionaries 枚举
* @return 转换结果
*/
static <E extends DictionaryEnum<?>> String of(List<E> dictionaries) {
if (CollectionUtil.isEmpty(dictionaries)) {
return null;
}
return dictionaries.stream()
.filter(Objects::nonNull)
.map(DictionaryEnum::getCode).collect(Collectors.joining(","));
}
/**
* 转换成集合枚举
*
* @param enumClass 枚举类
* @param dictionaries 枚举
* @return 转换结果
*/
static <E extends DictionaryEnum<?>> List<E> of(Class<E> enumClass, String dictionaries) {
if (StrUtil.isBlank(dictionaries)) {
return null;
}
final List<String> split = StrUtil.split(dictionaries, SEPARATOR);
return split.stream().filter(Objects::nonNull)
.map(type -> of(enumClass, Integer.parseInt(type)))
.collect(toList());
}
}
创建枚举类
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonValue;
import com.hisi.cloud.db.mybatis.DictionaryEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "(0:正常;1:我厂回收;)")
@JsonFormat
public enum SaleOrderTypeEnum implements DictionaryEnum<Integer> {
NORMAL(0, "成品"),
OUR_FACTORY_RECYCLES(1, "我厂回收")
;
@EnumValue
@JsonValue
private Integer type;
@Schema(description = "描述")
private String desc;
@JsonCreator
public static SaleOrderTypeEnum of(Integer type) {
if (type == null) {
return null;
}
for (SaleOrderTypeEnum info : values()) {
if (info.type.equals(type)) {
return info;
}
}
return null;
}
public boolean eq(String val) {
return this.name().equalsIgnoreCase(val);
}
public boolean eq(SaleOrderTypeEnum val) {
if (val == null) {
return false;
}
return eq(val.name());
}
@Override
public Integer getValue() {
return this.type;
}
@Override
public String toString() {
return String.valueOf(type);
}
}
MyBatis返回Map<String,List<Entity>> 格式数据
前言:@MapKey("")
只能查Map<key,value>
格式的数据(目前还没找到value可以=List<Entity>
的方法),现在介绍的方法可以做到Map<key,List<Entity>>
。
思路:
- 重点是把数据查出来在java里做map分组,不是sql里分组。
- 查数据库时将查询数据放到Entity的List里,然后在Entity额外定义一个字段存放map的key。
- mapper返回值是
List<Entity>
的数据,通过stream流根据Entity里额外的map key字段做分组。
@Data
public class SemiFinishedLibraryMapDTO {
/**
* key_id
*/
private Long containerId;
/**
* list
*/
private List<SemiFinishedLibrary> semiFinishedLibraries;
}
- mapper
List<SemiFinishedLibraryMapDTO> getSemiFinishedMapByContainerIds(@Param("containerIds") List<Long> containerIds);
注意:<collection> </collection>
里的<result/>
必须包含select查的所有字段,否则会封装不到List里。
<resultMap id="semifinishedMap" type="com.cngt.common.model.dto.SemiFinishedLibraryMapDTO">
<result column="container_id" jdbcType="BIGINT" property="containerId"/>
<collection property="semiFinishedLibraries" ofType="com.cngt.common.model.SemiFinishedLibrary">
<result column="container_id" jdbcType="BIGINT" property="containerId"/>
<result column="source_name" property="sourceName"/>
<result column="source_id" property="sourceId"/>
</collection>
</resultMap>
<select id="getSemiFinishedMapByContainerIds" resultMap="semifinishedMap">
SELECT container_id, source_name, source_id
FROM cngt_semi_finished_library csfl
WHERE deleted = 0
AND type = 1
AND container_id IN
<foreach collection="containerIds" item="containerId" open="(" close=")" separator=",">
#{containerId}
</foreach>
</select>
- service
List<SemiFinishedLibraryMapDTO> semiFinishedLibraryMapDTOS = baseMapper.getSemiFinishedMapByContainerIds(containerIds);
Map<Long, List<SemiFinishedLibrary>> semiFinishedMap = semiFinishedLibraryMapDTOS.stream()
.collect(Collectors.toMap(SemiFinishedLibraryMapDTO::getContainerId, SemiFinishedLibraryMapDTO::getSemiFinishedLibraries));
Spring相关
Spring Boot定时任务
注:spring boot版本需3.0以上。
SpringBoot使用@Scheduled注解实现定时任务
以下提供一个定时任务的模板(注:一般习惯建一个quartz
包来放定时任务):
- 定义一个名为(不强制)
QuartzConfiguration
类,用于配置定时任务。
import org.quartz.JobDetail;
import org.quartz.Trigger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
@Configuration
public class QuartzConfiguration {
/**
* 定时任务1
*
* @param taskHa
* @return
*/
@Bean(name = "taskHaDetail")
public MethodInvokingJobDetailFactoryBean taskHaDetail(TaskHa taskHa) {
MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();
// 是否并发执行
jobDetail.setConcurrent(false);
// 为需要执行的实体类对应的对象
jobDetail.setTargetObject(taskHa);
// 需要执行的方法
jobDetail.setTargetMethod("task");
return jobDetail;
}
// 配置定时任务1的触发器
@Bean(name = "taskHaTrigger")
public CronTriggerFactoryBean taskHaTrigger(JobDetail taskHaDetail) {
CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
trigger.setJobDetail(taskHaDetail);
// 定时任务每隔多少时间执行一次
trigger.setCronExpression("0 3/20 * * * ?");
return trigger;
}
/**
* 定时任务2
*
* @param sobeyK8sTask
* @return
*/
@Bean(name = "sobeyK8sTaskDetail")
public MethodInvokingJobDetailFactoryBean sobeyK8sTaskDetail(SobeyK8sTask sobeyK8sTask) {
MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();
// 是否并发执行
jobDetail.setConcurrent(false);
// 为需要执行的实体类对应的对象
jobDetail.setTargetObject(sobeyK8sTask);
// 需要执行的方法
jobDetail.setTargetMethod("task");
return jobDetail;
}
// 配置定时任务2触发器
@Bean(name = "sobeyK8sTaskTrigger")
public CronTriggerFactoryBean sobeyK8sTaskTrigger(JobDetail sobeyK8sTaskDetail) {
CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
trigger.setJobDetail(sobeyK8sTaskDetail);
// 定时任务每隔多少时间执行一次
trigger.setCronExpression("0 1/10 * * * ?");
return trigger;
}
// 配置Scheduler
@Bean(name = "scheduler")
public SchedulerFactoryBean schedulerFactory(Trigger sobeyK8sTaskTrigger, Trigger taskHaTrigger) {
SchedulerFactoryBean bean = new SchedulerFactoryBean();
// 延时启动,应用启动1秒后
bean.setStartupDelay(1);
// 注册触发器
bean.setTriggers(sobeyK8sTaskTrigger, taskHaTrigger);
return bean;
}
}
- 定义具体的定时任务内容。
@Slf4j
@Component
@EnableScheduling
public class SobeyK8sTask {
public void task() {
// 最好try catch一下
try {
// 任务内容
} catch (Exception e) {
e.printStackTrace();
log.error("执行定时任务失败:{}", e);
return;
}
}
}
- 配置定时任务详解:
定时任务的时间含义:
以trigger.setCronExpression("0 0 0/1 * * ?")
为例,表示定时任务将每小时执行一次。
Cron表达式的格式是秒 分 时 日 月 周 年,其中各个字段的含义如下:
秒(Seconds):0-59
分钟(Minutes):0-59
小时(Hours):0-23
日(Day of month):1-31
月(Month):1-12 或者 JAN-DEC
周(Day of week):1-7 或者 SUN-SAT
年(Year):可选字段,留空或者使用 “*” 表示每年都执行,也可以指定特定年份,例如:2022, 2023, 2024。
=================
对于*
和?
的解释:
在日字段中,*
表示每月的任意一天,而?表示该字段的设定是不确定的,只关注其他参数的设定。
在周字段中,*
表示每周的任意一天,而?同样表示该字段的设定是不确定的,只关注其他参数的设定。
其余字段用*
,表示每天、每分的任意时间。
=================
对于0/1
的解释:
固定值:使用具体的数字表示该时间单位,例如 1 表示每小时的第1小时。
递增/间隔值:使用 /n 或 n/m 的形式表示时间单位的递增或间隔。其中,/n 表示从最小值开始,每隔 n 个时间单位触发一次,而 n/m 表示从 n 开始,每隔 m 个时间单位触发一次。
复制Spring Boot模块后改名问题
原文章链接
确保彻底改成功而不损坏原项目,严格按下面步骤来:
1、重命名根目录名称,修改.idea目录下所有.xml文件里面的项目名称、包名
2、重命名根目录下.iml文件名
3、打开pom文件,修改groupId、artifactId、name等项目名、包名
4、idea打开改好的项目,选中要修改的包名,右键refactor-rename。
5、Application文件右键refactor-rename。
6、CTRL+SHIFT+R全局搜索旧包名,需要的地方修改。ok!
Validation参数校验
@Valid和@Validated区别:
- @Valid在Controller形参处,需配合BindingResult使用,在Controller方法里对BindingResult做处理。
- @Validated在Controller形参处,无需BindingResult,但需要全局异常处理器拦截其异常进行处理(否则会直接抛出异常不再执行方法下文)。
校验注解作用域:
- @Validated @Valid —— entity(实体)
- @NotBlank —— String
- @NotNull —— Integer
- @NotEmpty —— java.util.Collection(集合)
从Nacos拿配置的方式
跨域
注意:请求头一定设置允许跨域,因为浏览器询问服务器是否允许跨域时,这个询问请求是在请求头上的(Origin字段,向服务器声明请求的来源)。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Bean
public CorsFilter corsFilter()
{
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 设置访问源地址
config.addAllowedOriginPattern("*");
// 设置访问源请求头
config.addAllowedHeader("*");
// 设置访问源请求方法
config.addAllowedMethod("*");
// 有效期 1800秒
config.setMaxAge(1800L);
// 添加映射路径,拦截一切请求
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 返回新的CorsFilter
return new CorsFilter(source);
}
}
Excel导出自定义单元格
SpringBoot + Socket通信
Spring异步实现
自定义异步线程池,注册Bean交给Spring管理。
/**
* 线程池参数配置,多个线程池实现线程池隔离,@Async注解,默认使用系统自定义线程池,可在项目中设置多个线程池,在异步调用的时候,指明需要调用的线程池名称,比如:@Async("taskName")
*/
@Configuration
@EnableAsync
public class SyncConfiguration {
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor executor() {
// 返回可用处理器的Java虚拟机的数量
int i = Runtime.getRuntime().availableProcessors();
System.out.println("系统最大线程数 : " + i);
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//核心线程数
taskExecutor.setCorePoolSize(10);
//线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
taskExecutor.setMaxPoolSize(100);
//缓存队列
taskExecutor.setQueueCapacity(50);
//许的空闲时间,当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
taskExecutor.setKeepAliveSeconds(200);
//异步方法内部线程名称
taskExecutor.setThreadNamePrefix("async-");
/**
* 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
* 通常有以下四种策略:
* ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
* ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
* ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
* ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}
在需要使用异步的方法上加@Async(value = "asyncPoolTaskExecutor")
注解,value就是线程池的Bean名。
@Async(value = "asyncPoolTaskExecutor")
public void syncMethod(String param) {
}
Java相关
如何封装第三方工具
全文以OSS为例
- .yml写配置
- config.xxxConfig读取配置文件
@Component
@Getter
@Setter
public class OSSCloudConfig {
@Value("${OSS.endpoint}")
private String endPoint;
@Value("${OSS.accessKeyId}")
private String accessKeyId;
@Value("${OSS.accessKeySecret}")
private String accessKeySecret;
@Value("${OSS.bucketName}")
private String bucketName;
}
- client.xxxClient工具类,封装第三方工具的各种方法
@Component
public class OSSCloudClient {
@Autowired
private OSSCloudConfig ossCloudConfig;
/**
* @return 获取OSS客户端
*/
public OSS getOSSClient() {
return new OSSClientBuilder().build(ossCloudConfig.getEndPoint(), ossCloudConfig.getAccessKeyId(),
ossCloudConfig.getAccessKeySecret());
}
/**
* 关闭OSS客户端
*
* @param ossClient
*/
public void destroy(OSS ossClient) {
ossClient.shutdown();
}
/**
* 断点续传
*
* @param fileName 文件在OSS中的名字
* @param ossClient OSS连接客户端
*/
public void breakpointResume(String fileName, OSS ossClient) {
//使用完后在这里关闭
this.destroy(ossClient);
}
- 创建Controller接口,使用该工具类
@RestController
@RequestMapping("/download")
@Api(value = "OSS对象存储")
public class OSSController {
@GetMapping("/{url}")
@ApiOperation(value = "单个文件下载")
public HisiResult<String> downloadFile(String url) {
return null;
}
}
对List进行MP分页
需求:现在有一个List,需要做成MP的Page
分页传给前端,直接使用该工具类。
package com.hisi.pex.common.util;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: fn
* @Date: 2023/5/22 10:07
* @Description:
**/
public class PageListUtils {
/**
* 分页函数
*
* @param currentPage 当前页数
* @param pageSize 每一页的数据条数
* @param list 要进行分页的数据列表
* @return 当前页要展示的数据
*/
public static Page getPages(Integer currentPage, Integer pageSize, List list) {
Page page = new Page();
int size = list.size();
if (pageSize > size) {
pageSize = size;
}
// 求出最大页数,防止currentPage越界
int maxPage = size % pageSize == 0 ? size / pageSize : size / pageSize + 1;
if (currentPage > maxPage) {
currentPage = maxPage;
}
// 当前页第一条数据的下标
int curIdx = currentPage > 1 ? (currentPage - 1) * pageSize : 0;
List pageList = new ArrayList();
// 将当前页的数据放进pageList
for (int i = 0; i < pageSize && curIdx + i < size; i++) {
pageList.add(list.get(curIdx + i));
}
page.setCurrent(currentPage).setSize(pageSize).setTotal(list.size()).setRecords(pageList);
return page;
}
}
List的.add()添加元素被覆盖问题
- 问题重现,在循环里面添加对象到外部List中,List中的所有元素会被最后一个对象覆盖:
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<>();
for (int i = 0; i < 4; i++) {
integerList.add(i);
}
//TODO List的.add()深浅拷贝问题
List<Student> studentList = new ArrayList<>();
Student student = new Student(3, "张三");
for (int i = 0; i < integerList.size(); i++) {
//源码里是MP的查询对象出问题,循环多次查询并不会new多个wordOne,而是共用一个:HisiWordOne wordOne = this.getById(hisiWordTwo.getOneId());
student.setId(i);
student.setName(Integer.toString(i));
studentList.add(student);
}
System.out.println(studentList);
}
上面程序的结果
[Student{id = 3, name = 3}, Student{id = 3, name = 3}, Student{id = 3, name = 3}, Student{id = 3, name = 3}]
-
出现的原因:循环里的student没有新new也就没有新开辟内存地址,
ArrayList集合里存的是一个对象的引用
。当我们改变student时,因为ArrayList.add的是student的引用(虽然List里面有多个student),之前的元素都指向了同一个对象student,所以在改变student时,之前添加的也会随之改变。 -
解决办法:在循环里面新new一个Student,然后BeanUtils.copeproperties()拷贝两个对象的信息,最后.add(新new的student)。
参考链接:java中list.add添加元素覆盖之前的问题
自定义全局异常处理器
思路:
- 自定义一个异常类,继承Runtime Exception(此处思考应该继承Exception还是Runtime Exception?)。
- 自定义一个全局异常处理器类,类用
@RestControllerAdvice
注解修饰,拦截异常并统一处理。 - 在全局异常处理器中定义方法,使用
ExceptionHandler(异常类.class)
修饰,表明该方法用于拦截并处理哪一类具体的异常。 - 比如现在的业务中经常出现某一种该业务才有的异常,那就自定义一个异常,然后在全局异常处理器中写一个方法拦截该异常并进行处理,那么在写业务的时候如果有这个异常直接throw new 抛出,全局异常处理器就能统一处理,简化了代码。
下面给出一个简单示例:
自定义异常类
/**
* 自定义业务级异常
*/
public class BusinessException extends RuntimeException {
private Integer code;
public BusinessException(String message) {
super(message);
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public BusinessException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
全局异常处理器
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionAdvice {
/**
* 处理业务级异常
*
* @param be
* @return
*/
@ExceptionHandler(BusinessException.class)
public Result<String> doBusinessException(BusinessException be) {
log.info("捕捉到异常:{}", be.getMessage());
log.info("=============================");
if (be.getMessage().contains("Duplicate entry"))
return Result.error("该字段已存在,请重新命名");
return Result.error(be.getMessage());
}
/**
* 处理系统级异常
*
* @param se
* @return
*/
@ExceptionHandler(SystemException.class)
public Result<String> doSystemException(SystemException se) {
return null;
}
/**
* 处理其他异常
*
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public Result<String> doException(Exception e) {
return null;
}
}
其他
IntelliJ IDE 使用 Codeium 注意项
工具类
Hutool
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
- 将Json转为Bean:
JSONUtil.toBean(shopJson, Shop.class);
- 将Bean转为Json:
JSONUtil.toJsonStr(shop);
- 将Bean转为Map,Bean的字段名为key,值为value
//6.2 将User对象转为Hash存储(使用hutool的拷贝工具)
//此处需要将UserDTO的Long id转为String
//CopyOptions.create()表示设置需要忽略的某些字段,setIgnoreNullValue()空字段是否忽略,setFieldValueEditor()处理字段
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
- Boolean自动拆箱为bollean时,可能引发空指针问题,这时就需要
Boolean.TRUE.equals(success);
或者Hutool工具BooleanUtil.isTrue(success);
- 保存用户到Redis,这里使用hutool的UUID工具
String token = UUID.randomUUID().toString(true);
MD5加密
安装配置
Nacos鉴权
安装nacos后没有登录界面,开启鉴权方法:docker安装新版nacos鉴权(登陆密码)问题解决
使用Nginx解决跨域
找到Nginx的配置文件。
- 方式一,在
http
块中配置允许跨域。
add_header Access-Control-Allow-Origin '*'; # 指定发起请求的哪些域名允许跨域,*表示过来的所有域名都允许跨域
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
- 方式二,在
server
块的location
块中配置允许跨域。
location /hisi-pex/ {
proxy_pass http://web-pex.brtvcloud.com:9200/;
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
}
二者的区别,方式一是允许所有过来的请求允许跨域;方式二是允许过来的指定请求路径允许跨域。
配置含义解释
Stream流
根据list的某个字段去重
public List<WmsMemoryExcelVo> repetitionRepetitions(List<WmsMemoryExcelVo> excelListl) {
//对list数据 根据某个字段去掉重复数据 这里用的根据vin去除重复值,两个结果相同值去最后一条
List<WmsMemoryExcelVo> newList = excelListl.stream().collect(Collectors.collectingAndThen
(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing
(WmsMemoryExcelVo::getVin))), ArrayList::new));
return newList;
}
根据List某个字段排序
noteVoList = noteVoList.stream().filter(NoteVo -> NoteVo != null && NoteVo.getUpdateTime() != null)
.sorted(Comparator.comparing(NoteVo::getUpdateTime).reversed()).collect(Collectors.toList());
根据list中的某个字段分组
场景:通过UserIds查出一个地址List,现在需要根据userId将List分组为多个List,方便后续set等。
Map<Long, List<AddressVO>> addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
// map的key就是分组依据userId,后续根据key获取对应的List
统计List中数据的出现次数
场景1:现在有一个List<Long>
需要统计这个List里数字出现的次数,放在Map里,Map的key是数字,value是出现次数。
List<Long> primaryIds = Arrays.asList(1L, 4L, 3L, 2L, 1L, 4L);
Map<Long, Integer> countMap = new HashMap<>();
for (Long id : primaryIds) {
countMap.put(id, countMap.getOrDefault(id, 0) + 1);
}
System.out.println(countMap);
.getOrDefault(Object key, V defaultValue)
方法用于获取指定键对应的值,如果找不到该键,则返回设置的默认值。- 如果
Object key
存在,则返回这个key对应的value;如果不存在,则返回默认值V defaultValue
。
使用map计数
姿势1:
countMap.merge(radioactiveName, leadBox.getRemaining(), Integer::sum);
说明:
- 指定的键已存在于 HashMap 中,merge() 方法将根据提供的合并函数对键的当前值和指定的值进行合并。
- 键不存在,则直接将指定的键和值添加到 HashMap 中。
姿势2:
Integer remain = countMap.get(radioactiveName);
if (remain == null) {
countMap.put(radioactiveName, leadBox.getRemaining());
} else {
countMap.put(radioactiveName, remain + leadBox.getRemaining());
}
Centos命令
端口&防火墙
- 查看端口占用情况
lsof -i :88
- 查看端口占用情况,及对应的PID;参考链接
netstat -tunlp | grep 8080
- 查看firewalld规则
firewall-cmd --list-all
- 查看简版firewalld开放的端口
firewall-cmd --zone=public --list-ports
- 开放指定端口(如88)
firewall-cmd --zone=public --add-port=88/tcp --permanent
–permanent 为永久生效,不加为单次有效(重启失效) - 修改防火墙规则后需重载
firewall-cmd --reload
后台运行
- 安装screen
yum install -y screen
- 创建一个新的窗口
screen -S test
- 退出当前窗口
ctrl+a+d
screen -d
- 重新连接窗口
screen -r id或窗口名称
- 停止该窗口
Ctrl + C