本篇笔记中主要介绍了以下几个内容:1、使用@Valid注解进行表单验证;2、使用AOP来处理请求;3、统一异常处理;4、之前是使用postman进行测试,现在介绍如何使用单元测试。注意本篇笔记使用了前面一篇《2小时入门Springboot》的红包项目代码。(阅读本篇文章,最好有AOP基础,否则比较吃力)

首先回忆起前面创建单个红包时的create方法:

1
2
3
4
5
6
7
8
@PostMapping("/luckymoneys")
public Luckymoney create(@RequestParam("sender")String sender,
@RequestParam("money")BigDecimal money){
Luckymoney luckymoney = new Luckymoney();
luckymoney.setSender(sender);
luckymoney.setMoney(money);
return luckymoneyRepository.save(luckymoney);
}

现在假设红包不止这么几个属性,还有发送时间,接收时间等,那么还使用@RequestParam注解的方式其实是非常复杂的,此时你完全可以传入一个红包对象,这里面的属性可以直接从传入的红包对象中获取?你可能就要问了我们怎么传入红包对象?

我们知道SpringBoot是SpringMVC的升级版,而SpringMVC可以将表单的字段与Controller方法参数中对象的属性进行映射,前提是两者的名称和类型一致。比如表单中传入sender=xiaobai&money=100,方法参数中的Luckymoney对象也恰好有String senderBigDecimal money属性,SpringMVC将完成字段 –> 对象属性的映射并将数据绑定到Luckymoney对象的对应属性上。SpringMVC将我们输入字段的类型、名称与方法参数中Luckymoney对象中的属性一一对比,如果吻合将会自动映射过去并封装成对象,如果某些字段和对象的属性不能匹配,那么封装成的对象中该属性为null。

所以该方法其实也可以修改为这样:

1
2
3
4
@PostMapping("/luckymoneys")
public Luckymoney create(Luckymoney luckymoney){
return luckymoneyRepository.save(luckymoney);
}

@Valid注解

现在有一个需求需要控制红包的金额,要求单个红包最大只能200元,最小就是1元,当金额不在1-200之间会有红包金额太小或者太大的提示。

此时需要在Luckymoney实体类上使用@Min@Max注解进行说明,还需要使用value属性来指定判断条件,以及不满足所设定条件时的信息提示message。

1
2
3
@Min(value = 1,message = "红包金额太小")
@Max(value = 200,message = "红包金额太大")
private BigDecimal money; //红包金额

然后回到LuckymoneyController类的create方法中,使用@Valid注解用于标识对哪个对象进行验证,还需要传入一个BindingResult对象,这里对象就是验证结果,然后可以根据验证结果中是否包含错误信息来书写业务逻辑,以及将错误信息输出提示等:

1
2
3
4
5
6
7
8
9
10
@PostMapping("/luckymoneys")
public Luckymoney create(@Valid Luckymoney luckymoney, BindingResult bindingResult){
//判断是否验证通过
if(bindingResult.hasErrors()){
//不通过,停止对象创建
System.out.println(bindingResult.getFieldError().getDefaultMessage());
return null;
}
return luckymoneyRepository.save(luckymoney);
}

启动项目,在Postman中输入http://localhost:8081/luckymoney/luckymoneys,并新建测试属性sender和money,看看money为0.1和300时,IDEA是否会输出前面所设置的错误信息,测试发现没有问题。

AOP

AOP是一种编程范式,与语言无关,是一种程序设计思想。AOP(Aspect Oriented Programming)面向切面编程,注意不仅仅是java才有;OOP(Object Oriented Programming)面向对象编程;POP(Procedure Oriented Programming)面向过程编程。

面向过程和面向对象只是换个角度看世界,换个姿势处理问题。面向对象关注的是将需求功能垂直划分为不同的且相对独立的,它会封装成良好的类,并且让它们有属于自己的行为和属性 。而AOP面向切面编程则恰恰相反,利用一种横切技术,将面向对象所构建的类的庞大体系进行水平切割,并且将那些影响到多个类的公共行为封装成可以重用的模块,这个模块就称之为切面,所以AOP编程就称之为面向切面编程。AOP的关键思想就是将通用逻辑从业务逻辑中分离出来。

接下来以处理请求并且打印日志这个例子为说明进行介绍AOP和OOP这两种思想是怎样从水平和垂直进行划分的:

这两个都是非常简单的场景,其实这些可以进行拆分,如下图中圈出的信息,其实都是处理请求记录请求,以及记录回复,所以这两部分其实是可以作为切面提取出来,这样就将通用逻辑从业务逻辑中分离出来了。

