Archives 十二月 2018

springboot 与数据验证

springboot 与数据验证

一. 简述

在项目开发中,验证参数也是最经常使用的业务需求了。通常在开发的时候都需要根据业务需求,对参数进行必要验证。

当然一堆的 if-else 的验证在日常开发中时常可见。这种方式非常不友好:
1. 代码太长导致阅读不友好,更改需求可能只是简单的修改但是却需要阅读几十到几百行的代码
2. 有时候业务只是一两句但是验证代码却占用了很长时间

JSR-303Java 的验证规范,早期是在 Hibernate 框架中实现的,后面被抽取到 Java 体系。Spring-Boot 使用了 hibernate-validator 验证器,所以也包含了 JSR-303 在里面。当需要进行参数验证的时候,只需要几个注解即可实现复杂的验证。

阅读更多


springboot 与数据接口

springboot 与数据接口

一. 简述

从以往的 Spring 项目开发经验来看,SpringJSON 情有独钟,这也得益于 JSONJS 发明的一种轻量级的数据交换格式,因为本身 JS 是弱类型的语言,所以 JSON 便没有什么特定类型限制,使得其他各门语言都可以对 JSON 进行解析,从而序列化成各自的对象。

其实前面说的已经有一点半点的,基本都是返回 JSON 字符串,所以这里便加深一点,涉及 SpringMVC 可以用于开发的注解。

阅读更多


springboot 与 web 拦截器

springboot-web 拦截器的使用

一. 简述

用过 SpringMVC 的应该都知道拦截器,拦截器可以设置在 SpringMVC 接收请求之前处理(返回 true 继续执行或 false 拒绝执行),方法处理请求之后,以及处理完整个请求之后的操作。例如:单体项目的登陆拦截、在进入处理器之前先给线程栈设置一个公用的 UserThreadLocal 等等。

阅读更多


springboot 常用异常信息返回

springboot-web 常用异常信息返回

一. 简述

在日常的开发中,都会有多多少少的异常发生,如:不存在异常、参数异常等等。那这时候怎么向前端展示也是一个问题,通常的做法就是建立一个通用的数据返回类注入 ResultDto 然后存储是否顺利调用,数据,错误信息等信息。结合 spring-boot-web 的监听器,可以让我们尽量少的关注这些错误异常的发生,只要一句话或者一个注解【需要自己封装,使用 aop 】,就可以实现程序给我们的自动验证。

GitHub: https://github.com/WeidanLi/spring-boot-tutorial
示例代码:web-exception-resp

二. 开发

1. mvn 新增 web-starter 的依赖

<dependencies>
    <!-- 引入 web-starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2. 准备一个标准输出类和一些异常

标准数据响应类:

public class ResultDto<T> {

    private boolean success;
    private T data;
    private String message;

    public ResultDto(T data) {
        this.success = true;
        this.data = data;
    }

    public ResultDto(String message) {
        this.success = false;
        this.message = message;
    }

    public boolean isSuccess() {
        return success;
    }

    public T getData() {
        return data;
    }

    public String getMessage() {
        return message;
    }

}

准备了一些异常,一个基类,两个子类:

public class AbsException extends RuntimeException {

    public AbsException() {
    }

    public AbsException(String message) {
        super(message);
    }

}

public class ElementNotFoundException extends AbsException {

    public ElementNotFoundException(String elementName) {
        super(elementName + "不存在");
    }

}

public class ParamInvalidException extends AbsException {

    public ParamInvalidException() {
        super("参数错误");
    }

}

3. 资源和资源控制器

我假设,id 超过 100 的就是参数错误了,如果 id 是个偶数就是资源不存在

public class UserDto {

    private Long id;
    private String name;
    private Integer age;

    public UserDto(Long id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public UserDto() {
    }

    // 省略 getter 和 setter
}

@RestController
@RequestMapping("user")
public class UserEndpoint {


