yunjl 发表于 2021-11-19 10:45

【笔记】整理实际开发中遇到的问题及解决方法

本帖最后由 yunjl 于 2021-11-20 00:38 编辑

整理下我从开始工作遇到的一些问题吧,并附上解决方法。(持续更新)
有些问题可能会有更好的解决方法,欢迎补充。
## SQL 语句相关
### SQL 语句查集合数组(foreach )
foreach 也就是遍历迭代,在 SQL 中通常用在 in 这个关键词的后面
foreach 元素的属性主要有 item,index,collection,open,separator,close。
分别代表:
                        item 表示集合中每一个元素进行迭代时的别名,
                        index 用于表示在迭代过程中,每次迭代到的位置,
                        open 表示该语句以什么开始,
                        separator 表示在每次进行迭代之间以什么符号作为分隔 符,
                        close 表示以什么结束
                        
      <select id="selectByIds" resultType="com.txw.pojo.User">
                                        select * from user where id in
                                        <foreach collection="list" index="index" item="item" open="(" separator="," close=")">
                                                      #{item}
                                        </foreach>
      </select>

### SQL 表中没有数据插入有则特殊处理
首先看下表结构:id为主键
[!(https://z3.ax1x.com/2021/11/19/I75uaq.png)](https://imgtu.com/i/I75uaq)
#### 没有数据插入,有则不插入

      <insert id="insert">
                        INSERT INTO user ( user_id, user_name)
                        SELECT#{userId}, #{userName}
                        from DUAL
                        wherenot exists(select * from user where user_id=#{userId} and user_name = #{userName});
      </insert>
#### 没有数据插入,有则更新
主键(索引)唯一可区分的时候可以用       `ON DUPLICATE KEY UPDATE`
表中没有id为2的数据则插入“zhangsan”,有的话则更新为“lisi”

                insert into `user`(id, user_name)
                              values
                              (2, "zhangsan")
                              ON DUPLICATE KEY UPDATE
                                        id = 2 , user_name = "lisi";
当然也可以多条插入更新:

                        insert into `user`(id, user_name)
                                        values
                                        (2, "zhangsan"),
                                        (3,"yanyanyanyan")
                                        ON DUPLICATE KEY UPDATE
                                                 user_name = VALUES(user_name);
不过业务中最常用的就是这样吧:

         <insert id="insert">
                                        insert into user(user_id, user_name)
                                        values
                                                      ({userId}, #{userName})
                                        ON DUPLICATE KEY UPDATE
                                                #{userId}, #{userName}
                        </insert>
### 更新语句(有数据则更新,没有数据则不变)
      <update id="update" >
                        UPDATE user
                        SET
                        <if test="userId != null and userId !=''">user_id=#{userId},</if>
                        <if test="userName != null and userName !=''">user_name=#{userName},</if>
                        <if test="userPassword != null and userPassword !=''">user_password=#{userPassword},</if>
                        where
                        id = #{id}
      </update>
### 插入数据(更新数据)返回修改的数据的id
只需要在其对应xxxmapper.xml标签中加入以下属性即可:
useGeneratedKeys=”true” keyProperty=”id”
比如这样:

      <insert id="insert" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
                                                insert into user(user_id, user_name)
                                                values
                                                                ({userId}, #{userName})
      </insert>
useGeneratedKeys:默认为false,为true时,表示如果插入的表以自增列为主键,则允许 JDBC 支持自动生成主键,并可将自动生成的主键返回;
keyProperty:实体类对应主键的属性;
keyColumn:数据库中的主键;
这样的话插入的时候就会返回插入的的这条数据的id,在service层通过`user.getId`可以获取这个id
### 表中的一个字段含有多个值
字段名用 varchar 类型,值使用“,”拼接。
[!(https://z3.ax1x.com/2021/11/19/I75NZR.png)](https://imgtu.com/i/I75NZR)
筛选只包含“SALES”的数据 `FIND_IN_SET`

      SELECT
                        realname
      FROM
                        org_user
      WHERE
                        org_id = 23
                        AND FIND_IN_SET('SALES', org_role)

## 业务相关
### 多表操作的时候一定要加事务
最常见的就是添加事务注解    `@Transactional(rollbackFor = Exception.class)`可以加在方法上也可以加在类上

      @Override
      @Transactional(rollbackFor = Exception.class)
      public void add(User user) throws Exception {
                //XXX业务处理
      }
如果想深入了解这个注解何时会失效,自行百度哦,这里一句两句说不完。

### 报错:Request method 'POST' not supported
   可以把注解 @ Controller 换成 @ RestController 试试,出现这个问题的原因很多

### 唯一的随机数
说起唯一的随机数,可能很容易想到 java.util.UUID,如下,可以产生 32 位长度的字符串。
`String str = UUID.randomUUID().toString().replaceAll("-", "");`
但是这样并不是一定没问题的,单机多线程的情况下也可能出现重复的情况。
为确保产生的字符串唯一,可在 uuid 后加一个随机数,如果再能加唯一用户名,电话 xxx 的就更加万无一失了。

      String str = UUID.randomUUID().toString().replaceAll("-", "") + new Random().nextLong();
      // 产生的字符串太长,浪费存储,再进行 MD5
      // 可以使用 apache 的 org.apache.commons.codec.digest.DigestUtils
      // 也可以是使用 java.security.MessageDigest 进行加密
      // 注意返回的是长度为 16 的 byte 数组,使用 Hex 转换成 32 的 char 数组,在转成字符串
      String uuid = new String(Hex.encodeHex(DigestUtils.md5(str)));
简单版就这样就可以了:
private static String nonce = RandomUtil.randomString(16);   //唯一随机数

### 获取指定日期的月初和月末日期
需求:客户想要给定日期的近三个月的数据

      SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
      String time ="2021-10-12";
      Date timeA = format.parse(time);
      Calendar c = Calendar.getInstance();
      c.setTime(timeA);
      //设置为上个月1号
      c.add(Calendar.MONTH, -1);
      c.set(Calendar.DAY_OF_MONTH, 1);
      String startFormat = format.format(c.getTime());

      Calendar d = Calendar.getInstance();
      d.setTime(timeA);
      //设置为下个月最后一天
      d.add(Calendar.MONTH, +1);
      d.set(Calendar.DAY_OF_MONTH,d.getActualMaximum(Calendar.DAY_OF_MONTH));
      String endFormat = format.format(d.getTime());
      System.out.println(startFormat);
      System.out.println(endFormat);
执行结果:
      `2021-09-01`
      `2021-11-30`

### 依赖引入爆红
这是因为jdk版本引发的,虽然不影响运行,但是强迫症表示非常不舒服
[!(https://z3.ax1x.com/2021/11/19/I77fBQ.png)](https://imgtu.com/i/I77fBQ)
解决办法:
在 mapper 接口上加个注解`@Repository`
[!(https://z3.ax1x.com/2021/11/19/I7HPgK.png)](https://imgtu.com/i/I7HPgK)
### 解决“请在微信客户端打开链接”
[!(https://z3.ax1x.com/2021/11/19/I7bXk9.png)](https://imgtu.com/i/I7bXk9)
浏览器设置:
[!(https://z3.ax1x.com/2021/11/19/I7bzSx.png)](https://imgtu.com/i/I7bzSx)
MicroMessenger(如果是需要授权的界面会显示空白页)
请求:
请求头中设置参数
`User-Agent=Mozilla/5.0 (Linux; U; Android 2.3.6; zh-cn; GT-S5660 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 MicroMessenger/4.5.255`
[!(https://z3.ax1x.com/2021/11/19/I7qPmD.png)](https://imgtu.com/i/I7qPmD)
还有就是使用 Fiddler 对手机抓包解决,比较麻烦点不过也是最好用的,具体百度吧,步骤太多了。

### 13 位时间戳:

      String timestamp = System.currentTimeMillis()+"";       //时间戳

### 传给mapper.xml多个参数时
使用@Param注解标注
@Param 是 MyBatis 所提供的,作为 Dao 层的注解,作用是用于传递参数,从而可以与 SQL 中的的字段名相对应,一般在 2=<参数数<=5 时使用最佳。
      Mapper 接口方法:

      public int getUsersDetail(@Param("userid") int userid);
对应 Sql Mapper.xml 文件:

      <select id="getUserDetail" statementType="CALLABLE" resultMap="baseMap">
                                                SELECT * FROM user_detail WHERE user_id =#{userid}
         </select>
说明:
当你使用了@Param 注解来声明参数时,使用 #{} 或 ${} 的方式都可以,当你不使用@Param 注解来声明参数时,必须使用使用 #{}方式。如果使用 ${} 的方式,会报错。

### 文件上传的格式检验
这个一般前端进行校验就行,有的需求需要防止用户改文件后缀名进行上传,这个时候就要前后端一起校验了,
这个后端校验当时真是被搞的头大,我采用的是验证文件的文件头,写了一个工具类,列举了常用的文件格式的文件头,如下:

      private static void getAllFileType(){
                                        FILE_TYPE_MAP.put("ffd8ffe000104a464946", "jpg"); //JPEG (jpg)
                                        FILE_TYPE_MAP.put("89504e470d0a1a0a0000", "png"); //PNG (png)
                                        FILE_TYPE_MAP.put("47494638396126026f01", "gif"); //GIF (gif)
                                        FILE_TYPE_MAP.put("49492a00227105008037", "tif"); //TIFF (tif)
                                        FILE_TYPE_MAP.put("424d228c010000000000", "bmp"); //16 色位图(bmp)
                                        FILE_TYPE_MAP.put("424d8240090000000000", "bmp"); //24 位位图(bmp)
                                        FILE_TYPE_MAP.put("424d8e1b030000000000", "bmp"); //256 色位图(bmp)
                                        FILE_TYPE_MAP.put("41433130313500000000", "dwg"); //CAD (dwg)
                                        FILE_TYPE_MAP.put("3c21444f435459504520", "html"); //HTML (html)
                                        FILE_TYPE_MAP.put("3c21646f637479706520", "htm"); //HTM (htm)
                                        FILE_TYPE_MAP.put("48544d4c207b0d0a0942", "css"); //css
                                        FILE_TYPE_MAP.put("696b2e71623d696b2e71", "js"); //js
                                        FILE_TYPE_MAP.put("7b5c727466315c616e73", "rtf"); //Rich Text Format (rtf)
                                        FILE_TYPE_MAP.put("38425053000100000000", "psd"); //Photoshop (psd)
                                        FILE_TYPE_MAP.put("46726f6d3a203d3f6762", "eml"); //Email (eml)
                                        FILE_TYPE_MAP.put("d0cf11e0a1b11ae10000", "doc"); //MS Excel 注意:word、msi 和 excel 的文件头一样
                                        FILE_TYPE_MAP.put("255044462d312e350d0a", "pdf"); //Adobe Acrobat (pdf)
                                        FILE_TYPE_MAP.put("2e524d46000000120001", "rmvb"); //rmvb/rm 相同
                                 ...等
                        }
我写好我自己的电脑测试没问题,然后就提交了,这个时候离谱的就来了,我同事是苹果电脑,Excel文件在他的电脑上修改保存过文件头就变了,识别成其他的格式,而且不同版本的Excel创建的文件文件头都不一样,都识别成其他的格式,很乱,后来也没有找到很好的办法解决这个,只能在识别后的判断处放松点。
### @Valid 无法校验List`<E>`
@Valid只能校验JavaBean,而List<E>不是JavaBean所以校验会失败,尝试了三种解决办法,
#### 方法1:对List进行Wrapper
既然List不是JavaBean,那我们就把它封装成JavaBean,我们定义一个ListWrapper类如下:

      package com.wyq.firstdemo.util;
      import lombok.Getter;
      import lombok.Setter;
      import javax.validation.Valid;
      import java.util.ArrayList;
      import java.util.List;
      @Setter
      @Getter
      public class ListWrapper<E> {
                        @Valid
                        private List<E> list;

                        public ListWrapper() {
                                        list = new ArrayList<>();
                        }

                        publicListWrapper(List<E> list) {
                                        this.list = list;
                        }

      }
同时修改一下controller对应的方法:

      // 使用包装类对list进行验证
                @PostMapping("/insert/all")
                public ServerResponse<String> insertList(@Valid @RequestBody ListWrapper<UserEntity> listWrapper, BindingResult bindingResult) {
                              if(bindingResult.hasErrors()) {
                                                log.error(bindingResult.getFieldError().toString());
                                                return ServerResponse.createByErrorMessage(bindingResult.getFieldError().getDefaultMessage());
                              }

                              userService.insertList(listWrapper.getList());
                              return ServerResponse.createBySuccess();
                }
这样就可以对list进行校验了
注意:
由于对list进行了包装,如果我们传参的时候
[{},{}..]要改为{“list”: [{},{}..]}
#### 方法2:使用@Validated+@Valid
在controller类上面增加@Validated注解,并且删除方法参数中的BindingResult bindingResult(因为这个参数已经没有用了,异常统一有controller返回了)
[!(https://z3.ax1x.com/2021/11/19/Iqtrm4.png)](https://imgtu.com/i/Iqtrm4)

[!(https://z3.ax1x.com/2021/11/19/IqtckR.png)](https://imgtu.com/i/IqtckR)
运行一下
[!(https://z3.ax1x.com/2021/11/19/Iqtf1K.png)](https://imgtu.com/i/Iqtf1K)
可以看到可以对参数进行校验了,但还还有一个问题,那就是这个不是我们想要的返回格式,它controller自己返回的格式,所以我们需要做一个统一异常处理,代码如下:

      @Slf4j
      @RestControllerAdvice
      public class ControllerExceptionHandler {

                        @ExceptionHandler
                        @ResponseBody
                        public ServerResponse<String> handle(ConstraintViolationException exception) {
                                        log.error(String.valueOf(exception));
                                        Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();
                                        StringBuilder builder = new StringBuilder();
                                        for (ConstraintViolation violation : violations) {
                                                      builder.append(violation.getMessage());
                                                      break;
                                        }
                                        return ServerResponse.createByErrorMessage(builder.toString());
                        }
      }
经过统一异常处理,我们这边的返回结果就是我们想要的格式了
#### 方法3:自定义一个List(推荐吧)
先定义一个ValidList

      /**
         * 可被校验的List
         *
         * @param <E> 元素类型
         * @author Deolin
         */
         public class ValidList<E> implements List<E> {

                        @Valid
                        private List<E> list;

                        public ValidList() {
                                        this.list = new ArrayList<>();
                        }
                        public ValidList(List<E> list) {
                                        this.list = list;
                        }
                        public List<E> getList() {
                                        return list;
                        }
                        public void setList(List<E> list) {
                                        this.list = list;
                        }
                        ....重写方法
                }
对比方法3和方法1,有没有觉得代码有点相似,新建一个类,并且让他实现List接口,使这个类即具有了JavaBean的特性,又具有了List的特性,比方法1简单优雅很多。
只需要把List换成ValidList就可以了,还不需要多统一异常处理。
[!(https://z3.ax1x.com/2021/11/19/IqtvjS.png)](https://imgtu.com/i/IqtvjS)

yunjl 发表于 2021-11-19 11:05

可能是Markdown语法的问题,有些出现了错误,我明天更新的时候修改下,抱歉了

xiaolong23330 发表于 2021-11-19 11:12

特殊符号那块没看懂

yunjl 发表于 2021-11-19 11:16

xiaolong23330 发表于 2021-11-19 11:12
特殊符号那块没看懂

这是因为MarkDown语法导致的
原文是这样 &lt; &lt;= &gt; &gt;= &amp; &apos; &quot;

RatCreak 发表于 2021-11-19 13:32

特殊符号如何输入?

yunjl 发表于 2021-11-19 13:34

RatCreak 发表于 2021-11-19 13:32
特殊符号如何输入?

原符号 < <= > >= & ’ "
替换符号 &lt; &lt;= &gt; &gt;= &amp; &apos; &quot;

vkery 发表于 2021-11-19 13:52

日期的话试试LocalDate
      LocalDate of = LocalDate.of(2021, 10, 15);
      LocalDate start = of.minusMonths(1).with(TemporalAdjusters.firstDayOfMonth());
      System.out.println(start);
      LocalDate end = of.plusMonths(1).with(TemporalAdjusters.lastDayOfMonth());
      System.out.println(end);

yunjl 发表于 2021-11-19 16:17

本帖最后由 yunjl 于 2021-11-19 16:18 编辑

可以的
      
String str = "2019-03-03";
      //指定转换格式
      DateTimeFormatter fmt2 = DateTimeFormatter.ofPattern("yyyy-MM-dd");
      //进行转换
      LocalDate date = LocalDate.parse(str, fmt2);
      LocalDate start = date.minusMonths(1).with(TemporalAdjusters.firstDayOfMonth());
      System.out.println(start);
      LocalDate end = date.plusMonths(1).with(TemporalAdjusters.lastDayOfMonth());
      System.out.println(end);
页: [1]
查看完整版本: 【笔记】整理实际开发中遇到的问题及解决方法