前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >初识SpringBoot Web开发

初识SpringBoot Web开发

作者头像
端碗吹水
发布2020-09-23 14:13:03
4250
发布2020-09-23 14:13:03
举报

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

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

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

代码语言:javascript
复制
    <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类,在该类中需要验证的字段上加上验证注解。代码如下:

代码语言:javascript
复制
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类:

代码语言:javascript
复制
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;
    }
}

启动运行类,代码如下:

代码语言:javascript
复制
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岁的情况:

控制台打印结果:

代码语言:javascript
复制
未成年禁止注册

非空字段为空的情况:

控制台打印结果:

代码语言:javascript
复制
学生名字不能为空

使用AOP记录请求日志

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

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

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

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

代码语言:javascript
复制
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访问方式如下:

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

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


封装统一的返回数据对象

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

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

代码语言:javascript
复制
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;
    }
}

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

代码语言:javascript
复制
{
    "code": 0,
    "msg": "注册成功",
    "data": {
        "sname": "Max",
        "age": 18,
        "sex": "woman",
        "address": "湖南"
    }
}

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

代码语言:javascript
复制
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> createBySucce***esultMessage(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> createBySucce***esult(String msg, T data) {
        return new Result<T>(ResultEnum.SUCCESS.getCode(), msg, data);
    }

    public static <T> Result<T> createBySucce***esult() {
        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);
    }
}

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

代码语言:javascript
复制
@PostMapping("register.do")
public Result<Student> register(@Valid Student student, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return Result.createByErrorResultMessage(bindingResult.getFieldError().getDefaultMessage());
    }
    return Result.createBySucce***esult("注册成功", student);
}

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

学生姓名为空的情况:

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


统一异常处理

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

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

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

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

代码语言:javascript
复制
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 类,用于全局异常的拦截及处理。代码如下:

代码语言:javascript
复制
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快速入门

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

代码语言:javascript
复制
@Autowired
private IStudentService iStudentService;

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

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

代码语言:javascript
复制
package org.zero01.service;

public interface IStudentService {
    void checkAge(Integer age) throws Exception;
}

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

代码语言:javascript
复制
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 &lt; 10 的情况:

age &gt; 10 && age &lt; 16 的情况:

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

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

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


单元测试

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

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

代码语言:javascript
复制
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());
    }
}

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

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

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

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

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

代码语言:javascript
复制
package org.zero01.controller;

import org.junit.Test;

import static org.junit.Assert.*;

public class StudentControllerTest {

    @Test
    public void checkAge() {
    }
}

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

代码语言:javascript
复制
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访问日志的功能,所以可以直接通过控制台的输出日志来判断接口是否有被请求到:

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018-05-05 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 使用验证注解来实现表单验证
  • 使用AOP记录请求日志
  • 封装统一的返回数据对象
  • 统一异常处理
  • 单元测试
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档