    @GetMapping("{id}")
    public ResultDto<UserDto> idOf(@PathVariable("id") Long id) {
        if (id > 100) {
            throw new ParamInvalidException();
        }
        if (id % 2 == 0) {
            throw new ElementNotFoundException("用户");
        }
        UserDto userDto = new UserDto(id, "狗蛋", 18);
        return new ResultDto<>(userDto);
    }

}

4. 重点:使用 ControllerAdvice 监听控制器调用时出现的异常

@ControllerAdvice // 指定这个类是一个监听类,用于监听不同异常输出结果
@RestController
public class EndpointAdvice {

    @ExceptionHandler(ElementNotFoundException.class) // 元素未找到异常
    @ResponseStatus(HttpStatus.NOT_FOUND) // 返回 404
    public ResultDto<Void> elementNotFount(ElementNotFoundException e) {
        return new ResultDto<>(e.getMessage());
    }

    @ExceptionHandler(ParamInvalidException.class) // 参数错误异常
    @ResponseStatus(HttpStatus.BAD_REQUEST) // 返回 400
    public ResultDto<Void> paramInvalid(ParamInvalidException e) {
        return new ResultDto<>(e.getMessage());
    }

    @ExceptionHandler(Exception.class) // 未知错误
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 返回 500
    public ResultDto<Void> other(Exception e) {
        return new ResultDto<>(e.getMessage());
    }

}

5. 接口测试

使用 idea-2018http 测试工具请求:

GET http://localhost:8080/user/101

HTTP/1.1 400 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 19 Nov 2018 16:45:16 GMT
Connection: close

{
  "success": false,
  "data": null,
  "message": "参数错误"
}

Response code: 400; Time: 357ms; Content length: 46 bytes

---------------------------------------------------------

GET http://localhost:8080/user/100

HTTP/1.1 404 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 19 Nov 2018 16:46:46 GMT

{
  "success": false,
  "data": null,
  "message": "用户不存在"
}

Response code: 404; Time: 41ms; Content length: 47 bytes

---------------------------------------------------------

GET http://localhost:8080/user/99

HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 19 Nov 2018 16:47:09 GMT

{
  "success": true,
  "data": {
    "id": 99,
    "name": "狗蛋",
    "age": 18
  },
  "message": null
}

Response code: 200; Time: 140ms; Content length: 69 bytes

三. 总结

其实 spring-boot 的构建大部分都是一致的,上面前几步基本和简单的 web 项目一致,最重要的一步就是监听到不同的异常,然后返回不同的状态码以及输出信息。

因为现在大部分项目都是微服务架构的,所以建议对自己公司内部整理一个基本的框架,包含有不同异常的,基本输出类的,以及这个 ControllerAdvice 的配置。在微服务中,只要对此框架进行依赖,即可拥有上面的功能。

常用状态码:

状态码 说明
200 请求成功(多用于 get 请求资源、put 更新资源)
201 创建成功(多用于 post 创建资源)
400 请求错误,参数格式不正确或者参数不符合验证要求
404 未找到,资源不存在或者
500 服务器出现问题

一个简单的入门 web 项目

一个简单的入门 web 项目

一. 简述

其实,spring 在我们职业生涯中,大部分只做一件事情,那就是:web 项目 bean 的管理和整合。所以,web 应用是至关重要的,本文将从一个简单的 hello world 开始 web 的构建。

示例代码:web-simple

二. 开发

1. mvn 新增 web-starter 的依赖

<dependencies>
    <!-- 引入 web-starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

阅读更多


什么是 spring-boot

什么是 spring-boot

一. 简述

用了一年多的 spring-boot 进行项目开发了,我想我有必要把平时开发遇到的,我自己个人在项目中要求 spring-boot 做到的整理成一份主题博客。一来整理一下用了 spring-boot 这么久以来的收获,二来也好给自己有个知识的重新整理和归纳。

二. spring-boot 介绍

讲真,项目还在用 spring 的时候,听说别人在用 spring-boot 感觉,哇塞,好高大上的样子。于是乎我就去百度了解一圈,查了一下相关资料,按照我印象中来说:spring-boot 就是官方为了解决经常在项目中整合第三方中间件的时候,需要配置一堆 xml 的麻烦,开发出来的,通过非常简单的依赖和配置,从而完成与第三方中间件的整合,也可以解决常常因为版本的不兼容导致项目的出错,也可以减轻项目开发中对 spring 的重量级配置(这句话我下面会反驳)。

阅读更多


微服务Feign本地契约测试–SpringCloudContract

[TOC]

一. 简述

在项目开发的时候,特别是使用 TDD 进行开发的项目,测试便是不可或缺的一个环节。然而我们的服务一般都需要配合其他服务接口来进行开发,那么测试的时候就需要开启所有服务来配合测试,机器配置跟不上,在构建的时候也会出现很多问题。

这时候就需要有一个东西来把调用第三方接口的事情给做了。最近看了Clossoverjie的一篇文章 分享几个 SpringBoot 实用的小技巧,他很巧妙的利用 Spring 的容器把连接第三方接口的 bean 给替换掉。但我感觉始终还不是那么优雅(嗯,Spring 脑残粉,Spring 提供了就会用)。

现在 Spring-Cloud 提供了一个插件,Spring-Cloud-Contract 可以巧妙的对消费者项目进行打桩,让项目的测试调用访问的时候,可以模拟第三方业务,这也需要生产者提供一个 Contract 来使用。(当然,挺适合我这种一个人开发多个服务的开发者)

二. 搭建一个环境

OK,演示需要有个大概的业务示例来做。

我这里模拟了订单服务需要从产品服务获取产品的描述(不要吐槽,随便想到罢了)。那么调用订单服务就需要调用到产品服务了,我将演示如何在订单服务中将产品服务给 Mock 掉。

项目情况:

