後端框架開發須要注意的幾點

後端框架開發須要注意的幾點

筆者文筆功力尚淺,若有不妥,請慷慨指出,一定感激涕零前端

跌跌撞撞了在程序員的道路上也有一年的時間了,慢慢的以爲這一年的工做大部分時間都是在簡單的CRUD中度過,而有時候咱們在CRUD中有多少重複性的代碼呢?有些代碼咱們每次寫都須要重複性的寫一次,不只浪費時間,並且對於本身提高並無多大的提升。無心中看到了程序員你爲何這麼累文章後,才幡然醒悟,爲何咱們工做這麼久了不把一些公共部分抽取出來,減小了代碼量才能讓咱們更加專一於技術或者業務的提高不是嗎?java

結合着上面提到的文章中所描述的問題,而且又結合最近一年個人一些遭遇,因而在後端框架開發中可以抽取出來的公共部分有如下部分git

  • 自定義枚舉類
  • 自定義異常信息
  • 統一返回信息
  • 全局異常處理
  • 統一日誌打印

自定義枚舉類

對於一些咱們常常返回的錯誤信息,咱們能夠將其抽取出來封裝成公共部分,而後將變化的做爲參數傳入。例如咱們在業務中常常要校驗某個字段是否爲空,若是爲空的話就要返回錯誤信息xxx字段不能爲空,那麼咱們爲何不將xxx做爲一個變量參數傳遞過來呢。因而就想到了用枚舉類定義異常信息,而後用String.format()方法進行轉義程序員

public enum ResponseInfoEnum {

    SUCCESS(ResponseResult.OK,"處理成功"),
    PARAM_LENGTH_ERROR(ResponseResult.ERROR, "參數:%s,長度錯誤,max length: %s"),
    REQ_PARAM_ERROR(ResponseResult.ERROR, "請求報文必填參數%s缺失"),;

    private Integer code;
    private String message;

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

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

}

複製代碼

使用方法以下github

String.format(ResponseInfoEnum.REQ_PARAM_ERROR.getMessage(),"testValue")

複製代碼

能夠看到生成的錯誤信息是請求報文必填參數testValue缺失後端

自定義異常信息

首先咱們須要知道咱們爲何要用自定義異常信息呢?使用它有什麼好處呢?安全

  1. 首先咱們開發中確定是分模塊進行開發的,因此首先咱們統一了自定義異常類就統一了對外異常的展現方式。
  2. 使用自定義異常繼承相關的異常來拋出處理後的異常信息能夠隱藏底層的異常,這樣更安全,異常信息也更加的直觀。自定義異常能夠拋出咱們本身想要拋出的信息,能夠經過拋出的信息區分異常發生的位置,根據異常名咱們就能夠知道哪裏有異常,根據異常提示信息進行程序修改。
  3. 有時候咱們遇到某些校驗或者問題時,須要直接結束掉當前的請求,這時即可以經過拋出自定義異常來結束,若是你項目中使用了SpringMVC比較新的版本的話有控制器加強,能夠經過@ControllerAdvice註解寫一個控制器加強類來攔截自定義的異常並響應給前端相應的信息。

自定義異常咱們須要繼承RuntimeExceptionbash

public class CheckException extends RuntimeException{

    public CheckException() {
    }

    public CheckException(String message) {
        super(message);
    }

    public CheckException(ResponseInfoEnum responseInfoEnum,String ...strings) {
        super(String.format(responseInfoEnum.getMessage(),strings));
    }
}

複製代碼

統一返回信息

在我剛開始工做的一年中,所接觸的最多的項目就是先後端交互的項目了。因此有一個統一的返回信息不只對前端來講更加便利,對於咱們後面的AOP代理也有很大的好處。app

@Data
@NoArgsConstructor
public class ResponseResult<T> {
    public static final Integer OK = 0;
    public static final Integer ERROR = 100;

    private Integer code;
    private String message;
    private T data;
}

複製代碼

這樣先後端進行交互時就會更加便利了,若是要取業務數據那麼就從data中取,去過要取是否成功的標誌,那麼就從code碼中取,若是要取後端返回的信息,那麼就從message中取。框架

全局異常處理

在我以前的項目中每一個Controller方法中都充斥着try....catch...的代碼,而catch後的代碼都是大同小異,都是封裝了一下返回的錯誤信息之類的。那麼咱們爲何不將這些代碼抽取出來,利用Spring的全局異常處理簡化咱們的代碼呢?

@Slf4j
@ControllerAdvice
public class ControllerExceptionHandler {


    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ResponseResult<String> defaultErrorHandler(HttpServletRequest request, Exception exception){
        log.error(ControllerLog.getLogPrefix()+"Exception: {}"+exception);
        return handleErrorInfo(exception.getMessage());
    }

    @ExceptionHandler(CheckException.class)
    @ResponseBody
    public ResponseResult<String> checkExceptionHandler(HttpServletRequest request, CheckException exception){
        return handleErrorInfo(exception.getMessage());
    }

    private ResponseResult<String> handleErrorInfo(String message) {
        ResponseResult<String> responseEntity = new ResponseResult<>();
        responseEntity.setMessage(message);
        responseEntity.setCode(ResponseResult.ERROR);
        responseEntity.setData(message);
        ControllerLog.destoryThreadLocal();
        return responseEntity;
    }
}

複製代碼

其中全局異常處理中,咱們自定義的異常就沒有打印日誌,由於對於自定義的異常咱們是已知的異常,而且錯誤信息也已經很明確的返回了。而對於未知異常例如Exception就屬於未知的異常,咱們就須要打印日誌,若是這裏有特殊需求,例如發短信、發郵件通知相關人員的話,這裏也可以進行全局的配置。

統一日誌打印

統一日誌打印只是將項目中公共的打印日誌抽取出來,利用AOP來進行打印,例如咱們項目中基本上每一個Controller方法的入參和出參都會打印,因此就將此部分抽取出來進行統一管理。

@Slf4j
@Aspect
@Component
public class ControllerLog {

    private static final ThreadLocal<Long> START_TIME_THREAD_LOCAL =
            new NamedThreadLocal<>("ThreadLocal StartTime");

    private static final ThreadLocal<String> LOG_PREFIX_THREAD_LOCAL =
            new NamedThreadLocal<>("ThreadLocal LogPrefix");

    /**
     * <li>Before       : 在方法執行前進行切面</li>
     * <li>execution    : 定義切面表達式</li>
     * <p>public * com.example.javadevelopmentframework.javadevelopmentframework.controller..*.*(..))
     *      <li>public :匹配全部目標類的public方法,不寫則匹配全部訪問權限</li>
     *      <li>第一個* :方法返回值類型,*表明全部類型 </li>
     *      <li>第二個* :包路徑的通配符</li>
     *      <li>第三個..* :表示impl這個目錄下全部的類,包括子目錄的類</li>
     *      <li>第四個*(..) : *表示全部任意方法名,..表示任意參數</li>
     * </p>
     * @param
     */
    @Pointcut("execution(public * com.example.javadevelopmentframework.javadevelopmentframework.controller..*.*(..))")
    public void exectionMethod(){}


    @Before("exectionMethod()")
    public void doBefore(JoinPoint joinPoint){
        START_TIME_THREAD_LOCAL.set(System.currentTimeMillis());
        StringBuilder argsDes = new StringBuilder();
        //獲取類名
        String className = joinPoint.getSignature().getDeclaringType().getSimpleName();
        //獲取方法名
        String methodName = joinPoint.getSignature().getName();
        //獲取傳入目標方法的參數
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            argsDes.append("第" + (i + 1) + "個參數爲:" + args[i]+"\n");
        }
        String logPrefix = className+"."+methodName;
        LOG_PREFIX_THREAD_LOCAL.set(logPrefix);
        log.info(logPrefix+"Begin 入參爲:{}",argsDes.toString());
    }

    @AfterReturning(pointcut="exectionMethod()",returning = "rtn")
    public Object doAfter(Object rtn){
        long endTime = System.currentTimeMillis();
        long begin = START_TIME_THREAD_LOCAL.get();
        log.info(LOG_PREFIX_THREAD_LOCAL.get()+"End 出參爲:{},耗時:{}",rtn,endTime-begin);
        destoryThreadLocal();
        return rtn;
    }

    public static String getLogPrefix(){
        return LOG_PREFIX_THREAD_LOCAL.get();
    }

    public static void destoryThreadLocal(){
        START_TIME_THREAD_LOCAL.remove();
        LOG_PREFIX_THREAD_LOCAL.remove();
    }

}

複製代碼

測試

咱們在Conroller中寫以下測試

@RestController
public class TestFrameworkController {

    @RequestMapping("/success/{value}")
    public String success(@PathVariable String value){
        return "Return "+value;
    }

    @RequestMapping("/error/{value}")
    public String error(@PathVariable String value){
        int i = 10/0;
        return "Return "+value;
    }
}

複製代碼

單元測試中代碼以下

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = JavadevelopmentframeworkApplication.class)
@AutoConfigureMockMvc
public class JavadevelopmentframeworkApplicationTests {

	@Autowired
	private MockMvc mockMvc;

	@Test
	public void success() throws Exception {
		mockMvc.perform(get("/success/11"));
		mockMvc.perform(get("/error/11"));
	}

}

複製代碼

能夠看到打印以下

2019-09-03 20:38:22.248  INFO 73902 --- [           main] c.e.j.j.aop.ControllerLog                : TestFrameworkController.successBegin 入參爲:第1個參數爲:11
2019-09-03 20:38:22.257  INFO 73902 --- [           main] c.e.j.j.aop.ControllerLog                : TestFrameworkController.successEnd 出參爲:Return 11,耗時:10
2019-09-03 20:38:22.286  INFO 73902 --- [           main] c.e.j.j.aop.ControllerLog                : TestFrameworkController.errorBegin 入參爲:第1個參數爲:11
2019-09-03 20:38:22.288 ERROR 73902 --- [           main] c.e.j.j.aop.ControllerExceptionHandler   : TestFrameworkController.errorException: {}java.lang.ArithmeticException: / by zero

複製代碼

能夠看到每一個訪問Controller的方法入參、出參、整個方法的執行時間都已經打印出來了。另外在第二個測試的方法中異常信息捕捉到並打印日誌了。

完整代碼

總結

在編寫代碼過程當中咱們也須要不斷的總結,不管是需求的變動仍是系統的搭建,咱們都須要考慮哪一部分是變化的哪一部分是不變的,將不變的抽取出來,變化的封裝起來。這樣在之後不管是系統擴展仍是需求變動中咱們都可以以最小的代價來完成任務。

相關文章
相關標籤/搜索