SpringBoot相知


前言

這篇文章的將介紹表單驗證,AOP處理請求和統一異常處理,案例是延續上一篇 SpringBoot初識

表單驗證

現在將要攔截未滿18歲的女生,在之前GirlController里面添加一個女生的方法如下:

方法的形參使用的都是屬性,那以后當屬性變多的時候再來管理就會變得很復雜,直接傳遞Girl對象就是最好的方法。

現在要對年齡做限制,先進入Girl實體為age屬性添加 @Min注解

接着在添加女生的方法上添加 @Valid注解,表示要驗證這個對象。而驗證完之后要知道是驗證通過還是沒通過,它會將驗證的結果返回到BindingResult對象里,如果有錯誤,要將它打印出來。

    @PostMapping("/girls")
    public Girl girlAdd(@Valid Girl girl, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            System.out.println(bindingResult.getFieldError().getDefaultMessage());
            return null;
        }

        return girlRepository.save(girl);
    }

此時傳入一個年齡合法的女生:

再傳入一個年齡小於18歲的女生:

控制台報錯並打印錯誤信息:

數據庫中也沒有添加剛才的信息:

AOP處理請求

AOP是一種編程范式,與語言無關,它是一種程序設計思想。面向對象關注的是將需求功能垂直划分為不同的並且相對獨立的,它會封裝為良好的類,並且有屬於自己的行為。而AOP則是利用橫切的技術,將面向對象構建的龐大類的體系進行水平的切割,並且會將影響到了多個類的公共行為封裝為一個可重用的模塊,這個模塊就稱為切面。AOP的關鍵思想就是將通用邏輯從業務邏輯中分離出來。

添加pom依賴

    <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

創建切面

在新建的aspect包里新建HttpAspect類,設置切點攔截GirlController類里面的所有方法,然后把 @Pointcut注解放在一個空的方法上log(),之后的前置增強和后置增強就直接使用 @Before("log()")注解作用在方法上即可。

為了優雅的打印結果,就不在使用system.out了,使用日志打印結果.

package com.zzh.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;


@Aspect
@Component
public class HttpAspect {

    private final static Logger logger = LoggerFactory.getLogger(HttpAspect.class);


    @Pointcut("execution(public * com.zzh.controller.GirlController.*(..))")
    public void log() {
    }

    @Before("log()")
    public void doBefore() {
        logger.info("This is Before");
    }

    @After("log()")
    public void doAfter() {
        logger.info("This is After ");
    }
}

接着在Controller的查詢方法里面添加一行日志打印來觀察日志輸出順序

查看打印結果:

采用記錄日志的方式,會更為詳細的打印出該條語句相關的信息,比System.out好了很多。

打印Http請求

package com.zzh.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;


@Aspect
@Component
public class HttpAspect {

    private final static Logger logger = LoggerFactory.getLogger(HttpAspect.class);


    @Pointcut("execution(public * com.zzh.controller.GirlController.*(..))")
    public void log() {
    }
    

    @Before("log()")
    //記錄Http請求
    public void doBefore(JoinPoint joinPoint) {

        ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        //url
        logger.info("url={}",request.getRequestURL());

        //method
        logger.info("method={}",request.getMethod());

        //ip
        logger.info("ip={}",request.getRemoteAddr());

        //類方法
        logger.info("class_method={}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());

        //參數
        logger.info("args={}",joinPoint.getArgs());
    }

    @After("log()")
    public void doAfter() {
        logger.info("This is After ");
    }

}

執行查詢后,控制台打印:

AfterReturning注解

使用這個注解可以得到執行方法之后的返回信息,也就是

添加注解:

再次執行查詢,控制台打印:

可以看到這里的response打印出了對象,但是具體的信息沒有打印出來,此時需要在實體Girl里面重寫toString方法即可。

重新執行查詢,可以看到具體信息打印出來了:

異常統一處理

在實體Girl中增加money字段,同時在money屬性上增加 @NotNull注解,也就是當我們不傳入money時會報錯。

不傳入money信息:

控制台報錯:

這里出現了空指針異常,它是HttpAspect類中doAfterReturning拋出的,這是因為在Controller的girlAdd方法里增加了表單驗證,返回了null,而到了doAfterReturning方法時,還調用了object.toString方法所以拋出了異常。

當沒有傳入金額時,“金額必傳”是由控制台打印輸出,而如果改為在網頁上輸出,改變Controller中的girlAdd方法,將錯誤信息直接return給網頁,注意返回類型需要改為Object,因為成功的時候是返回Girl對象。

繼續添加一個沒有傳入金額的女生,網頁返回字符串:

控制台打印“字符串”,因為現在的對象就是這個錯誤信息:

規范返回格式

上面介紹了如果出現錯誤返回字符串,如果正確就返回json,這樣格式很混亂,所以需要進行整理。

比如如果金額不符合,就返回{"code":1, "msg":"金額必傳", "data":null}。成功的話就是{"code":0, "msg":"成功", "data":{"id":20,"cupSize":"B","age":25,"money":1.2}}這樣的格式。

創建Result類

Result類作為http請求返回的最外層對象

package com.zzh.domain;


public class Result<T> {

