初识SpringBoot Web开发

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

虽说前端的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岁的情况:

控制台打印结果:

未成年禁止注册

非空字段为空的情况:

控制台打印结果:

学生名字不能为空

使用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访问方式如下:

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

如此,我们就完成了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进行测试,数据正常的情况:

学生姓名为空的情况:

如上,可以看到,返回的数据格式都是一样的,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 &lt; 10 的情况:

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

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

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

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


单元测试

我们一般会在开发完项目中的某一个功能的时候,就会进行一个单元测试。以确保交付项目时,我们的代码都是通过测试并且功能正常的,这是一个开发人员基本的素养。所以本节将简单介绍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());
    }
}

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

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

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

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

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

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

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

原文地址:http://blog.51cto.com/zero01/2113134

时间: 2024-08-30 08:12:09

初识SpringBoot Web开发的相关文章

【SpringBoot】SpringBoot Web开发(八)

本周介绍SpringBoot项目Web开发的项目内容,及常用的CRUD操作,阅读本章前请阅读[SpringBoot]SpringBoot与Thymeleaf模版(六)的相关内容 Web开发 项目搭建 1.新建一个SpringBoot的web项目.pom.xml文件如下: 1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/PO

SpringBoot(4)SpringBoot web开发

一.Web应用插件 1.自定义Filter 我们常常在项目中会使用filters用于录调用日志.排除有XSS威胁的字符.执行权限验证等等.Spring Boot自动添加了OrderedCharacterEncodingFilter和HiddenHttpMethodFilter,并且我们可以自定义Filter. 两个步骤: 实现Filter接口,实现Filter方法添加@Configuration 注解,将自定义Filter加入过滤链@Configurationpublic class WebCo

[Java Web] 1\Web开发初识——一大堆历史和技术名词

LZ前言 LZ最近发现网络真是个神奇的东西,以前做的好玩的只能自娱自乐(或者说顾影自怜),现在只要发一个帖子,写一个博客,很快能引来一大群小伙伴的围观(有时候还能遇见几个大牛给个战略性的指导)...LZ本来是搞硬件的:从CPU的制造(VHDL).数电.模电再到计算机组成原理.汇编.接口技术,底层的东西算是走马观花地懂了点皮毛,正好大一的时候又了解一点计算机的编程知识(当时第一次用C++Build写出来个Hollo World那个欣喜呀~后来又从win32学到MFC再到C#,嘿嘿,基本上还是皮毛吧

SpringBoot与Web开发

web开发1).创建SpringBoot应用,选中我们需要的模块:2).SpringBoot已经默认将这些场景已经配置好了,只需要在配置文件中指定少量配置就可以运行起来3).自己编写业务代码: 自动配置原理?这个场景SpringBoot帮我们配置了扫码?能不能修改?能不能改哪些配置?能不能扩展?xxxxxxAutoConfiguration:帮我们给容器中自动配置组件:xxxProperties:配置类来 封装配置文件的内容: 2.SpringBoot对静态资源的 映射规则 @Configura

SpringBoot的Web开发

Web开发是开发中至关重要的一部分,web开发的核心内容主要包括servelet容器和SpringMVC. 1.SpringBoot的Web开发支持. SpringBoot提供了spring-boot-starter-web为web开发予以支持,spring-boot-starter-web提供了内嵌的Tomcat以及SpringMVC的依赖 而web相关的自动配置存储在spring-boot-autoconfigure.jar的org.srpingframework.boot.autoconf

Springboot 系列(五)Spring Boot web 开发之静态资源和模版引擎

前言 Spring Boot 天生的适合 web 应用开发,它可以快速的嵌入 Tomcat, Jetty 或 Netty 用于包含一个 HTTP 服务器.且开发十分简单,只需要引入 web 开发所需的包,然后编写业务代码即可. 自动配置原理? 在进行 web 开发之前让我再来回顾一下自动配置,可以参考系列文章第三篇.Spring Boot 为 Spring MVC 提供了自动配置,添加了如下的功能: 视图解析的支持. 静态资源映射,WebJars 的支持. 转换器 Converter 的支持.

Springboot 系列(六)Spring Boot web 开发之拦截器和三大组件

1. 拦截器 Springboot 中的 Interceptor 拦截器也就是 mvc 中的拦截器,只是省去了 xml 配置部分.并没有本质的不同,都是通过实现 HandlerInterceptor 中几个方法实现.几个方法的作用一一如下. preHandle 进入 Habdler 方法之前执行,一般用于身份认证授权等. postHandle 进入 Handler 方法之后返回 modelAndView 之前执行,一般用于塞入公共模型数据等. afterCompletion 最后处理,一般用于日

【SpringBoot】Web开发

一.简介 1.1 引入SpringBoot模块 1.2 SpringBoot对静态资源的映射规则 二.模版引擎 2.1 简介 2.2 引入thymeleaf 2.3 Thymeleaf使用 一.简介 1.1 引入SpringBoot模块 在介绍Web开发模块之前,先总结一下SpringBoot中如何引入某一个模块,我们知道,SpringBoot将功能模块封装为一个个的Starter : 1).创建SpringBoot应用,选中我们需要的模块; 2).SpringBoot已经默认将这些场景配置好了

4.SpringBoot的web开发1

一.回顾 好的,同学们,那么接下来呢,我们开始学习SpringBoot与Web开发,从这一章往后,就属于我们实战部分的内容了: 其实SpringBoot的东西用起来非常简单,因为SpringBoot最大的特点就是自动装配. 使用SpringBoot的步骤: 创建一个SpringBoot应用,选择我们需要的模块,SpringBoot就会默认将我们的需要的模块自动配置好 手动在配置文件中配置部分配置项目就可以运行起来了 专注编写业务代码,不需要考虑以前那样一大堆的配置了. 要熟悉掌握开发,之前学习的