接下来以记录每一个http请求为例,介绍如何在spring中集成AOP,且让它来统一处理日志。可能说记录每一个http请求有些困惑,这样以实现登录页面访问限制为例来介绍可能更具体一些,也就是说某些页面只有用户登录以后才能查看,否则无法进行查看。按照以往的思路就是在每一个方法上添加登录验证,也就是判断用户是否登录,登录可以访问,否则就跳转到登录页面,其实这个需要在每一个方法上添加,这样会导致业务代码和通用代码混合。

如果你有一个想法就是在每一个controller中定义一个无参的构造方法,在构造方法中进行判断,这种想法是可以的,但是你忘了spring在启动的时候就将这些类给实例化了,当你在进行http访问的时候,不会再次执行这个无参的构造方法,所以这种方式也是不可行的。

其实怎么解决这个问题?用AOP啊,因为它的关键思想就是将通用逻辑从业务逻辑中分离出来。

第一步,在pom.xml中添加依赖:

1
2
3
4
5
<!--添加aop依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

第二步,注意按照以往的套路都是在启动类上添加一个注解,但是aop不需要,因此这一步可以忽略。

第三步,新建aspect包,接着在里面定义httpAspect类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.envy.luckymoney.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class HttpAspect {

//在controller方法中的list方法执行前执行
@Before("execution(* com.envy.luckymoney.controller.LuckymoneyController.list(..))")
public void beforeLog(){
System.out.println("前置通知");
}

//在controller方法中的findById方法执行后执行
@After("execution(* com.envy.luckymoney.controller.LuckymoneyController.findById(..))")
public void afterLog(){
System.out.println("最终通知");
}
}

上面就是AOP当中最常用的前置通知和最终通知。前置通知在拦截方法前面执行,最终通知在拦截方法后执行后(不论是否有异常)执行。现在你发现两个代码中都存在重复的"execution(* com.envy.luckymoney.controller.LuckymoneyController代码,此时你想能不能去掉重复的啊?答案是当然可以的,往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Aspect
@Component
public class HttpAspect {
@Pointcut("execution(* com.envy.luckymoney.controller.LuckymoneyController.*(..))")
public void log(){}

//在controller方法中的list方法执行前执行
@Before("log()")
public void beforeLog(){
System.out.println("前置通知");
}

//在controller方法中的findById方法执行后(不论是否有异常)执行
@After("log()")
public void afterLog(){
System.out.println("最终通知");
}
}

先使用@Pointcut注解来表示切入点,然后只需在通知上直接调用这个方法即可。你看到了在各个方法中我们都是使用syso标准输出方式,其实还可以使用Log输出,它是springboot自带的测试类(注意使用org.slf4j类中的Logger ):

1
2
private static final Logger logger = LoggerFactory.getLogger(HttpAspect.class);
logger.info("日志输出信息");

我们知道既然是记录http请求,那么肯定是在该方法执行前记录,也就是使用到前置通知,我们主要获取http请求中的url、method、ip、类方法、参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//在controller方法中的list方法执行前执行
@Before("log()")
public void beforeLog(JoinPoint joinPoint){
logger.info("前置通知");
//用于获取请求中的url、method、ip、类方法、参数

ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) servletRequestAttributes.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());
}

然后运行项目,Postman中输入http://localhost:8081/luckymoney/luckymoney/3,就可以看到控制台有信息输出了:

1
2
3
4
5
6
7
前置通知
url=http://localhost:8081/luckymoney/luckymoney/3
method=GET
ip=0:0:0:0:0:0:0:1
class_method=com.envy.luckymoney.controller.LuckymoneyController.findById
args=3
最终通知

注意当你在浏览器中设置的访问是localhost,那么对应的IP就是0:0:0:0:0:0:0:1,如果是127.0.0.1就是127.0.0.1

现在你又有一个问题了,既然获取到了http请求,那么是不是也能获取response回应呢?是的,的确可以,不过此时需要使用到@AfterReturning注解,也就是后置通知。它可以定义方法的返回值作为参数,也可以传入JointPoint对象。

1
2
3
4
5
@AfterReturning(pointcut="log()",returning = "rtobject")
public void afterReturnLog(Object rtobject){
logger.info("后置通知");
logger.info("response={}",rtobject);
}

我们知道它最后肯定是返回Luckymoney对象,因此需要在Luckymoney实体类中添加toString方法用于输出,最终控制台输出以下信息:

