这里记录一些项目中一些细节,便于以后查找方便。
一、实体类的定义
我们在定义实体类时,一般会定义三个:param,po,vo三个实体对象,一般这三个对象内部属性大体上保持一致,只是功能不一样,param用于从前端接受参数,po用于数据库持久层操作,vo用于向前端传递参数。那么问题时,必要时,我们需要转换这三个对象,用的工具是spring里面的BeanUtils,即:
import org.springframework.beans.BeanUtils;
方法是:
BeanUtils.copyProperties(Object source,Object target);
比如说,我想进行持久层对象操作时,需要将param对象转换为po对象,可以如下操作:
BeanUtils.copyProperties(param,po);
以上即为spring给我们提供的对象的转换。
二、项目中遇到两个有意思的问题:
第一个是抛出异常 "Caused by: java.sql.SQLException: Value '0000-00-00 00:00:00' can not be represented" 非常有意思,当我在定义数据库日期类型的时候遇到的。
第一个是我定义起始时间,没什么问题,也不是这个时间报出的错误:
第二次,我定义修改时间时,出现了这个默认时间的强制bug,我不知道是不是mysql自身的问题?也是由于这个时间导致了我报这个错误。
原因我不明白,网上的解决办法这里有,实测有效:
jdbc:mysql://localhost:8066/coralglobal_no1?useUnicode=true&characterEncoding=utf-8&useSSL=false&zeroDateTimeBehavior=convertToNull
就是在jdbc地址后面挂个参数:&zeroDateTimeBehavior=convertToNull
后来找到原因,是mysql版本的问题,我用的是5.5.36版本的,不支持多个timestamp同时存在CURRENT_TIMESTAMP默认值,最新版的没问题。
第二个问题是线程的问题,我在 service层进行插入数据后马上进行查询时,导致没查询到数据
我觉得可能是查询的线程比插入的线程快了一小步导致的,所以我在插入 和查询操作之间强行用Thread.sleep()睡了一下,确实能解决问题,那证明我的猜想是正确 的。这里最有解我觉得是插入和查询置于两个线程中,然后插入线程用join()方法 先结束线程再进行查询操作,或者将这两个线程置于两个方法中,排定先后次序也可以解决 。后来发现不是这个问题,而是数据库做了读写分离,写是往主库中写的,读是在从库中读的,当我们写入到主库中时候,主库还要同步到从库中,而在这个空挡时间我们恰好去从库中读数据,肯定读不出数据的,我暂时没找到什么好的方法不牺牲性能的情况下解决这个问题。
三、@TableId(value = "id", type = IdType.AUTO)
当我们在数据库表中设置主键为自增时,又在持久层实体类主键使用注解开发时,需得添加(value = "id", type = IdType.AUTO)这个属性,要不然会报出如下这个错误:
Caused by: org.apache.ibatis.reflection.ReflectionException: Could not set property 'id' of 'class cn.coralglobal.model.po.UserBankAccountLog' with value '1158662342683152386' Cause: java.lang.IllegalArgumentException: argument type mismatch
自动分配一个非常大的id值,会导致参数不匹配错误,所以需要在后面加上(value = "id", type = IdType.AUTO)这个属性。
四、有时候我们需要将一个表中的数据导入到另一个表中(这里仅限于同库)
例如:上述我们需要将user表中的数据全部导入到ano_user表中:sql语句如下所述:
INSERT INTO ano_user(user_name,password,state,gmt_modify) SELECT user_name,password,type,gmt_create FROM user;
就类似于一个sql插入语句,但是要注意的是导入的字段和被导入的字段对应,类型也要对应,例如varchar导入成int类型就不行。
五、mybatis时间区间查询
有时候我们需要从数据库中查询位于某一时间区间的数据,有一下两种方式可以实现:
1)即数据库时间格式化函数date_format(),我们将之转换为统一的yyyy-MM-dd HH:mm:ss时间格式来比较。并且使用
<![CDATA[]]>来避免符号冲突。如下:
<if test="startDate != null">
AND DATE_FORMAT(gmt_create,'%Y-%m-%d %H:%i:%s') <![CDATA[>=]]> DATE_FORMAT(#{startDate,jdbcType=TIMESTAMP},'%Y-%m-%d %H:%i:%s')
</if>
<if test="endDate != null">
AND DATE_FORMAT(gmt_create,'%Y-%m-%d %H:%i:%s') <![CDATA[<=]]> DATE_FORMAT(#{endDate,jdbcType=TIMESTAMP},'%Y-%m-%d %H:%i:%s')
</if>
2)不统一格式化,直接使用<![CDATA[]]>避免符号冲突来比较,如下:
<if test="startDate != null">
AND gmt_create<![CDATA[>=]]> #{startDate,jdbcType=TIMESTAMP}
</if>
<if test="endDate != null">
AND gmt_create <![CDATA[<=]]> #{endDate,jdbcType=TIMESTAMP}
</if>
六、mybatis模糊查询
在mybatis中使用模糊查询时,连接%和字段的方法如下所示:
<if test="buyerName != null">
AND buyer_name LIKE concat('%',#{buyerName , jdbcType=VARCHAR},'%')
</if>
这是左右模糊,如果只是左模糊,那么去掉右边的%,右模糊同理,如下:
<if test="buyerName != null">
AND buyer_name LIKE concat('%',#{buyerName , jdbcType=VARCHAR})
</if>
<if test="buyerName != null">
AND buyer_name LIKE concat(#{buyerName , jdbcType=VARCHAR},'%')
</if>
七、读取excel数据去空格操作
我们读取excel表格中的数据存在空格的时候,一般用replaceAll(" ", "")或trim()方法去空格,但是有时候存在一种叫做不换行空格,这在合并单元格时经常出现,此时上述两种方式无法去除该空格,可用下面的方法:
replaceAll("\u00A0", "")
八、mybatisplus分页查询
单表操作分页查询可直接用mybatisplus代码实现,无需写xml文件,如下:
Page<BigSellerBankCard> p = new Page<>(pageSearch.getPage(), pageSearch.getLimit()); //第一个参数是页码,第二个是每页显示数
LambdaQueryWrapper<BigSellerBankCard> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BigSellerBankCard::getUserId, userId);
wrapper.eq(BigSellerBankCard::getIsDeteled, 0);
if(!StringUtils.isEmpty(pageSearch.getQueryStr())){
wrapper.and(i -> i.like(BigSellerBankCard::getUserId, pageSearch.getQueryStr())
.or().like(BigSellerBankCard::getCardholderName, pageSearch.getQueryStr())
.or().like(BigSellerBankCard::getBankCardNum, pageSearch.getQueryStr())
.or().like(BigSellerBankCard::getBankName, pageSearch.getQueryStr())
.or().like(BigSellerBankCard::getCity, pageSearch.getQueryStr())
.or().like(BigSellerBankCard::getSwiftCode, pageSearch.getQueryStr())
.or().like(BigSellerBankCard::getCompanyAddress, pageSearch.getQueryStr())
.or().like(BigSellerBankCard::getBankAddress, pageSearch.getQueryStr())
);
} //pageSearch.getQueryStr()这个参数是模糊查询输入的值
wrapper.orderByDesc(BigSellerBankCard::getCreateTime); //排序方式
IPage<BigSellerBankCard> bsbcPage = bigSellerBankCardMapper.selectPage(p, wrapper);
入参实体类如下:
package cn.coralglobal.common.model;
import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import java.io.Serializable;
/**
* 通用-分页查询
*
* @Author cjw
* @Date 2021/1/15 15:10
*/
@Data
public class PageSearch<T> implements Serializable {
/**
* 搜索关键字
*/
private String queryStr;
/**
* 当前页数
* 页数 必须大于等于0
*/
@Min(0)
private Integer page = 1;
/**
* 每页显示数
* 每页显示必须大于等于1 小于等于300
*/
@Max(300000)
@Min(1)
private Integer limit = 10;
/**
* 排序字段
*/
private String sortCol;
/**
* 排序类型
* 0 倒序 1 正序
*/
private Integer sortType = 0;
/**
* 其他查询参数实体
*/
private T queryEntity;
}
这里还要配合mybatisplus分页插件使用
package com.chen.mysql.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description: mybatisplus分页插件
* @Author chenjianwen
* @Date 2021/4/29
**/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//向Mybatis过滤器链中添加分页拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
//还可以添加i他的拦截器
return interceptor;
}
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
}
还有入参非空校验的注解如下:
@NotNull(message = "银行卡号不能为空")
@Length(min = 10, message = "银行卡号最低10位")
private String bankCardNum;
九、注解转换时间戳
前后端日期都是用时间戳传递的,我们后端拿到时间戳还要手动转换成Date类型,现在有一个注解可以帮我们自动转换,如下:
@JsonFormat(pattern="yyyy-MM-dd",timezone = "GMT+8")
十、返回自定义参数非空信息
有时候controller接口我们会校验参数非空,我们希望返回自定义的返回信息 ,如下这样
我们需要在接口校验的类后面紧跟一个 BindingResult这个类才行,如下
引入的是这个类中的
十一、关于使用JSON.toJSONString()返回中文乱码问题
有时候我们希望利用alibaba的fastjson将数据转换为String返回给接口,当数据中含有中文时会出现乱码的问题,此时我们可以在map中添加produces = "text/plain;charset=UTF-8";属性,如下
十二、事务中使用try{}catch(){}导致事务不会滚的问题
我们会在多数据库写的操作时,一般会添加事务,如下:
并且在事务里会用到try{}catch(){}用来捕获异常,但是如果不做一些处理的话,发生异常被捕获了但是事务并没有回滚,这个问题只需要在捕获的异常里面抛出运行时异常就行,异常信息通过日志打印出来就行,如下:
十三、获取请求头数据
有时候别人调用我们接口数据需要类似token的数据验证,这些数据一般放在请求头里面,如下这样:
我们需要获取这些参数,然后验证。获取方式有两种:
(1)使用@RequestHeader注解获取
(2) 使用httpServletRequest获取
推荐使用第二种。
十四、使用http调用远程服务
(1)post请求,需要请求头Authorization验证
/**
*
* @param url 远程url
* @param json 请求体数据
* @param authorization 验证token
* @return
*/
public static String doPost(String url, JSONObject json, String authorization) {
HttpClient client = HttpClientBuilder.create().build(); // 获取DefaultHttpClient请求
HttpPost post = new HttpPost(url);
String response = null;
try {
if(json != null){
StringEntity s = new StringEntity(json.toString());
s.setContentEncoding("UTF-8");
s.setContentType("application/json");//发送json数据需要设置contentType
post.setEntity(s);
}
if(StringUtils.isNotBlank(authorization)){
post.setHeader("Authorization", authorization);
}
HttpResponse res = client.execute(post);
response = EntityUtils.toString(res.getEntity());// 返回json格式:
} catch (Exception e) {
throw new RuntimeException(e);
}
return response;
}
参数jsonObject如下定义
(2)get请求
/**
*
* @param url 远程url
* @param authorization 验证token
* @return
*/
public static String doGet(String url, String authorization){
HttpClient client = HttpClientBuilder.create().build(); // 获取DefaultHttpClient请求
HttpGet get = new HttpGet(url);
String response = null;
try {
if(StringUtils.isNotBlank(authorization)){
get.setHeader("Authorization", authorization);
}
HttpResponse res = client.execute(get);
response = EntityUtils.toString(res.getEntity());// 返回json格式:
} catch (Exception e) {
throw new RuntimeException(e);
}
return response;
}
(3)post传递文件
/**
* http上传thunes文件
* @param url 远程url
* @param file 文件
* @param contentType 文件后缀名类型
* @param authorization 验证token
* @return
*/
public static String httpPostFile(String url, File file, String contentType, String authorization){
CloseableHttpClient httpClient = HttpClients.createDefault();
String result = "";
//每个post参数之间的分隔。随意设定,只要不会和其他的字符串重复即可。
String boundary = "----form-boundary-123";
try {
//文件名
HttpPost httpPost = new HttpPost(url);
//设置请求头
httpPost.setHeader("Content-Type", "multipart/form-data; boundary="+boundary);
//HttpEntity builder
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
//字符编码
builder.setCharset(Charset.forName("UTF-8"));
//模拟浏览器
builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
builder.setContentType(ContentType.MULTIPART_FORM_DATA);
//boundary
builder.setBoundary(boundary);
//multipart/form-data
// builder.addPart("document", new InputStreamBody(file, ContentType.create(contentType)));
builder.addPart("document", new FileBody(file, ContentType.create(contentType)));
//HttpEntity
HttpEntity entity = builder.build();
httpPost.setEntity(entity);
if(StringUtils.isNotBlank(authorization)){
httpPost.setHeader("Authorization", authorization);
}
// 执行提交
HttpResponse response = httpClient.execute(httpPost);
//响应
HttpEntity responseEntity = response.getEntity();
// 将响应内容转换为字符串
result = EntityUtils.toString(responseEntity, Charset.forName("UTF-8"));
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
其中contentType文件类型参考如下
十五、将一张网络文件写到本地
将网络文件写到本地File类型,可用于十四里面上传文件需求
/**
*
* @param fileUrl 网络文件,如 https://images1.coralglobal.cn/20180410/5763452865647252.jpg 这样
* @param localPath 本地文件夹,如我的是mac系统 "/Users/chenjianwen/file"
* @return
*/
public static File urlToFile(String fileUrl, String localPath) throws Exception {
String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
//获取该链接的字节数组
URL url =new URL(fileUrl);
InputStream inputStream = url.openStream();
byte[] res = IoUtil.readBytes(inputStream);
//将文件字节数组写到一个临时文件中
File dir = new File(localPath);
if(!dir.exists()){
dir.mkdir();
}
File file = new File(localPath + File.separator + fileName);
FileOutputStream fos = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(fos);
bos.write(res);
return file;
}
十六、mysql报Data truncated for column at row 1这个错误
有些表的字段是这样的:
需要对这个字段进行modify操作变成NOT NULL DEFAULT ""类型,就会报Data truncated for column at row 1错误,因此我们需要modify之前将null类型的数据全部改成空字符串,如下:
update user_auth set RECORD_IMAGE = "" where RECORD_IMAGE is null;
ALTER TABLE user_auth MODIFY COLUMN RECORD_IMAGE varchar(500) NOT NULL DEFAULT '' COMMENT '企业备案登记表图片如果是香港认证则为商业登记证';
十七、加@Transactionl事物无法会滚的问题
@Transactional
public void insertUser(User user) throws Exception{
boolean flag = true;
try{
// 新增用户
insertUser(user);
// 新增用户与角色管理
insertUserRole(user);
} catch (Exception e){
e.printStackTrace();
}
}
这种捕获到异常后事物无法会滚,比如新增用户正常插入数据,新增用户与角色关系管理报错,插入了一条数据,这是不对的。因为e.printStackTrace();已经处理了异常,不会会滚,如下这样才会会滚:
@Transactional
public void insertUser(User user) throws Exception{
boolean flag = true;
try{
// 新增用户
insertUser(user);
// 新增用户与角色管理
insertUserRole(user);
} catch (Exception e){
e.printStackTrace();
throw new RuntimeException("系统异常");
}
}
抛出运行时异常会会滚。
十八、String.format()的使用
format(String format, Object... args)的使用,format文本信息中替换文本为占位符,args会替换占位符的参数。
如String.format("我是%s,我的年龄是%d岁", "韩跑跑", 1800); //我是韩跑跑,我的年龄是1800岁。
如果format中含有json串,需要使用3个"""双引号,以此来避免转义字符。
@Test
public void test5(){
String format = String.format(
"""
实验室材料%s,学生专业%s,JSON字段:
{"examName":"","type":"","question":"","options":"","answer":"","analysis":"","serialNum":""}
"""
,
"高分子材料",
"无机非金属");
System.out.println(format);
}