消除代码里的坏味道

1.参数校验能简单点吗

1.1 常规校验

通常我们校验前端传来的参数是在controller层写很多if...else来进行检验,如下:

     /**
     * 查询用户信息
     *
     * @param userVo
     * @return
     */
    @RequestMapping("/getUser")
    public ShareResult getUser(@RequestBody UserVo userVo) {
        this.check(userVo);
        return shareService.getUser(userVo);
    }

    /**
     * 参数校验
     *
     * @param userVo
     */
    private void check(UserVo userVo) {
        if (StrUtil.isBlank(userVo.getUserName())) {
            throw new RuntimeException("name is not null");
        }
        if (Objects.isNull(userVo.getId())) {
            throw new RuntimeException("id is not null");
        }
        if (Objects.isNull(userVo.getAge())) {
            throw new RuntimeException("age is not null");
        }
        if (userVo.getAge() < 0 || userVo.getAge() > 130) {
            throw new RuntimeException("age is illegal");
        }
    }

假如校验的字段多的话上面的校验代码非常长,不简洁和美观,也不容易维护,可能漏校验某些字段,我们可以使用@Valid注解配合validation注解进行校验,非常好用。

1.2 使用@Valid注解配合validation注解校验

// controller
  @RequestMapping("/getUserBetter")
    public ShareResult getUserBetter(@Valid @RequestBody UserVo userVo) {
        return shareService.getUser(userVo);
    }

// 实体类
@Data
@Accessors(chain = true)
public class UserVo implements Serializable {
  
@NotNull(message = "id不能为空")
@Range(min = 0, message = "id不能小于0")
private Long id;

@NotBlank(message = "姓名不能为空")
private String userName;

@NotNull(message = "id不能为空")
@Range(min = 0, max = 130, message = "年龄输入范围0-130")
private Integer age;

@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式错误",
        regexp = "^([a-z0-9A-Z]+[-|_|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$")
private String email;
  }

在实体类就已经把参数给校验了,不用再写一大长串的if...else...了。对于@Valid不满足的校验我们只能单独处理了,大部分的校验还是能满足的。

1.3 常见的validation注解使用

限制说明
@Null限制只能为null
@NotNull限制必须不为null
@AssertFalse限制必须为false
@AssertTrue限制必须为true
@DecimalMax(value)限制必须为一个不大于指定值的数字
@DecimalMin(value)限制必须为一个不小于指定值的数字
@Future限制必须是一个将来的日期
@Max(value)限制必须为一个不大于指定值的数字
@Min(value)限制必须为一个不小于指定值的数字
@Past限制必须是一个过去的日期
@Pattern(value)限制必须符合指定的正则表达式
@Size(max,min)限制字符长度必须在min到max之间
@NotEmpty字符串长度不为0、集合大小不为0)
@NotBlank验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Email验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式

2.去掉讨厌的非null校验

2.1 常规的非空校验

平时我们开发中会有很多判断 xxx==null,xxx!=null,这样的校验,简单粗暴好用,但是比较丑陋,

假设有这样的场景,根据一些用户信息去查询数据库,然后把查出的结果用户姓名转大写,我们通常的写法是这样的

     /**
     * 查询用户信息
     *
     * @param userVo
     * @return
     */
    @Override
    public ShareResult getUser(UserVo userVo){
        UserEntity user = this.getUserInfo(userVo);
        if (user != null) {
            UserEntity userEntity = this.doSomething(user);
            return ShareResult.of().setCode(0).setData(userEntity).setMessage("ok");
        } else {
            throw new MyException(2,"查询失败");
        }
    }

   /**
     * 查询数据库方法
     *
     * @param userVo
     * @return
     */
    private UserEntity getUserInfo(UserVo userVo) {
        // 省略查库方法
        UserEntity userEntity = new UserEntity();
        return userEntity;
    }

    /**
     * 数据处理
     *
     * @param userEntity
     * @return
     */
    private UserEntity doSomething(UserEntity userEntity) {
        userEntity.setUser_name(userEntity.getUser_name().toUpperCase());
        return userEntity;
    }

上面的代码完全满足了需求,但是代码写的太丑了,下面对user != null进行优化

