SpringBoot相知

前言

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

表單驗證

如今將要攔截未滿18歲的女生,在以前GirlController裏面添加一個女生的方法以下:java

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

如今要對年齡作限制,先進入Girl實體爲age屬性添加 @Min註解github

接着在添加女生的方法上添加 @Valid註解,表示要驗證這個對象。而驗證完以後要知道是驗證經過仍是沒經過,它會將驗證的結果返回到BindingResult對象裏,若是有錯誤,要將它打印出來。web

@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);
    }

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

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

控制檯報錯並打印錯誤信息:編程

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

AOP處理請求

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

添加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

相關文章
相關標籤/搜索