    //錯誤碼
    private Integer code;

    //提示信息
    private String msg;

    //具體內容
    private T data;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

修改Controller中girlAdd方法

添加一條沒有金額的女生:

添加一條有金額但是未滿18歲的女生:

添加一條信息正確的女生:

規范重復代碼

可以看到上面的代碼的result的相關操作已經重復調用了,所以新創建ResultUtil類來封裝重復操作。

package com.zzh.utils;

import com.zzh.domain.Result;


public class ResultUtil {

    public static Result success(Object object) {
        Result result = new Result();
        result.setCode(0);
        result.setMsg("成功");
        result.setData(object);
        return result;
    }

    public static Result success() {
        
        return success(null);
    }

    public static Result error(Integer code, String msg) {
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }
}

此時Controller中的girlAdd方法簡化如下:

測試得到的結果跟之前的一樣,但是Controller中的重復代碼省略了。


異常處理

現在需要獲取女生的年齡並判斷,如果小於10,就返回一個字符串,如果大於10小於16又返回另外一個字符串。首先想到的就是直接在Service中寫一個判斷邏輯,返回類型設為String,符合條件的直接return那個字符串就行,這樣做也可以,但是如果判斷完之后我還要做一些其他的事情,那么這個返回類型就已經限制了功能的擴展。

這時用異常來處理就很好,滿足條件,直接throw給上一層,也就是Controller,然后Controller繼續拋出,這樣當條件滿足時,這個異常信息(也就是那個字符串)就會在控制台上出現。

Controller中新添加一個方法

實現邏輯通過service來處理

不過這樣還是沒有達到本來的目的,我們的目的是,瀏覽器返回的Json要是之前設置好的code,msg,data,然后msg字段就用來顯示拋出的字符串。

解決的方法就是對Controller拋出的內容進行捕獲,取到需要的內容封裝起來再返回給瀏覽器。

創建異常捕獲類

這里是對Controller進行異常捕獲,需要加上 @ControllerAdvice注解

package com.zzh.handle;

import com.zzh.domain.Result;
import com.zzh.utils.ResultUtil;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;


@ControllerAdvice
public class ExceptionHandler {

    @org.springframework.web.bind.annotation.ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result handle(Exception e) {
        return ResultUtil.error(100, e.getMessage());
    }
}

此時在數據庫中設置一條記錄:

通過方法測試第三條數據:

自定義異常

現在的異常信息返回的code都是100,如果要划分異常,比如年齡小於10的code設為100,而大於10小於16的code設為101,划分之后更方便排查問題。而Exception里面只能傳message,不能再傳code進去了,所以需要自己定義異常。

自定義異常沒有繼承Exception,而是繼承RuntimeException是有原因的,RuntimeException是繼承Exception,但是Spring只對RuntimeException進行事務回滾,如果拋出的是Exception是不會回滾的。

package com.zzh.exception;



public class GirlException extends RuntimeException{

    private Integer code;

    
    public GirlException(Integer code,String message) {
        super(message);
        this.code = code;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }
}

Service中的方法也需要修改,將拋出的異常改為自定義的異常:

在之前設定的ExceptionHandler捕獲的是Exception,所以需要進行判斷異常是不是自己定義的異常。如果不是就把code設置為-1,message設置為未知錯誤。

package com.zzh.handle;

import com.zzh.domain.Result;
import com.zzh.exception.GirlException;
import com.zzh.utils.ResultUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;


@ControllerAdvice
public class ExceptionHandler {

    private final static Logger logger = LoggerFactory.getLogger(ExceptionHandler.class);