1
2
后置通知
response=Luckymoney{id=3, money=120.00, sender='小红', receiver='小思'}

请注意前置通知也会执行和输出,不过这里就省略了。

统一异常处理

异常,我们也称之为意外或者例外,它本质上就是程序上的错误。那么统一异常处理又是什么?为什么需要进行统一异常处理呢?通过一个需求来进行验证。

假设现在我们需要通过微信红包的金额来判断某人与某人的亲密关系(仅仅是案例需要,没有任何科学依据,请不要相信,否则后果自负),条件是这样的:如果红包金额大于1元同时小于88元,说明关系一般;金额大于等于88元,又小于166元,说明关系不错;大于或等于166元,说明关系亲密。是不是很简单?那就发一个红包试试呗。在Postman中使用http://localhost:8081/luckymoney/luckymoneys链接,让小天发300元红包,运行你会发现控制台出错了,因为money验证不通过,我们前面设置了money在1-200元之间,在create方法中对红包这个对象进行了验证,如果不满足条件就会报错返回null,而你刚才在后置通知中又调用了rtobject.toString方法,进而引发了空指针异常:

1
2
3
4
5
6
7
8
9
10
@PostMapping("/luckymoneys")
public Luckymoney create(@Valid Luckymoney luckymoney, BindingResult bindingResult){
//判断是否验证通过
if(bindingResult.hasErrors()){
//不通过,停止对象创建
System.out.println(bindingResult.getFieldError().getDefaultMessage());
return null;
}
return luckymoneyRepository.save(luckymoney);
}

但是当你将create方法中的Luckymoney修改为Object,并且希望将错误显示在浏览器上,此时代码修改为:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 创建单个红包(发红包)(统一异常处理示例)
* **/
@PostMapping("/luckymoneys")
public Object create(@Valid Luckymoney luckymoney, BindingResult bindingResult){
//判断是否验证通过
if(bindingResult.hasErrors()){
//不通过,停止对象创建,并将错误信息显示到浏览器上面
return bindingResult.getFieldError().getDefaultMessage();
}
return luckymoneyRepository.save(luckymoney);
}

然后再来运行一下项目,依旧是发送300元的红包,你会看到错误信息已经出现在浏览器页面上了:“红包金额太大”。

那现在问题来了,红包金额在1-200之间页面返回的是Json对象,而大于200或者小于1元返回的就是“红包金额太大”(“红包金额太小”)这六个字,这让前端人员该如何应对?我们希望成功和出现异常返回类似的json格式,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"code": "1",
"msg": "红包金额太大(太小)",
"data": null
}

{
"code": "0",
"msg": "成功",
"data": {
"id": 20,
"sender": "小东",
"money": 300,
"receiver": null
}
}

为了完成这个任务,其实就是返回一个Object,而这个对象具有三个属性code、msg和data,那就在domain包下面新建Result.java文件,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* http请求返回的最外层对象
* **/
public class Result<T> {

//状态码
private Integer code;

//提示信息
private String msg;

//具体的数据
private T data;

//getter和setter方法
}

然后修改LuckymoneyController类中的create方法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PostMapping("/luckymoneys")
public Object create(@Valid Luckymoney luckymoney, BindingResult bindingResult){
Result result= new Result();
//判断是否验证通过
if(bindingResult.hasErrors()){
//不通过,停止对象创建,并将错误信息显示到浏览器上面
result.setCode(1);
result.setMsg(bindingResult.getFieldError().getDefaultMessage());
return result;
}
result.setCode(0);
result.setMsg("成功");
result.setData(luckymoneyRepository.save(luckymoney));
return result;
}

运行项目,在Postman中使用http://localhost:8081/luckymoney/luckymoneys链接,让小天发300和100元红包,你会发现输出的格式都统一了:

1
2
3
4
5
{
"code": "",
"msg": "",
"data": {}
}

但是细心的你发现LuckymoneyController类中的create方法里面存在大量的重复代码,所以需要进行优化,切记代码优化一定是边写边优化,不要等到以后,因为那样你可能会忘记了。新建util包,并在里面新建ResultUtil.java类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ResultUtil {
private static Result result = new Result();

//成功,且返回状态码和数据
public static Result success(Integer code,Object object){
result.setCode(code);
result.setMsg("成功");
result.setData(object);
return result;
}

//成功,只返回状态码
public static Result success(Integer code){
return success(code,null);
}

//失败,且返回状态码和提示
public static Result error(Integer code,String msg){
result.setCode(code);
result.setMsg(msg);
result.setData(null);
return result;
}
}