  • Eureka: 注册中心
  • product-server:产品服务
  • order-server:订单服务
  • SpringCloud:Edgware.SR3
  • SpringBoot:1.5.10.RELEASE

2.1 订单服务

获取一个订单的时候,需要获取产品信息的接口

@RestController
public class OrderEndpoint {

    private ProductClient productClient;

    @Autowired
    public OrderEndpoint(ProductClient productClient) {
        this.productClient = productClient;
    }

    @GetMapping
    public Order get(@RequestParam("productUuid") String productUuid) {
        // 这里需要调用产品服务接口来获取产品信息
        ProductDesciption productDesciption = productClient.uuidOf(productUuid);
        return new Order(UUID.randomUUID().toString(), productDesciption);
    }

}

// 产品客户端
@FeignClient(name = "product-server")
public interface ProductClient {

    @RequestMapping(method = RequestMethod.GET)
    ProductDesciption uuidOf(@RequestParam("uuid") String uuid);

}

// 订单DTO类
public class Order {

    private String uuid;

    private ProductDesciption productDesciption;

    // 省略getter&setter
}

// 产品类,用于接收上游接口返回的信息
public class ProductDesciption {

    private String prodName;

    // 省略getter&setter

}

2.2 产品服务

产品服务随意的提供了产品信息。

@RestController
public class ProductEndpoint {

    @GetMapping
    public Product uuidOf(@RequestParam("uuid") String uuid) {
        switch (uuid) {
            case "1":
                return new Product("1", "电视");
            case "2":
                return new Product("2", "iPhone");
        }
        return new Product();
    }

}
public class Product {

    private String uuid;

    private String prodName;

