原文:Mkyong
Spring 3 + Quartz 1.8.6 调度器示例
【2012 年 7 月 25 日更新–升级文章以使用 Spring 3 和 Quartz 1.8.6(原来是 Spring 2.5.6 和 Quartz 1.6)
在本教程中,我们将向您展示如何将 Spring 与 Quartz scheduler 框架集成。Spring 附带了许多方便的类来支持 Quartz,并将您的类解耦到 Quartz APIs。
使用的工具:
- 释放弹簧
- 石英
- Eclipse 4.2
- maven3
Why NOT Quartz 2?
Currently, Spring 3 is still NOT support Quartz 2 APIs, see this SPR-8581 bug report. Will update this article again once bug fixed is released.
1.项目依赖性
集成 Spring 3 和 Quartz 1.8.6 需要以下依赖项
文件:pom.xml
...
<dependencies>
<!-- Spring 3 dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
<!-- QuartzJobBean in spring-context-support.jar -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
<!-- Spring + Quartz need transaction -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
<!-- Quartz framework -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>1.8.6</version>
</dependency>
</dependencies>
...
2.调度程序任务
创建一个普通的 Java 类,这是您想要在 Quartz 中调度的类。
文件:RunMeTask.java
package com.mkyong.common;
public class RunMeTask {
public void printMe() {
System.out.println("Spring 3 + Quartz 1.8.6 ~");
}
}
3.声明 Quartz 调度程序作业
使用 Spring,您可以通过两种方式声明 Quartz 作业:
3.1 MethodInvokingJobDetailFactoryBean
这是最简单直接的方法,适合简单的调度器。
<bean id="runMeJob"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="runMeTask" />
<property name="targetMethod" value="printMe" />
</bean>
3.2 JobDetailBeanT5QuartzJobBean
更加灵活,适合复杂的调度器。你需要创建一个扩展 Spring 的QuartzJobBean
类,并在executeInternal()
方法中定义你想要调度的方法,并通过 setter 方法传递调度器任务(RunMeTask)。
文件:RunMeJob.java
package com.mkyong.common;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
public class RunMeJob extends QuartzJobBean {
private RunMeTask runMeTask;
public void setRunMeTask(RunMeTask runMeTask) {
this.runMeTask = runMeTask;
}
protected void executeInternal(JobExecutionContext context)
throws JobExecutionException {
runMeTask.printMe();
}
}
通过jobClass
配置目标类,通过jobDataAsMap
配置方法运行。
<bean name="runMeJob" class="org.springframework.scheduling.quartz.JobDetailBean">
<property name="jobClass" value="com.mkyong.common.RunMeJob" />
<property name="jobDataAsMap">
<map>
<entry key="runMeTask" value-ref="runMeTask" />
</map>
</property>
</bean>
4.引发
配置 Quartz 触发器以定义何时运行调度程序作业。支持两种类型触发器:
4.1 SimpleTrigger
它允许设置运行作业的开始时间、结束时间、重复间隔。
<!-- Simple Trigger, run every 5 seconds -->
<bean id="simpleTrigger"
class="org.springframework.scheduling.quartz.SimpleTriggerBean">
<property name="jobDetail" ref="runMeJob" />
<property name="repeatInterval" value="5000" />
<property name="startDelay" value="1000" />
</bean>
4.2 CronTrigger
它允许 Unix cron 表达式指定运行作业的日期和时间。
<!-- Cron Trigger, run every 5 seconds -->
<bean id="cronTrigger"
class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="runMeJob" />
<property name="cronExpression" value="0/5 * * * * ?" />
</bean>
Note
The Unix cron expression is highly flexible and powerful, read more in following websites :
- http://en . Wikipedia . org/wiki/cron _ expression
- http://www.quartz-scheduler.org/docs/examples/Example3.html
5.调度程序工厂
创建一个调度器工厂 bean,将作业细节和触发器集成在一起。
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="jobDetails">
<list>
<ref bean="runMeJob" />
</list>
</property>
<property name="triggers">
<list>
<ref bean="simpleTrigger" />
</list>
</property>
</bean>
6.Spring Bean 配置文件
完成 Spring 的 bean 配置文件。
文件:Spring-Quartz.xml
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="runMeTask" class="com.mkyong.common.RunMeTask" />
<!-- Spring Quartz -->
<bean name="runMeJob" class="org.springframework.scheduling.quartz.JobDetailBean">
<property name="jobClass" value="com.mkyong.common.RunMeJob" />
<property name="jobDataAsMap">
<map>
<entry key="runMeTask" value-ref="runMeTask" />
</map>
</property>
</bean>
<!--
<bean id="runMeJob"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="runMeTask" />
<property name="targetMethod" value="printMe" />
</bean>
-->
<!-- Simple Trigger, run every 5 seconds -->
<bean id="simpleTrigger"
class="org.springframework.scheduling.quartz.SimpleTriggerBean">
<property name="jobDetail" ref="runMeJob" />
<property name="repeatInterval" value="5000" />
<property name="startDelay" value="1000" />
</bean>
<!-- Cron Trigger, run every 5 seconds -->
<bean id="cronTrigger"
class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="runMeJob" />
<property name="cronExpression" value="0/5 * * * * ?" />
</bean>
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="jobDetails">
<list>
<ref bean="runMeJob" />
</list>
</property>
<property name="triggers">
<list>
<ref bean="simpleTrigger" />
</list>
</property>
</bean>
</beans>
7.演示
跑吧~
package com.mkyong.common;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App
{
public static void main( String[] args ) throws Exception
{
new ClassPathXmlApplicationContext("Spring-Quartz.xml");
}
}
输出到控制台。
Jul 25, 2012 3:23:09 PM org.springframework.scheduling.quartz.SchedulerFactoryBean startScheduler
INFO: Starting Quartz Scheduler now
Spring 3 + Quartz 1.8.6 ~ //run every 5 seconds
Spring 3 + Quartz 1.8.6 ~
下载源代码
Download it – Spring3-Quartz-Example.zip (25 KB)
参考
Tags : integration quartz scheduler spring spring3
相关文章
从资源文件夹中读取文件
在 Spring 中,我们可以使用ClassPathResource
或ResourceLoader
轻松地从类路径中获取文件。
用弹簧 5.1.4 测试 P.S .释放
1.资源中心/主要/资源/
例如,src/main/resources/
文件夹中的图像文件
2.ClassPathResource
import org.springframework.core.io.Resource;
import org.springframework.core.io.ClassPathResource;
import java.io.File;
import java.io.InputStream;
Resource resource = new ClassPathResource("android.png");
InputStream input = resource.getInputStream();
File file = resource.getFile();
3.资源加载器
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import java.io.File;
import java.io.InputStream;
@Autowired
ResourceLoader resourceLoader;
Resource resource = resourceLoader.getResource("classpath:android.png");
InputStream input = resource.getInputStream();
File file = resource.getFile();
4.资源工具
请不要使用这个ResourceUtils
即使它有效,这个类主要是在框架内部使用。阅读 ResourceUtils JavaDocs
import org.springframework.util.ResourceUtils;
File file = ResourceUtils.getFile("classpath:android.png");
参考
带有 ResourceBundleMessageSource 示例的 Spring 资源包
在 Spring 中,您可以使用ResourceBundleMessageSource
来解析来自属性文件的文本消息,基于所选择的地区。请参见以下示例:
1.目录结构
查看此示例的目录结构。
2.属性文件
创建两个属性文件,一个用于英文字符(messages_en_US.properties
),另一个用于中文字符(messages_zh_CN.properties
)。将其放入项目类路径中(见上图)。
文件:messages_en_US.properties
customer.name=Yong Mook Kim, age : {
0}, URL : {
1}
文件:messages_zh_CN.properties
customer.name=\ufeff\u6768\u6728\u91d1, age : {
0}, URL : {
1}
'\ u eff \ u 6768 \ u 6728 \ u91d 1’为中文 Unicode 字符。
Note
To display the Chinese characters correctly, you have to use “native2ascii” tool to convert the Chinese characters into Unicode characters.
3.Bean 配置文件
将属性文件包含到 bean 配置文件中。“messages _ en _ us . properties”和“messages _ zh _ cn . properties”在 Spring 中都被认为是一个文件,你只需要包含文件名一次,Spring 就会自动找到正确的区域设置。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>locale\customer\messages</value>
</property>
</bean>
</beans>
假设这两个文件都位于“资源\区域\客户”文件夹。
4.运行它
package com.mkyong.common;
import java.util.Locale;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
public static void main(String[] args) {
ApplicationContext context
= new ClassPathXmlApplicationContext("locale.xml");
String name = context.getMessage("customer.name",
new Object[] {
28,"http://www.mkyong.com" }, Locale.US);
System.out.println("Customer name (English) : " + name);
String namechinese = context.getMessage("customer.name",
new Object[] {
28, "http://www.mkyong.com" },
Locale.SIMPLIFIED_CHINESE);
System.out.println("Customer name (Chinese) : " + namechinese);
}
}
输出
Note
Make sure your Eclipse is able to display Chinese output.
说明
1.在context.getMessage()
中,第二个参数是消息参数,你必须将其作为对象数组传递。如果没有可用的参数值,可以只传递一个 null。
context.getMessage("customer.name",null, Locale.US);
2.地点。US 将从’messages _ en _ US . properties’中检索消息。简体中文将从“messages _ zh _ cn . properties中检索消息。
More …
Read this article to know how to access the MessageSource inside a bean.
下载源代码
Download it – Spring-MessageSource-Example.zipTags : resource bundle spring
带有 getResource()示例的 Spring 资源加载器
Spring 的资源加载器提供了一个非常通用的 getResource() 方法来从文件系统、类路径或 URL 获取资源,比如(文本文件、媒体文件、图像文件……)。您可以从应用程序上下文中获取 getResource() 方法。
下面的例子展示了如何使用 getResource() 从
1。文件系统
Resource resource = appContext.getResource("file:c:\\testing.txt");
2。URL 路径
Resource resource =
appContext.getResource("url:http://www.yourdomain.com/testing.txt");
3。类别路径
Resource resource =
appContext.getResource("classpath:com/mkyong/common/testing.txt");
您只需要指定资源位置,Spring 会处理剩下的工作并返回一个资源对象。
使用getResource()
方法的完整示例。
package com.mkyong.common;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.Resource;
public class App
{
public static void main( String[] args )
{
ApplicationContext appContext =
new ClassPathXmlApplicationContext(new String[] {
"If-you-have-any.xml"});
Resource resource =
appContext.getResource("classpath:com/mkyong/common/testing.txt");
try{
InputStream is = resource.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
Bean 资源加载器(ResourceLoaderAware)
既然 bean 没有应用程序上下文访问权限,那么 bean 如何访问资源呢?解决方法是实现resource loader ware接口,并为 ResourceLoader 对象创建 setter 方法。Spring 会将资源加载器插入到 bean 中。
package com.mkyong.customer.services;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
public class CustomerService implements ResourceLoaderAware
{
private ResourceLoader resourceLoader;
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public Resource getResource(String location){
return resourceLoader.getResource(location);
}
}
Bean 配置文件
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="customerService"
class="com.mkyong.customer.services.CustomerService" />
</beans>
运行它
package com.mkyong.common;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.Resource;
import com.mkyong.customer.services.CustomerService;
public class App
{
public static void main( String[] args )
{
ApplicationContext appContext =
new ClassPathXmlApplicationContext(new String[] {
"Spring-Customer.xml"});
CustomerService cust =
(CustomerService)appContext.getBean("customerService");
Resource resource =
cust.getResource("classpath:com/mkyong/common/testing.txt");
try{
InputStream is = resource.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
现在,您可以从 bean 中获取资源。
结论
如果没有这个 getResource()方法,您将需要用不同的解决方案处理不同的资源,比如文件系统资源的文件对象,URL 资源的 URL 对象。Spring 确实用这个超级通用的 getResource() 方法做得很好,它确实节省了我们处理资源的时间。
下载源代码
Download it – Spring-getResource-Example.zipspring
弹簧座错误处理示例
在本文中,我们将向您展示 Spring Boot REST 应用程序中的错误处理。
使用的技术:
- Spring Boot 2.1.2 .版本
- 弹簧 5.1.4 释放
- maven3
- Java 8
1./错误
1.1 默认情况下,Spring Boot 为处理所有错误的/error
映射提供了一个BasicErrorController
控制器,并为生成包含错误细节、HTTP 状态和异常消息的 JSON 响应提供了一个getErrorAttributes
。
{
"timestamp":"2019-02-27T04:03:52.398+0000",
"status":500,
"error":"Internal Server Error",
"message":"...",
"path":"/path"
}
BasicErrorController.java
package org.springframework.boot.autoconfigure.web.servlet.error;
//...
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//...
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<>(body, status);
}
在 IDE 中,在这个方法中放置一个断点,你将理解 Spring Boot 如何生成默认的 JSON 错误响应。
2.自定义异常
在 Spring Boot,我们可以使用@ControllerAdvice
来处理自定义异常。
2.1 自定义异常。
BookNotFoundException.java
package com.mkyong.error;
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(Long id) {
super("Book id not found : " + id);
}
}
如果没有找到图书 id,控制器抛出上述BookNotFoundException
BookController.java
package com.mkyong;
//...
@RestController
public class BookController {
@Autowired
private BookRepository repository;
// Find
@GetMapping("/books/{id}")
Book findOne(@PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
//...
}
默认情况下,Spring Boot 生成以下 JSON 错误响应,http 500 error。
Terminal
curl localhost:8080/books/5
{
"timestamp":"2019-02-27T04:03:52.398+0000",
"status":500,
"error":"Internal Server Error",
"message":"Book id not found : 5",
"path":"/books/5"
}
2.2 如果没有找到图书 id,它应该返回 404 错误而不是 500,我们可以像这样覆盖状态代码:
CustomGlobalExceptionHandler.java
package com.mkyong.error;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
// Let Spring BasicErrorController handle the exception, we just override the status code
@ExceptionHandler(BookNotFoundException.class)
public void springHandleNotFound(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.NOT_FOUND.value());
}
//...
}
2.3 它现在返回一个 404。
Terminal
curl localhost:8080/books/5
{
"timestamp":"2019-02-27T04:21:17.740+0000",
"status":404,
"error":"Not Found",
"message":"Book id not found : 5",
"path":"/books/5"
}
2.4 此外,我们可以定制整个 JSON 错误响应:
CustomErrorResponse.java
package com.mkyong.error;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
public class CustomErrorResponse {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss")
private LocalDateTime timestamp;
private int status;
private String error;
//...getters setters
}
CustomGlobalExceptionHandler.java
package com.mkyong.error;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.time.LocalDateTime;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
public ResponseEntity<CustomErrorResponse> customHandleNotFound(Exception ex, WebRequest request) {
CustomErrorResponse errors = new CustomErrorResponse();
errors.setTimestamp(LocalDateTime.now());
errors.setError(ex.getMessage());
errors.setStatus(HttpStatus.NOT_FOUND.value());
return new ResponseEntity<>(errors, HttpStatus.NOT_FOUND);
}
//...
}
Terminal
curl localhost:8080/books/5
{
"timestamp":"2019-02-27 12:40:45",
"status":404,
"error":"Book id not found : 5"
}
3.JSR 303 验证错误
3.1 对于 Spring @valid
验证错误,会抛出handleMethodArgumentNotValid
CustomGlobalExceptionHandler.java
package com.mkyong.error;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
//...
// @Validate For Validating Path Variables and Request Parameters
@ExceptionHandler(ConstraintViolationException.class)
public void constraintViolationException(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.BAD_REQUEST.value());
}
// error handle for @Valid
@Override
protected ResponseEntity<Object>
handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", new Date());
body.put("status", status.value());
//Get all fields errors
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(x -> x.getDefaultMessage())
.collect(Collectors.toList());
body.put("errors", errors);
return new ResponseEntity<>(body, headers, status);
}
}
4.ResponseEntityExceptionHandler
4.1 如果我们不确定 Spring Boot 抛出了什么异常,就在这个方法中放一个断点进行调试。
ResponseEntityExceptionHandler.java
package org.springframework.web.servlet.mvc.method.annotation;
//...
public abstract class ResponseEntityExceptionHandler {
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
MethodArgumentNotValidException.class,
MissingServletRequestPartException.class,
BindException.class,
NoHandlerFoundException.class,
AsyncRequestTimeoutException.class
})
@Nullable
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
HttpHeaders headers = new HttpHeaders();
if (ex instanceof HttpRequestMethodNotSupportedException) {
HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
HttpStatus status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status, request);
}
//...
}
//...
}
5.DefaultErrorAttributes
5.1 为了覆盖所有异常的默认 JSON 错误响应,创建一个 bean 并扩展DefaultErrorAttributes
CustomErrorAttributes.java
package com.mkyong.error;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
// Let Spring handle the error first, we will modify later :)
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
// format & update timestamp
Object timestamp = errorAttributes.get("timestamp");
if (timestamp == null) {
errorAttributes.put("timestamp", dateFormat.format(new Date()));
} else {
errorAttributes.put("timestamp", dateFormat.format((Date) timestamp));
}
// insert a new key
errorAttributes.put("version", "1.2");
return errorAttributes;
}
}
现在,日期时间被格式化,一个新字段–version
被添加到 JSON 错误响应中。
curl localhost:8080/books/5
{
"timestamp":"2019/02/27 13:34:24",
"status":404,
"error":"Not Found",
"message":"Book id not found : 5",
"path":"/books/5",
"version":"1.2"
}
curl localhost:8080/abc
{
"timestamp":"2019/02/27 13:35:10",
"status":404,
"error":"Not Found",
"message":"No message available",
"path":"/abc",
"version":"1.2"
}
完成了。
下载源代码
$ git clone https://github.com/mkyong/spring-boot.git
$ cd spring-rest-error-handling
$ mvn spring-boot:run
参考
- Spring Boot 错误处理参考文献
- 默认错误属性文档
- ResponseEntityExceptionHandler
- 弹簧座验证示例
- Spring Boot 安全特征
- Hello Spring Security with Boot
- 维基百科–休息
春季休息你好世界示例
在本文中,我们将向您展示如何开发一个 Spring Boot REST 风格的 web 服务来处理来自 H2 内存数据库的 CRUD 操作。
使用的技术:
- Spring Boot 2.1.2 .版本
- 弹簧 5.1.4 释放
- 春季数据 JPA 2.1.4.RELEASE
- H2 内存数据库 1.4.197
- Tomcat Embed 9.0.14
- JUnit 4.12
- maven3
- Java 8
1.项目目录
2.专家
包括用于 Spring MVC 和 REST 结构的spring-boot-starter-web
,用于 CRUD 库的spring-boot-starter-data-jpa
。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-rest-hello-world</artifactId>
<packaging>jar</packaging>
<name>Spring Boot REST API Example</name>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
</parent>
<!-- Java 8 -->
<properties>
<java.version>1.8</java.version>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</properties>
<dependencies>
<!-- spring mvc, rest -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jpa, crud repository -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- in-memory database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- unit test rest -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- test patch operation need this -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.7</version>
<scope>test</scope>
</dependency>
<!-- hot swapping, disable cache for template, enable live reload -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<addResources>true</addResources>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
</plugin>
</plugins>
</build>
</project>
项目依赖关系。
$ mvn dependency:tree
[INFO] org.springframework.boot:spring-rest-hello-world:jar:1.0
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.1.2.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.1.2.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:2.1.2.RELEASE:compile
[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] | | | | \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.11.1:compile
[INFO] | | | | \- org.apache.logging.log4j:log4j-api:jar:2.11.1:compile
[INFO] | | | \- org.slf4j:jul-to-slf4j:jar:1.7.25:compile
[INFO] | | +- javax.annotation:javax.annotation-api:jar:1.3.2:compile
[INFO] | | \- org.yaml:snakeyaml:jar:1.23:runtime
[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:2.1.2.RELEASE:compile
[INFO] | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.9.8:compile
[INFO] | | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.9.0:compile
[INFO] | | | \- com.fasterxml.jackson.core:jackson-core:jar:2.9.8:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.9.8:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.9.8:compile
[INFO] | | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.9.8:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.1.2.RELEASE:compile
[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.14:compile
[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.14:compile
[INFO] | | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.14:compile
[INFO] | +- org.hibernate.validator:hibernate-validator:jar:6.0.14.Final:compile
[INFO] | | +- javax.validation:validation-api:jar:2.0.1.Final:compile
[INFO] | | +- org.jboss.logging:jboss-logging:jar:3.3.2.Final:compile
[INFO] | | \- com.fasterxml:classmate:jar:1.4.0:compile
[INFO] | +- org.springframework:spring-web:jar:5.1.4.RELEASE:compile
[INFO] | | \- org.springframework:spring-beans:jar:5.1.4.RELEASE:compile
[INFO] | \- org.springframework:spring-webmvc:jar:5.1.4.RELEASE:compile
[INFO] | +- org.springframework:spring-aop:jar:5.1.4.RELEASE:compile
[INFO] | +- org.springframework:spring-context:jar:5.1.4.RELEASE:compile
[INFO] | \- org.springframework:spring-expression:jar:5.1.4.RELEASE:compile
[INFO] +- org.springframework.boot:spring-boot-starter-data-jpa:jar:2.1.2.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-aop:jar:2.1.2.RELEASE:compile
[INFO] | | \- org.aspectj:aspectjweaver:jar:1.9.2:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-jdbc:jar:2.1.2.RELEASE:compile
[INFO] | | +- com.zaxxer:HikariCP:jar:3.2.0:compile
[INFO] | | \- org.springframework:spring-jdbc:jar:5.1.4.RELEASE:compile
[INFO] | +- javax.transaction:javax.transaction-api:jar:1.3:compile
[INFO] | +- javax.xml.bind:jaxb-api:jar:2.3.1:compile
[INFO] | | \- javax.activation:javax.activation-api:jar:1.2.0:compile
[INFO] | +- org.hibernate:hibernate-core:jar:5.3.7.Final:compile
[INFO] | | +- javax.persistence:javax.persistence-api:jar:2.2:compile
[INFO] | | +- org.javassist:javassist:jar:3.23.1-GA:compile
[INFO] | | +- net.bytebuddy:byte-buddy:jar:1.9.7:compile
[INFO] | | +- antlr:antlr:jar:2.7.7:compile
[INFO] | | +- org.jboss:jandex:jar:2.0.5.Final:compile
[INFO] | | +- org.dom4j:dom4j:jar:2.1.1:compile
[INFO] | | \- org.hibernate.common:hibernate-commons-annotations:jar:5.0.4.Final:compile
[INFO] | +- org.springframework.data:spring-data-jpa:jar:2.1.4.RELEASE:compile
[INFO] | | +- org.springframework.data:spring-data-commons:jar:2.1.4.RELEASE:compile
[INFO] | | +- org.springframework:spring-orm:jar:5.1.4.RELEASE:compile
[INFO] | | +- org.springframework:spring-tx:jar:5.1.4.RELEASE:compile
[INFO] | | \- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] | \- org.springframework:spring-aspects:jar:5.1.4.RELEASE:compile
[INFO] +- com.h2database:h2:jar:1.4.197:compile
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:2.1.2.RELEASE:test
[INFO] | +- org.springframework.boot:spring-boot-test:jar:2.1.2.RELEASE:test
[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.1.2.RELEASE:test
[INFO] | +- com.jayway.jsonpath:json-path:jar:2.4.0:test
[INFO] | | \- net.minidev:json-smart:jar:2.3:test
[INFO] | | \- net.minidev:accessors-smart:jar:1.2:test
[INFO] | | \- org.ow2.asm:asm:jar:5.0.4:test
[INFO] | +- junit:junit:jar:4.12:test
[INFO] | +- org.assertj:assertj-core:jar:3.11.1:test
[INFO] | +- org.mockito:mockito-core:jar:2.23.4:test
[INFO] | | +- net.bytebuddy:byte-buddy-agent:jar:1.9.7:test
[INFO] | | \- org.objenesis:objenesis:jar:2.6:test
[INFO] | +- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] | +- org.hamcrest:hamcrest-library:jar:1.3:test
[INFO] | +- org.skyscreamer:jsonassert:jar:1.5.0:test
[INFO] | | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] | +- org.springframework:spring-core:jar:5.1.4.RELEASE:compile
[INFO] | | \- org.springframework:spring-jcl:jar:5.1.4.RELEASE:compile
[INFO] | +- org.springframework:spring-test:jar:5.1.4.RELEASE:test
[INFO] | \- org.xmlunit:xmlunit-core:jar:2.6.2:test
[INFO] +- org.apache.httpcomponents:httpclient:jar:4.5.7:test
[INFO] | +- org.apache.httpcomponents:httpcore:jar:4.4.10:test
[INFO] | \- commons-codec:commons-codec:jar:1.11:test
[INFO] \- org.springframework.boot:spring-boot-devtools:jar:2.1.2.RELEASE:compile (optional)
[INFO] +- org.springframework.boot:spring-boot:jar:2.1.2.RELEASE:compile
[INFO] \- org.springframework.boot:spring-boot-autoconfigure:jar:2.1.2.RELEASE:compile
3.弹簧支架
3.1 一个 REST 控制器创建以下 REST API 端点:
HTTP 方法 | 上呼吸道感染 | 描述 |
---|---|---|
得到 | /书籍 | 列出所有书籍。 |
邮政 | /书籍 | 存一本书。 |
得到 | /books/{id} | 找到一本 id = {:id}的书。 |
放 | /books/{id} | 在 id = {:id}处更新或保存图书。 |
修补 | /books/{id} | 更新单个字段,其中 id = {:id}。 |
删除 | /books/{id} | 删除 id = {:id}的图书。 |
BookController.java
package com.mkyong;
import com.mkyong.error.BookNotFoundException;
import com.mkyong.error.BookUnSupportedFieldPatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
public class BookController {
@Autowired
private BookRepository repository;
// Find
@GetMapping("/books")
List<Book> findAll() {
return repository.findAll();
}
// Save
//return 201 instead of 200
@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/books")
Book newBook(@RequestBody Book newBook) {
return repository.save(newBook);
}
// Find
@GetMapping("/books/{id}")
Book findOne(@PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
// Save or update
@PutMapping("/books/{id}")
Book saveOrUpdate(@RequestBody Book newBook, @PathVariable Long id) {
return repository.findById(id)
.map(x -> {
x.setName(newBook.getName());
x.setAuthor(newBook.getAuthor());
x.setPrice(newBook.getPrice());
return repository.save(x);
})
.orElseGet(() -> {
newBook.setId(id);
return repository.save(newBook);
});
}
// update author only
@PatchMapping("/books/{id}")
Book patch(@RequestBody Map<String, String> update, @PathVariable Long id) {
return repository.findById(id)
.map(x -> {
String author = update.get("author");
if (!StringUtils.isEmpty(author)) {
x.setAuthor(author);
// better create a custom method to update a value = :newValue where id = :id
return repository.save(x);
} else {
throw new BookUnSupportedFieldPatchException(update.keySet());
}
})
.orElseGet(() -> {
throw new BookNotFoundException(id);
});
}
@DeleteMapping("/books/{id}")
void deleteBook(@PathVariable Long id) {
repository.deleteById(id);
}
}
4.春季数据 JPA
4.1 用 JPA 注释标注的模型。
Book.java
package com.mkyong;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.math.BigDecimal;
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
private String name;
private String author;
private BigDecimal price;
//setters, getters, constructors...
}
4.2 创建一个类并扩展JpaRepository
,它包含所有的 CRUD 操作。
BookRepository.java
package com.mkyong;
import org.springframework.data.jpa.repository.JpaRepository;
// Spring Data magic :)
public interface BookRepository extends JpaRepository<Book, Long> {
}
5.基本错误处理
创建一个@ControllerAdvice
来处理控制器抛出的定制异常。
CustomGlobalExceptionHandler
package com.mkyong.error;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
// Let Spring handle the exception, we just override the status code
@ExceptionHandler(BookNotFoundException.class)
public void springHandleNotFound(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.NOT_FOUND.value());
}
@ExceptionHandler(BookUnSupportedFieldPatchException.class)
public void springUnSupportedFieldPatch(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.METHOD_NOT_ALLOWED.value());
}
}
BookNotFoundException.java
package com.mkyong.error;
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(Long id) {
super("Book id not found : " + id);
}
}
BookUnSupportedFieldPatchException.java
package com.mkyong.error;
import java.util.Set;
public class BookUnSupportedFieldPatchException extends RuntimeException {
public BookUnSupportedFieldPatchException(Set<String> keys) {
super("Field " + keys.toString() + " update is not allow.");
}
}
6.Spring Boot
6.1 创建一个@SpringBootApplication
类来启动 REST web 应用程序,并将 3 个预定义的 book 对象插入到 H2 内存数据库中进行演示。
StartBookApplication.java
package com.mkyong;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.math.BigDecimal;
@SpringBootApplication
public class StartBookApplication {
public static void main(String[] args) {
SpringApplication.run(StartBookApplication.class, args);
}
// init bean to insert 3 books into h2 database.
@Bean
CommandLineRunner initDatabase(BookRepository repository) {
return args -> {
repository.save(new Book("A Guide to the Bodhisattva Way of Life", "Santideva", new BigDecimal("15.41")));
repository.save(new Book("The Life-Changing Magic of Tidying Up", "Marie Kondo", new BigDecimal("9.69")));
repository.save(new Book("Refactoring: Improving the Design of Existing Code", "Martin Fowler", new BigDecimal("47.99")));
};
}
}
7.演示
启动 Spring Boot 应用程序,用curl
命令测试 REST API 端点。
$ mvn spring-boot:run
7.1 查找全部—GET /books
> curl -v localhost:8080/books
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /books HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
>
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 06:33:57 GMT
<
[
{
"id":1,"name":"A Guide to the Bodhisattva Way of Life","author":"Santideva","price":15.41},
{
"id":2,"name":"The Life-Changing Magic of Tidying Up","author":"Marie Kondo","price":9.69},
{
"id":3,"name":"Refactoring: Improving the Design of Existing Code","author":"Martin Fowler","price":47.99}
]
7.2 找到一个—GET /books/1
> curl -v localhost:8080/books/1
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /books/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
>
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 06:37:26 GMT
<
{
"id":1,
"name":"A Guide to the Bodhisattva Way of Life",
"author":"Santideva",
"price":15.41
}
7.3 试验 404-GET /books/5
> curl -v localhost:8080/books/5
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /books/5 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
>
< HTTP/1.1 404
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 06:38:45 GMT
<
{
"timestamp":"2019-02-19T06:38:45.743+0000",
"status":404,
"error":"Not Found",
"message":"Book id not found : 5",
"path":"/books/5"
}
7.4 测试保存—POST /books -d {json}
#Run this on Windows, need \"
> curl -v -X POST localhost:8080/books -H "Content-type:application/json" -d "{\"name\":\"Spring REST tutorials\",\"author\":\"mkyong\",\"price\":\"9.99\"}"
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /books HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
> Content-type:application/json
> Content-Length: 65
>
* upload completely sent off: 65 out of 65 bytes
< HTTP/1.1 201
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 07:33:01 GMT
<
{
"id":4,
"name":
"Spring REST tutorials",
"author":"mkyong",
"price":9.99
}
7.5 测试更新—PUT /books/4 -d {json}
> curl -v -X PUT localhost:8080/books/4 -H "Content-type:application/json" -d "{\"name\":\"Spring Forever\",\"author\":\"pivotal\",\"price\":\"9.99\"}"
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> PUT /books/4 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
> Content-type:application/json
> Content-Length: 59
>
* upload completely sent off: 59 out of 59 bytes
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 07:36:49 GMT
<
{
"id":4,
"name":"Spring Forever",
"author":"pivotal",
"price":9.99
}
> curl localhost:8080/books/4
{
"id":4,"name":"Spring Forever","author":"pivotal","price":9.99}
7.6 测试更新“作者”字段—PATCH /books/4 -d {json}
> curl -v -X PATCH localhost:8080/books/4 -H "Content-type:application/json" -d "{\"author\":\"oracle\"}"
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> PATCH /books/4 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
> Content-type:application/json
> Content-Length: 19
>
* upload completely sent off: 19 out of 19 bytes
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 07:39:53 GMT
{
"id":4,
"name":"Spring Forever",
"author":"oracle",
"price":9.99
}
7.6.1 测试更新“名称”字段—PATCH /books/4 -d {json}
> curl -v -X PATCH localhost:8080/books/4 -H "Content-type:application/json" -d "{\"name\":\"New Spring REST\"}"
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> PATCH /books/4 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
> Content-type:application/json
> Content-Length: 26
>
* upload completely sent off: 26 out of 26 bytes
< HTTP/1.1 405
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 19 Feb 2019 07:40:47 GMT
<
{
"timestamp":"2019-02-19T07:40:47.740+0000",
"status":405,
"error":"Method Not Allowed",
"message":"Field [name] update is not allow.",
"path":"/books/4"
}
7.7 测试删除-DELETE /books/4
> curl -v -X DELETE localhost:8080/books/4
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> DELETE /books/4 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> Accept: <strong>/</strong>
>
< HTTP/1.1 200
< Content-Length: 0
< Date: Tue, 19 Feb 2019 07:44:24 GMT
<
> curl localhost:8080/books/4
{
"timestamp":"2019-02-19T07:44:39.432+0000",
"status":404,
"error":"Not Found",
"message":"Book id not found : 4",
"path":"/books/4"
}
> curl localhost:8080/books
[
{
"id":1,"name":"A Guide to the Bodhisattva Way of Life","author":"Santideva","price":15.41},
{
"id":2,"name":"The Life-Changing Magic of Tidying Up","author":"Marie Kondo","price":9.69},
{
"id":3,"name":"Refactoring: Improving the Design of Existing Code","author":"Martin Fowler","price":47.99}
]
下载源代码
$ git clone https://github.com/mkyong/spring-boot.git
$ cd spring-rest-hello-world
$ mvn spring-boot:run
参考
弹簧座集成测试示例
在本文中,我们将向您展示如何测试 Spring Boot REST 应用程序。通常,我们使用MockMvc
或TestRestTemplate
进行集成测试。
使用的技术:
- Spring Boot 2.1.2 .版本
- 弹簧 5.1.4 释放
- maven3
- Java 8
pom.xml
<!-- spring integration test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- spring integration test for security-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
1. MockMvc
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class BookControllerTest {
@Autowired
private MockMvc mockMvc;
1.1 本 Spring REST Hello World 示例的 CRUD 测试
package com.mkyong;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
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.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
//@WebMvcTest(BookController.class)
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class BookControllerTest {
private static final ObjectMapper om = new ObjectMapper();
@Autowired
private MockMvc mockMvc;
@MockBean
private BookRepository mockRepository;
@Before
public void init() {
Book book = new Book(1L, "Book Name", "Mkyong", new BigDecimal("9.99"));
when(mockRepository.findById(1L)).thenReturn(Optional.of(book));
}
@Test
public void find_bookId_OK() throws Exception {
mockMvc.perform(get("/books/1"))
/*.andDo(print())*/
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("Book Name")))
.andExpect(jsonPath("$.author", is("Mkyong")))
.andExpect(jsonPath("$.price", is(9.99)));
verify(mockRepository, times(1)).findById(1L);
}
@Test
public void find_allBook_OK() throws Exception {
List<Book> books = Arrays.asList(
new Book(1L, "Book A", "Ah Pig", new BigDecimal("1.99")),
new Book(2L, "Book B", "Ah Dog", new BigDecimal("2.99")));
when(mockRepository.findAll()).thenReturn(books);
mockMvc.perform(get("/books"))
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].id", is(1)))
.andExpect(jsonPath("$[0].name", is("Book A")))
.andExpect(jsonPath("$[0].author", is("Ah Pig")))
.andExpect(jsonPath("$[0].price", is(1.99)))
.andExpect(jsonPath("$[1].id", is(2)))
.andExpect(jsonPath("$[1].name", is("Book B")))
.andExpect(jsonPath("$[1].author", is("Ah Dog")))
.andExpect(jsonPath("$[1].price", is(2.99)));
verify(mockRepository, times(1)).findAll();
}
@Test
public void find_bookIdNotFound_404() throws Exception {
mockMvc.perform(get("/books/5")).andExpect(status().isNotFound());
}
@Test
public void save_book_OK() throws Exception {
Book newBook = new Book(1L, "Spring Boot Guide", "mkyong", new BigDecimal("2.99"));
when(mockRepository.save(any(Book.class))).thenReturn(newBook);
mockMvc.perform(post("/books")
.content(om.writeValueAsString(newBook))
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
/*.andDo(print())*/
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("Spring Boot Guide")))
.andExpect(jsonPath("$.author", is("mkyong")))
.andExpect(jsonPath("$.price", is(2.99)));
verify(mockRepository, times(1)).save(any(Book.class));
}
@Test
public void update_book_OK() throws Exception {
Book updateBook = new Book(1L, "ABC", "mkyong", new BigDecimal("19.99"));
when(mockRepository.save(any(Book.class))).thenReturn(updateBook);
mockMvc.perform(put("/books/1")
.content(om.writeValueAsString(updateBook))
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("ABC")))
.andExpect(jsonPath("$.author", is("mkyong")))
.andExpect(jsonPath("$.price", is(19.99)));
}
@Test
public void patch_bookAuthor_OK() throws Exception {
when(mockRepository.save(any(Book.class))).thenReturn(new Book());
String patchInJson = "{\"author\":\"ultraman\"}";
mockMvc.perform(patch("/books/1")
.content(patchInJson)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk());
verify(mockRepository, times(1)).findById(1L);
verify(mockRepository, times(1)).save(any(Book.class));
}
@Test
public void patch_bookPrice_405() throws Exception {
String patchInJson = "{\"price\":\"99.99\"}";
mockMvc.perform(patch("/books/1")
.content(patchInJson)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andExpect(status().isMethodNotAllowed());
verify(mockRepository, times(1)).findById(1L);
verify(mockRepository, times(0)).save(any(Book.class));
}
@Test
public void delete_book_OK() throws Exception {
doNothing().when(mockRepository).deleteById(1L);
mockMvc.perform(delete("/books/1"))
/*.andDo(print())*/
.andExpect(status().isOk());
verify(mockRepository, times(1)).deleteById(1L);
}
private static void printJSON(Object object) {
String result;
try {
result = om.writerWithDefaultPrettyPrinter().writeValueAsString(object);
System.out.println(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
1.2 此弹簧座验证的测试
package com.mkyong;
import com.fasterxml.jackson.databind.ObjectMapper;
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.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class BookControllerTest {
private static final ObjectMapper om = new ObjectMapper();
@Autowired
private MockMvc mockMvc;
@MockBean
private BookRepository mockRepository;
/*
{
"timestamp":"2019-03-05T09:34:13.280+0000",
"status":400,
"errors":["Author is not allowed.","Please provide a price","Please provide a author"]
}
*/
@Test
public void save_emptyAuthor_emptyPrice_400() throws Exception {
String bookInJson = "{\"name\":\"ABC\"}";
mockMvc.perform(post("/books")
.content(bookInJson)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.timestamp", is(notNullValue())))
.andExpect(jsonPath("$.status", is(400)))
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors", hasSize(3)))
.andExpect(jsonPath("$.errors", hasItem("Author is not allowed.")))
.andExpect(jsonPath("$.errors", hasItem("Please provide a author")))
.andExpect(jsonPath("$.errors", hasItem("Please provide a price")));
verify(mockRepository, times(0)).save(any(Book.class));
}
/*
{
"timestamp":"2019-03-05T09:34:13.207+0000",
"status":400,
"errors":["Author is not allowed."]
}
*/
@Test
public void save_invalidAuthor_400() throws Exception {
String bookInJson = "{\"name\":\" Spring REST tutorials\", \"author\":\"abc\",\"price\":\"9.99\"}";
mockMvc.perform(post("/books")
.content(bookInJson)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.timestamp", is(notNullValue())))
.andExpect(jsonPath("$.status", is(400)))
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors", hasSize(1)))
.andExpect(jsonPath("$.errors", hasItem("Author is not allowed.")));
verify(mockRepository, times(0)).save(any(Book.class));
}
}
1.3 用@WithMockUser
对该弹簧座安全进行测试
package com.mkyong;
import org.junit.Before;
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.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import java.math.BigDecimal;
import java.util.Optional;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class BookControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private BookRepository mockRepository;
@Before
public void init() {
Book book = new Book(1L, "A Guide to the Bodhisattva Way of Life", "Santideva", new BigDecimal("15.41"));
when(mockRepository.findById(1L)).thenReturn(Optional.of(book));
}
//@WithMockUser(username = "USER")
@WithMockUser("USER")
@Test
public void find_login_ok() throws Exception {
mockMvc.perform(get("/books/1"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("A Guide to the Bodhisattva Way of Life")))
.andExpect(jsonPath("$.author", is("Santideva")))
.andExpect(jsonPath("$.price", is(15.41)));
}
@Test
public void find_nologin_401() throws Exception {
mockMvc.perform(get("/books/1"))
.andDo(print())
.andExpect(status().isUnauthorized());
}
}
2.TestRestTemplate
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // for restTemplate
@ActiveProfiles("test")
public class BookControllerRestTemplateTest {
@Autowired
private TestRestTemplate restTemplate;
2.1 本 Spring REST Hello World 示例的 CRUD 测试
package com.mkyong;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.json.JSONException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // for restTemplate
@ActiveProfiles("test")
public class BookControllerRestTemplateTest {
private static final ObjectMapper om = new ObjectMapper();
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private BookRepository mockRepository;
@Before
public void init() {
Book book = new Book(1L, "Book Name", "Mkyong", new BigDecimal("9.99"));
when(mockRepository.findById(1L)).thenReturn(Optional.of(book));
}
@Test
public void find_bookId_OK() throws JSONException {
String expected = "{id:1,name:\"Book Name\",author:\"Mkyong\",price:9.99}";
ResponseEntity<String> response = restTemplate.getForEntity("/books/1", String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(MediaType.APPLICATION_JSON_UTF8, response.getHeaders().getContentType());
JSONAssert.assertEquals(expected, response.getBody(), false);
verify(mockRepository, times(1)).findById(1L);
}
@Test
public void find_allBook_OK() throws Exception {
List<Book> books = Arrays.asList(
new Book(1L, "Book A", "Ah Pig", new BigDecimal("1.99")),
new Book(2L, "Book B", "Ah Dog", new BigDecimal("2.99")));
when(mockRepository.findAll()).thenReturn(books);
String expected = om.writeValueAsString(books);
ResponseEntity<String> response = restTemplate.getForEntity("/books", String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
verify(mockRepository, times(1)).findAll();
}
@Test
public void find_bookIdNotFound_404() throws Exception {
String expected = "{status:404,error:\"Not Found\",message:\"Book id not found : 5\",path:\"/books/5\"}";
ResponseEntity<String> response = restTemplate.getForEntity("/books/5", String.class);
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
}
@Test
public void save_book_OK() throws Exception {
Book newBook = new Book(1L, "Spring Boot Guide", "mkyong", new BigDecimal("2.99"));
when(mockRepository.save(any(Book.class))).thenReturn(newBook);
String expected = om.writeValueAsString(newBook);
ResponseEntity<String> response = restTemplate.postForEntity("/books", newBook, String.class);
assertEquals(HttpStatus.CREATED, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
verify(mockRepository, times(1)).save(any(Book.class));
}
@Test
public void update_book_OK() throws Exception {
Book updateBook = new Book(1L, "ABC", "mkyong", new BigDecimal("19.99"));
when(mockRepository.save(any(Book.class))).thenReturn(updateBook);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(om.writeValueAsString(updateBook), headers);
ResponseEntity<String> response = restTemplate.exchange("/books/1", HttpMethod.PUT, entity, String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
JSONAssert.assertEquals(om.writeValueAsString(updateBook), response.getBody(), false);
verify(mockRepository, times(1)).findById(1L);
verify(mockRepository, times(1)).save(any(Book.class));
}
@Test
public void patch_bookAuthor_OK() {
when(mockRepository.save(any(Book.class))).thenReturn(new Book());
String patchInJson = "{\"author\":\"ultraman\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(patchInJson, headers);
ResponseEntity<String> response = restTemplate.exchange("/books/1", HttpMethod.PUT, entity, String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
verify(mockRepository, times(1)).findById(1L);
verify(mockRepository, times(1)).save(any(Book.class));
}
@Test
public void patch_bookPrice_405() throws JSONException {
String expected = "{status:405,error:\"Method Not Allowed\",message:\"Field [price] update is not allow.\"}";
String patchInJson = "{\"price\":\"99.99\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(patchInJson, headers);
ResponseEntity<String> response = restTemplate.exchange("/books/1", HttpMethod.PATCH, entity, String.class);
assertEquals(HttpStatus.METHOD_NOT_ALLOWED, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
verify(mockRepository, times(1)).findById(1L);
verify(mockRepository, times(0)).save(any(Book.class));
}
@Test
public void delete_book_OK() {
doNothing().when(mockRepository).deleteById(1L);
HttpEntity<String> entity = new HttpEntity<>(null, new HttpHeaders());
ResponseEntity<String> response = restTemplate.exchange("/books/1", HttpMethod.DELETE, entity, String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
verify(mockRepository, times(1)).deleteById(1L);
}
private static void printJSON(Object object) {
String result;
try {
result = om.writerWithDefaultPrettyPrinter().writeValueAsString(object);
System.out.println(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
2.2 此弹簧座验证的测试
package com.mkyong;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.json.JSONException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // for restTemplate
@ActiveProfiles("test")
public class BookControllerRestTemplateTest {
private static final ObjectMapper om = new ObjectMapper();
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private BookRepository mockRepository;
/*
{
"timestamp":"2019-03-05T09:34:13.280+0000",
"status":400,
"errors":["Author is not allowed.","Please provide a price","Please provide a author"]
}
*/
@Test
public void save_emptyAuthor_emptyPrice_400() throws JSONException {
String bookInJson = "{\"name\":\"ABC\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(bookInJson, headers);
// send json with POST
ResponseEntity<String> response = restTemplate.postForEntity("/books", entity, String.class);
//printJSON(response);
String expectedJson = "{\"status\":400,\"errors\":[\"Author is not allowed.\",\"Please provide a price\",\"Please provide a author\"]}";
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
JSONAssert.assertEquals(expectedJson, response.getBody(), false);
verify(mockRepository, times(0)).save(any(Book.class));
}
/*
{
"timestamp":"2019-03-05T09:34:13.207+0000",
"status":400,
"errors":["Author is not allowed."]
}
*/
@Test
public void save_invalidAuthor_400() throws JSONException {
String bookInJson = "{\"name\":\" Spring REST tutorials\", \"author\":\"abc\",\"price\":\"9.99\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(bookInJson, headers);
//Try exchange
ResponseEntity<String> response = restTemplate.exchange("/books", HttpMethod.POST, entity, String.class);
String expectedJson = "{\"status\":400,\"errors\":[\"Author is not allowed.\"]}";
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
JSONAssert.assertEquals(expectedJson, response.getBody(), false);
verify(mockRepository, times(0)).save(any(Book.class));
}
private static void printJSON(Object object) {
String result;
try {
result = om.writerWithDefaultPrettyPrinter().writeValueAsString(object);
System.out.println(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
2.3 本弹簧座安全性测试
package com.mkyong;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import java.math.BigDecimal;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class BookControllerRestTemplateTest {
private static final ObjectMapper om = new ObjectMapper();
//@WithMockUser is not working with TestRestTemplate
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private BookRepository mockRepository;
@Before
public void init() {
Book book = new Book(1L, "A Guide to the Bodhisattva Way of Life", "Santideva", new BigDecimal("15.41"));
when(mockRepository.findById(1L)).thenReturn(Optional.of(book));
}
@Test
public void find_login_ok() throws Exception {
String expected = "{id:1,name:\"A Guide to the Bodhisattva Way of Life\",author:\"Santideva\",price:15.41}";
ResponseEntity<String> response = restTemplate
.withBasicAuth("user", "password")
.getForEntity("/books/1", String.class);
printJSON(response);
assertEquals(MediaType.APPLICATION_JSON_UTF8, response.getHeaders().getContentType());
assertEquals(HttpStatus.OK, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
}
@Test
public void find_nologin_401() throws Exception {
String expected = "{\"status\":401,\"error\":\"Unauthorized\",\"message\":\"Unauthorized\",\"path\":\"/books/1\"}";
ResponseEntity<String> response = restTemplate
.getForEntity("/books/1", String.class);
printJSON(response);
assertEquals(MediaType.APPLICATION_JSON_UTF8, response.getHeaders().getContentType());
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
JSONAssert.assertEquals(expected, response.getBody(), false);
}
private static void printJSON(Object object) {
String result;
try {
result = om.writerWithDefaultPrettyPrinter().writeValueAsString(object);
System.out.println(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
下载源代码
$ git clone https://github.com/mkyong/spring-boot.git
$ CD spring-rest-hello-world
$ mvn 测试
$ cd 弹簧支架-验证
$ mvn 测试
$ cd 弹簧-支架-安全
$ mvn 测试