然后修改LuckymoneyController类中的create方法为:

1
2
3
4
5
6
7
8
9
@PostMapping("/luckymoneys")
public Object create(@Valid Luckymoney luckymoney, BindingResult bindingResult){
//判断是否验证通过
if(bindingResult.hasErrors()){
//不通过,停止对象创建,并将错误信息显示到浏览器上面
return ResultUtil.error(1,bindingResult.getFieldError().getDefaultMessage());
}
return ResultUtil.success(0,luckymoneyRepository.save(luckymoney));
}

言归正传,异常处理统一之后,现在开始进行需求的功能实现:如果红包金额大于1元同时小于88元,说明关系一般;金额大于等于88元,又小于166元,说明关系不错;大于或等于166元,说明关系亲密。

第一步,在LuckymoneyService类中书写业务逻辑。这里有一个问题就是前面定义的Result的msg属性应该怎样设置?我们这里有三种情况,所以msg应该有三种情况:关系一般、关系不错、关系亲密。我们可以获取红包的金额,然后根据红包的金额大小进行判断,然后返回不同的标志如A、B、C等,然后在Controller中根据标志的不同来设置Result的msg属性,这的确是一种可行的方案,但是假设情况很多,那么标志就复杂难看了,所以需要另一种方式。这里考虑使用exception,把每种情况作为exception抛出去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void getMoney(Integer id) throws Exception{
Optional<Luckymoney> optional= luckymoneyRepository.findById(id);
if(optional.isPresent()){
Luckymoney luckymoney = optional.get();
BigDecimal money = luckymoney.getMoney();
if(money.intValue()>=1&&money.intValue()<88){
//说明关系一般
throw new Exception("关系一般");
}else if(money.intValue()>=88&&money.intValue()<166){
//说明关系不错
throw new Exception("关系不错");
}else{
//说明关系亲密
throw new Exception("关系亲密");
}
}
}

第二步,在LuckymoneyController中新建一个获取获取红包金额的方法getMoney,然后调用service中的方法(注意这个getMoney方法业务要将异常抛出去,不去捕获,因为我们需要在页面显示异常的message):

1
2
3
4
5
//获取红包金额
@GetMapping("/luckymoneys/getmoney/{id}")
public void getMoney(@PathVariable("id")Integer id) throws Exception {
luckymoneyService.getMoney(id);
}

运行项目,在Postman中使用http://localhost:8081/luckymoney/luckymoneys/getmoney/15链接,你会发现异常信息确实输出来了:

1
2
3
4
5
6
7
{
"timestamp": "2018-04-10T07:05:04.857+0000",
"status": 500,
"error": "Internal Server Error",
"message": "关系一般",
"path": "/luckymoney/luckymoneys/getmoney/15"
}

但是这样又不是和我们Result三个属性保持一致了,破坏了我们之前预想的格式。怎么解决这个问题呢?此时我们可以将这个message提取出来然后对其进行封装为Result,最后将其返回给浏览器。新建handle包,并在里面新建ExceptionHandle类,这个类用于处理之前的异常:

1
2
3
4
5
6
7
8
9
10
11
12
@ControllerAdvice
public class ExceptionHandle {
/**
* ExceptionHandle用于处理异常
* **/

@ExceptionHandler(value = Exception.class)
@ResponseBody
public Result handle(Exception exception){
return ResultUtil.error(1,exception.getMessage());
}
}

这样通过使用@ControllerAdvice这样用于增强的Controller的注解,这里使用了它的全局异常处理功能,且配合@ExceptionHandler(value = Exception.class)来实现实现全局异常处理,只需要定义类,并添加该注解即可。@ExceptionHandler注解用来指明异常的处理类型,即如果这里指定为NullpointerException,则数组越界异常就不会进到这个方法中来,具体的使用后续文章会进行介绍。

重新运行项目,在Postman中使用http://localhost:8081/luckymoney/luckymoneys/getmoney/15链接,你会发现信息终于按照之前的设想显示出来了:

1
2
3
4
5
{
"code": 1,
"msg": "关系一般",
"data": null
}

