做Java开发,你需要了解这些

前言:

在开发中,我们写的代码肯定是越少越好,代码层次越清晰越好。那么下面就介绍一些可以减少代码量、可以让结构更清晰的好东西。本文涉及vo、dto的使用、全局异常处理、表单验证以及一些小工具的使用。


欢迎大家关注我的公众号 javawebkf,目前正在慢慢地将简书文章搬到公众号,以后简书和公众号文章将同步更新,且简书上的付费文章在公众号上将免费。


一、lombok的使用:

lombok是一个可以减少代码量的小工具,使用非常简单,只需要添加如下依赖:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

如果开发工具为idea,那么再安装一下lombok插件即可。安装方法:
setting ---> plugins ---> browse repositories,然后搜索lombok。


image.png

如果开发工具是eclipse,安装lombok插件就麻烦一些,百度上有很多教程,此处就不再搬运了。
添加了依赖,安装好了插件,就可以使用了。下面主要介绍它的两个注解。

1、@Data:
我们在写实体类时,每个实体类都要写set、get方法以及toString方法等,虽然编辑器可以自动生成,但还是有些麻烦,而且大量的set、get方法使代码看起来不清爽。那么可以使用@Data来搞定。在实体类上加上此注解,就相当于写了set、get、toString、equals等方法。

@Entity
@Data
public class OrderDetail {
    @Id
    private String detailId;
    private String orderId;
    private String productId;
    private String productName;
    private BigDecimal productPrice;
    private Integer productQuantity;
    private String productIcon;
}

这样的代码看起来就干净清爽多了。

2、@Slf4j:
这也是一个常用的注解。一般我们需要用日志都会像下面这样写:

Logger log = (Logger) LoggerFactory.getLogger(当前类.class);
log.error("【查询商品】商品不存在,productId={}",productId);

而加了这个注解,就不用自己创建log对象了,要用时直接用log调用对应方法就行了。

log.error("【查询商品】商品不存在,productId={}",productId);

关于lombok还有好多注解,暂且先介绍这两个常用的。

二、createTime和updateTime问题:

一般的表中,我们都会加上createTime和updateTime两个字段。然后有记录存入数据库时,要实体.setCreateTime(new Date())来赋值,更新时就要实体.setUpdateTime(new Date())。每次有新增记录或有更新时都要这样set一下,有些麻烦。其实这两个时间字段可以交给数据库管理。建表时createTime和updateTime字段这样写:

create table 'product_category'(
   ......
   `create_time` timestamp not null default current_timestamp comment '创建时间',
   `update_time` timestamp not null default current_timestamp 
                 on update current_timestamp comment '修改时间',
   ......
)

然后在对应的实体类上加@DynamicUpdate注解即可实现这两个字段的自动更新。创建时不用setCreateTime,更新时也不用setUpdateTime了。

@Entity
@DynamicUpdate // 动态更新
@Data
public class ProductCategory {
    ......
    private Date createTime;
    private Date updateTime;
}

三、枚举类的使用:

关于枚举类,我之前只是在学Java基础的时候了解过,后来老师教的一些demo中都没用到过,最近看的一个项目用到了,在此记录其用法。先看下面的代码:

@Entity
@Data
public class ProductInfo {
    ......
    private Integer productStatus;//商品状态(0正常,1下架)
}

有一个商品类,productStatus是其状态,0是上架,1是下架。如果现在要查询所有上架商品,那么做法如下:

 List<ProductInfo> productInfoList = 
       productInfoRepository.findByProductStatus(0);

这里可能看得还挺清楚,0表示上架,查询上架的,那么就是productStatus = 0 的,所以传入0即可。但是在项目中,实体类一多,用0和1表示的东西一多,就很容易搞错,到时候自己都要看半天才知道0代表什么1代表什么。这种情况就可以用枚举类来处理。新建一个ProductStatusEnum枚举类:

@Getter
public enum ProductStatusEnum {
    UP(0,"上架"),
    DOWN(1,"下架")
    ;
    private Integer code;
    private String message;
    ProductStatusEnum(Integer code,String message) {
        this.code = code;
        this.message = message;
    }
}

那么查询方法就可以这样写了:

 List<ProductInfo> productInfoList = 
       productInfoRepository.findByProductStatus(ProductStatusEnum.UP.getCode());

ProductStatusEnum.UP.getCode()就是0,这样一看就知道是查上架的商品。

四、VO的使用:

VO是view object的简称,中文意思是视图对象,也就是我们在controller中返回给前端的内容。一般开发时,我们要按照前端给的文档给前端返回相应的内容,比如现有api如下:

路由:GET /sell/buyer/product/list
参数:无
返回:

{
    "code": 0,
    "msg": "成功",
    "data": [
        {
            "name": "类目名1",
            "type": 1,
            "foods": [
                {
                    "id": "123456",
                    "name": "商品名1",
                    "price": 4.5,
                    "description": "xxxxx",
                    "icon": "http://xxx.com",
                }
            ]
        },
        {
            "name": "类目名2",
            "type": 2,
            "foods": [
                {
                    "id": "123457",
                    "name": "商品名2",
                    "price": 10.9,
                    "description": "xxxxx",
                    "icon": "http://xxx.com",
                },
                {
                    "id": "123457",
                    "name": "商品名3",
                    "price": 10.9,
                    "description": "xxxxx",
                    "icon": "http://xxx.com",
                }
            ]
        }
    ]
}

咋一看很复杂,其实不然。一个中括号就表示里面的是list。那么可以知道,最外层是由code、msg和类目的list组成;第二层就是由类目名name、type和商品的list组成;商品的list就包含了商品的信息。那么要如何构造这样的返回对象呢?先从最外层开始写。根据最外层的三个字段,可以写出ResultVo类:

/**
 * 返回给前端的最外层对象
 * Create by zhu on 2018/10/7
 */
@Data
public class ResultVo<T> {
    private Integer code;//错误码
    private String msg;//提示信息
    private T data;//返回的内容
}

这里data定义为泛型,这样就可以通用。
现在将ResultVo对象返回给前端:

 @GetMapping("/test")
 public ResultVo test(){
        ResultVo resultVo = new ResultVo();
        resultVo.setCode(0);
        resultVo.setMsg("test");
        resultVo.setData("这是内容");
        return resultVo;
 }

就会得到如下效果:


image.png

说明最外层是没有错的,根据api提供的信息又可以写出如下vo:

@Data
public class ProductVo {
    //这里写的字段名与前端api需要的不一致没关系,加上如下注解,直接里面写api需要的字段名
    @JsonProperty("name")
    private String categoryName;
    @JsonProperty("type")
    private Integer categoryType;
    @JsonProperty("foods")//这个foods是一个商品的list
    private List<ProductInfoVo> productInfoVoList;
}
//这个就是最里层的商品对象
@Data
public class ProductInfoVo {
    @JsonProperty("id")
    private String productId;
    @JsonProperty("name")
    private String productName;
    @JsonProperty("price")
    private BigDecimal productPrice;
    @JsonProperty("description")
    private String productDescription;
    @JsonProperty("icon")
    private String productIcon;
}

注意,前端需要的name其实是类目名,如果这个vo也直接定义变量name,到时候会搞不清楚到底是商品的name还是类目的name。解决方案是:这里应该是什么就写什么,然后通过@JsonProperty("xxx")来指定返回给前端时的名字。

写好后再去controller中:

  @GetMapping("/test")
    public ResultVo test() {
        ResultVo resultVo = new ResultVo();
        ProductVo productVo = new ProductVo();
        ProductInfoVo productInfoVo = new ProductInfoVo();

        productInfoVo.setProductName("商品1");
        productVo.setProductInfoVoList(Arrays.asList(productInfoVo));
        productVo.setCategoryName("类目1");

        resultVo.setData(Arrays.asList(productVo));
        resultVo.setMsg("成功");
        resultVo.setCode(0);

        return resultVo;
    }

把最里层的赋好值设置给中间层,中间层赋好值设置给最外层,再把最外层返回给前端,效果如下:


image.png

这样返回的格式就对了,和api一致。接下来要做的事就是从数据库查出相应的记录,然后赋给这三个对象就行了。
从上面的controller中可以发现,我们每次都要new一个最外层的ResultVo对象,然后setCode、setMsg、setData。每个方法中都new一个还是很麻烦的,所以可以封装一下:

public class ResultVoUtil {
    /** 成功时使用 */
    public static ResultVo success(Object object){
        ResultVo resultVo = new ResultVo();
        resultVo.setData(object);
        resultVo.setCode(0);
        resultVo.setMsg("成功");
        return resultVo;
    }

    /** 成功时且不需要返回值时使用 */
    public static ResultVo success(){
        return success(null);
    }

    /** 请求错误时使用 */
    public static ResultVo error(Integer code,String msg){
        ResultVo resultVo = new ResultVo();
        resultVo.setCode(code);
        resultVo.setMsg(msg);
        return resultVo;
    }
}

那么上面的controller就可以改成:

 @GetMapping("/test")
    public ResultVo test() {
        ProductVo productVo = new ProductVo();
        ProductInfoVo productInfoVo = new ProductInfoVo();

        productInfoVo.setProductName("商品1");
        productVo.setProductInfoVoList(Arrays.asList(productInfoVo));
        productVo.setCategoryName("类目1");

        return ResultVoUtil.success(Arrays.asList(productVo));
    }

这样代码看起来就简洁多了,而且这个ResultVoUtil也是通用的,传入相应的Object对象即可。