2.2 用Optional来代替非空校验,代码优化一

  /**
     * 查询用户信息-better
     * isPresent判断是不是空
     * get获取结果
     *
     * @param userVo
     * @return
     */
    private ShareResult getUserBetter(UserVo userVo) {
        Optional<UserEntity> user = this.getUserInfoBetter(userVo);
        if (user.isPresent()) {
            UserEntity entity = user.get();
            UserEntity userEntity = this.doSomething(entity);
            return ShareResult.of().setCode(0).setData(userEntity).setMessage("ok");
        } else {
            throw new RuntimeException("查询失败");
        }
    }

    /**
     * 查询数据库方法-better
     * mybatis已经支持Optional了,雨燕框架目前还不支持,需要我们对结果手动包一下返回
     *
     * @param userVo
     * @return
     */
    private Optional<UserEntity> getUserInfoBetter(UserVo userVo) {
        // 省略查库方法
        // Optional<UserEntity> entity = userDao.getUserInfoBetter()
        return Optional.of(new UserEntity());
    }

优化的地方主要有两个地,一是数据库结果返回用Optional.of()把结果包起来了,第二个是之前的user != null换成了user.isPresent(),这俩改动我们后面在说,现在我们已经把null校验给干掉了,上面的代码看起来很简洁了,但是我们还是可以利用Optional提供的api继续优化。

2.3 Optional优化代码二

/**
     * 查询用户信息-better2
     * Optional的map操作和orElseThrow操作
     * map操作,其实就是取上一步返回的非空结果
     * orElseThrow取到值就返回,否则抛异常
     *
     * @param userVo
     * @return
     */
    private ShareResult getUserBetter2(UserVo userVo) {
        return this.getUserInfoBetter(userVo)
                .map(this::doSomething)
                .map(s -> ShareResult.of().setCode(0).setData(s).setMessage("ok"))
                .orElseThrow(() -> new RuntimeException("查询失败"));
    }

上面的代码用1行就完成了最开始的几行代码,主要是用了map和orElseThrow这俩Optional提供的api,接下来就讲下 Optional的使用

2.4 Optional的使用讲解

Java8中引入了一个叫做java.util.Optional的新类可以避免null引起的一些问题,直接来看使用吧。

创建Optional对象的3种方式
1.Optional.of(object) 创建的对象不能为空,否则会报错,如果我们确认对象一定不为空可以用,否则用下面的
2.Optional.ofNullable(object) 创建的对象可为空
3.Optional.empty() 创建一个空对象
其他api介绍
Optional.isPresent() 判断是不是为空
Optional.get() 获取对象
Optional.ifPresent(u->{}) 如果对象存在会执行里面的lambda表达式代码
Optional.map(u->{return u;}) 如果对象存在会执行里面的lambda表达式代码,必须有return返回值,如果返回一行代码可省略
Optional.filter(u->{return u;}) 如果对象存在会执行里面的lambda表达式代码过滤,必须有返回值
Optional.orElseThrow() 如果对象为空,抛异常,否则执行map或者filter链式操作的代码
Optional.orElseGet(() -> new Object()) 如果返回对象不为空则返回该对象,否则返回括号里面的对象
Optional.orElse(new Object()) 如果返回对象不为空则返回该对象,否则返回括号里面的对象

    @Test
    public void studyOptional() {
        UserVo userVo = new UserVo();
        UserVo userVo2 = new UserVo()
                .setUserName("cq")
                .setAge(12)
                .setEmail("6@6.srl")
                .setId(1L);

        // 创建Optional对象
        Optional<UserVo> userVoNotNull = Optional.of(userVo);
        Optional<UserVo> userVoCanNull = Optional.ofNullable(userVo2);

        // Optional.isPresent()
        if (userVoNotNull.isPresent()) {
            // Optional.get()
            UserVo vo = userVoNotNull.get();
            System.out.println(vo.getEmail());
        }
  
       // Optional.isPresent()
        if (userVoCanNull.isPresent()) {
            UserVo vo = userVoCanNull.get();
            System.out.println(vo.getEmail());
        }

        // Optional.ifPresent()
        userVoNotNull.ifPresent(u -> {
            String email = u.getEmail();
            System.out.println(email);
        });

        // Optional.map()
        // Optional.filter()
        // Optional.orElseThrow()
        // Optional.orElseGet()
        // Optional.orElse()
        UserVo userVoHandle = userVoCanNull
                .map(u -> {
                    return u.setEmail(u.getEmail().toUpperCase());
                })
                .map(u -> u.setUserName(u.getUserName().toUpperCase()))
                .filter(u -> {
                    return u.getAge() > 10;
                })
                //.orElseGet(() -> new UserVo());
                //.orElse(new UserVo());
                .orElseThrow(() -> new RuntimeException("null"));

        System.out.println(JSON.toJSONString(userVoHandle));
    }