但是现在还有一个问题就是这里设置的code都是1,这肯定是不好的,因为我们希望通过code状态码来判断具体是什么问题。这个Exception对象中只能传一个message,不能再传code,这时候应该怎样操作呢?聪明的你可能想到了,既然这个Exception不能再传code,但是我们可以自己写一个可以传code的Exception啊,对就是这样。新建exception包,并在里面新建LuckymoneyException类,这个类用于处理之前的异常并且可以支持code传参(注意此时我们需要继承RuntimeException类,而不是Exception类,因为spring框架仅仅对RuntimeException才会进行事务回滚):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LuckymoneyException extends RuntimeException {
private Integer code;

public LuckymoneyException(Integer code,String message) {
super(message);
this.code = code;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}
}

然后修改LuckymoneyService类中的getMoney方法,修改抛出现在自定义的LuckymoneyException异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void getMoney(Integer id) throws Exception{
Optional<Luckymoney> optional= luckymoneyRepository.findById(id);
if(optional.isPresent()){
Luckymoney luckymoney = optional.get();
BigDecimal money = luckymoney.getMoney();
if(money.intValue()>=1&&money.intValue()<88){
//说明关系一般,code为100
throw new LuckymoneyException(100,"关系一般");
}else if(money.intValue()>=88&&money.intValue()<166){
//说明关系不错,code为101
throw new LuckymoneyException(101,"关系不错");
}else{
//说明关系亲密,code为102
throw new LuckymoneyException(102,"关系亲密");
}
}
}

还没完,还需要修改ExceptionHandle类,因为现在需要处理的异常是自定义的LuckymoneyException异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ControllerAdvice
public class ExceptionHandle {
/**
* ExceptionHandle用于处理异常
* **/

private static final Logger logger = LoggerFactory.getLogger(ExceptionHandle.class);

@ExceptionHandler(value = LuckymoneyException.class)
@ResponseBody
public Result handle(Exception exception){
if(exception instanceof LuckymoneyException){
LuckymoneyException luckymoneyException = (LuckymoneyException) exception;
return ResultUtil.error(luckymoneyException.getCode(),luckymoneyException.getMessage());
}else{
logger.error("系统异常:{}=",exception);
return ResultUtil.error(-1,"未知错误");
}
}
}

然后对之前的异常进行细化,如果异常是自定义的LuckymoneyException实例,那么强制转换并调用对应的方法,如果不是那就暂且定义为未知错误,并通过日志信息输出。

你现在可能还有一个问题就是现在哪里还有系统异常,你还记得在LuckymoneyController类中的create方法中,我们对提交的表单进行验证,不通过返回的是1,注意了这里我们就可以认为是系统异常,因为它不是我们需要的异常,因此将create方法修改为:

1
2
3
4
5
6
7
8
9
@PostMapping("/luckymoneys")
public Object create(@Valid Luckymoney luckymoney, BindingResult bindingResult){
//判断是否验证通过
if(bindingResult.hasErrors()){
//不通过,停止对象创建,并将错误信息显示到浏览器上面
return null;
}
return ResultUtil.success(0,luckymoneyRepository.save(luckymoney));
}

这样返回一个null,还记得前面的后置通知吗?我们设置的是所有方法都进行拦截,因此当你创建一个金额大于200的红包时,它会报null错误,而后置通知是方法无论是否异常都会执行,既然你都返回一个null,你还调用它的toString方法,那么肯定会引起空指针异常,因此此时我们这个系统未知错误就显示了。

1
2
3
4
5
@AfterReturning(pointcut="log()",returning = "rtobject")
public void afterReturnLog(Object rtobject){
logger.info("后置通知");
logger.info("response={}",rtobject.toString());
}

现在异常似乎已经统一处理的较为完美了,但是code和message管理却非常混乱,比较好的做法就是弄一个枚举类。新建enums包,并在里面新建LuckymoneyEnum枚举类,这个类用于管理code和message:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.envy.luckymoney.enums;

//枚举类,用于管理code和message
public enum LuckmoneyEnum {
UNKNOWN_ERROR(-1,"未知错误"),
SUCCESS(0,"成功"),
RELATIONSHIP_NORMAL(100,"关系一般"),
RELATIONSHIP_GOOD(101,"关系不错"),
RELATIONSHIP_BEAUTIFUL(102,"关系亲密"),
;

private Integer code;

private String message;

LuckmoneyEnum(Integer code, String message) {
this.code = code;
this.message = message;
}

public Integer getCode() {
return code;
}

public String getMessage() {
return message;
}
}

既然使用了枚举类,那么首先需要修改LuckymoneyException这个自定义异常类的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.envy.luckymoney.exception;

import com.envy.luckymoney.enums.LuckmoneyEnum;

