業(yè)務邏輯層是被Controller直接調(diào)用的層(Controller不允許直接調(diào)用持久層),通常,在業(yè)務邏輯層中編寫的代碼是為了保證數(shù)據(jù)的完整性和安全性,使得數(shù)據(jù)是隨著我們設定的規(guī)則而產(chǎn)生或發(fā)生變化。
通常,在業(yè)務邏輯層的代碼會由接口和實現(xiàn)類組件,其中,接口被視為是必須的
在接口中,聲明抽象方法時,僅以操作成功為前提來設計返回值類型(不考慮失?。?,如果業(yè)務在執(zhí)行過程可能出現(xiàn)某些失?。ú环纤O定的規(guī)則),可以通過拋出異常來表示!
關于拋出的異常,通常是自定義的異常,并且,自定義異常通常是`RuntimeException`的子類,主要原因:
所以,在實際編寫業(yè)務邏輯層之前,應該先規(guī)劃異常,例如先創(chuàng)建`ServiceException`類:
public class ServiceException extends RuntimeException { //TODU}
接下來,再創(chuàng)建具體的對應某種“失敗”的異常,例如,在添加管理員時,可能因為“用戶名已經(jīng)存在”而失敗,則創(chuàng)建對應的`UsernameDuplicateException`異常:
public class UsernameDuplicateException extends ServiceException { //TODU}
另外,當插入數(shù)據(jù)時,如果返回的受影響行數(shù)不是1時,必然是某種錯誤,則創(chuàng)建對應的插入數(shù)據(jù)異常:
public class InsertException extends ServiceException { //TODU}
關于抽象方法的參數(shù),應該設計為客戶端提交的數(shù)據(jù)類型或?qū)姆庋b類型,不可以是數(shù)據(jù)表對應的實體類型!如果使用封裝的類型,這種類型在類名上應該添加某種后綴,例如`DTO`或其它后綴,例如:
public class AdminAddNewDTO implements Serializable { private String username; private String password; private String nickname; private String avatar; private String phone; private String email; private String description; // Setters & Getters // hashCode(), equals() // toString()}
然后,在`cn.celinf.boot.demo.service`包下聲明接口及抽象方法:
public interface IAdminService { void addNew(AdminAddNewDTO adminAddNewDTO);}
并在以上`service`包下創(chuàng)建`impl`子包,再創(chuàng)建`AdminServiceImpl`類:
import cn.celinf.boot.demo.entity.Admin;import cn.celinf.boot.demo.ex.InsertException;import cn.celinf.boot.demo.ex.UsernameDuplicateException;import cn.celinf.boot.demo.mapper.AdminMapper;import cn.celinf.boot.demo.pojo.dto.AdminAddNewDTO;import cn.celinf.boot.demo.service.IAdminService;import org.springframework.beans.BeanUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.time.LocalDateTime;@Servicepublic class AdminServiceImpl implements IAdminService { @Autowired private AdminMapper adminMapper; @Override public void addNew(AdminAddNewDTO adminAddNewDTO) { // 通過參數(shù)獲取用戶名 String username = adminAddNewDTO.getUsername(); // 調(diào)用adminMapper的Admin getByUsername(String username)方法執(zhí)行查詢 Admin queryResult = adminMapper.getByUsername(username); // 判斷查詢結(jié)果是否不為null if (queryResult != null) { // 是:表示用戶名已經(jīng)被占用,則拋出UsernameDuplicateException throw new UsernameDuplicateException(); } // 通過參數(shù)獲取原密碼 String password = adminAddNewDTO.getPassword(); // 通過加密方式,得到加密后的密碼encodedPassword String encodedPassword = password; // 創(chuàng)建當前時間對象now > LocalDateTime.now() LocalDateTime now = LocalDateTime.now(); // 創(chuàng)建Admin對象 Admin admin = new Admin(); // 補全Admin對象的屬性值:通過參數(shù)獲取username,nickname…… admin.setUsername(username); admin.setNickname(adminAddNewDTO.getNickname()); admin.setAvatar(adminAddNewDTO.getAvatar()); admin.setPhone(adminAddNewDTO.getPhone()); admin.setEmail(adminAddNewDTO.getEmail()); admin.setDescription(adminAddNewDTO.getDescription()); // 以上這些從一個對象中把屬性賦到另一個對象中,還可以使用: // BeanUtils.copyProperties(adminAddNewDTO, admin); // 補全Admin對象的屬性值:password > encodedPassword admin.setPassword(encodedPassword); // 補全Admin對象的屬性值:isEnable > 1 admin.setIsEnable(1); // 補全Admin對象的屬性值:lastLoginIp > null // 補全Admin對象的屬性值:loginCount > 0 admin.setLoginCount(0); // 補全Admin對象的屬性值:gmtLastLogin > null // 補全Admin對象的屬性值:gmtCreate > now admin.setGmtCreate(now); // 補全Admin對象的屬性值:gmtModified > now admin.setGmtModified(now); // 調(diào)用adminMapper的insert(Admin admin)方法插入管理員數(shù)據(jù),獲取返回值 int rows = adminMapper.insert(admin); // 判斷以上返回的結(jié)果是否不為1,拋出InsertException異常 if (rows != 1) { throw new InsertException(); } }}
以上代碼未實現(xiàn)對密碼的加密處理!關于密碼加密,相關的代碼應該定義在別的某個類中,不應該直接將加密過程編寫在以上代碼中,因為加密的代碼需要在多處應用(添加用戶、用戶登錄、修改密碼等),并且,從分工的角度上來看,也不應該是業(yè)務邏輯層的任務!所以,在`cn.celinf.boot.demo.util`(包不存在,則創(chuàng)建)下創(chuàng)建`PasswordEncoder`類,用于處理密碼加密:
package cn.celinf.boot.demo.util;@Componentpublic class PasswordEncoder { public String encode(String rawPassword) { return “aaa” + rawPassword + “aaa”; }}
完成后,需要在`AdminServiceImpl`中自動裝配以上`PasswordEncoder`,并在需要加密時調(diào)用`PasswordEncoder`對象的`encode()`方法。
控制器層開發(fā)
Spring MVC是用于處理控制器層開發(fā)的,在使用Spring Boot時,在`pom.xml`中添加`spring-boot-starter-web`即可整合Spring MVC框架及相關的常用依賴項(包含`jackson-databind`),可以將已存在的`spring-boot-starter`直接改為`spring-boot-starter-web`,因為在`spring-boot-starter-web`中已經(jīng)包含了`spring-boot-starter`。
先在項目的根包下創(chuàng)建`controller`子包,并在此子包下創(chuàng)建`AdminController`,此類應該添加`@RestController`和`@RequestMapping(value = “/admins”, produces = “application/json; charset=utf-8”)`注解,例如:
@RestController@RequestMapping(values = “/admins”, produces = “application/json; charset=utf-8”)public class AdminController {}
由于已經(jīng)決定了服務器端響應時,將響應JSON格式的字符串,為保證能夠響應JSON格式的結(jié)果,處理請求的方法返回值應該是自定義的數(shù)據(jù)類型,則從此前學習的`spring-mvc`項目中找到`JsonResult`類及相關類型,復制到當前項目中來。
@Autowiredprivate IAdminService adminService;// 注意:暫時使用@RequestMapping,不要使用@PostMapping,以便于直接在瀏覽器中測試// http://localhost:8080/admins/add-new?username=root&password=1234@RequestMapping(“/add-new”)public JsonResult addNew(AdminAddNewDTO adminAddNewDTO) { adminService.addNew(adminAddNewDTO); return JsonResult.ok();}
完成后,運行啟動類,即可啟動整個項目,在`spring-boot-starter-web`中,包含了Tomcat的依賴項,在啟動時,會自動將當前項目打包并部署到此Tomcat上,所以,執(zhí)行啟動類時,會執(zhí)行此Tomcat,同時,因為是內(nèi)置的Tomcat,只為當前項目服務,所以,在將項目部署到Tomcat時,默認已經(jīng)將Context Path(例如spring_mvc_war_exploded)配置為空字符串,所以,在啟動項目后,訪問的URL中并沒有此前遇到的Context Path值。
當項目啟動成功后,即可在瀏覽器的地址欄中輸入網(wǎng)址進行測試訪問!
【注意】:如果是未添加的管理員賬號,可以成功執(zhí)行結(jié)束,如果管理員賬號已經(jīng)存在,由于尚未處理異常,會提示500錯誤。
public enum State {OK(200),ERR_USERNAME(201),ERR_PASSWORD(202),ERR_INSERT(500); // 新增的枚舉值// 原有其它代碼}
然后,在`cn.celinf.boot.demo.controller`下創(chuàng)建`handler.GlobalExceptionHandler`類,用于統(tǒng)一處理異常,例如:
package cn.celinf.boot.demo.controller.handler;import cn.celinf.boot.demo.ex.ServiceException;import cn.celinf.boot.demo.ex.UsernameDuplicateException;import cn.celinf.boot.demo.web.JsonResult;import cn.celinf.boot.demo.web.State;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(ServiceException.class) public JsonResult handleServiceException(ServiceException e) { if (e instanceof UsernameDuplicateException) { return JsonResult.fail(State.ERR_USERNAME, “用戶名錯誤!”); } else { return JsonResult.fail(State.ERR_INSERT, “插入數(shù)據(jù)失?。?#8221;); } }}
完成后,重新啟動項目,當添加管理員時的用戶名沒有被占用時,將正常添加,當用戶名已經(jīng)被占用時,會根據(jù)處理異常的結(jié)果進行響應!
由于在統(tǒng)一處理異常的機制下,同一種異常,無論是在哪種業(yè)務中出現(xiàn),處理異常時的描述信息都是完全相同的,也無法精準的表達錯誤信息,這是不合適的!另外,基于面向?qū)ο蟮摹胺止ぁ彼枷?,關于錯誤信息(異常對應的描述信息),應該是由Service來描述,即“誰拋出誰描述”,因為拋出異常的代碼片段是最了解、最明確出現(xiàn)異常的原因的!
為了更好的描述異常的原因,應該在自定義的`ServiceException`和其子孫類異常中添加基于父類的全部構(gòu)造方法(5個),然后,在`AdminServiceImpl`中,當拋出異常時,可以在異常的構(gòu)造方法中添加`String`類型的參數(shù),對異常發(fā)生的原因進行描述,例如:
@Overridepublic void addNew(AdminAddNewDTO adminAddNewDTO) { // ===== 原有其它代碼 ===== // 判斷查詢結(jié)果是否不為null if (queryResult != null) { // 是:表示用戶名已經(jīng)被占用,則拋出UsernameDuplicateException log.error(“此賬號已經(jīng)被占用,將拋出異常”); throw new UsernameDuplicateException(“添加管理員失敗,用戶名(” + username + “)已經(jīng)被占用!”); } // ===== 原有其它代碼 ===== // 判斷以上返回的結(jié)果是否不為1,拋出InsertException異常 if (rows != 1) { throw new InsertException(“添加管理員失敗,服務器忙,請稍后再次嘗試!”); }}
最后,在處理異常時,可以調(diào)用異常對象的`getMessage()`方法獲取拋出時封裝的描述信息,例如:
@ExceptionHandler(ServiceException.class)public JsonResult handleServiceException(ServiceException e) { if (e instanceof UsernameDuplicateException) { return JsonResult.fail(State.ERR_USERNAME, e.getMessage()); } else { return JsonResult.fail(State.ERR_INSERT, e.getMessage()); }}
完成后,再次重啟項目,當用戶名已經(jīng)存在時,可以顯示在Service中描述的錯誤信息!
//響應的JSON數(shù)據(jù)例如:{“state”:200,”message”:null,”data”:null}//添加失敗時,響應的JSON數(shù)據(jù)例如:{“state”:201,”message”:”添加管理員失敗,用戶名(liuguobin)已經(jīng)被占用!”,”data”:null}
可以看到,無論是成功還是失敗,響應的JSON中都包含了不必要的數(shù)據(jù)(為`null`的數(shù)據(jù)),這些數(shù)據(jù)屬性是沒有必要響應到客戶端的,如果需要去除這些不必要的值,可以在對應的屬性上使用注解進行配置,例如:
//NON_NULL 則響應的JSON中只會包含不為`null`的部分。@Datapublic class JsonResult implements Serializable { // 狀態(tài)碼,例如:200 private Integer state; // 消息,例如:”登錄失敗,用戶名不存在” @JsonInclude(JsonInclude.Include.NON_NULL) private String message; // 數(shù)據(jù) @JsonInclude(JsonInclude.Include.NON_NULL) private T data; // ===== 原有其它代碼 =====}
此注解還可以添加在類上,則作用于當前類中所有的屬性,例如:
@Data@JsonInclude(JsonInclude.Include.NON_NULL)public class JsonResult implements Serializable { // ===== 原有其它代碼 =====}
即使添加在類上,也只對當前類的3個屬性有效,后續(xù),當響應某些數(shù)據(jù)時,`data`屬性可能是用戶、商品、訂單等類型,這些類型的數(shù)據(jù)中為`null`的部分依然會被響應到客戶端去,所以,還需要對這些類型也添加相同的注解配置!
以上做法相對比較繁瑣,可以在`application.properties` / `application.yml`中添加全局配置,則作用于當前項目中所有響應時涉及的類,例如在`properties`中配置為:
//yml文件配置寫法spring: jackson: default-property-inclusion: non_null
注意:當你需要在`yml`中添加以上配置時,前綴屬性名可能已經(jīng)存在,則不允許出現(xiàn)重復的前綴屬性名的:
spring: profiles: active: dev jackson: default-property-inclusion: non_null
最后,以上配置只是“默認”配置,如果在某些類型中還有不同的配置需求,仍可以在類或?qū)傩陨贤ㄟ^`@JsonInclude`進行配置。
15. 解決跨域問題
在使用前后端分離的開發(fā)模式下,前端項目和后端項目可能是2個完全不同的項目,并且,各自己獨立開發(fā),獨立部署,在這種做法中,如果前端直接向后端發(fā)送異步請求,默認情況下,在前端會出現(xiàn)類似以下錯誤:
//web端 跨域錯誤Access to XMLHttpRequest at ‘http://localhost:8080/admins/add-new’ from origin ‘http://localhost:8081’ has been blocked by CORS policy:No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
以上錯誤信息的關鍵字是`CORS`,通常稱之為“跨域問題”。
在基于Spring MVC框架的項目中,當需要解決跨域問題時,需要一個Spring MVC的配置類(實現(xiàn)了`WebMvcConfigurer`接口的類),并重寫其中的方法,以允許指定條件的跨域訪問,例如:
//解決跨域的config配置import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.CorsRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class SpringMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(“/**”) .allowedOriginPatterns(“*”) .allowedMethods(“*”) .allowedHeaders(“*”) .allowCredentials(true) .maxAge(3600); }}
16. 關于客戶端提交請求參數(shù)的格式
通常,客戶端向服務器端發(fā)送請求時,請求參數(shù)可以有2種形式,第1種是直接通過`&`拼接各參數(shù)與值,例如:
// FormData//第一種 直接url地址拼接// username=root&password=123456&nickname=jackson&phone=13800138001let data = ‘username=’ + this.ruleForm.username+ ‘&password=’ + this.ruleForm.password+ ‘&nickname=’ + this.ruleForm.nickname+ ‘&phone=’ + this.ruleForm.phone//第2種方式是使用JSON語法來組織各參數(shù)與值let data = {‘username’: this.ruleForm.username, // ‘root”password’: this.ruleForm.password, // ‘123456”nickname’: this.ruleForm.nickname, // ‘jackson”phone’: this.ruleForm.phone, // ‘13800138001’};
具體使用哪種做法,取決于服務器端的設計:
– 如果服務器端處理請求的方法中,在參數(shù)前添加了`@RequestBody`,則允許使用以上第2種做法(JSON數(shù)據(jù))提交請求參數(shù),不允許使用以上第1種做法(使用`&`拼接)
– 如果沒有使用`@RequestBody`,則只能使用以上第1種做法
學習記錄,如有侵權(quán)請聯(lián)系刪除