五、使用BeanUtils进行属性拷贝:

上面说到把从数据库查到的productInfo的属性的值赋给productInfoVo对应的属性,这里说一下赋值的问题:
ProductInfo类如下:

@Entity
@Data
public class ProductInfo {
    @Id
    private String productId;
    private String productName;
    private BigDecimal productPrice;
    private Integer productStock;//库存
    private String productDescription;//描述
    private String productIcon;//小图
    private Integer productStatus;//商品状态(0正常,1下架)
    private Integer categoryType;//类目编号
}

ProductInfoVo类上面已给出,对比可以发现,ProductInfoVo与ProductInfo相比,就是少几个属性。我们现在从数据库查出来的是productInfo,而前端需要的是productInfoVo,所以需要将productInfo里的值设置到productInfoVo里去。常规做法是先从productInfo中get再set到productInfoVo中去:

ProductInfo productInfo = productService.findOne(productId);
ProductInfoVo productInfoVo = new ProductInfoVo();
productInfoVo.setProductName(productInfo.getProductName());
productInfoVo.setProductPrice(productInfo.getProductPrice());
......

如果属性少问题也不大,如果属性很多,那么就要写一大堆这样的代码。可以使用spring提供的一个属性拷贝工具,不管多少个属性,只需一行代码:

BeanUtils.copyProperties(productInfo, productInfoVo);

这就表示把productInfo的属性拷贝到productInfoVo对象中去。
注意:使用这个工具有两点要注意,第一个就是这两个对象的属性名要一致;第二就是null值也会拷贝进去,所以如果productInfo中有个属性值为null,进行拷贝后productInfoVo对应的属性值也会是null,就算拷贝之前设置了值也会覆盖掉,所以要先拷贝再赋值。

六、dto的使用:

dto全称是data transfer object,中文意思为数据传输对象。那么dto有什么作用?什么时候该用dto?如何使用呢?看下面的例子:
假如我数据库有两张表,一张学生表student,一张班级表class。它们对应的实体类如下:

@Data
public class Student {
    @Id
    private String studentId;
    private String classId;
    private String name;
}

@Data
public class Class {
    @Id
    private String classId;
    private String  className;
}

假如我现在要查一个班级的信息,班级应该是包含了多个学生的,因为一个班级有多个学生,那么通常class实体类应该这样设计:

@Data
public class Class {
    @Id
    private String classId;
    private String  className;
    private List<Student> studentList;
}

那么问题来了,这样实体类和表就对应不上了,因为在class表中没有与studentList这个属性对应的字段。虽然可以在studentList上加上@Transient注解,这样jpa在与数据表对应时就会忽略这个属性。但是这样不好,感觉就是污染了这个与数据表对应的实体类,我们还是要让实体类与数据表 一 一对应,所以class类不能增加这个字段。那么我们就新建一个实体类,叫ClassDto:

@Data
public class ClassDto {
    private String classId;
    private String  className;
    private List<Student> studentList;
}

这就是dto的作用,最常用的就是当某两个实体类存在关系时,而数据表对应的实体类为了跟数据表一致,没有体现这种关系,那么就可以使用dto。dto不对应数据表,所@Id注解也不需要了。

七、异常处理:

平时我们用异常可能直接throw一个exception就完事了,但是这样不好,因为这样抛出去自己也看不懂是什么异常,所以可以像下面这样处理:
自定义一个异常类继承RuntimeException:

public class GlobalException extends RuntimeException{
    private Integer code;
    public GlobalException(ExceptionEnum exceptionEnum ){
        super(resultEnum.getMessage());
        this.code = resultEnum.getCode();
    }

    public GlobalException(Integer code,String message){
        super(message);
        this.code = code;
    }

}

用到的枚举类:

@Getter
public enum ExceptionEnum {
    PRODUCT_NOT_EXIST(10,"商品不存在"),
    PRODUCT_STOCK_ERROR(11,"库存不足"),
    ;
    private Integer code;
    private String message;