    // 省略getter&setter
}

2.3 请求订单接口

我用了 idea 自带的 HTTP 工具来测试接口,返回正常

GET http://127.0.0.1:8082?productUuid=1

HTTP/1.1 200 
X-Application-Context: order-server:8082
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 11 Dec 2018 02:41:59 GMT

{
  "uuid": "53ddfe48-0bf9-43d9-abb5-65a59468a5b5",
  "productDesciption": {
    "prodName": "电视"
  }
}

三. 生产者提供contract包

3.1 引入相关包

<!--契约测试服务提供端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
    <scope>test</scope>
</dependency>
<!--以下需要放在 plugins 标签中-->
<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <!-- Don't forget about this value !! -->
    <extensions>true</extensions>
    <configuration>
        <!-- MvcMockTest为生成本地测试案例的基类 -->
        <baseClassForTests>com.springboot.services.producer.MvcMockTest</baseClassForTests>
    </configuration>
</plugin>

3.1 编写 Contract

可以使用 groovy 或者 yaml 进行编写,我就提供 groovy 版本了。

需要放在 src/test/resources/contracts/ProductEndpoint.groovy 中,注意资源包下的那个目录。

package contracts

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    request {
        // 请求方法
        method 'GET'
        // 路径
        url('/') {
            queryParameters {
                parameter("uuid", "1")
            }
        }
    }
    response { // 响应设置
        status 200
        body("""
          {
              "uuid": "1",
              "prodName": "电视"
            }
  """)
        headers {
            header('Content-Type': 'application/json;charset=UTF-8')
        }
    }
}

3.2 生成桩

生成 Mapping.json,放在product-server/target/stubs/META-INF/cn.liweidan.contract/product-server/1.0.0-SNAPSHOT/mappings/ProductEndpoint.json 中,里面是对请求响应的设定

cd product-server # pom.xml 所在目录
mvn spring-cloud-contract:convert # 转换成mapping.json
mvn spring-cloud-contract:generateStubs # 生成 jar 包
mvn install:install-file -DgroupId=cn.liweidan.contract \
    -DartifactId=product-server -Dversion=1.0.0-SNAPSHOT \
    -Dpackaging=jar -Dclassifier=stubs -Dfile=target/product-server-1.0.0-SNAPSHOT-stubs.jar # 安装到本地仓库

OK,已经将 product-server 的桩打进 maven 仓库了,现在可以在消费者那边进行使用

四. 消费者使用contract包

4.1 引入相关包

<!-- SpringBoot 测试 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!--契约测试服务提供端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    <scope>test</scope>
</dependency>

4.2 编写测试类

这里跟参考资料作者写的就有点区别了,可能是因为升级了版本 @AutoConfigureStubRunnerstubsMode 属性已经取消了。

这里是去 maven 仓库查找。

ids 属性指定的是刚刚打包的桩的坐标

格式是:groupId:artifactId:version:classifier:port

package cn.liweidan.contract.order.endpoint;

import org.hamcrest.core.Is;
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.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
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;

/**
 * Description:测试订单端口
 *
 * @author liweidan
 * @version 1.0
 * @date 2018-12-11 12:02
 * @email toweidan@126.com
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
// ids 指定需要打桩的服务,记住写 workOffline
@AutoConfigureStubRunner(ids = {"cn.liweidan.contract:product-server:1.0.0-SNAPSHOT:stubs:9080"}, workOffline = true)
public class OrderEndpointTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void testGetOrder() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/").param("productUuid", "1"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("productDesciption.prodName", Is.is("电视")));
    }

}

4.3 配置服务名称

我已经顺便把测试的时候把 eureka 的链接关闭了。

完整配置(test/resources):

eureka:
  client:
    enabled: false
server:
  port: 8082
spring:
  application:
    name: order-server
stubrunner:
  idsToServiceIds: # 用于指定 feign 名字对应的 stubs 包。前面是 stubs 包的 artifactId,后面是  feign 名字
    product-server: product-server

4.4 运行测试

OK,单机测试可以用过了。

当然测试还包括数据库使用内存数据库等等,可以防止在 maven 编译的时候报错,这块其他文章再说

参考资料

示例代码

基于Feign的微服务调用之契约测试 Spring Cloud Contract

Contract-dsl

SpringCloudContractDocs