    @org.springframework.web.bind.annotation.ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result handle(Exception e) {
        //判斷異常是不是自己定義的異常
        if (e instanceof GirlException) {
            GirlException girlException = (GirlException) e;
            return ResultUtil.error(girlException.getCode(), girlException.getMessage());
        } else {
            logger.error("[系統異常] {}", e);
            return ResultUtil.error(-1, "未知錯誤");
        }
    }
}

測試:

要測試自定義異常里的系統異常要怎么樣做呢?比如通過不傳入金額讓它報系統異常,稍微改動一點就可以了:

為什么要改為return null呢,如果不改的話code就會是1了,只有改為了null,切面里的object.toString才會報錯。

不傳入金額:

之前在ExceptionHandler設置了Logger,現在控制台就可以找到該系統異常問題所在:

使用枚舉封裝code和message

在前面所拋出的GirlException中,是直接將code和message作為參數進行傳遞,這樣很不容易做后期維護,如果code和message統一封裝起來就很方便進行維護了。

枚舉里面只需要有屬性的Getter方法即可,因為枚舉的使用都是通過構造方法來創建,不會再使用Setter。

package com.zzh.enums;


public enum ResultEnum {
    UNKONW_ERROR(-1, "未知錯誤"),
    SUCCESS(0, "成功"),
    PRIMARY_SCHOOL(100, "你可能還在上小學"),
    MIDDLE_SCHOOL(101, "你可能還在上初中"),;

    private Integer code;

    private String msg;

    ResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

修改Service中的方法

GirlException中的構造方法也要修改:


ResultUtil中的無參success方法

在ResultUtil中總共定義了3個方法,一個是有參的success方法,當添加女生信息正確的時候需要將Girl對象作為參數傳給success方法,再由ResultUtil進行封裝后傳給瀏覽器。
而ResultUtil中的error方法也類似,反正就是將code和錯誤信息進行封裝。
那這里的無參success方法是用在什么地方呢,我先執行一下Controller中刪除單個女生的方法:

數據正常刪除,不過返回信息和控制台信息卻不是很友好:

原因顯而易見了,設置的切面AfterReturning中有object.toString方法,我Controller中這個刪除的方法沒有返回值(void)。自然就報了空指針異常,然后這個異常被ExceptionHandler捕獲,設置了code和msg值,以此傳遞給瀏覽器。

修改的方法就是使用無參的success方法:

設置了Result作為返回值,切面就不會報錯,同時無參success方法體里再調用有參的success方法,只不過object為null,這樣一來就很友好的顯示了。

執行方法:

完美刪除!

P.S 說個笑話,剛才在使用RESTClient進行刪除操作時,Ctrl+Enter是執行的快捷鍵,也就是可以替代點擊綠色的按鈕。我先按下了Ctrl,然后再按下了Enter,報錯!!但是數據正常刪除,仔細查看控制台輸出錯誤信息,上面顯示我執行了兩次刪除操作,對同一個id進行兩次刪除想想都知道肯定會報錯,但是我只按了一次快捷鍵呀,然后我嘗試不用快捷鍵而是去點擊綠色執行按鈕,無論是控制台還是瀏覽器返回都TM正常!帶着疑惑吃了飯回來,腦洞大開同時按下Ctrl+Enter,一切問題解決,都不需要Google,扎心了。


單元測試

測試Service

在GirlService中新建要測試的方法:

接着按下Ctrl+Shift+T,快速創建一個測試類,勾選要測試的方法:

在測試類中使用斷言,將指定id女生的年齡提取出來與設置進行比較。

package com.zzh.service;

import com.zzh.domain.Girl;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;


@RunWith(SpringRunner.class)
@SpringBootTest
public class GirlServiceTest {

    @Autowired
    private GirlService girlService;

    @Test
    public void findOne() throws Exception {
        Girl girl = girlService.findOne(2);
        Assert.assertEquals(new Integer(25), girl.getAge());
    }

}

測試結果:

現在將設置的年齡改為17,也就是: Assert.assertEquals(new Integer(17), girl.getAge());

測試很友好的告訴了我們,這個ID對應的真實年齡是25,但是我們期待的是17。Service測試完畢。

測試API

選擇對Controller中girlList方法進行測試:

這里使用的不是girlController對象調用girlList方法,這樣一來跟URL完全沒有關系了,這里的測試需要像之前使用的RESTClient,給一個地址,然后發出Get請求,得到結果,這樣才是API測試。

這就需要使用MockMvc這個類了,注意添加 @AutoConfigureMockMvc注解:

package com.zzh.controller;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import static org.junit.Assert.*;


@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class GirlControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void girlList() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/girls"))
                .andExpect(MockMvcResultMatchers.status().isOk());

    }

}

這樣做就會對這個請求地址的狀態碼進行判斷:

現在將請求地址故意改錯:("/girls234")

可以看到我們期待的狀態是200,但是實際為404.

除了狀態之外還可以做其他判斷,比如對返回的內容進行判斷,期待的是abc,但實際是一個json字符串:

測試:

對API的測試和對Service的測試區別在於要使用MockMvc進行測試。


總結

本文簡單介紹了如何使用 @Valid表單驗證,然后是使用AOP處理請求,接着是統一異常處理,最后是對Service和API的單元測試。

Github地址
SpringBoot-girl


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM