1. 程式人生 > >後端框架開發需要注意的幾點

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

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

筆者文筆功力尚淺,如有不妥,請慷慨指出,必定感激不盡

跌跌撞撞了在程式設計師的道路上也有一年的時間了,慢慢的覺得這一年的工作大部分時間都是在簡單的CRUD中度過,而有時候我們在CRUD中有多少重複性的程式碼呢?有些程式碼我們每次寫都需要重複性的寫一次,不僅浪費時間,而且對於自己提升並沒有多大的提高。無意中看到了程式設計師你為什麼這麼累文章後,才幡然醒悟,為什麼我們工作這麼久了不把一些公共部分抽取出來,減少了程式碼量才能讓我們更加專注於技術或者業務的提升不是嗎?

結合著上面提到的文章中所描述的問題,並且又結合最近一年我的一些遭遇,於是在後端框架開發中能夠抽取出來的公共部分有以下部分

  • 自定義列舉類
  • 自定義異常資訊
  • 統一返回資訊
  • 全域性異常處理
  • 統一日誌列印

自定義列舉類

對於一些我們經常返回的錯誤資訊,我們可以將其抽取出來封裝成公共部分,然後將變化的作為引數傳入。例如我們在業務中經常要校驗某個欄位是否為空,如果為空的話就要返回錯誤資訊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;
    }

}

使用方法如下

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

可以看到生成的錯誤資訊是請求報文必填引數testValue缺失

自定義異常資訊

首先我們需要知道我們為什麼要用自定義異常資訊呢?使用它有什麼好處呢?

  1. 首先我們開發中肯定是分模組進行開發的,所以首先我們統一了自定義異常類就統一了對外異常的展示方式。
  2. 使用自定義異常繼承相關的異常來丟擲處理後的異常資訊可以隱藏底層的異常,這樣更安全,異常資訊也更加的直觀。自定義異常可以丟擲我們自己想要丟擲的資訊,可以通過丟擲的資訊區分異常發生的位置,根據異常名我們就可以知道哪裡有異常,根據異常提示資訊進行程式修改。
  3. 有時候我們遇到某些校驗或者問題時,需要直接結束掉當前的請求,這時便可以通過丟擲自定義異常來結束,如果你專案中使用了SpringMVC比較新的版本的話有控制器增強,可以通過@ControllerAdvice註解寫一個控制器增強類來攔截自定義的異常並響應給前端相應的資訊。

自定義異常我們需要繼承RuntimeException

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代理也有很大的好處。

@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的方法入參、出參、整個方法的執行時間都已經打印出來了。另外在第二個測試的方法中異常資訊捕捉到並列印日誌了。

完整程式碼

總結

在編寫程式碼過程中我們也需要不斷的總結,無論是需求的變更還是系統的搭建,我們都需要考慮哪一部分是變化的哪一部分是不變的,將不變的抽取出來,變化的封裝起來。這樣在以後無論是系統擴充套件還是需求變更中我們都能夠以最小的代