项目细节总结

该博客记录项目开发细节,涵盖实体类定义与转换、MyBatis查询(时间区间、模糊、分页)、MySQL问题处理(版本、数据导入、报错),还涉及事务回滚、请求头获取、远程服务调用等,为开发人员提供实用参考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       这里记录一些项目中一些细节,便于以后查找方便。

 一、实体类的定义

        我们在定义实体类时,一般会定义三个: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);
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值