    ExceptionEnum (Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

自定义一个异常类,搭配枚举一起使用。那么在抛异常的时候就可以这样写:

throw new GlobalException(ExceptionEnum.PRODUCT_NOT_EXIST);

这样前端就可以看到“商品不存在”这样的提示,而不是一串看不懂的异常。枚举类种我只是列举了两个例子,有异常就可以往枚举种添加,然后像上面那样用就行了。

八、生成随机数:

有时候数据表的Id没有设置自增,需要我们自己设置Id。Id要求是必须唯一,提供如下工具类:

public class KeyUtil {
    /**
     * 生成唯一主键
     * 格式:当前时间+随机数
     */
    public static synchronized String genUniqueKey(){
        Random random = new Random();
        //Integer a =random.nextInt(90) + 10;//生成两位随机数
        //生成六位随机数
        Integer number =random.nextInt(900000) + 100000;
        return  System.currentTimeMillis() + String.valueOf(number);

    }
}

九、表单验证:

前端给后台传参数的时候,我们要在controller中获取前端传入的参数,一般有以下几种做法:

  • HttpServletRequest:
    用这个一般要编写一个工具类,用来获取指定类型的参数:
public class HttpServletRequestUtil {
    public static int getInt(HttpServletRequest request, String name) {
        try {
            return Integer.decode(request.getParameter(name));
        } catch (Exception e) {
            return -1;
        }
    }

    public static long getLong(HttpServletRequest request, String name) {
        try {
            return Long.valueOf(request.getParameter(name));
        } catch (Exception e) {
            return -1;
        }
    }

    public static Double getDouble(HttpServletRequest request, String name) {
        try {
            return Double.valueOf(request.getParameter(name));
        } catch (Exception e) {
            return -1d;
        }
    }

    public static Boolean getBoolean(HttpServletRequest request, String name) {
        try {
            return Boolean.valueOf(request.getParameter(name));
        } catch (Exception e) {
            return false;
        }
    }

    public static String getString(HttpServletRequest request, String name) {
        try {
            String result = request.getParameter(name);
            if (result != null) {
                result = result.trim();
            }
            if ("".equals(result))
                result = null;
            return result;
        } catch (Exception e) {
            return null;
        }
    }
}

然后在controller中这样用:

@RequestMapping(value = "/getproductlistbyshop")
public ResultVo list(HttpServletRequest request) {
        // 获取前台传过来的页码
        int pageIndex = HttpServletRequestUtil.getInt(request, "pageIndex");
        // 获取前台传过来的每页显示的数量
        int pageSize = HttpServletRequestUtil.getInt(request, "pageSize");
                ......
}

但是这样获取参数,如果要校验的话需要自己写if语句来判断,比如:

if (pageIndex == null || pageSize == null){
    log.error(...);
    throw new GlobalException(...);
}

看第二种获取前端参数的方法:

  • @RequestParam:
 @GetMapping("/list")
 public ResultVo list(@RequestParam(value = "page",defaultValue = "0") Integer page,
                      @RequestParam(value = "size",defaultValue = "10") Integer size){
       ......    
    }

用这个还可以用defaultValue指定默认值,当前端没传时默认就为defaultValue指定的值。这种方法呢其实就是第一种方法的注解形式,如果要对获取的参数判断,还是要自己写if语句。
接下来看第三种方法:

  • 表单对象:
    如果前端传过来的参数很多,用上面两种方法写未免有些麻烦,而且还要自己一个个的判断传过来的参数是否为空,为空的话又要给前端什么提示。我们可以把前端的参数封装成一个对象,然后在controller中直接获取该对象即可,而且对于参数的验证都可以在封装的这个对象中完成,这就是springmvc提供的表单验证。看例子:
@Data
public class OrderForm {
    @NotEmpty(message = "姓名必填")
    private String name;
    @NotEmpty(message = "手机号必填")
    private String phone;
    @NotEmpty(message = "地址必填")
    private String address;
    @NotEmpty(message = "openid必填")
    private String openid;
    @NotEmpty(message = "购物车不能为空")
    private String items;
}

比如从前端获取的参数有这么多,那么就可以封装成这样一个OrderForm表单对象。加上@NotEmpty注解就表示这个参数不能为空,里面的message就是当该参数为空时给前端的提示。接下来看如何在controller中使用该对象:

@PostMapping("/create")
 public ResultVo create(@Valid OrderForm orderForm, BindingResult bindingResult){
      //判断表单校验后有没有错误
      if (bindingResult.hasErrors()) {
          log.error("【创建订单】参数不正确,orderForm={}",orderForm);
          throw new GlobalException(ExceptionEnum.PARAM_ERROR.getCode(),
                    bindingResult.getFieldError().getDefaultMessage());
      }
      ......
}

@Valid注解就可以使用该对象,bindingResult就是验证的结果,如果验证参数不正确,通过bindingResult.getFieldError().getDefaultMessage()就可以获取到刚才@NotEmpty注解里面message的内容,配合全局异常使用,就可以把这个message返回给前端。

总结:

上面的介绍的lombok、创建时间和更新时间的处理、BeanUtils的使用都可以减少代码量,而dto、vo、全局异常处理、表单验证等可以使代码结构更加清晰,使程序更加健壮。希望大家喜欢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容

  • 文章核心 其实,本不想把标题写的那么恐怖,只是发现很多人干了几年java以后,都自认为是一个不错的java程序员了...
    java菜阅读 1,913评论 6 49
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,742评论 25 707
  • 人生最美的风景不在于彼岸的鲜花与掌声,而在于来时的路。这句话说的没错,但是人的一生的奋斗,大多不都为了将来得到一个...
    莫易念阅读 214评论 0 1