3.聊聊Bean.copyProperties

刚来公司熟悉框架代码时候发现属性的赋值都是get,set,字段多的话甚至几百行的代码都在赋值,我在想为啥不用Bean.copyProperties呢,后来咨询几位大佬,告知是雨燕框架规定了不能用,因为效率比较低,所以一直都没人用,后来我看了下框架的规定:

之前我自己使用的一直都是Spring提供的属性copy工具类,Apache的没用过,性能真的很差吗,直接上代码吧

     /**
     * 雨燕框架规定:
     * 【强制】 避免用 Apache Beanutils 进行属性的 copy。说明: Apache BeanUtils 性能较差。
     * <p>
     * 几种属性copy方式性能对比
     * 1.cglib BeanCopier
     * 2.spring BeanUtils.copyProperties
     * 3.apache PropertyUtils
     * 4.apache BeanUtils
     */
    @Test
    public void testCope() {
  Map<String, Object> map = Maps.newHashMap();
    UserVo userVo = new UserVo()
            .setUserName("cq")
            .setAge(10)
            .setId(1L)
            .setEmail("6@6.srl");
    long l1 = System.currentTimeMillis();
    for (int i = 0; i < 100000; i++) {
        UserEntity entity = new UserEntity();
        BeanUtil.copyProperties(userVo, entity);
    }
    map.put("hutool cglib", System.currentTimeMillis() - l1);

    long l2 = System.currentTimeMillis();
    for (int i = 0; i < 100000; i++) {
        UserEntity entity = new UserEntity();
        BeanUtils.copyProperties(userVo, entity);
    }
    map.put("spring", System.currentTimeMillis() - l2);

    long l3 = System.currentTimeMillis();
    for (int i = 0; i < 100000; i++) {
        UserEntity entity = new UserEntity();
        try {
            org.apache.commons.beanutils.PropertyUtils.copyProperties(entity, userVo);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    map.put("apache PropertyUtils", System.currentTimeMillis() - l3);

    long l4 = System.currentTimeMillis();
    for (int i = 0; i < 100000; i++) {
        try {
            UserEntity entity = new UserEntity();
            org.apache.commons.beanutils.BeanUtils.copyProperties(entity, userVo);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    map.put("apache BeanUtils", System.currentTimeMillis() - l4);
    System.out.println("result:" + JSON.toJSONString(map));
}

结论:通过几次测试我们发现,在copy的次数少量的情况下,目前几款属性copy都是可以用的,性能都不错,但是copy次数多的情况下,spring和cglib的性能要比Apache要高。但是雨燕框架entity里面不支持驼峰,需要对每个字段单独处理,还是比较费劲的,所以暂时还是不建议使用属性copy工具。

4.异步操作必须用线程池吗

一般我们开始一个异步的操作直接使用的是线程池,JAVA8在JUC工具类给我们提供了更好用的CompletableFuture,直接来看使用吧。

4.1 常用的api:

1.CompletableFuture.supplyAsync() 创建异步线程方式一 需要返回值
2.CompletableFuture.runAsync() 创建异步线程方式二 不需要返回值 这种方式不会输出异常信息,需要自己处理
3.thenApply() 把上个输出流变成输入流进入
4.thenAccept() 拿到输出结果进行处理
5.get();拿到返回结果
6.CompletableFuture.supplyAsync(() -> do(), executor) 使用我们自己创建的线程池,非自带的ForkJoinPool.commonPool()

4.2 上面的api工作中已经差不多够用了,下面是其他的api,感兴趣可以了解下 -.-

7.allOf().join(); 线程走到这里会暂停直到取到所有的future结果
8.thenCompose方法允许你对两个异步操作进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作
9.whenComplete((v, e)->) v是上个流水线返回值,e输出异常信息
10.handle() 对上个流水线返回值处理
....

4.3 其他

这里单独说下异步操作使用自己的线程池 -> CompletableFuture.supplyAsync(() -> do(), executor)
简单介绍:ForkJoinPool 是java8默认的公用共享线程池。
异步操作supplyAsync()和runAsync(), 集合stream并发流操作 parallelStream().forEach(),默认用的都是这个。
不是为了替代ExecutorService,而是它的补充,在某些应用场景下性能比ExecutorService更好。
核心是分治算法实现,适合的是计算密集型任务。
构造源码:{@link ForkJoinPool#makeCommonPool()} ()}
(parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0)
默认创建的线程数是当前机器核心数-1,但是可以手动设置:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20");

ForkJoinPool原理:

1) ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。
2) 每个工作线程在运行中产生新的任务时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是 LIFO(后进先出) 方式,也就是说每次从队尾取出任务来执行。
3) 每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务执行。
4) 在既没有自己的任务,也没有可以窃取的任务时,进入休眠。

    @Test
    @SneakyThrows
    public void testSyncCompletableFuture() {
        // 1.查询用户信息
        Optional<UserEntity> userInfoBetter = this.getUserInfoBetter(new UserVo());
        Long userId = userInfoBetter
                .map(UserEntity::getId)
                .orElseThrow(() -> new RuntimeException("用户不存在!"));
     // 2.根据用户ID异步查询会员信息,然后再根据会员信息查询优惠信息
    // 2.1 supplyAsync()
    // thenApply()
    // thenAccept()
    CompletableFuture
            .supplyAsync(() -> this.findVipInfo(userId))
            .thenApply(vipInfoDto -> this.findDiscount(vipInfoDto.getId()))
            .thenAccept(disDto -> this.checkDiscountInfo(disDto.getId()));

    // 2.2 runAsync()
    CompletableFuture.runAsync(() -> this.findVipInfoNoReturn(userId));

    // 2.3 get()
    CompletableFuture<VipInfoDto> future = CompletableFuture
            .supplyAsync(() -> this.findVipInfo(userId));
    VipInfoDto vipInfoDto = future.get();
    this.findDiscount(vipInfoDto.getId());

    // 2.4 使用自己的线程池
    CompletableFuture<VipInfoDto> future2 = CompletableFuture
            .supplyAsync(() -> this.findVipInfo(userId), executor);
    future2.get();
}

5.一些"好玩的"注解

  1. 链式注解 @Accessors(chain = true) 这个比较常用 ---- 写在实体类上
  2. 我们不想用new来创建对象了 可以用自定义创建对象注解 @RequiredArgsConstructor(staticName = "of") ---- 写在实体类上,一般这种写法用在返回前端封装的对象上面
  3. @RequiredArgsConstructor的高级玩法
    有时候我们一个serviceImpl里面会写很多@Autowired注解自动注入,能不能不写?可以!在serviceImpl层加上面的注解就OK! @RequiredArgsConstructor(onConstructor=@__(@Autowired)) 需要注意的是被注入的对象必须final修饰, 该注解了解下即可 -.-
  4. @Builder注解 直接用.属性名来赋值,代替set
   @Override
    public void annotationTest() {
        // 一般写法
        UserEntity entitySimple = new UserEntity();
        entitySimple.setId(1L);
        entitySimple.setAge(10);
        entitySimple.setUser_name("cq");
        System.out.println("entitySimple:" + JSON.toJSONString(entitySimple));
  
    // @Accessors
    UserEntity entity = new UserEntity()
            .setId(1L)
            .setAge(10)
            .setUser_name("cq");
    System.out.println("entity:" + JSON.toJSONString(entity));
  
    // @RequiredArgsConstructor(staticName = "of")
    ShareResult shareResult = ShareResult.of().setCode(0).setMessage("OK!");
    System.out.println("shareResult:" + JSON.toJSONString(shareResult));

    // @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    List<User> all = userService.findAll();
    System.out.println("allUsers:" + JSON.toJSONString(all));

    // @Builder
    UserEntity buildUser = UserEntity
            .builder()
            .id(2L)
            .user_name("cq")
            .build();
    System.out.println("buildUser:" + JSON.toJSONString(buildUser));
}

6.总结

今天分享的这些主要是java8的一些新特性和lombok注解,工作中可能用的上可能用不上,反正了解下也是好的嘛,不对的地方欢迎大家指正交流

最后修改:2021 年 09 月 06 日 04 : 46 PM