做Java開發,你需要了解這些
前言:
在開發中,我們寫的程式碼肯定是越少越好,程式碼層次越清晰越好。那麼下面就介紹一些可以減少程式碼量、可以讓結構更清晰的好東西。本文涉及vo、dto的使用、全域性異常處理、表單驗證以及一些小工具的使用。
一、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 StringclassName; }
假如我現在要查一個班級的資訊,班級應該是包含了多個學生的,因為一個班級有多個學生,那麼通常class實體類應該這樣設計:
@Data public class Class { @Id private String classId; private StringclassName; private List<Student> studentList; }
那麼問題來了,這樣實體類和表就對應不上了,因為在class表中沒有與studentList這個屬性對應的欄位。雖然可以在studentList上加上 @Transient
註解,這樣jpa在與資料表對應時就會忽略這個屬性。但是這樣不好,感覺就是汙染了這個與資料表對應的實體類,我們還是要讓實體類與資料表 一 一對應,所以class類不能增加這個欄位。那麼我們就新建一個實體類,叫ClassDto:
@Data public class ClassDto { private String classId; private StringclassName; 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; returnSystem.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、全域性異常處理、表單驗證等可以使程式碼結構更加清晰,使程式更加健壯。希望大家喜歡!