前言:
在開發(fā)中,我們寫的代碼肯定是越少越好,代碼層次越清晰越好。那么下面就介紹一些可以減少代碼量、可以讓結(jié)構(gòu)更清晰的好東西。本文涉及vo、dto的使用、全局異常處理、表單驗(yàn)證以及一些小工具的使用。
歡迎大家關(guān)注我的公眾號(hào) javawebkf,目前正在慢慢地將簡書文章搬到公眾號(hào),以后簡書和公眾號(hào)文章將同步更新,且簡書上的付費(fèi)文章在公眾號(hào)上將免費(fèi)。
一、lombok的使用:
lombok是一個(gè)可以減少代碼量的小工具,使用非常簡單,只需要添加如下依賴:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
如果開發(fā)工具為idea,那么再安裝一下lombok插件即可。安裝方法:
setting ---> plugins ---> browse repositories,然后搜索lombok。

如果開發(fā)工具是eclipse,安裝lombok插件就麻煩一些,百度上有很多教程,此處就不再搬運(yùn)了。
添加了依賴,安裝好了插件,就可以使用了。下面主要介紹它的兩個(gè)注解。
1、@Data:
我們在寫實(shí)體類時(shí),每個(gè)實(shí)體類都要寫set、get方法以及toString方法等,雖然編輯器可以自動(dòng)生成,但還是有些麻煩,而且大量的set、get方法使代碼看起來不清爽。那么可以使用@Data來搞定。在實(shí)體類上加上此注解,就相當(dāng)于寫了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:
這也是一個(gè)常用的注解。一般我們需要用日志都會(huì)像下面這樣寫:
Logger log = (Logger) LoggerFactory.getLogger(當(dāng)前類.class);
log.error("【查詢商品】商品不存在,productId={}",productId);
而加了這個(gè)注解,就不用自己創(chuàng)建log對象了,要用時(shí)直接用log調(diào)用對應(yīng)方法就行了。
log.error("【查詢商品】商品不存在,productId={}",productId);
關(guān)于lombok還有好多注解,暫且先介紹這兩個(gè)常用的。
二、createTime和updateTime問題:
一般的表中,我們都會(huì)加上createTime和updateTime兩個(gè)字段。然后有記錄存入數(shù)據(jù)庫時(shí),要實(shí)體.setCreateTime(new Date())來賦值,更新時(shí)就要實(shí)體.setUpdateTime(new Date())。每次有新增記錄或有更新時(shí)都要這樣set一下,有些麻煩。其實(shí)這兩個(gè)時(shí)間字段可以交給數(shù)據(jù)庫管理。建表時(shí)createTime和updateTime字段這樣寫:
create table 'product_category'(
......
`create_time` timestamp not null default current_timestamp comment '創(chuàng)建時(shí)間',
`update_time` timestamp not null default current_timestamp
on update current_timestamp comment '修改時(shí)間',
......
)
然后在對應(yīng)的實(shí)體類上加@DynamicUpdate注解即可實(shí)現(xiàn)這兩個(gè)字段的自動(dòng)更新。創(chuàng)建時(shí)不用setCreateTime,更新時(shí)也不用setUpdateTime了。
@Entity
@DynamicUpdate // 動(dòng)態(tài)更新
@Data
public class ProductCategory {
......
private Date createTime;
private Date updateTime;
}
三、枚舉類的使用:
關(guān)于枚舉類,我之前只是在學(xué)Java基礎(chǔ)的時(shí)候了解過,后來老師教的一些demo中都沒用到過,最近看的一個(gè)項(xiàng)目用到了,在此記錄其用法。先看下面的代碼:
@Entity
@Data
public class ProductInfo {
......
private Integer productStatus;//商品狀態(tài)(0正常,1下架)
}
有一個(gè)商品類,productStatus是其狀態(tài),0是上架,1是下架。如果現(xiàn)在要查詢所有上架商品,那么做法如下:
List<ProductInfo> productInfoList =
productInfoRepository.findByProductStatus(0);
這里可能看得還挺清楚,0表示上架,查詢上架的,那么就是productStatus = 0 的,所以傳入0即可。但是在項(xiàng)目中,實(shí)體類一多,用0和1表示的東西一多,就很容易搞錯(cuò),到時(shí)候自己都要看半天才知道0代表什么1代表什么。這種情況就可以用枚舉類來處理。新建一個(gè)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中返回給前端的內(nèi)容。一般開發(fā)時(shí),我們要按照前端給的文檔給前端返回相應(yīng)的內(nèi)容,比如現(xiàn)有api如下:
路由:GET /sell/buyer/product/list
參數(shù):無
返回:
{
"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",
}
]
}
]
}
咋一看很復(fù)雜,其實(shí)不然。一個(gè)中括號(hào)就表示里面的是list。那么可以知道,最外層是由code、msg和類目的list組成;第二層就是由類目名name、type和商品的list組成;商品的list就包含了商品的信息。那么要如何構(gòu)造這樣的返回對象呢?先從最外層開始寫。根據(jù)最外層的三個(gè)字段,可以寫出ResultVo類:
/**
* 返回給前端的最外層對象
* Create by zhu on 2018/10/7
*/
@Data
public class ResultVo<T> {
private Integer code;//錯(cuò)誤碼
private String msg;//提示信息
private T data;//返回的內(nèi)容
}
這里data定義為泛型,這樣就可以通用。
現(xiàn)在將ResultVo對象返回給前端:
@GetMapping("/test")
public ResultVo test(){
ResultVo resultVo = new ResultVo();
resultVo.setCode(0);
resultVo.setMsg("test");
resultVo.setData("這是內(nèi)容");
return resultVo;
}
就會(huì)得到如下效果:

說明最外層是沒有錯(cuò)的,根據(jù)api提供的信息又可以寫出如下vo:
@Data
public class ProductVo {
//這里寫的字段名與前端api需要的不一致沒關(guān)系,加上如下注解,直接里面寫api需要的字段名
@JsonProperty("name")
private String categoryName;
@JsonProperty("type")
private Integer categoryType;
@JsonProperty("foods")//這個(gè)foods是一個(gè)商品的list
private List<ProductInfoVo> productInfoVoList;
}
//這個(gè)就是最里層的商品對象
@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其實(shí)是類目名,如果這個(gè)vo也直接定義變量name,到時(shí)候會(huì)搞不清楚到底是商品的name還是類目的name。解決方案是:這里應(yīng)該是什么就寫什么,然后通過@JsonProperty("xxx")來指定返回給前端時(shí)的名字。
寫好后再去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;
}
把最里層的賦好值設(shè)置給中間層,中間層賦好值設(shè)置給最外層,再把最外層返回給前端,效果如下:

這樣返回的格式就對了,和api一致。接下來要做的事就是從數(shù)據(jù)庫查出相應(yīng)的記錄,然后賦給這三個(gè)對象就行了。
從上面的controller中可以發(fā)現(xiàn),我們每次都要new一個(gè)最外層的ResultVo對象,然后setCode、setMsg、setData。每個(gè)方法中都new一個(gè)還是很麻煩的,所以可以封裝一下:
public class ResultVoUtil {
/** 成功時(shí)使用 */
public static ResultVo success(Object object){
ResultVo resultVo = new ResultVo();
resultVo.setData(object);
resultVo.setCode(0);
resultVo.setMsg("成功");
return resultVo;
}
/** 成功時(shí)且不需要返回值時(shí)使用 */
public static ResultVo success(){
return success(null);
}
/** 請求錯(cuò)誤時(shí)使用 */
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));
}
這樣代碼看起來就簡潔多了,而且這個(gè)ResultVoUtil也是通用的,傳入相應(yīng)的Object對象即可。
五、使用BeanUtils進(jìn)行屬性拷貝:
上面說到把從數(shù)據(jù)庫查到的productInfo的屬性的值賦給productInfoVo對應(yīng)的屬性,這里說一下賦值的問題:
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;//商品狀態(tài)(0正常,1下架)
private Integer categoryType;//類目編號(hào)
}
ProductInfoVo類上面已給出,對比可以發(fā)現(xiàn),ProductInfoVo與ProductInfo相比,就是少幾個(gè)屬性。我們現(xiàn)在從數(shù)據(jù)庫查出來的是productInfo,而前端需要的是productInfoVo,所以需要將productInfo里的值設(shè)置到productInfoVo里去。常規(guī)做法是先從productInfo中g(shù)et再set到productInfoVo中去:
ProductInfo productInfo = productService.findOne(productId);
ProductInfoVo productInfoVo = new ProductInfoVo();
productInfoVo.setProductName(productInfo.getProductName());
productInfoVo.setProductPrice(productInfo.getProductPrice());
......
如果屬性少問題也不大,如果屬性很多,那么就要寫一大堆這樣的代碼。可以使用spring提供的一個(gè)屬性拷貝工具,不管多少個(gè)屬性,只需一行代碼:
BeanUtils.copyProperties(productInfo, productInfoVo);
這就表示把productInfo的屬性拷貝到productInfoVo對象中去。
注意:使用這個(gè)工具有兩點(diǎn)要注意,第一個(gè)就是這兩個(gè)對象的屬性名要一致;第二就是null值也會(huì)拷貝進(jìn)去,所以如果productInfo中有個(gè)屬性值為null,進(jìn)行拷貝后productInfoVo對應(yīng)的屬性值也會(huì)是null,就算拷貝之前設(shè)置了值也會(huì)覆蓋掉,所以要先拷貝再賦值。
六、dto的使用:
dto全稱是data transfer object,中文意思為數(shù)據(jù)傳輸對象。那么dto有什么作用?什么時(shí)候該用dto?如何使用呢?看下面的例子:
假如我數(shù)據(jù)庫有兩張表,一張學(xué)生表student,一張班級(jí)表class。它們對應(yīng)的實(shí)體類如下:
@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;
}
假如我現(xiàn)在要查一個(gè)班級(jí)的信息,班級(jí)應(yīng)該是包含了多個(gè)學(xué)生的,因?yàn)橐粋€(gè)班級(jí)有多個(gè)學(xué)生,那么通常class實(shí)體類應(yīng)該這樣設(shè)計(jì):
@Data
public class Class {
@Id
private String classId;
private String className;
private List<Student> studentList;
}
那么問題來了,這樣實(shí)體類和表就對應(yīng)不上了,因?yàn)樵赾lass表中沒有與studentList這個(gè)屬性對應(yīng)的字段。雖然可以在studentList上加上@Transient注解,這樣jpa在與數(shù)據(jù)表對應(yīng)時(shí)就會(huì)忽略這個(gè)屬性。但是這樣不好,感覺就是污染了這個(gè)與數(shù)據(jù)表對應(yīng)的實(shí)體類,我們還是要讓實(shí)體類與數(shù)據(jù)表 一 一對應(yīng),所以class類不能增加這個(gè)字段。那么我們就新建一個(gè)實(shí)體類,叫ClassDto:
@Data
public class ClassDto {
private String classId;
private String className;
private List<Student> studentList;
}
這就是dto的作用,最常用的就是當(dāng)某兩個(gè)實(shí)體類存在關(guān)系時(shí),而數(shù)據(jù)表對應(yīng)的實(shí)體類為了跟數(shù)據(jù)表一致,沒有體現(xiàn)這種關(guān)系,那么就可以使用dto。dto不對應(yīng)數(shù)據(jù)表,所@Id注解也不需要了。
七、異常處理:
平時(shí)我們用異??赡苤苯觮hrow一個(gè)exception就完事了,但是這樣不好,因?yàn)檫@樣拋出去自己也看不懂是什么異常,所以可以像下面這樣處理:
自定義一個(gè)異常類繼承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;
}
}
自定義一個(gè)異常類,搭配枚舉一起使用。那么在拋異常的時(shí)候就可以這樣寫:
throw new GlobalException(ExceptionEnum.PRODUCT_NOT_EXIST);
這樣前端就可以看到“商品不存在”這樣的提示,而不是一串看不懂的異常。枚舉類種我只是列舉了兩個(gè)例子,有異常就可以往枚舉種添加,然后像上面那樣用就行了。
八、生成隨機(jī)數(shù):
有時(shí)候數(shù)據(jù)表的Id沒有設(shè)置自增,需要我們自己設(shè)置Id。Id要求是必須唯一,提供如下工具類:
public class KeyUtil {
/**
* 生成唯一主鍵
* 格式:當(dāng)前時(shí)間+隨機(jī)數(shù)
*/
public static synchronized String genUniqueKey(){
Random random = new Random();
//Integer a =random.nextInt(90) + 10;//生成兩位隨機(jī)數(shù)
//生成六位隨機(jī)數(shù)
Integer number =random.nextInt(900000) + 100000;
return System.currentTimeMillis() + String.valueOf(number);
}
}
九、表單驗(yàn)證:
前端給后臺(tái)傳參數(shù)的時(shí)候,我們要在controller中獲取前端傳入的參數(shù),一般有以下幾種做法:
-
HttpServletRequest:
用這個(gè)一般要編寫一個(gè)工具類,用來獲取指定類型的參數(shù):
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) {
// 獲取前臺(tái)傳過來的頁碼
int pageIndex = HttpServletRequestUtil.getInt(request, "pageIndex");
// 獲取前臺(tái)傳過來的每頁顯示的數(shù)量
int pageSize = HttpServletRequestUtil.getInt(request, "pageSize");
......
}
但是這樣獲取參數(shù),如果要校驗(yàn)的話需要自己寫if語句來判斷,比如:
if (pageIndex == null || pageSize == null){
log.error(...);
throw new GlobalException(...);
}
看第二種獲取前端參數(shù)的方法:
- @RequestParam:
@GetMapping("/list")
public ResultVo list(@RequestParam(value = "page",defaultValue = "0") Integer page,
@RequestParam(value = "size",defaultValue = "10") Integer size){
......
}
用這個(gè)還可以用defaultValue指定默認(rèn)值,當(dāng)前端沒傳時(shí)默認(rèn)就為defaultValue指定的值。這種方法呢其實(shí)就是第一種方法的注解形式,如果要對獲取的參數(shù)判斷,還是要自己寫if語句。
接下來看第三種方法:
-
表單對象:
如果前端傳過來的參數(shù)很多,用上面兩種方法寫未免有些麻煩,而且還要自己一個(gè)個(gè)的判斷傳過來的參數(shù)是否為空,為空的話又要給前端什么提示。我們可以把前端的參數(shù)封裝成一個(gè)對象,然后在controller中直接獲取該對象即可,而且對于參數(shù)的驗(yàn)證都可以在封裝的這個(gè)對象中完成,這就是springmvc提供的表單驗(yàn)證??蠢樱?/li>
@Data
public class OrderForm {
@NotEmpty(message = "姓名必填")
private String name;
@NotEmpty(message = "手機(jī)號(hào)必填")
private String phone;
@NotEmpty(message = "地址必填")
private String address;
@NotEmpty(message = "openid必填")
private String openid;
@NotEmpty(message = "購物車不能為空")
private String items;
}
比如從前端獲取的參數(shù)有這么多,那么就可以封裝成這樣一個(gè)OrderForm表單對象。加上@NotEmpty注解就表示這個(gè)參數(shù)不能為空,里面的message就是當(dāng)該參數(shù)為空時(shí)給前端的提示。接下來看如何在controller中使用該對象:
@PostMapping("/create")
public ResultVo create(@Valid OrderForm orderForm, BindingResult bindingResult){
//判斷表單校驗(yàn)后有沒有錯(cuò)誤
if (bindingResult.hasErrors()) {
log.error("【創(chuàng)建訂單】參數(shù)不正確,orderForm={}",orderForm);
throw new GlobalException(ExceptionEnum.PARAM_ERROR.getCode(),
bindingResult.getFieldError().getDefaultMessage());
}
......
}
用@Valid注解就可以使用該對象,bindingResult就是驗(yàn)證的結(jié)果,如果驗(yàn)證參數(shù)不正確,通過bindingResult.getFieldError().getDefaultMessage()就可以獲取到剛才@NotEmpty注解里面message的內(nèi)容,配合全局異常使用,就可以把這個(gè)message返回給前端。
總結(jié):
上面的介紹的lombok、創(chuàng)建時(shí)間和更新時(shí)間的處理、BeanUtils的使用都可以減少代碼量,而dto、vo、全局異常處理、表單驗(yàn)證等可以使代碼結(jié)構(gòu)更加清晰,使程序更加健壯。希望大家喜歡!