前言

这篇文章的将介绍表单验证,AOP处理请求和统一异常处理,案例是延续上一篇 SpringBoot初识

表单验证

现在将要拦截未满18岁的女生,在之前GirlController里面添加一个女生的方法如下:

方法的形参使用的都是属性,那以后当属性变多的时候再来管理就会变得很复杂,直接传递Girl对象就是最好的方法。

现在要对年龄做限制,先进入Girl实体为age属性添加 @Min注解

接着在添加女生的方法上添加 @Valid注解,表示要验证这个对象。而验证完之后要知道是验证通过还是没通过,它会将验证的结果返回到BindingResult对象里,如果有错误,要将它打印出来。

  1. @PostMapping("/girls")
  2. public Girl girlAdd(@Valid Girl girl, BindingResult bindingResult) {
  3. if (bindingResult.hasErrors()) {
  4. System.out.println(bindingResult.getFieldError().getDefaultMessage());
  5. return null;
  6. }
  7. return girlRepository.save(girl);
  8. }

此时传入一个年龄合法的女生:

再传入一个年龄小于18岁的女生:

控制台报错并打印错误信息:

数据库中也没有添加刚才的信息:

AOP处理请求

AOP是一种编程范式,与语言无关,它是一种程序设计思想。面向对象关注的是将需求功能垂直划分为不同的并且相对独立的,它会封装为良好的类,并且有属于自己的行为。而AOP则是利用横切的技术,将面向对象构建的庞大类的体系进行水平的切割,并且会将影响到了多个类的公共行为封装为一个可重用的模块,这个模块就称为切面。AOP的关键思想就是将通用逻辑从业务逻辑中分离出来。

添加pom依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-aop</artifactId>
  4. </dependency>

创建切面

在新建的aspect包里新建HttpAspect类,设置切点拦截GirlController类里面的所有方法,然后把 @Pointcut注解放在一个空的方法上log(),之后的前置增强和后置增强就直接使用 @Before("log()")注解作用在方法上即可。

为了优雅的打印结果,就不在使用system.out了,使用日志打印结果.

  1. package com.zzh.aspect;
  2. import org.aspectj.lang.JoinPoint;
  3. import org.aspectj.lang.annotation.*;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import org.springframework.stereotype.Component;
  7. import javax.servlet.http.HttpServletRequest;
  8. @Aspect
  9. @Component
  10. public class HttpAspect {
  11. private final static Logger logger = LoggerFactory.getLogger(HttpAspect.class);
  12. @Pointcut("execution(public * com.zzh.controller.GirlController.*(..))")
  13. public void log() {
  14. }
  15. @Before("log()")
  16. public void doBefore() {
  17. logger.info("This is Before");
  18. }
  19. @After("log()")
  20. public void doAfter() {
  21. logger.info("This is After ");
  22. }
  23. }

接着在Controller的查询方法里面添加一行日志打印来观察日志输出顺序

查看打印结果:

采用记录日志的方式,会更为详细的打印出该条语句相关的信息,比System.out好了很多。

