美文网首页
Spring Boot Web开发快速入门

Spring Boot Web开发快速入门

作者: 端碗吹水 | 来源:发表于2019-11-17 18:49 被阅读0次

    使用验证注解来实现表单验证

    虽说前端的h5和js都可以完成表单的字段验证,但是这只能是防止一些小白、误操作而已。如果是一些别有用心的人,是很容易越过这些前端验证的,有句话就是说永远不要相信客户端传递过来的数据。所以前端验证之后,后端也需要再次进行表单字段的验证,以确保数据到后端后是正确的、符合规范的。本节就简单介绍一下,在SpringBoot的时候如何进行表单验证。

    首先创建一个SpringBoot工程,其中pom.xml配置文件主要配置内容如下:

        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.0.1.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    

    创建一个pojo类,在该类中需要验证的字段上加上验证注解。代码如下:

    package org.zero01.domain;
    
    import javax.validation.constraints.Min;
    import javax.validation.constraints.NotNull;
    
    public class Student {
    
        @NotNull(message = "学生名字不能为空")
        private String sname;
    
        @Min(value = 18,message = "未成年禁止注册")
        private int age;
    
        @NotNull(message = "性别不能为空")
        private String sex;
    
        @NotNull(message = "联系地址不能为空")
        private String address;
    
        public String toString() {
            return "Student{" +
                    "sname='" + sname + '\'' +
                    ", age=" + age +
                    ", sex='" + sex + '\'' +
                    ", address='" + address + '\'' +
                    '}';
        }
    
        ... getter setter 略 ...
    }
    

    创建一个Controller类:

    package org.zero01.controller;
    
    import org.springframework.validation.BindingResult;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.zero01.domain.Student;
    
    import javax.validation.Valid;
    
    @RestController
    public class StudentController {
    
        @PostMapping("register.do")
        public Student register(@Valid Student student, BindingResult bindingResult){
            if (bindingResult.hasErrors()) {
                // 打印错误信息
                System.out.println(bindingResult.getFieldError().getDefaultMessage());
                return null;
            }
            return student;
        }
    }
    

    启动运行类,代码如下:

    package org.zero01;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class SbWebApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SbWebApplication.class, args);
        }
    }
    

    使用postman进行测试,年龄不满18岁的情况:


    image.png

    控制台打印结果:

    未成年禁止注册
    

    非空字段为空的情况:


    image.png

    控制台打印结果:

    学生名字不能为空
    

    使用AOP记录请求日志

    我们都知道在Spring里的两大核心模块就是AOP和IOC,其中AOP为面向切面编程,这是一种编程思想或者说范式,它并不是某一种语言所特有的语法。

    我们在开发业务代码的时候,经常有很多代码是通用且重复的,这些代码我们就可以作为一个切面提取出来,放在一个切面类中,进行一个统一的处理,这些处理就是指定在哪些切点织入哪些切面。

    例如,像日志记录,检查用户是否登录,检查用户是否拥有管理员权限等十分通用且重复的功能代码,就可以被作为一个切面提取出来。而框架中的AOP模块,可以帮助我们很方便的去实现AOP的编程方式,让我们实现AOP更加简单。

    本节将承接上一节,演示一下如何利用AOP实现简单的http请求日志的记录。首先创建一个切面类如下:

    package org.zero01.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 static final Logger logger = LoggerFactory.getLogger(HttpAspect.class);
    
        @Pointcut("execution(public * org.zero01.controller.StudentController.*(..))")
        public void log() {
        }
    
        @Before("log()")
        public void beforeLog(JoinPoint joinPoint) {
            // 日志格式:url method clientIp classMethod param
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
    
            logger.info("url = {}", request.getRequestURL());
            logger.info("method = {}", request.getMethod());
            logger.info("clientIp = {}", request.getRemoteHost());
            logger.info("class_method = {}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
            logger.info("param = {}", joinPoint.getArgs());
        }
    
        @AfterReturning(returning = "object", pointcut = "log()")
        public void afterReturningLog(Object object) {
            // 打印方法返回值内容
            logger.info("response = {}", object);
        }
    }
    

    使用PostMan访问方式如下:


    image.png

    访问成功后,控制台输出日志如下:


    image.png

    如此,我们就完成了http请求日志的记录。


    封装统一的返回数据对象

    我们在控制器类的方法中,总是需要返回各种不同类型的数据给客户端。例如,有时候需要返回集合对象、有时候返回字符串、有时候返回自定义对象等等。而且在一个方法里可能会因为处理的结果不同,而返回不同的对象。那么当一个方法中需要根据不同的处理结果返回不同的对象时,我们应该怎么办呢?可能有人会想到把方法的返回类型设定为Object不就可以了,的确是可以,但是这样返回的数据格式就不统一。前端接收到数据时,很不方便去展示,后端写接口文档的时候也不好写。所以我们应该统一返回数据的格式,而使用Object就无法做到这一点了。

    所以我们需要将返回的数据统一封装在一个对象里,然后统一在控制器类的方法中,把这个对象设定为返回值类型即可,这样我们返回的数据格式就有了一个标准。那么我们就来开发一个这样的对象吧,首先新建一个枚举类,因为我们需要把一些通用的常量数据都封装在枚举类里,以后这些数据发生变动时,只需要修改枚举类即可。如果将这些常量数据硬编码写在代码里就得逐个去修改了,十分的难以维护。代码如下:

    package org.zero01.enums;
    
    public enum ResultEnum {
    
        UNKONW_ERROR(-1, "未知错误"),
        SUCCESS(0, "SUCCESS"),
        ERROR(1, "ERROR"),
        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;
        }
    }
    

    然后就是创建我们的返回数据封装对象了,在此之前,我们需要先定义好这个数据的一个标准格式。我这里定义的格式如下:

    {
        "code": 0,
        "msg": "注册成功",
        "data": {
            "sname": "Max",
            "age": 18,
            "sex": "woman",
            "address": "湖南"
        }
    }
    

    明确了数据的格式后,就可以开发我们的返回数据封装对象了。新建一个类,代码如下:

    package org.zero01.domain;
    
    import org.zero01.enums.ResultEnum;
    
    /**
     * @program: sb-web
     * @description: 服务器统一的返回数据封装对象
     * @author: 01
     * @create: 2018-05-05 18:03
     **/
    public class Result<T> {
    
        // 错误/正确码
        private Integer code;
        // 提示信息
        private String msg;
        // 返回的数据
        private T data;
    
        private Result(Integer code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    
        private Result(Integer code) {
            this.code = code;
        }
    
        private Result(Integer code, String msg, T data) {
            this.code = code;
            this.msg = msg;
            this.data = data;
        }
    
        private Result() {
        }
    
        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;
        }
    
        public static <T> Result<T> createBySuccessResultMessage(String msg) {
            return new Result<T>(ResultEnum.SUCCESS.getCode(), msg);
        }
    
        public static <T> Result<T> createBySuccessCodeResult(Integer code, String msg) {
            return new Result<T>(code, msg);
        }
    
        public static <T> Result<T> createBySuccessResult(String msg, T data) {
            return new Result<T>(ResultEnum.SUCCESS.getCode(), msg, data);
        }
    
        public static <T> Result<T> createBySuccessResult() {
            return new Result<T>(ResultEnum.SUCCESS.getCode());
        }
    
        public static <T> Result<T> createByErrorResult() {
            return new Result<T>(ResultEnum.ERROR.getCode());
        }
    
        public static <T> Result<T> createByErrorResult(String msg, T data) {
            return new Result<T>(ResultEnum.ERROR.getCode(), msg, data);
        }
    
        public static <T> Result<T> createByErrorCodeResult(Integer errorCode, String msg) {
            return new Result<T>(errorCode, msg);
        }
    
        public static <T> Result<T> createByErrorResultMessage(String msg) {
            return new Result<T>(ResultEnum.ERROR.getCode(), msg);
        }
    }
    

    接着修改我们之前的注册接口代码如下:

    @PostMapping("register.do")
    public Result<Student> register(@Valid Student student, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return Result.createByErrorResultMessage(bindingResult.getFieldError().getDefaultMessage());
        }
        return Result.createBySuccessResult("注册成功", student);
    }
    

    使用PostMan进行测试,数据正常的情况:


    image.png

    学生姓名为空的情况:


    image.png

    如上,可以看到,返回的数据格式都是一样的,code字段的值用于判断是一个success的结果还是一个error的结果,msg字段的值是提示信息,data字段则是存储具体的数据。有这样一个统一的格式后,前端也好解析这个json数据,我们后端在写接口文档的时候也好写了。


    统一异常处理

    一个系统或应用程序在运行的过程中,由于种种因素,肯定是会有抛异常的情况的。在系统出现异常时,由于服务的中断,数据可能会得不到返回,亦或者返回的是一个与我们定义的数据格式不相符的一个数据,这是我们不希望出现的问题。所以我们得进行一个全局统一的异常处理,拦截系统中会出现的异常,并进行处理。下面我们用一个小例子来做为演示。

    例如,现在有一个业务需求如下:

    • 获取某学生的年龄进行判断,小于10,抛出异常并返回“小学生”提示信息,大于10且小于16,抛出异常并返回“初中生”提示信息。

    首先我们需要自定义一个异常,因为默认的异常构造器只接受一个字符串类型的数据,而我们返回的数据中有一个code,所以我们得自己定义个异常类。代码如下:

    package org.zero01.exception;
    
    /**
     * @program: sb-web
     * @description: 自定义异常
     * @author: 01
     * @create: 2018-05-05 19:01
     **/
    public class StudentException extends RuntimeException {
    
        private Integer code;
    
        public StudentException(Integer code, String msg) {
            super(msg);
            this.code = code;
        }
    
        public Integer getCode() {
            return code;
        }
    }
    

    新建一个 ErrorHandler 类,用于全局异常的拦截及处理。代码如下:

    package org.zero01.handle;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.zero01.domain.Result;
    import org.zero01.enums.ResultEnum;
    import org.zero01.exception.StudentException;
    
    /**
     * @program: sb-web
     * @description: 全局异常处理类
     * @author: 01
     * @create: 2018-05-05 18:48
     **/
    // 定义全局异常处理类
    @ControllerAdvice
    // Lombok的一个注解,用于日志打印
    @Slf4j
    public class ErrorHandler {
    
        // 声明异常处理方法,传递哪一个异常对象的class,就代表该方法会拦截哪一个异常对象包括其子类
        @ExceptionHandler(value = Exception.class)
        @ResponseBody
        public Result exceptionHandle(Exception e) {
            if (e instanceof StudentException) {
                StudentException studentException = (StudentException) e;
                // 返回统一的数据格式
                return Result.createByErrorCodeResult(studentException.getCode(), studentException.getMessage());
            }
            // 打印异常日志
            log.error("[系统异常]{}", e);
            // 返回统一的数据格式
            return Result.createByErrorCodeResult(ResultEnum.UNKONW_ERROR.getCode(), "服务器内部出现未知错误");
        }
    }
    

    注:我这里使用到了Lombok,如果对Lombok不熟悉的话,可以参考我之前写的一篇Lombok快速入门

    在之前的控制类中,增加如下代码:

    @Autowired
    private IStudentService iStudentService;
    
    @GetMapping("check_age.do")
    public void checkAge(Integer age) throws Exception {
        iStudentService.checkAge(age);
        age.toString();
    }
    

    我们都知道具体的逻辑都是写在service层的,所以新建一个service包,在该包中新建一个接口。代码如下:

    package org.zero01.service;
    
    public interface IStudentService {
        void checkAge(Integer age) throws Exception;
    }
    

    然后新建一个类,实现该接口。代码如下:

    package org.zero01.service;
    
    import org.springframework.stereotype.Service;
    import org.zero01.enums.ResultEnum;
    import org.zero01.exception.StudentException;
    
    @Service("iStudentService")
    public class StudentService implements IStudentService {
    
        public void checkAge(Integer age) throws StudentException {
            if (age < 10) {
                throw new StudentException(ResultEnum.PRIMARY_SCHOOL.getCode(), ResultEnum.PRIMARY_SCHOOL.getMsg());
            } else if (age > 10 && age < 16) {
                throw new StudentException(ResultEnum.MIDDLE_SCHOOL.getCode(), ResultEnum.MIDDLE_SCHOOL.getMsg());
            }
        }
    }
    

    完成以上的代码编写后,就可以开始进行测试了。age < 10 的情况:

    image.png

    age > 10 && age < 16 的情况:

    image.png

    age字段为空,出现系统异常的情况:


    image.png

    因为我们打印了日志,所以出现系统异常的时候也会输出日志信息,不至于我们无法定位到异常:


    image.png

    从以上的测试结果中可以看到,即便抛出了异常,我们返回的数据格式依旧是固定的,这样就不会由于系统出现异常而返回不一样的数据格式。


    单元测试

    我们一般会在开发完项目中的某一个功能的时候,就会进行一个单元测试。以确保交付项目时,我们的代码都是通过测试并且功能正常的,这是一个开发人员基本的素养。所以本节将简单介绍service层的测试与controller层的测试方式。

    首先是service层的测试方式,service层的单元测试和我们平时写的测试没太大区别。在工程的test目录下,新建一个测试类,代码如下:

    package org.zero01;
    
    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;
    import org.zero01.domain.Result;
    import org.zero01.domain.Student;
    import org.zero01.service.IStudentService;
    
    /**
     * @program: sb-web
     * @description: Student测试类
     * @author: 01
     * @create: 2018-05-05 21:46
     **/
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class StudentServiceTest {
    
        @Autowired
        private IStudentService iStudentService;
    
        @Test
        public void findOneTest() {
            Result<Student> result = iStudentService.findOne(1);
            Student student = result.getData();
            Assert.assertEquals(18, student.getAge());
        }
    }
    

    执行该测试用例,运行结果如下:


    image.png

    我们修改一下年龄为15,以此模拟一下测试不通过的情况:


    image.png

    service层的测试比较简单,就介绍到这。接下来我们看一下controller层的测试方式。IDEA中有一个比较方便的功能可以帮我们生成测试方法,到需要被测试的controller类中,按 Ctrl + Shift + t 就可以快速创建测试方法。如下,点击Create New Test:


    image.png

    然后选择需要测试的方法:


    image.png

    生成的测试用例代码如下:

    package org.zero01.controller;
    
    import org.junit.Test;
    
    import static org.junit.Assert.*;
    
    public class StudentControllerTest {
    
        @Test
        public void checkAge() {
        }
    }
    

    接着我们来完成这个测试代码,controller层的测试和service层不太一样,因为需要访问url,而不是直接调用方法进行测试。测试代码如下:

    package org.zero01.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;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class StudentControllerTest {
    
        @Autowired
        private MockMvc mockMvc;
    
        @Test
        public void checkAge() throws Exception {
            mockMvc.perform(MockMvcRequestBuilders.get("/check_age.do")  // 使用get请求
                    .param("age","18"))  // url参数
                    .andExpect(MockMvcResultMatchers.status().isOk());  // 判断返回的状态是否正常
        }
    }
    

    运行该测试用例,因为我们之前实现了一个记录http访问日志的功能,所以可以直接通过控制台的输出日志来判断接口是否有被请求到:


    image.png

    单元测试就介绍到这,毕竟一般我们不会在代码上测试controller层,而是使用postman或者restlet client等工具进行测试。

    相关文章

      网友评论

          本文标题:Spring Boot Web开发快速入门

          本文链接:https://www.haomeiwen.com/subject/vhxmictx.html