public class LuckymoneyException extends RuntimeException {
private Integer code;

public LuckymoneyException(LuckmoneyEnum luckmoneyEnum) {
super(luckmoneyEnum.getMessage());
this.code = luckmoneyEnum.getCode();
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}
}

接着修改LuckymoneyService中的getMoney方法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void getMoney(Integer id) throws Exception{
Optional<Luckymoney> optional= luckymoneyRepository.findById(id);
if(optional.isPresent()){
Luckymoney luckymoney = optional.get();
BigDecimal money = luckymoney.getMoney();
if(money.intValue()>=1&&money.intValue()<88){
//说明关系一般,code为100
throw new LuckymoneyException(LuckmoneyEnum.RELATIONSHIP_NORMAL);
}else if(money.intValue()>=88&&money.intValue()<166){
//说明关系不错,code为101
throw new LuckymoneyException(LuckmoneyEnum.RELATIONSHIP_GOOD);
}else{
//说明关系亲密,code为102
throw new LuckymoneyException(LuckmoneyEnum.RELATIONSHIP_BEAUTIFUL);
}
}
}

当然还有其他使用到code和message的地方,都需要修改这里就不一一而足了。

单元测试

Service测试

本次就介绍如何对Service和API进行介绍。测试Service时,新建一个通过id来查询某一个红包的信息,也就是下面的方法(注意在LuckymoneyService类中):

1
2
3
4
//通过id来查询红包信息
public Luckymoney findById(Integer id){
return luckymoneyRepository.findById(id).orElse(null);
}

在test文件夹下与LuckymoneyApplicationTests类同级目录下,新建一个测试类LuckymoneyServiceTest,里面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.envy.luckymoney;

import com.envy.luckymoney.domain.Luckymoney;
import com.envy.luckymoney.service.LuckymoneyService;
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 java.math.BigDecimal;

@RunWith(SpringRunner.class)
@SpringBootTest
public class LuckymoneyServiceTest {

@Autowired
private LuckymoneyService luckymoneyService;

@Test
public void findByIdTest(){
Luckymoney luckymoney = luckymoneyService.findById(20);
Assert.assertEquals(new BigDecimal("100.00"),luckymoney.getMoney());
}
}

@RunWith(SpringRunner.class)注解表示使用了Spring的SpringRunner,以便在测试开始的时候自动创建Spring的应用上下文。其他的想创建spring容器的话,就得在web.xml配置classloader。 @RunWith注解就可以直接使用spring容器。底层使用了Junit测试工具。如果你仅仅使用了@Test注解,那么它不会启动spring容器。@SpringBootTest注解表示这个项目在springboot容器中运行,将会启动springboot工程。

运行findByIdTest测试方法,发现测试通过。不知道你发现没有这种测试方式的代码几乎和你在Service中写的代码一模一样,所以可以考虑有没有什么便捷的方式可以进行测试?答案是没有的,但是IDEA给我们提供了便捷的测试方式。在Service中,将鼠标放在需要测试的方法上,右键选择出现的GOTO,然后按照图示进行操作:

看到没有,通过使用IDEA就可以少些代码,它就给你提供了一个测试方法,你只需要在里面书写测试逻辑即可。

API测试

其实所谓的API测试就是Controller测试,以获取所有红包信息为例,也就是下面的方法(注意在LuckymoneyController类中):

1
2
3
4
@GetMapping("/luckymoneys")
public List<Luckymoney> list(){
return luckymoneyRepository.findAll();
}

按照上面的测试方法,先使用IDEA来创建测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.envy.luckymoney.controller;

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 javax.swing.*;

import static org.junit.Assert.*;

@RunWith(SpringRunner.class)
@SpringBootTest
public class LuckymoneyControllerTest {

@Autowired
private LuckymoneyController luckymoneyController;

@Test
public void list() {
luckymoneyController.list();
}
}

运行项目,发现测试没有问题,你以为这是在测试API吗?拜托这和url没有一点关系,所以并不是这样测试。正确的测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.envy.luckymoney.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 javax.swing.*;

import static org.junit.Assert.*;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class LuckymoneyControllerTest {

@Autowired
private MockMvc mvc;

@Test
public void list() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/luckymoneys")).andExpect(MockMvcResultMatchers.status().isOk());
}
}

在这里我们使用到了@AutoConfigureMockMvc注解,该注解将会自动配置mockMvc的单元测试。因为@SpringBootTest注解默认不会启动服务器。.get()就是指使用get方法进行测试,andExcept()里面可以填写期望的信息,诸如类似的还有andReturn()等。