打印Http请求

  1. package com.zzh.aspect;
  2. import org.aspectj.lang.JoinPoint;
  3. import org.aspectj.lang.annotation.*;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import org.springframework.stereotype.Component;
  7. import org.springframework.web.context.request.RequestContextHolder;
  8. import org.springframework.web.context.request.ServletRequestAttributes;
  9. import javax.servlet.http.HttpServletRequest;
  10. @Aspect
  11. @Component
  12. public class HttpAspect {
  13. private final static Logger logger = LoggerFactory.getLogger(HttpAspect.class);
  14. @Pointcut("execution(public * com.zzh.controller.GirlController.*(..))")
  15. public void log() {
  16. }
  17. @Before("log()")
  18. //记录Http请求
  19. public void doBefore(JoinPoint joinPoint) {
  20. ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
  21. HttpServletRequest request = attributes.getRequest();
  22. //url
  23. logger.info("url={}",request.getRequestURL());
  24. //method
  25. logger.info("method={}",request.getMethod());
  26. //ip
  27. logger.info("ip={}",request.getRemoteAddr());
  28. //类方法
  29. logger.info("class_method={}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
  30. //参数
  31. logger.info("args={}",joinPoint.getArgs());
  32. }
  33. @After("log()")
  34. public void doAfter() {
  35. logger.info("This is After ");
  36. }
  37. }

执行查询后,控制台打印:

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请求返回的最外层对象

  1. package com.zzh.domain;
  2. public class Result<T> {
  3. //错误码
  4. private Integer code;
  5. //提示信息
  6. private String msg;
  7. //具体内容
  8. private T data;
  9. public Integer getCode() {
  10. return code;
  11. }
  12. public void setCode(Integer code) {
  13. this.code = code;
  14. }
  15. public String getMsg() {
  16. return msg;
  17. }
  18. public void setMsg(String msg) {
  19. this.msg = msg;
  20. }
  21. public T getData() {
  22. return data;
  23. }
  24. public void setData(T data) {
  25. this.data = data;
  26. }
  27. }

修改Controller中girlAdd方法

添加一条没有金额的女生:

添加一条有金额但是未满18岁的女生:

添加一条信息正确的女生:

规范重复代码

可以看到上面的代码的result的相关操作已经重复调用了,所以新创建ResultUtil类来封装重复操作。

  1. package com.zzh.utils;
  2. import com.zzh.domain.Result;
  3. public class ResultUtil {
  4. public static Result success(Object object) {
  5. Result result = new Result();
  6. result.setCode(0);
  7. result.setMsg("成功");
  8. result.setData(object);
  9. return result;
  10. }
  11. public static Result success() {
  12. return success(null);
  13. }
  14. public static Result error(Integer code, String msg) {
  15. Result result = new Result();
  16. result.setCode(code);
  17. result.setMsg(msg);
  18. return result;
  19. }
  20. }

此时Controller中的girlAdd方法简化如下:

测试得到的结果跟之前的一样,但是Controller中的重复代码省略了。


异常处理

现在需要获取女生的年龄并判断,如果小于10,就返回一个字符串,如果大于10小于16又返回另外一个字符串。首先想到的就是直接在Service中写一个判断逻辑,返回类型设为String,符合条件的直接return那个字符串就行,这样做也可以,但是如果判断完之后我还要做一些其他的事情,那么这个返回类型就已经限制了功能的扩展。

这时用异常来处理就很好,满足条件,直接throw给上一层,也就是Controller,然后Controller继续抛出,这样当条件满足时,这个异常信息(也就是那个字符串)就会在控制台上出现。

Controller中新添加一个方法

实现逻辑通过service来处理

不过这样还是没有达到本来的目的,我们的目的是,浏览器返回的Json要是之前设置好的code,msg,data,然后msg字段就用来显示抛出的字符串。

解决的方法就是对Controller抛出的内容进行捕获,取到需要的内容封装起来再返回给浏览器。

创建异常捕获类

这里是对Controller进行异常捕获,需要加上 @ControllerAdvice注解

  1. package com.zzh.handle;
  2. import com.zzh.domain.Result;
  3. import com.zzh.utils.ResultUtil;
  4. import org.springframework.web.bind.annotation.ControllerAdvice;
  5. import org.springframework.web.bind.annotation.ResponseBody;
  6. @ControllerAdvice
  7. public class ExceptionHandler {
  8. @org.springframework.web.bind.annotation.ExceptionHandler(value = Exception.class)
  9. @ResponseBody
  10. public Result handle(Exception e) {
  11. return ResultUtil.error(100, e.getMessage());
  12. }
  13. }

此时在数据库中设置一条记录:

通过方法测试第三条数据:

自定义异常

现在的异常信息返回的code都是100,如果要划分异常,比如年龄小于10的code设为100,而大于10小于16的code设为101,划分之后更方便排查问题。而Exception里面只能传message,不能再传code进去了,所以需要自己定义异常。

自定义异常没有继承Exception,而是继承RuntimeException是有原因的,RuntimeException是继承Exception,但是Spring只对RuntimeException进行事务回滚,如果抛出的是Exception是不会回滚的。

  1. package com.zzh.exception;
  2. public class GirlException extends RuntimeException{
  3. private Integer code;
  4. public GirlException(Integer code,String message) {
  5. super(message);
  6. this.code = code;
  7. }
  8. public Integer getCode() {
  9. return code;
  10. }
  11. public void setCode(Integer code) {
  12. this.code = code;
  13. }
  14. }

Service中的方法也需要修改,将抛出的异常改为自定义的异常:

在之前设定的ExceptionHandler捕获的是Exception,所以需要进行判断异常是不是自己定义的异常。如果不是就把code设置为-1,message设置为未知错误。

  1. package com.zzh.handle;
  2. import com.zzh.domain.Result;
  3. import com.zzh.exception.GirlException;
  4. import com.zzh.utils.ResultUtil;
  5. import org.slf4j.Logger;
  6. import org.slf4j.LoggerFactory;
  7. import org.springframework.web.bind.annotation.ControllerAdvice;
  8. import org.springframework.web.bind.annotation.ResponseBody;
  9. @ControllerAdvice
  10. public class ExceptionHandler {
  11. private final static Logger logger = LoggerFactory.getLogger(ExceptionHandler.class);
  12. @org.springframework.web.bind.annotation.ExceptionHandler(value = Exception.class)
  13. @ResponseBody
  14. public Result handle(Exception e) {
  15. //判断异常是不是自己定义的异常
  16. if (e instanceof GirlException) {
  17. GirlException girlException = (GirlException) e;
  18. return ResultUtil.error(girlException.getCode(), girlException.getMessage());
  19. } else {
  20. logger.error("[系统异常] {}", e);
  21. return ResultUtil.error(-1, "未知错误");
  22. }
  23. }
  24. }

测试:

要测试自定义异常里的系统异常要怎么样做呢?比如通过不传入金额让它报系统异常,稍微改动一点就可以了:

为什么要改为return null呢,如果不改的话code就会是1了,只有改为了null,切面里的object.toString才会报错。

不传入金额:

之前在ExceptionHandler设置了Logger,现在控制台就可以找到该系统异常问题所在:

使用枚举封装code和message

在前面所抛出的GirlException中,是直接将code和message作为参数进行传递,这样很不容易做后期维护,如果code和message统一封装起来就很方便进行维护了。

枚举里面只需要有属性的Getter方法即可,因为枚举的使用都是通过构造方法来创建,不会再使用Setter。

  1. package com.zzh.enums;
  2. public enum ResultEnum {
  3. UNKONW_ERROR(-1, "未知错误"),
  4. SUCCESS(0, "成功"),
  5. PRIMARY_SCHOOL(100, "你可能还在上小学"),
  6. MIDDLE_SCHOOL(101, "你可能还在上初中"),;
  7. private Integer code;
  8. private String msg;
  9. ResultEnum(Integer code, String msg) {
  10. this.code = code;
  11. this.msg = msg;
  12. }
  13. public Integer getCode() {
  14. return code;
  15. }
  16. public String getMsg() {
  17. return msg;
  18. }
  19. }

修改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女生的年龄提取出来与设置进行比较。

  1. package com.zzh.service;
  2. import com.zzh.domain.Girl;
  3. import org.junit.Assert;
  4. import org.junit.Test;
  5. import org.junit.runner.RunWith;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.boot.test.context.SpringBootTest;
  8. import org.springframework.test.context.junit4.SpringRunner;
  9. @RunWith(SpringRunner.class)
  10. @SpringBootTest
  11. public class GirlServiceTest {
  12. @Autowired
  13. private GirlService girlService;
  14. @Test
  15. public void findOne() throws Exception {
  16. Girl girl = girlService.findOne(2);
  17. Assert.assertEquals(new Integer(25), girl.getAge());
  18. }
  19. }

测试结果:

现在将设置的年龄改为17,也就是: Assert.assertEquals(new Integer(17), girl.getAge());

测试很友好的告诉了我们,这个ID对应的真实年龄是25,但是我们期待的是17。Service测试完毕。

测试API

选择对Controller中girlList方法进行测试:

这里使用的不是girlController对象调用girlList方法,这样一来跟URL完全没有关系了,这里的测试需要像之前使用的RESTClient,给一个地址,然后发出Get请求,得到结果,这样才是API测试。

这就需要使用MockMvc这个类了,注意添加 @AutoConfigureMockMvc注解:

  1. package com.zzh.controller;
  2. import org.junit.Test;
  3. import org.junit.runner.RunWith;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
  6. import org.springframework.boot.test.context.SpringBootTest;
  7. import org.springframework.test.context.junit4.SpringRunner;
  8. import org.springframework.test.web.servlet.MockMvc;
  9. import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
  10. import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
  11. import static org.junit.Assert.*;
  12. @RunWith(SpringRunner.class)
  13. @SpringBootTest
  14. @AutoConfigureMockMvc
  15. public class GirlControllerTest {
  16. @Autowired
  17. private MockMvc mvc;
  18. @Test
  19. public void girlList() throws Exception {
  20. mvc.perform(MockMvcRequestBuilders.get("/girls"))
  21. .andExpect(MockMvcResultMatchers.status().isOk());
  22. }
  23. }

这样做就会对这个请求地址的状态码进行判断:

现在将请求地址故意改错:("/girls234")

可以看到我们期待的状态是200,但是实际为404.

除了状态之外还可以做其他判断,比如对返回的内容进行判断,期待的是abc,但实际是一个json字符串:

测试:

对API的测试和对Service的测试区别在于要使用MockMvc进行测试。


总结

本文简单介绍了如何使用 @Valid表单验证,然后是使用AOP处理请求,接着是统一异常处理,最后是对Service和API的单元测试。

Github地址

SpringBoot-girl

SpringBoot相知的更多相关文章

  1. 解决 Springboot Unable to build Hibernate SessionFactory @Column命名不起作用

    问题: Springboot启动报错: Caused by: org.springframework.beans.factory.BeanCreationException: Error creati ...

  2. 【微框架】Maven +SpringBoot 集成 阿里大鱼 短信接口详解与Demo

    Maven+springboot+阿里大于短信验证服务 纠结点:Maven库没有sdk,需要解决 Maven打包找不到相关类,需要解决 ps:最近好久没有写点东西了,项目太紧,今天来一篇 一.本文简介 ...

  3. Springboot搭建web项目

    最近因为项目需要接触了springboot,然后被其快速零配置的特点惊呆了.关于springboot相关的介绍我就不赘述了,大家自行百度google. 一.pom配置 首先,建立一个maven项目,修 ...

  4. Java——搭建自己的RESTful API服务器(SpringBoot、Groovy)

    这又是一篇JavaWeb相关的博客,内容涉及: SpringBoot:微框架,提供快速构建服务的功能 SpringMVC:Struts的替代者 MyBatis:数据库操作库 Groovy:能与Java ...

  5. 解决 SpringBoot 没有主清单属性

    问题:SpringBoot打包成jar后运行提示没有主清单属性 解决:补全maven中的bulid信息 <plugin> <groupId>org.springframewor ...

  6. SpringBoot中yaml配置对象

    转载请在页首注明作者与出处 一:前言 YAML可以代替传统的xx.properties文件,但是它支持声明map,数组,list,字符串,boolean值,数值,NULL,日期,基本满足开发过程中的所 ...

  7. springboot 学习资源推荐

    springboot 是什么?对于构建生产就绪的Spring应用程序有一个看法. Spring Boot优先于配置的惯例,旨在让您尽快启动和运行.(这是springboot的官方介绍) 我们为什么要学 ...

  8. Springboot框架

    本片文章主要分享一下,Springboot框架为什么那么受欢迎以及如何搭建一个Springboot框架. 我们先了解一下Springboot是个什么东西,它是干什么用的.我是刚开始接触,查了很多资料, ...

  9. 如何在SpringBoot中使用JSP ?但强烈不推荐,果断改Themeleaf吧

    做WEB项目,一定都用过JSP这个大牌.Spring MVC里面也可以很方便的将JSP与一个View关联起来,使用还是非常方便的.当你从一个传统的Spring MVC项目转入一个Spring Boot ...

随机推荐

  1. angluarjs ng-repeat 行号

    参考 https://zhidao.baidu.com/question/1882914672116911828.html $index

  2. goim源码分析与二次开发-comet分析二

    这篇就是完全原版了,作为一个开始,先介绍comet入口文件main.go 第一步是初始化配置,还有白名单.还有性能监口,整体来说入口代码简洁可读性很强 然后开始初始化监控,还有bukcet这里buck ...

  3. Ubuntu安装libevent

    背景: 版本: libevent 2.1.6beta linux下: 按照github官方做法: $ sudo apt-get install openssl $ mkdir build && ...

  4. Python错误:close failed in file object destructor

    我遇到的情况: 二进制程序调shell再调Python后,shell退出,Python进程挂到init上(不是僵尸进程),但 此时二进制程序未退出,这时候中断而二进制程序出现此提示. 经查询: 应该是 ...

  5. xcode10 出现 框架 或者 pod 出错

    1. 报错 Showing Recent Messages :-1: Multiple commands produce '/Users/apple/Library/Developer/Xcode/D ...

  6. 43-python-自己的词典

    可以用python实现一个自己的词典, 就是在网上下一个英汉词典,作为自己的词库,然后整理出一个json文件,存起来,查词时,直接读取查询: 处理时可以用正则表达式处理: https://www.cn ...

  7. 【转】HttpRuntime的认识与加深理解

    原文:http://www.cnblogs.com/whtydn/archive/2009/10/16/1584418.html   下面最先介绍HttpRuntime的Web.config里的配置 ...

  8. TPshop学习

    笔记大纲: tpshop目录结构 功能模块 函数库 重要配置 助手函数 插件 模板 1.TPshop目录结构 目录结构(来自官方视频截图) 看这个图,目录结构一目了然.下面要讲的内容也是根据这个图展开 ...

  9. code4906 删数问题

    题目: 键盘输入一个高精度的正整数n(<=240位), 去掉任意s个数字后剩下的数字按原左右次序将组成一个新的正整数. 编程对给定的n和s,寻找一种方案,使得剩下的数最小. Simple Inp ...

  10. OSGi 系列(十)之 Blueprint

    OSGi 系列(十)之 Blueprint blueprint 是 OSGi 的一个规范,类似于 spring 的 IOC,用来处理 OSGi 的动态特性,可以大大简化服务的使用. blueprint ...