This commit is contained in:
六如
2024-12-02 23:03:25 +08:00
parent c0cfdbafd9
commit 942826f4b0
107 changed files with 247 additions and 3432 deletions

View File

@@ -4,10 +4,8 @@ VOLUME /sop
# 将所有应用放到一个镜像当中 # 将所有应用放到一个镜像当中
ADD sop-gateway/target/*.jar sop/sop-gateway/sop-gateway.jar ADD sop-gateway/target/*.jar sop/sop-gateway/sop-gateway.jar
ADD sop-admin/sop-admin-server/target/*.jar sop/sop-admin/sop-admin.jar ADD sop-admin/sop-admin-backend/backend-boot/target/*.jar sop/sop-admin/sop-admin.jar
ADD sop-website/sop-website-server/target/*.jar sop/sop-website/sop-website.jar ADD sop-example/example-story/target/*.jar sop/sop-story/sop-story.jar
ADD sop-auth/target/*.jar sop/sop-auth/sop-auth.jar
ADD sop-example/sop-story/target/*.jar sop/sop-story/sop-story.jar
# 拷贝启动脚本 # 拷贝启动脚本
COPY docker-entrypoint.sh /usr/local/bin/ COPY docker-entrypoint.sh /usr/local/bin/

27
doc/.gitignore vendored
View File

@@ -1,27 +0,0 @@
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
*.png
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/

View File

@@ -1,11 +0,0 @@
# 开发文档
文档放在docs/files下写完文档记得执行下`SidebarTest.main()`方法
配合gitee pages服务使用gitee pages服务指定docs目录。
## 本地查看开发文档
- 前提先安装好npm[npm安装教程](https://blog.csdn.net/zhangwenwu2/article/details/52778521)。建议使用淘宝镜像。
- 安装docsify执行npm命令`npm i docsify-cli -g --registry=https://registry.npm.taobao.org`
- cd到当前目录运行命令`docsify serve docs`,然后访问:`http://localhost:3000`即可查看。

View File

@@ -1,3 +0,0 @@
# SOP开发文档
Git地址[SOP](https://gitee.com/durcframework/SOP)

View File

@@ -1 +0,0 @@
include: [_navbar,_sidebar]

View File

@@ -1,12 +0,0 @@
![logo](_media/icon.svg)
# docsify <small>4.6.10</small>
> A magical documentation site generator.
* Simple and lightweight (~19kB gzipped)
* No statically built html files
* Multiple themes
[GitHub](https://github.com/QingWei-Li/docsify/)
[Get Started](#docsify)

View File

@@ -1,3 +0,0 @@
- 关于
- [帮助](/zh-cn/)
- [API](/)

View File

@@ -1,36 +0,0 @@
* [首页](/?t=1616211903021)
* 开发文档
* [快速体验](files/10010_快速体验.md?t=1616211903027)
* [项目接入到SOP](files/10011_项目接入到SOP.md?t=1616211903048)
* [新增接口](files/10020_新增接口.md?t=1616211903048)
* [开发流程](files/10021_开发流程.md?t=1616211903048)
* [用户注册](files/10022_用户注册.md?t=1616211903049)
* [业务参数校验](files/10030_业务参数校验.md?t=1616211903049)
* [错误处理](files/10040_错误处理.md?t=1616211903049)
* [编写文档](files/10041_编写文档.md?t=1616211903049)
* [接口交互详解](files/10050_接口交互详解.md?t=1616211903049)
* [使用签名校验工具](files/10080_使用签名校验工具.md?t=1616211903049)
* [ISV管理](files/10085_ISV管理.md?t=1616211903050)
* [自定义返回结果](files/10087_自定义返回结果.md?t=1616211903050)
* [自定义过滤器](files/10088_自定义过滤器.md?t=1616211903050)
* [自定义校验token](files/10089_自定义校验token.md?t=1616211903050)
* [网关拦截器](files/10090_网关拦截器.md?t=1616211903050)
* [路由授权](files/10091_路由授权.md?t=1616211903050)
* [接口限流](files/10092_接口限流.md?t=1616211903051)
* [路由监控](files/10093_路由监控.md?t=1616211903051)
* [SDK开发](files/10095_SDK开发.md?t=1616211903051)
* [应用授权](files/10097_应用授权.md?t=1616211903051)
* [提供restful接口](files/10100_提供restful接口.md?t=1616211903051)
* [文件上传](files/10104_文件上传.md?t=1616211903051)
* [配置Sleuth链路追踪](files/10109_配置Sleuth链路追踪.md?t=1616211903052)
* [预发布灰度发布](files/10110_预发布灰度发布.md?t=1616211903052)
* [动态修改请求参数](files/10111_动态修改请求参数.md?t=1616211903052)
* [使用eureka](files/10112_使用eureka.md?t=1616211903052)
* [超时设置](files/10113_超时设置.md?t=1616211903052)
* 原理分析
* [网关性能测试](files/90001_网关性能测试.md?t=1616211903052)
* [原理分析之如何存储路由](files/90011_原理分析之如何存储路由.md?t=1616211903052)
* [原理分析之如何路由](files/90012_原理分析之如何路由.md?t=1616211903052)
* [原理分析之文档归纳](files/90013_原理分析之文档归纳.md?t=1616211903052)
* [原理分析之预发布灰度发布](files/90014_原理分析之预发布灰度发布.md?t=1616211903052)
* [常见问题](files/90100_常见问题.md?t=1616211903053)

View File

@@ -1,52 +0,0 @@
# 快速体验
## 方式1
> 运行环境JDK8Maven3[Nacos](https://nacos.io/zh-cn/docs/what-is-nacos.html)Mysql
- 安装并启动Nacos[安装教程](https://nacos.io/zh-cn/docs/quick-start.html)
- 执行Mysql脚本`sop.sql`(Mysql版本5.6+)5.6以下运行`sop-mysql5.6以下版本.sql`
- IDE安装lombok插件然后打开项目(IDEA下可以打开根pom.xml然后open as project)
- 启动网关打开sop-gateway下的`application-dev.properties`
1. 修改数据库`username/password`
2. 指定nacos地址如果nacos安装在本机则不用改
3. 运行`SopGatewayApplication.java`
- 启动微服务:打开`sop-example/sop-story`下的`application-dev.properties`文件
1. 指定nacos地址如果nacos安装在本机则不用改
2. 运行`SopStoryApplication.java`
- 找到sop-test运行`com.gitee.sop.test.AlipayClientPostTest.testGet`进行接口调用测试
## 方式2docker
> 前提安装好docker
- 安装并启动Nacos[安装教程](https://nacos.io/zh-cn/docs/quick-start.html)
- 执行Mysql脚本`sop.sql`(Mysql版本5.6+)5.6以下运行`sop-mysql5.6以下版本.sql`
- 打开`docker-entrypoint.sh`修改mysqlnacos配置
- 执行`docker-build.sh`
- 找到sop-test运行`com.gitee.sop.test.AlipayClientPostTest.testGet`进行接口调用测试
> - admin地址http://ip:8082 登录账号admin/123456
> - 文档地址http://ip:8083/
## 使用admin
- 找到`sop-admin/sop-admin-server`工程打开sop-admin-server下的`application-dev.properties`,修改相关配置
- 运行`SopAdminServerApplication.java`
- 访问:`http://localhost:8082`
登录账号admin/123456
## 启动门户网站
见:`sop-website/sop-portal/README.md`
## 启动文档中心/ISV后台
文档中心代码在sop-website工程中
- 确保注册中心、网关、微服务正常启动
- 修改sop-website-server下的application-dev.properties相关配置
- 运行WebsiteServerApplication.java
- 访问http://localhost:8083

View File

@@ -1,217 +0,0 @@
# 项目接入到SOP
以springboot项目为例完整项目可参考sop-example下的sop-story
- pom.xml添加版本配置
```xml
<!-- springboot 版本-->
<spring-boot.version>2.6.15</spring-boot.version>
<!-- spring cloud 版本 -->
<spring-cloud.version>2021.0.5</spring-cloud.version>
<!-- spring cloud alibaba 版本 -->
<!-- 具体版本对应关系见https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E -->
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
```
- pom.xml添加`<dependencyManagement>`控制版本
```xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```
- pom.xml依赖sop-service-common和nacos服务发现
```xml
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-service-common</artifactId>
<version>最新版本</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
```
- application.properties配置文件添加
```properties
server.port=2222
spring.application.name=story-service
# nacos注册中心
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
```
- 在springboot启动类上添加`@EnableDiscoveryClient`
- 新增一个配置类,继承`AlipayServiceConfiguration.java`,内容为空
```java
@Configuration
public class OpenServiceConfig extends AlipayServiceConfiguration {
}
```
- 全局异常处理
在微服务项目的全局异常处理中添加一句:`ExceptionHolder.hold(request, response, exception);`
```java
@ExceptionHandler(Exception.class)
@ResponseBody
public Object exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception exception) {
...
// 在返回前加这一句
ExceptionHolder.hold(request, response, exception);
...
return ..;
}
```
如果没有配置全局异常,可参考下面配置
```java
@ControllerAdvice
@Slf4j
public class StoryGlobalExceptionHandler {
/**
* 捕获手动抛出的异常
*
* @param request request
* @param response response
* @param exception 异常信息
* @return 返回提示信息
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Object exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception exception) {
// 在返回前加这一句
ExceptionHolder.hold(request, response, exception);
// 下面可以实现自己的全局异常处理
return new ErrorResult(500, exception.getMessage());
}
@Data
@AllArgsConstructor
public static class ErrorResult {
private int code;
private String msg;
}
}
```
到此准备工作就完成了,接下来可前往`新增接口`查看如何新增接口。
## 非Java项目接入
如果是非Java项目接入如php需要做到以下几点
> 1. 提供路由配置接口
> 2. 服务注册到nacos并在nacos的metadata中指定接口路径metadata的key为`sop.routes.path`
- 提供路由配置接口
php应用提供的接口需要返回如下json内容
假设请求的接口为:`http://open.xxx.com/get_routes`
```json
{
"serviceId": "goods-service",
"createTime": 1568603471646,
"updateTime": 1568603471646,
"description": null,
"routeDefinitionList": [
{
"id": "goods.list1.0",
"name": "goods.list",
"version": "1.0",
"uri": "lb://goods-service",
"path": "/goods/list_goods",
"order": 0,
"ignoreValidate": 0,
"status": 1,
"mergeResult": 1,
"permission": 0
},
...
]
}
```
json参数说明
|参数名|是否必填|说明|
|:----|:----|:----|
|serviceId |是|serviceId服务id |
|createTime |是|创建时间Unix timestamp毫秒 |
|updateTime |是|修改时间Unix timestamp毫秒 |
|description|否|描述|
|routeDefinitionList元素参数说明|是|路由配置routeDefinitionList元素参数说明|
routeDefinitionList元素参数说明
|参数名|是否必填|说明|
|:----|:----|:----|
|id |是|路由id全局唯一格式接口名+版本号 |
|name|是|接口名称|
|version|是|版本号|
|uri|是|格式lb:// + serviceIdlb://goods-service|
|path|是|接口path填端口号后面的path如你的接口为`http://open.domain.com:8080/goods/list_goods`,填:`/goods/list_goods`|
|order|是|固定填0|
|ignoreValidate|是|忽略签名验证10否|
|status|是|启用状态1启用2禁用|
|mergeResult|是|是否统一返回结果10否|
|permission|是|是否需要权限访问10否|
- 服务注册到nacos
可前往nacos官网参考[open-api](https://nacos.io/zh-cn/docs/open-api.html)使用nacos提供的接口完成服务注册
- 在nacos的metadata中指定接口路径
伪代码如下:
```java
Instance instance = new Instance();
instance.setServiceName("goods-service");
instance.setIp("192.168.0.11");
instance.setPort(8080);
// 在nacos的metadata中指定接口路径
instance.getMetadata().put("sop.routes.path", "http://open.xxx.com/get_routes");
namingService.registerInstance(serviceId, instance);
```
完成以上步骤后php服务注册到nacos网关会触发监听事件获取新注册的服务然后会向你的服务拉取路由配置。

View File

@@ -1,147 +0,0 @@
# 新增接口
假设要对下面这个接口提供开放能力。
```java
@RestController
public class StoryDemoController {
@RequestMapping("/story/get")
public StoryResult getStory() {
StoryResult result = new StoryResult();
result.setId(1L);
result.setName("海底小纵队");
return result;
}
}
```
只需要在方法上新增一个`@Open`注解,指定接口名即可
```java
// 添加一个@Open注解
@Open("story.demo.get")
@RequestMapping("/story/get")
public StoryResult getStory() {
StoryResult result = new StoryResult();
result.setId(1L);
result.setName("海底小纵队");
return result;
}
```
如果要加上版本号,指定`version`参数:`@Open(value = "story.demo.get", version = "2.0")`
- 重启服务,这样接口就可以使用了。
## 绑定业务参数
网关校验通过后,请求参数会传递到微服务上来,完整的参数如下所示:
```
请求参数charset=utf-8&biz_content={"goods_remark":"iphone6"}&method=goods.add&format=json&app_id=2019032617262200001&sign_type=RSA2&version=1.0&timestamp=2019-04-29 19:18:38
```
其中biz_content部分是我们想要的在方法上申明一个对象对应biz_content中的内容即可完成参数绑定并且对参数进行JSR-303校验。
**注意:接口方法必须有且只有一个对象参数,如果申明多个会出现参数绑定失败**
```java
@Open("goods.add")
@RequestMapping("/goods/add")
public Object addGoods(GoodsParam param) {
return param;
}
@Data
public class GoodsParam {
@NotEmpty(message = "不能为空") // 支持JSR-303校验
private String goods_remark;
}
```
一般情况下,只需要获取业务参数即可,如果想要获取更多的参数,可在后面跟一个`HttpServletRequest`对象。
```java
@Open("goods.add")
@RequestMapping("/goods/add")
public Object addGoods(GoodsParam param, HttpServletRequest request) {
System.out.println(request.getParameter("method"));
return param;
}
```
## 接口命名
接口命名没有做强制要求,但我们还是推荐按照下面的方式进行命名:
接口名的命名规则为:`服务模块.业务模块.功能模块.行为`,如:
- mini.user.userinfo.get 小程序服务.用户模块.用户信息.获取
- member.register.total.get 会员服务.注册模块.注册总数.获取
如果觉得命名规则有点长可以精简为:`服务模块.功能模块.行为`,如`member.usercount.get`,前提是确保前缀要有所区分,不和其它服务冲突。
## 测试接口
- 在sop-test工程下新建一个测试用例`StoryDemoTest`继承TestBase
```java
public class StoryDemoTest extends TestBase {
String url = "http://localhost:8081/api";
String appId = "2019032617262200001";
// 私钥
String privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=";
@Test
public void testDemo() throws Exception {
// 公共请求参数
Map<String, String> params = new HashMap<String, String>();
params.put("app_id", appId);
// 这里对应@Open.value属性
params.put("method", "story.demo.get");
params.put("format", "json");
params.put("charset", "utf-8");
params.put("sign_type", "RSA2");
params.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
// 这里对应@Open.version属性
params.put("version", "1.0");
// 业务参数
Map<String, String> bizContent = new HashMap<>();
params.put("biz_content", JSON.toJSONString(bizContent));
System.out.println("----------- 请求信息 -----------");
System.out.println("请求参数:" + buildParamQuery(params));
System.out.println("商户秘钥:" + privateKey);
String content = AlipaySignature.getSignContent(params);
System.out.println("待签名内容:" + content);
String sign = AlipaySignature.rsa256Sign(content, privateKey, "utf-8");
System.out.println("签名(sign)" + sign);
params.put("sign", sign);
System.out.println("----------- 返回结果 -----------");
String responseData = post(url, params);// 发送请求
System.out.println(responseData);
}
}
```
- 请求成功后,控制台会打印:
```
----------- 请求信息 -----------
请求参数charset=utf-8&biz_content={}&method=story.demo.get&format=json&app_id=alipay_test&sign_type=RSA2&version=1.0&timestamp=2019-03-23 15:41:22
商户秘钥MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=
待签名内容app_id=alipay_test&biz_content={}&charset=utf-8&format=json&method=story.demo.get&sign_type=RSA2&timestamp=2019-03-23 15:41:22&version=1.0
签名(sign)YMbxTPdovi6htcn1K3USTS6/Tbg6MOAMigG6x/kG0kQFCYH8ljvxXzcY86UT056nUG3OXxnj0xkw07eV6E03HMlu7bn3/jrT3PCcV3YguhA92aWz720x2xJWdfXY13OUPS9VOCC9zIVxu6EBD+PoZ7ojYChYvOfCR5I8bR/oOc0ZLjK63PWTBdf0eFS4sybXzRf81uNLMROsMhmBDDy0Fhml3ml77qzWBIpsmq5ECZ+89rMPbkNhAUcnFAe7ik7xZIL6WcUhAOhKVa8ZQK1GMjoGnAbGRed1FbuOHZGubgffg4/vMqrY10Bcy6h9jt/zK5w9L3HVgK3aPgQlfP16Gg==
----------- 返回结果 -----------
{"story_demo_get_response":{"msg":"Success","code":"10000","name":"白雪公主","id":1},"sign":"YMbxTPdovi6htcn1K3USTS6/Tbg6MOAMigG6x/kG0kQFCYH8ljvxXzcY86UT056nUG3OXxnj0xkw07eV6E03HMlu7bn3/jrT3PCcV3YguhA92aWz720x2xJWdfXY13OUPS9VOCC9zIVxu6EBD+PoZ7ojYChYvOfCR5I8bR/oOc0ZLjK63PWTBdf0eFS4sybXzRf81uNLMROsMhmBDDy0Fhml3ml77qzWBIpsmq5ECZ+89rMPbkNhAUcnFAe7ik7xZIL6WcUhAOhKVa8ZQK1GMjoGnAbGRed1FbuOHZGubgffg4/vMqrY10Bcy6h9jt/zK5w9L3HVgK3aPgQlfP16Gg=="}
```

View File

@@ -1,20 +0,0 @@
# 开发流程
如果您打算使用SOP做开放平台开发流程大致如下
- cd到/SOP/sop-common目录执行命令`mvn clean deploy`把jar上传到maven私服如果没有maven私服可以打包到本地`mvn clean install`
- 打包`sop-gateway`(网关)、`sop-admin`(后台管理)、`sop-website`(文档),部署到服务器上
以上服务是固定的,启动一次即可,后续不用做改动。
- 你的项目接入到SOP参考[项目接入到SOP](10011_项目接入到SOP.md)在微服务端开发接口编写swagger注解文档
- 接口开发完成,启动微服务,注册到注册中心。
- 【可选】编写sdk`SDK管理`页发布SDK提供下载地址
## ISV对接流程
假设接口开发完毕开始对接ISV大致步骤如下
1. ISV访问门户网站注册账号并登陆
2. 上传应用公钥
3. 如果有SDK下载SDK进行接口调用

View File

@@ -1,11 +0,0 @@
# 用户ISV注册
新增ISV有两种方式
- 方式1
启动sop-admin在admin后台`ISV管理添加`
- 方式2
启动`sop-website-server`,用户访问自主注册

View File

@@ -1,88 +0,0 @@
# 业务参数校验
业务参数校验采用JSR-303方式关于JSR-303介绍可以参考这篇博文[JSR 303 - Bean Validation 介绍及最佳实践](https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/)
在参数中使用注解即可框架会自动进行验证。如下面一个添加商品接口它的参数是GoodsParam
```java
@Open("goods.add")
@RequestMapping("/goods/add")
public void addGoods(GoodsParam param) {
...
}
```
在GoodsParam中添加JSR-303注解:
```java
@Data
public class GoodsParam {
@NotEmpty(message = "商品名称不能为空")
private String goods_name;
}
```
如果不传商品名称则返回
```
{"goods_add_response":{"msg":"Success","code":"10000","sub_msg":"商品名称不能为空","sub_code":"isv.invalid-parameter"},"sign":"Eh3Z5CxDCHsb4MyYFVxsPSmBpwVi1LISJdOkrzglxXoqG7RVyEOt4ef1kNpznUvMI3FDQU1suR7Rsmx6NjGdEVS6NSH2Kt0d8TFBRpLhWz8hApnxOtgzqMqbYeMuJie7X5gF6m8hTnvuuxF21IrkixMe+lyBcXw7dk0C3w1SwdEZkHQ+xC+M4bLqAZt5/3kl79/FWSMFJWHiZmg5YeEi8e8XhYCNcz+xlJRJL0x2Y87fFxqSY0UYWNxbQHgdVI8xRfn1n31nzkcLxiAtTh4LPtNRrG7w7absK/C1Oi/vczuBlFeq2EWUsYVWOVpKiJifUwvYVUUsztSLElzplzOjbg=="}
```
- 校验顺序
如果存在多个注解可以指定groups来控制校验顺序如下代码所示
```java
@NotBlank(message = "NotBlank", groups = Group1.class)
// 优先校验Group2
// 可交换下面Group2,Group3看下校验顺序
@Length(min = 2, max = 20, message = "length must 10~20", groups = Group2.class)
@Pattern(regexp = "[a-zA-Z]*", message = "name must letters", groups = Group3.class)
private String name;
```
优先校验`@Length`,通过后再校验`@Pattern`
## 参数校验国际化
国际化的配置方式如下:
```java
@NotEmpty(message = "{goods.remark.notNull}")
private String goods_remark;
```
国际化资源文件`bizerror_en.properties`中添加:
```
goods.remark.notNull=The goods_remark can not be null
```
bizerror_zh_CN.properties中添加
```
# 商品备注不能为空
goods.remark.notNull=\u5546\u54c1\u5907\u6ce8\u4e0d\u80fd\u4e3a\u7a7a
```
## 参数校验国际化传参
下面校验商品评论的长度要求大于等于3且小于等于20。数字3和20要填充到国际化资源中去。
```
// 传参的格式:{xxx}=value1,value2...
@Length(min = 3, max = 20, message = "{goods.comment.length}=3,20")
private String goods_comment;
```
bizerror_en.properties:
```
goods.comment.length=The goods_comment length must >= {0} and <= {1}
```
bizerror_zh_CN.properties中添加
```
# 商品评论长度必须在{0}和{1}之间
goods.comment.length=\u5546\u54c1\u8bc4\u8bba\u957f\u5ea6\u5fc5\u987b\u5728{0}\u548c{1}\u4e4b\u95f4
```
这样value1value2会分别填充到{0},{1}中

View File

@@ -1,91 +0,0 @@
# 错误处理
SOP对错误处理已经封装好了简单做法是`throw ServiceException`在最顶层的Controller会做统一处理。例如
```java
if(StringUtils.isEmpty(param.getGoods_name())) {
throw new ServiceException("goods_name不能为null");
}
```
为了保证编码风格的一致性推荐统一使用ServiceException
## i18n国际化
SOP支持国际化消息。通过Request对象中的getLocale()来决定具体返回那种语言客户端通过设置Accept-Language头部来决定返回哪种语言中文是zh英文是en。
SOP通过模块化来管理国际化消息这样做的好处结构清晰维护方便。下面就来讲解如何设置国际化消息。
以story服务为例假设我们要对商品模块进行设置步骤如下
-`resource/i18n/isp`目录下新建goods_error_zh_CN.properties属性文件
属性文件的文件名有规律, **i18n/isp/goods_error** 表示模块路径, **_zh_CN.properties** 表示中文错误消息。如果要使用英文错误,则新建一个`goods_error_en.properties`即可。
- 在goods_error_zh_CN.properties中配置错误信息
```
# 商品名字不能为空
isp.goods_error_100=\u5546\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
```
isp.goods_error_为固定前缀100为错误码这两个值后续会用到。
接下来是把属性文件加载到国际化容器当中。
- 添加国际化配置在OpenServiceConfig中的static块中添加代码如下
```java
@Configuration
public class OpenServiceConfig extends AlipayServiceConfiguration {
static {
ServiceConfig.getInstance().getI18nModules().add("i18n/isp/goods_error");
}
}
```
- 新建一个枚举用来定义错误
```java
// 按模块来定义异常消息,团队开发可以分开进行
public enum GoodsErrorEnum {
/** 参数错误 */
NO_GOODS_NAME("100"),
;
private ServiceErrorMeta errorMeta;
StoryErrorEnum(String subCode) {
this.errorMeta = new ServiceErrorMeta("isp.goods_error_", subCode);
}
public ServiceErrorMeta getErrorMeta() {
return errorMeta;
}
}
```
接下来就可以使用了
```java
if (StringUtils.isEmpty(param.getGoods_name())) {
throw GoodsErrorEnum.NO_GOODS_NAME.getErrorMeta().getException();
}
```
### 国际化消息传参
即代码中变量传入到properties文件中去做法是采用{0},{1}占位符。0代表第一个参数1表示第二个参数。
```
# 商品名称太短,不能小于{0}个字
isp.goods_error_101=\u5546\u54C1\u540D\u79F0\u592A\u77ED\uFF0C\u4E0D\u80FD\u5C0F\u4E8E{0}\u4E2A\u5B57
```
```java
if (param.getGoods_name().length() <= 3) {
throw GoodsErrorEnum.LESS_GOODS_NAME_LEN.getErrorMeta().getException(3);
}
```
直接放进getException(Object... params)方法参数中,因为是可变参数,可随意放。

View File

@@ -1,126 +0,0 @@
# 编写文档
作为开放平台必须要提供API文档。
SOP采用微服务架构实现因此文档应该由各个微服务各自实现。难点就是如何统一归纳各个微服务端提供的文档信息并且统一展示。
写完接口后使用swagger注解来定义自己的文档信息。步骤如下
- maven添加swagger
```xml
<!-- swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.5</version>
</dependency>
```
- 在config中添加swagger配置
```java
@Configuration
public class OpenServiceConfig extends AlipayServiceConfiguration {
/**
* 开启文档本地微服务文档地址http://localhost:2222/doc.html
* http://ip:port/v2/api-docs
*/
@Configuration
@EnableSwagger2
public static class Swagger2 extends SwaggerSupport {
@Override
protected String getDocTitle() {
return "故事API";
}
}
}
```
其中`getDocTitle()`返回文档模块名,不能和其它微服务重复。比如订单服务返回:`订单API`;库存服务返回:`库存API`
- 编写swagger注解
分别在请求参数和返回结果类中编写`@ApiModelProperty`
```java
// 请求参数
@Data
public class StoryParam {
@ApiModelProperty(value = "故事ID", example = "111")
private int id;
@ApiModelProperty(value = "故事名称", required = true, example = "白雪公主")
private String name;
}
// 返回结果
@Data
public class StoryResult {
@ApiModelProperty(value = "故事ID", example = "1")
private Long id;
@ApiModelProperty(value = "故事名称", example = "海底小纵队")
private String name;
@ApiModelProperty(value = "创建时间", example = "2019-04-14 19:02:12")
private Date gmt_create;
}
```
- 在接口方法上编写`@ApiOperation`注解
```java
/**
* 参数绑定
*
* @param story 对应biz_content中的内容并自动JSR-303校验
* @return
*/
@ApiOperation(value = "获取故事信息", notes = "说明接口的详细信息,介绍,用途,注意事项等。")
@Open(value = "alipay.story.find", bizCode = {
// 定义业务错误码,用于文档显示
@BizCode(code = "100001", msg = "姓名错误", solution = "填写正确的姓名"),
@BizCode(code = "100002", msg = "备注错误", solution = "填写正确备注"),
})
public StoryResult getStory2(StoryParam story) {
log.info("获取故事信息参数, story: {}", story);
// 获取其它参数
OpenContext openContext = ServiceContext.getCurrentContext().getOpenContext();
String app_id = openContext.getAppId();
StoryResult result = new StoryResult();
result.setName("白雪公主, app_id:" + app_id);
result.setGmt_create(new Date());
return result;
}
```
其中`value`属性填接口名称,简明扼要。`notes`填写接口的详细信息,介绍,用途,注意事项等。
## 查看文档
- 确保注册中心、网关、微服务正常启动
- 运行WebsiteServerApplication.java
- 访问http://localhost:8083
效果图如下
![预览](images/10041_1.png "10041_1.png")
## 注解对应关系
swagger注解和文档界面显示关系如下图所示
![预览](images/10041_2.png "10041_2.png")
![预览](images/10041_3.png "10041_3.png")
![预览](images/10041_4.png "10041_4.png")

View File

@@ -1,61 +0,0 @@
# 接口交互详解
开放平台所提供的接口有几十个到几百个不等,同样支持的服务也是多个的。就拿[支付宝开放平台](https://docs.open.alipay.com/api)来说
它所提供的服务有支付服务、会员服务、店铺服务、芝麻信用服务等。相信这些服务接口肯定不是写在同一个项目中但是它的接口地址只有一个https://openapi.alipay.com/gateway.do
从地址信息中可以看到,这是一个网关服务。也就是说,网关是所有请求的入口,然后通过请求分发的方式,把请求路由到具体某个服务中去。
虽然支付宝开放平台的实现方式我们不得而知,但是这种思路是可行的。
SOP也是采用这种方式实现大致步骤如下
- 每个服务注册到nacos
- 网关启动时同样注册到nacos然后从各服务中拉取路由信息
- 网关收到客户端请求后,先进行签名校验,通过之后根据接口信息找到对应的服务,然后进行路由
- 网关对返回结果进行处理(或不处理),返回给客户端。
如何通过接口参数找到对应的服务呢?
在网关定义一个`Map<String, RouteInfo> routeMap = ...`key为接口名+版本号。
网关启动时从各微服务中获取路由信息并保存到routeMap中
```java
routeMap = requestFormServices();
```
接口请求进来后,根据`方法名+版本号`获取路由信息,然后进行路由转发。
```java
String method = request.getParameter("method");
String version = request.getParameter("version");
RouteInfo routeInfo = routeMap.get(method + version);
doRoute(routeInfo);
```
因为nacos需要拉取各个微服务的路由信息接口名有可能会冲突因此需要确保接口名唯一`method`全局唯一。
我们推荐接口名的命名规则应该是:`服务模块.业务模块.功能模块.行为`,如:
mini.user.userinfo.get 小程序服务.用户模块.用户信息.获取
member.register.total.get 会员服务.注册模块.注册总数.获取
如果觉得命名规则有点长可以精简为:`服务模块.功能模块.行为`,如`member.usercount.get`,前提是确保前缀要有所区分,不和其它服务冲突。
得益于Spring Cloud的注册中心和和网关功能我们能很方便的进行接口路由并且还能实现LoadBalance不需要自己再去实现。
整个SOP的架构如下图所示
![架构图](https://images.gitee.com/uploads/images/2019/1227/145216_c9b45109_332975.png "sop2.png")
- 完整请求路线
```
客户端生成签名串 → 客户端发送请求 →【网关签名校验 → 权限校验 → 限流处理 → 路由转发】→ {微服务端业务参数校验 → 处理业务逻辑 → 微服务端返回结果}
客户端业务处理 ← 客户端验证服务端签名 ← 客户端收到结果 ← -------------【网关返回最终结果 ← 生成服务端签名 ← 网关处理结果】← 结果返回到网关
【】:表示网关处理
{}:表示微服务端处理
```

View File

@@ -1,41 +0,0 @@
# 使用签名校验工具
## 生成公私钥
SOP默认签名算法仿照的是支付宝开放平台因此我们可以使用支付宝开放平台提供的密钥生成工具[下载地址](https://docs.open.alipay.com/291/105971/)
工具下载完后,运行工具
- 秘钥格式选择PKCS8(JAVA适用)
- 秘钥长度2048
然后点击`生成秘钥`,下面文本框会生成,公私钥,如下图所示:
![示例图](https://gw.alipayobjects.com/zos/skylark/6dbc42cc-6b9b-4691-83f1-e7b875e1a602/2018/png/e6b725d0-8257-4a71-b7a0-f5479c9d43d0.png)
sop-admin创建一个新ISV将公私钥放入对应文本框中保存。
接着私钥放入客户端进行调用。参见AlipayClientPostTest类
## 签名校验
验证工具切换到`签名`tab页
例如执行com.gitee.sop.AlipayClientPostTest.testPost()方法,控制台会打印如下信息:
```
----------- 请求信息 -----------
请求参数charset=utf-8&biz_content={"name":"葫芦娃","id":"1"}&method=alipay.story.get&format=json&app_id=2019032617262200001&sign_type=RSA2&version=1.0&timestamp=2019-03-26 17:37:41
商户秘钥MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=
待签名内容app_id=2019032617262200001&biz_content={"name":"葫芦娃","id":"1"}&charset=utf-8&format=json&method=alipay.story.get&sign_type=RSA2&timestamp=2019-03-26 17:37:41&version=1.0
签名(sign)JCZMSFkXSjw/4TokyM9/9shyrMl7KxQGIZDHIm7+Bvl49Z816/iF/xXLYjUiPXWAXYfp+HlEs3VVQp1Kjh4tIKuKX/i1+exNVs+ICcqVGBewPSZwiWHGpZTfEUiYOoPyUL/eoRIj7Mvlaow0sI9uP7NXNo0kxEFjUOMCzZA7eKm/pu2FHRXt4OhgXq2Go30K5a9oCbbMc/2xcQCc2+zwvOgV3o0A6eMyeAXDJW+eQ2KLhtlqPQvbRV+xyfSut7TkwYSEuNXVVQAfN2lwAS3ru9CQIs8Uz7lK1ITkLu80yLapZVL7tS1PdxK0e3QYToCWD43Wtuoow4ZdDwwzir90HQ==
----------- 返回结果 -----------
{"alipay_story_get_response":{"msg":"Success","code":"10000","name":"海底小纵队(alipay.story.get)","id":1},"sign":"JCZMSFkXSjw/4TokyM9/9shyrMl7KxQGIZDHIm7+Bvl49Z816/iF/xXLYjUiPXWAXYfp+HlEs3VVQp1Kjh4tIKuKX/i1+exNVs+ICcqVGBewPSZwiWHGpZTfEUiYOoPyUL/eoRIj7Mvlaow0sI9uP7NXNo0kxEFjUOMCzZA7eKm/pu2FHRXt4OhgXq2Go30K5a9oCbbMc/2xcQCc2+zwvOgV3o0A6eMyeAXDJW+eQ2KLhtlqPQvbRV+xyfSut7TkwYSEuNXVVQAfN2lwAS3ru9CQIs8Uz7lK1ITkLu80yLapZVL7tS1PdxK0e3QYToCWD43Wtuoow4ZdDwwzir90HQ=="}
```
字符集选UTF-8签名方式RSA2
把控制台中的`请求参数``商户秘钥`填入文本框中,然后点击`开始签名`下方会出现待签名内容和sign。
通过比对判断签名过程是否正确。

View File

@@ -1,24 +0,0 @@
# ISV管理
ISV独立软体开发商independent software vendor即接入方或者说接口调用者在SOP中称为ISV。
---
在1.1.0版本中新增了ISV管理功能在sop-admin中ISV管理模块下。功能如下
- 基本信息的增查改
- 设置对应角色
界面如下图所示:
![admin预览](images/10085_1.png "10085_1.png")
## 秘钥管理
点击操作列的`秘钥管理`可对ISV的秘钥进行设置。
- 如果采用淘宝开放平台签名方式,签名方式选择`MD5`,如果采用支付宝开放平台签名方式,选择`RSA`
- 如果对接的开发者使用非Java语言秘钥格式选择`PKCS1`
- 带 ★ 的分配给开发者
![admin预览](images/10085_2.png "10085_2.png")

View File

@@ -1,110 +0,0 @@
# 自定义返回结果
网关默认对业务结果进行合并,然后返回统一的格式。
针对`alipay.story.find`接口,微服务端返回结果如下:
```json
{
"name": "白雪公主",
"id": 1,
"gmtCreate": 1554193987378
}
```
网关合并后,最终结果如下
```json
{
"alipay_story_find_response": {
"msg": "Success",
"code": "10000",
"name": "白雪公主",
"id": 1,
"gmtCreate": 1554193987378
},
"sign": "xxxxx"
}
```
其中`alipay_story_find_response`是它的数据节点。规则是:
> 将接口名中的点`.`转换成下划线`_`,后面加上`_response`
代码实现如下:
```java
String method = "alipay.story.find";
return method.replace('.', '_') + "_response";
```
详见`DefaultDataNameBuilder.java`
如果要更改数据节点,比如`result`,可使用`CustomDataNameBuilder.java`
```java
@Configuration
public class MyConfig {
static {
...
ApiConfig.getInstance().setDataNameBuilder(new CustomDataNameBuilder());
...
}
}
```
设置后,网关统一的返回结果如下:
```json
{
"result": {
...
},
"sign": "xxxxx"
}
```
此外,构造方法可指定自定义字段名称:`new CustomDataNameBuilder("data");`
设置后,数据节点将变成`data`
```json
{
"data": {
...
},
"sign": "xxxxx"
}
```
**注**网关设置了CustomDataNameBuilder后SDK也要做相应的更改`OpenConfig.dataNameBuilder = new CustomDataNameBuilder();`
## 自定义结果处理
如果想要对微服务结果做更深一步处理,步骤如下:
1. 新增一个类,继承`GatewayResultExecutor.java`,并重写`String mergeResult(T request, String serviceResult)`方法
2. 配置自定义类
```java
@Configuration
public class MyConfig {
static {
...
ApiConfig.getInstance().setGatewayResultExecutor(new MyGatewayResultExecutor());
...
}
}
```
## 不合并结果
如果不希望对结果进行合并可在application.properties中设置`sop.api-config.merge-result=false`
这样,网关最终返回结果即为微服务端的返回结果。

View File

@@ -1,40 +0,0 @@
# 自定义过滤器
演示在网关追加一个header
```java
public class CustomFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 演示在网关追加header
ApiParam apiParam = ServerWebExchangeUtil.getApiParam(exchange);
String token = apiParam.fetchAccessToken();
ServerWebExchange serverWebExchange = ServerWebExchangeUtil.addHeaders(exchange, httpHeaders -> {
httpHeaders.add("token", token);
});
return chain.filter(serverWebExchange);
}
@Override
public int getOrder() {
// 自定义过滤器可以从0开始
return 0;
}
}
```
使用过滤器在sop-gateway中找到MyConfig添加
```java
@Configuration
public class MyConfig {
...
@Bean
CustomFilter customFilter() {
return new CustomFilter();
}
...
}
```

View File

@@ -1,52 +0,0 @@
# 自定义校验token
`@Open`注解中有一个属性`needToken`用来告诉网关是否校验token
```java
/**
* 是否需要appAuthToken设置为true网关端会校验token是否存在
*/
boolean needToken() default false;
```
使用方式:
```java
@ApiOperation(value="传递token", notes = "传递token")
@Open(value = "story.get.token", needToken = true/* 设置true网关会校验token是否存在 */)
@RequestMapping("token")
public StoryResult token(StoryParam story, HttpServletRequest request) {
OpenContext openContext = ServiceContext.getCurrentContext().getOpenContext();
StoryResult result = new StoryResult();
result.setName("appAuthToken:" + openContext.getAppAuthToken());
return result;
}
```
指定了needToken=true后网关会判断客户端是否传了`app_auth_token`参数,没有传则返回错误信息。
网关默认简单校验参数值是否存在,如果要校验有效性,需要自己实现。
自己实现步骤:
- 设置`ApiConfig中的tokenValidator属性`
`TokenValidator`是一个函数式接口可以直接使用Lambda表达式示例代码如下
```java
@Component
public class MyConfig {
@PostConstruct
public void after() {
ApiConfig.getInstance().setTokenValidator(apiParam -> {
// 获取客户端传递过来的token
String token = apiParam.fetchAccessToken();
return !StringUtils.isBlank(token);
// TODO: 校验token有效性可以从redis中读取
// 返回true表示这个token真实、有效
});
}
}
```

View File

@@ -1,57 +0,0 @@
# 网关拦截器
从3.1.0开始新增了网关拦截器,使用该拦截器可做一些数据统计,日志记录等工作。
使用方法如下:
- 在sop-gateway工程下新增一个类实现`RouteInterceptor`接口,实现接口中的方法。别忘了加`@Component`
```java
@Component
public class MyRouteInterceptor implements RouteInterceptor {
@Override
public void preRoute(RouteInterceptorContext context) {
ApiParam apiParam = context.getApiParam();
System.out.println("请求接口:" + apiParam.fetchNameVersion());
}
@Override
public void afterRoute(RouteInterceptorContext context) {
System.out.println("请求成功,微服务返回结果:" + context.getServiceResult());
}
@Override
public int getOrder() {
return 0;
}
}
```
RouteInterceptor接口方法说明
- `public void preRoute(RouteInterceptorContext context)`
路由转发前执行,在签名验证通过之后会立即执行这个方法。
- `public void afterRoute(RouteInterceptorContext context)`
路由转发完成后,即拿到微服务返回结果后执行这个方法
- `public int getOrder()`
指定拦截执行顺序数字小的优先执行建议从0开始。
- `default boolean match(RouteInterceptorContext context)`
是否匹配返回true执行拦截器默认true
RouteInterceptorContext参数存放了各类参数信息。
参考类:
- `com.gitee.sop.gatewaycommon.interceptor.RouteInterceptor` 拦截器接口
- `com.gitee.sop.gatewaycommon.interceptor.RouteInterceptorContext` 拦截器上下文
- `com.gitee.sop.gatewaycommon.interceptor.MonitorRouteInterceptor` 默认实现的拦截器,用于收集监控数据

View File

@@ -1,25 +0,0 @@
# 路由授权
1.1.0版本新增了路由授权功能采用RBAC权限管理方式实现。
- 每个ISVappKey对应一个或多个角色
- 每个角色分配多个路由权限
接口跟角色相关联ISV拥有哪些角色就具有角色对应的接口访问权限。
假设把路由a,b,c分配给了`VIP角色`那么具有VIP角色的ISV可以访问a,b,c三个路由。
默认情况下接口访问时公开的ISV都能访问。如果要设置某个接口访问权限`@Open`注解中指定permission=true。
如:`@Open(value = "permission.story.get", permission = true)`。这样该接口是需要经过授权给ISV才能访问的。
重启服务后登录admin服务管理-路由列表界面中,`访问权限`列会出现一个点击授权,点击出现授权窗口,勾选对应的角色即可完成授权。
- `点击授权`,进行角色授权
![admin预览](images/10090_1.png "10090_1.png")
- 勾选对应角色,点击保存
![admin预览](images/10090_2.png "10090_2.png")
这里演示的是具有普通权限的ISV能够访问`permission.story.get`接口,运行`PermissionDemoPostTest`测试用例进行验证

View File

@@ -1,80 +0,0 @@
# 接口限流
SOP提供了简单的接口限流策略
- 窗口策略:每秒处理固定数量的请求,超出请求返回错误信息。
- 令牌桶策略:每秒放置固定数量的令牌数,每个请求进来后先去拿令牌,拿到了令牌才能继续,拿不到则等候令牌重新生成了再拿。
如果一个接口设置了窗口策略假设接口每秒可处理5个请求一秒内同时有6个请求进来前5个接口是能够访问的第六个请求将返回错误信息。
如果设置了令牌桶策略桶的容量是5那么每秒中生成5个令牌同一时间有6个请求进来那么前5个能成功拿到令牌继续第六个则等待令牌重新生成了再拿。
默认情况下接口的限流功能是关闭的可在sop admin中配置并开启。功能在`路由管理-->限流管理`下。
## 多维度限流
- 可针对接口进行限流,所有访问该接口的请求都被限流
- 可针对appKey进行限流某个appKey请求过来后对他限流
- 可针对IP进行限流某个IP请求过来后对他限流
此外还可以进行组合
- 可针对接口+appKey进行限流这个appKey调用某个接口比较频繁可以将它限制住
- 可针对接口+IP进行限流某个ip在频繁调用接口可以将它限制住
由于存在组合情况,一个接口可能会配置多个限流规则。在这种情况下会优先取排序值小的那一条,如果排序值一样,则默认取第一条。
假设有下面几个限流规则:
- 接口:`goods.get` 排序值1 每秒可处理请求数10
- 接口:`goods.get` appKeyxxxx 排序值0 每秒可处理请求数5
- 接口:`goods.get` ip172.1.2.2 排序值2 每秒可处理请求数6
客户端调用接口:`http://open.domain.com?method=goods.get&app_id=xxxx`客户端IP为`172.1.2.2`
这种情况下上面三条限流规则都命中了,由于排序值小优先执行,因此第二条规则命中.
具体设置方式可在sop admin中配置功能在`服务管理-->限流管理`下。执行`com.gitee.sop.test.LimitTest`测试用例验证限流情况
![限流配置](images/10092_1.png "10092_1.png")
![限流配置](images/10092_2.png "10092_2.png")
## 分布式限流
默认的限流方式是单机的,如果要部署多台网关实例,需要使用分布式限流
SOP使用redis进行分布式限流只支持窗口策略操作步骤如下
- sop-gateway/pom.xml添加redis依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
```
- sop-gateway下的application-dev.properties文件添加redis配置
```properties
# redis
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
```
-`com.gitee.sop.gateway.config.MyConfig`中添加如下代码:
```java
@Autowired
private RedisTemplate redisTemplate;
@PostConstruct
public void after() {
ApiConfig.getInstance().setLimitManager(new RedisLimitManager(redisTemplate));
...
}
```

View File

@@ -1,27 +0,0 @@
# 路由监控
路由监控功能可以查看各个接口的调用情况,监控信息收集采用拦截器实现,前往【服务管理】-【路由监控】查看
- 后台预览
![监控日志](images/10093_1.png "10093_1.png")
![监控日志](images/10093_2.png "10093_2.png")
- 注意事项
处理完错误后,请及时`标记解决`一个接口默认保存50条错误信息采用LRU机制淘汰老的。标记解决后则会空出一个位置存放新的错误信息。
重复的错误只会存放一条记录,然后累加错误次数,重复错误定义如下:
`instanceId + routeId + errorMsg`,即一个实例 + 路由id + 错误信息确定一个错误
可在网关设置`sop.monitor.error-count-capacity=50`参数调整错误容量
考虑到数据库压力网关收到错误信息后并不会立即保存到数据库而是先保存到内容中然后定时保存到时间默认时间隔为30秒
可通过`sop.monitor.flush-period-seconds=30`调整间隔时间。
相关类:
- com.gitee.sop.gateway.interceptor.MonitorRouteInterceptor

View File

@@ -1,343 +0,0 @@
# 开发SDK
开放平台把接口开发完毕后一般需要开发对应的SDK提供给ISV。SOP提供了一个基础的SDK开发包
开发者可以在此基础上做开发就拿sdk-java来说具体步骤如下
## sdk-java
SDK依赖了三个jar包
- okhttp.jar 用于网络请求
- fastjson.jar 用于json处理
- commons-logging.jar 日志处理
### 接口封装步骤
比如获取故事信息接口
- 接口名alipay.story.find
- 版本号1.0
- 参数name 故事名称
- 返回信息
```json
{
"alipay_story_find_response": {
"msg": "Success",
"code": "10000",
"name": "白雪公主",
"id": 1,
"gmtCreate": 1554193987378
},
"sign": "xxxxx"
}
```
针对这个接口,封装步骤如下:
1.在`model`包下新建一个类,定义业务参数
```java
@Data
public class GetStoryModel {
@JSONField(name = "name")
private String name;
}
```
2.在`response`包下新建一个返回类GetStoryResponse继承`BaseResponse`
里面填写返回的字段
```
@Data
public class GetStoryResponse extends BaseResponse {
private Long id;
private String name;
private Date gmtCreate;
}
```
3.在`request`包下新建一个请求类,继承`BaseRequest`
BaseRequest中有个泛型参数`GetStoryResponse`类,表示这个请求对应的返回类。
重写`method()`方法,填接口名。
如果要指定版本号,可重写`version()`方法,或者后续使用`request.setVersion(version)`进行设置
```java
public class GetStoryRequest extends BaseRequest<GetStoryResponse> {
@Override
protected String method() {
return "alipay.story.find";
}
}
```
可重写`getRequestMethod()`方法指定HTTP请求method默认是POST。
```java
@Override
protected RequestMethod getRequestMethod() {
return RequestMethod.GET;
}
```
**建议读请求用GET写请求用POST**
### 使用方式
```java
String url = "http://localhost:8081/api";
String appId = "2019032617262200001";
String privateKey = "你的私钥";
// 声明一个就行
OpenClient client = new OpenClient(url, appId, privateKey);
// 标准用法
@Test
public void testGet() {
// 创建请求对象
GetStoryRequest request = new GetStoryRequest();
// 请求参数
GetStoryModel model = new GetStoryModel();
model.setName("白雪公主");
request.setBizModel(model);
// 发送请求
GetStoryResponse response = client.execute(request);
if (response.isSuccess()) {
// 返回结果
System.out.println(String.format("成功response:%s\n响应原始内容:%s",
JSON.toJSONString(response), response.getBody()));
} else {
System.out.println("错误subCode:" + response.getSubCode() + ", subMsg:" + response.getSubMsg());
}
}
```
### 使用方式2(懒人版)
如果不想添加Request,Response,Model。可以用这种方式返回body部分是字符串后续自己处理
body对应的是alipay_story_find_response部分
```java
@Test
public void testLazy() {
// 创建请求对象
CommonRequest request = new CommonRequest("alipay.story.find");
// 请求参数
Map<String, Object> bizModel = new HashMap<>();
bizModel.put("name", "白雪公主");
request.setBizModel(bizModel);
// 发送请求
CommonResponse response = client.execute(request);
if (response.isSuccess()) {
// 返回结果body对应的是alipay_story_find_response部分
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
System.out.println(jsonObject);
} else {
System.out.println(response);
}
}
```
## sdk-.net
### 接口封装步骤
比如获取故事信息接口
- 接口名alipay.story.find
- 版本号1.0
- 参数name 故事名称
- 返回信息
```
{
"alipay_story_find_response": {
"msg": "Success",
"code": "10000",
"name": "白雪公主",
"id": 1,
"gmtCreate": 1554193987378
},
"sign": "xxxxx"
}
```
针对这个接口,封装步骤如下:
1.在`Model`包下新建一个类,定义业务参数
```
namespace SDKCSharp.Model
{
public class GetStoryModel
{
/// <summary>
/// 故事名称
/// </summary>
/// <value>The name.</value>
[JsonProperty("name")]
public string Name { get; set; }
}
}
```
`[JsonProperty("name")]`是Newtonsoft.Json组件中的类用于Json序列化括号中的是参数名称。
类似于Java中的注解`@JSONField(name = "xx")`
2.在`Response`包下新建一个返回类GetStoryResponse继承`BaseResponse`
里面填写返回的字段
```
namespace SDKCSharp.Response
{
public class GetStoryResponse: BaseResponse
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("gmt_create")]
public string GmtCreate { get; set; }
}
}
```
3.在`Request`文件夹下新建一个请求类,继承`BaseRequest`
BaseRequest中有个泛型参数`GetStoryResponse`类,表示这个请求对应的返回类。
重写`GetMethod()`方法,填接口名。
如果要指定版本号,可重写`GetVersion()`方法,或者后续使用`request.Version = version`进行设置
```
namespace SDKCSharp.Request
{
public class GetStoryRequest : BaseRequest<GetStoryResponse>
{
public override string GetMethod()
{
return "alipay.story.find";
}
}
}
```
### 使用方式
```
class MainClass
{
static string url = "http://localhost:8081/api";
static string appId = "2019032617262200001";
// 平台提供的私钥
static string privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=";
// 声明一个就行
static OpenClient client = new OpenClient(url, appId, privateKey);
public static void Main(string[] args)
{
TestGet();
}
// 标准用法
private static void TestGet()
{
// 创建请求对象
GetStoryRequest request = new GetStoryRequest();
// 请求参数
GetStoryModel model = new GetStoryModel();
model.Name = "白雪公主";
request.BizModel = model;
// 发送请求
GetStoryResponse response = client.Execute(request);
if (response.IsSuccess())
{
// 返回结果
Console.WriteLine("故事名称:{0}", response.Name);
}
else
{
Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}",
response.Code, response.Msg, response.SubCode, response.SubMsg);
}
}
}
```
### 使用方式2(懒人版)
如果不想添加Request,Response,Model。可以用这种方式返回data部分是Dictionary<string, object>,后续自己处理
```
// 懒人版如果不想添加Request,Response,Model。可以用这种方式返回Dictionary<string, object>,后续自己处理
private static void TestCommon()
{
// 创建请求对象
CommonRequest request = new CommonRequest("alipay.story.find");
// 请求参数
Dictionary<string, string> bizModel = new Dictionary<string, string>
{
["name"] = "白雪公主"
};
request.BizModel = bizModel;
// 发送请求
CommonResponse response = client.Execute(request);
if (response.IsSuccess())
{
// 返回结果
string body = response.Body;
Dictionary<string, object> dict = JsonUtil.ParseToDictionary(body);
Console.WriteLine("Dictionary内容:");
foreach (var item in dict)
{
Console.WriteLine("{0}:{1}", item.Key, item.Value);
}
}
else
{
Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}",
response.Code, response.Msg, response.SubCode, response.SubMsg);
}
}
```
## 发布SDK
将编写好的SDK打包后上传到云服务器如百度网盘
前往sop-admin点击`SDK管理`菜单,点击`发布SDK`填写SDK语言、版本、下载地址网盘地址、调用示例
保存后ISV端会看到发布的SDK

View File

@@ -1,117 +0,0 @@
# 应用授权
## 概述
- 1、用户对开发者进行应用授权后开发者可以帮助用户完成相应的业务逻辑。
- 2、授权采用标准的OAuth 2.0流程。
## 授权流程
![授权流程](images/10097_1.png "10097_1.png")
## 快速接入
- 第一步应用授权URL拼装
拼接规则:
http://openauth.yourdomain.com/oauth2/appToAppAuth?app_id=2019032617262200001&redirect_uri=http%3a%2f%2flocalhost%3a8087%2foauth2callback
参数说明:
| 参数 | 参数名称 | 类型 | 必填 | 描述 | 范例 |
|--------------|-------------|--------|----|---------------|--------------------------|
| app_id | 开发者应用的AppId | String | 是 | 开发者应用的AppId | 2015101400446982 |
| redirect_uri | 回调页面 | String | 是 | 参数需要UrlEncode | http%3A%2F%2Fexample.com |
- 第二步获取code
授权成功后会跳转至开发者定义的回调页面即redirect_uri参数对应的url在回调页面请求中会带上当次授权的授权码code和开发者的app_id示例如下
http://www.xxx.com/oauth2callback?app_id=2015101400446982&code=ca34ea491e7146cc87d25fca24c4cD11
- 第三步使用code换取app_auth_token
接口名称open.auth.token.app
开发者通过code可以换取app_auth_token、授权用户的userId。
**注意**应用授权的code唯一code使用一次后失效有效期24小时 app_auth_token永久有效。
**请求参数说明**
| 参数 | 参数名称 | 类型 | 必填 | 描述 | 范例 |
|---------------|------|--------|----|---------------------------------------------------------------------------------|------------------------------------------|
| grant_type | 授权类型 | String | 是 | 如果使用code换取token则为authorization_code如果使用refresh_token换取新的token则为refresh_token | authorization_code |
| code | 授权码 | String | 否 | 与refresh_token二选一用户对应用授权后得到即第一步中开发者获取到的code值 | bf67d8d5ed754af297f72cc482287X62 |
| refresh_token | 刷新令牌 | String | 否 | 与code二选一可为空刷新令牌时使用 | 201510BB0c409dd5758b4d939d4008a525463X62 |
**接口请求SDK示例**
```java
@GetMapping("oauth2callback")
@ResponseBody
public String callback(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
String app_id = servletRequest.getParameter("app_id");
String code = servletRequest.getParameter("code");
OpenAuthTokenAppRequest request = new OpenAuthTokenAppRequest();
OpenAuthTokenAppModel model = new OpenAuthTokenAppModel();
model.setCode(code);
model.setGrant_type("authorization_code");
request.setBizModel(model);
// 根据code获取token
OpenAuthTokenAppResponse response = openClient.execute(request);
if (response.isSuccess()) {
// 成功拿到token开发者在这里保存token信息
// 后续使用token进行接口访问
log.info("授权成功body:{}", response.getBody());
}
return response.getBody();
}
```
**同步响应参数说明**
| 参数 | 参数名称 | 类型 | 必填 | 描述 | 范例 |
|-------------------|---------|--------|----|----------------------------------------------------------|----------------------------------|
| app_auth_token | 用户授权令牌 | String | 是 | 通过该令牌来帮助用户发起请求,完成业务 | 856faf8d77d3b985c1073557ce6ea724 |
| user_id | 授权用户的ID | String | 是 | 授权者id | 1 |
| expires_in | 令牌有效期 | Long | 是 | 负值表示永久有效 | -1 |
| re_expires_in | 刷新令牌有效期 | Long | 是 | 负值表示永久有效 | -3 |
| app_refresh_token | 刷新令牌时使用 | String | 是 | 刷新令牌后我们会保证老的app_auth_token从刷新开始10分钟内可继续使用请及时替换为最新token | 88e68196d2359667f0dc8136e6c86803 |
**同步响应结果示例**
```json
{
"open_auth_token_app_response": {
"code": "10000",
"msg": "Success",
"app_auth_token": "88e68196d2359667f0dc8136e6c86803",
"app_refresh_token": "856faf8d77d3b985c1073557ce6ea724",
"expires_in": -1,
"re_expires_in": -3,
"user_id": "1"
},
"sign": "xxx"
}
```
**刷新token**
```java
OpenAuthTokenAppRequest request = new OpenAuthTokenAppRequest();
OpenAuthTokenAppModel model = new OpenAuthTokenAppModel();
model.setGrant_type("refresh_token");
model.setRefresh_token("856faf8d77d3b985c1073557ce6ea724");
request.setBizModel(model);
OpenAuthTokenAppResponse response = openClient.execute(request);
if (response.isSuccess()) {
// 成功拿到token开发者在这里保存token信息
// 后续使用token进行接口访问
log.info("换取token成功body:{}", response.getBody());
}
```

View File

@@ -1,147 +0,0 @@
# 提供restful接口
有些接口没有被开放但是也想要通过网关来访问SOP提供一个固定的请求格式来访问。
请求格式:
`http://ip:port/rest/服务id/your_path`,其中`http://ip:port/rest/`为固定部分,后面跟微服务请求路径。
下面是一个微服务的接口例子
```java
@RestController
@RequestMapping("food")
public class TraditionalWebappController {
@RequestMapping(value = "getFoodById", method = RequestMethod.GET)
public Food getFoodById(Integer id) {
Food food = new Food();
food.setId(id);
food.setName("香蕉");
food.setPrice(new BigDecimal(20.00));
return food;
}
}
```
这是一个`食品服务`例子serviceId为`food-service`假设网关ip为10.0.1.11端口8081食品服务ip为10.0.1.22端口2222
1. 网关访问:`http://10.0.1.11:8081/rest/food-service/food/getFoodById?id=2`
2. 本地访问:`http://10.0.1.22:2222/food/getFoodById/?id=2`
由此可见,对于前端调用者来说,它把网关看做一个大服务,只访问网关提供的请求,不需要关心网关后面的路由转发。网关后面各个微服务独自管理,
微服务之间的调用可以使用dubbo或feign有了版本号的管理可以做到服务的平滑升级对用户来说都是无感知的。结合SOP-Admin提供的上下线功能
可实现预发布环境功能。
默认情况下,`http://ip:port/rest/`为固定部分,如果想要更改`rest`,可在网关配置文件指定:`sop.restful.path=/aaa`
请求前缀将变成:`http://ip:port/aaa/`
- 关闭restful功能
如果不想用该功能,在配置文件指定:
```properties
# 开启restful请求默认开启
sop.restful.enable=false
```
- 封装请求工具【可选】
封装请求方便调用针对vue的封装如下
```js
import axios from 'axios'
// 创建axios实例
const client = axios.create({
baseURL: process.env.BASE_API, // api 的 base_url
timeout: 5000, // 请求超时时间
headers: { 'Content-Type': 'application/json' }
})
const RequestUtil = {
/**
* 请求接口
* @param url 请求路径如http://localhost:8081/rest/food-service/food/getFoodById
* @param data 请求数据json格式
* @param callback 成功回调
* @param errorCallback 失败回调
*/
post: function(url, data, callback, errorCallback) {
client.post(url, data)
.then(function(response) {
const resp = response.result
const code = resp.code
// 成功,网关正常且业务正常
if (code === '10000' && !resp.sub_code) {
callback(resp)
} else {
// 报错
Message({
message: resp.msg,
type: 'error',
duration: 5 * 1000
})
}
})
.catch(function(error) {
console.log('err' + error) // for debug
errorCallback && errorCallback(error)
})
}
}
export default RequestUtil
```
jQuery版本如下
```js
var RequestUtil = {
/**
* 请求接口
* @param url 请求路径如http://localhost:8081/rest/food-service/food/getFoodById
* @param data 请求数据json格式
* @param callback 成功回调
* @param errorCallback 失败回调
*/
post: function(url, data, callback, errorCallback) {
$.ajax({
url: 'http://localhost:8081' // 网关url
, type: 'post'
, headers: { 'Content-Type': 'application/json' }
, data: data
,success:function(response) {
var resp = response.result
var code = resp.code
// 成功,网关正常且业务正常
if (code === '10000' && !resp.sub_code) {
callback(resp)
} else {
// 报错
alert(resp.msg);
}
}
, error: function(error) {
errorCallback && errorCallback(error)
}
});
}
}
```
jQuery调用示例
```js
$(function () {
var data = {
id: 1
,name: '葫芦娃'
}
RequestUtil.post('http://localhost:8081/rest/food-service/food/getFoodById', data, function (result) {
console.log(result)
});
})
```

View File

@@ -1,111 +0,0 @@
# 文件上传
请求接口时带上文件
## 客户端调用
```java
DemoFileUploadRequest request = new DemoFileUploadRequest();
DemoFileUploadModel model = new DemoFileUploadModel();
model.setRemark("上传文件参数");
request.setBizModel(model);
List<UploadFile> files = new ArrayList<>();
String root = System.getProperty("user.dir");
System.out.println(root);
// 这里演示将resources下的两个文件上传到服务器
files.add(new UploadFile("file1", new File(root + "/src/main/resources/file1.txt")));
files.add(new UploadFile("file2", new File(root + "/src/main/resources/file2.txt")));
request.setFiles(files);
DemoFileUploadResponse response = client.execute(request);
System.out.println("--------------------");
if (response.isSuccess()) {
List<DemoFileUploadResponse.FileMeta> responseFiles = response.getFiles();
System.out.println("您上传的文件信息:");
responseFiles.stream().forEach(file->{
System.out.println(file);
});
} else {
System.out.println("errorCode:" + response.getCode() + ",errorMsg:" + response.getMsg());
}
System.out.println("--------------------");
```
客户端使用`UploadFile`添加上传文件
```java
/**
* @param name 表单名称,不能重复
* @param file 文件
* @throws IOException
*/
public UploadFile(String name, File file) throws IOException {
this(name, file.getName(), FileUtil.toBytes(file));
}
```
其中`name`表示字段名称,相当于`<input type="file" name="file1"/>`中的name
源码详见sop-sdk
## 服务端接收文件
- 方式1
```java
/**
* 方式1将文件写在参数中可直接获取。好处是可以校验是否上传
* @param param
* @return
*/
@Open("file.upload")
@RequestMapping("file1")
public FileUploadVO file1(FileUploadParam param) {
System.out.println(param.getRemark());
// 获取上传的文件
MultipartFile file1 = param.getFile1();
MultipartFile file2 = param.getFile2();
...
return vo;
}
@Data
public class FileUploadParam {
private String remark;
// 上传文件字段名称对应表单中的name属性值
@NotNull(message = "文件1不能为空")
private MultipartFile file1;
@NotNull(message = "文件2不能为空")
private MultipartFile file2;
}
```
方式1的好处是可以使用`JSR-303`校验文件是否上传
- 方式2
```java
/**
* 方式2从request中获取上传文件
*
* @param param
* @return
*/
@Open("file.upload2")
@RequestMapping("file2")
public FileUploadVO file2(FileUploadParam2 param, HttpServletRequest request) {
System.out.println(param.getRemark());
FileUploadVO vo = new FileUploadVO();
// 获取上传的文件
Collection<MultipartFile> uploadFiles = UploadUtil.getUploadFiles(request);
...
return vo;
}
```
微服务端源码参考`FileUploadDemoController.java`

View File

@@ -1,104 +0,0 @@
# 配置Sleuth链路追踪
配置了Sleuth可以很方便查看微服务的调用路线图可快速定位问题。
SOP基于SpringCloud因此只要整合[Spring Cloud Sleuth](https://spring.io/projects/spring-cloud-sleuth)即可。
除此之外还需要支持dubbo的链路的跟踪Sleuth在2.0已经对dubbo做了支持详见[brave-instrumentation-dubbo-rpc](https://github.com/openzipkin/brave/tree/master/instrumentation/dubbo-rpc)
接入Spring Cloud Sleuth步骤如下
- 下载zipkin服务器
以mac环境为例执行下面命令下载jar并启动zipkin服务
```
curl -sSL https://zipkin.io/quickstart.sh | bash -s
java -jar zipkin.jar
```
默认端口是9411更多安装方式详见[quickstart](https://zipkin.io/pages/quickstart.html)
- sop-gateway/pom.xml添加依赖
```xml
<!--开启zipkin服务链路跟踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
```
配置文件新增
```properties
# zipkin服务跟踪
spring.zipkin.base-url=http://127.0.0.1:9411/
# 设置sleuth收集信息的比率默认0.1最大是1数字越大越耗性能
spring.sleuth.sampler.probability=1
```
重启sop-gateway
- 打开sop-story-web/pom.xml
添加依赖:
```xml
<!--开启zipkin服务链路跟踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<!-- zipkin支持dubbo -->
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-instrumentation-dubbo-rpc</artifactId>
<version>5.6.6</version>
</dependency>
```
配置文件新增:
```properties
# zipkin服务跟踪
spring.zipkin.base-url=http://127.0.0.1:9411/
# 设置sleuth收集信息的比率默认0.1最大是1数字越大越耗性能
spring.sleuth.sampler.probability=1
# dubbo使用zipkin过滤器
dubbo.provider.filter=tracing
dubbo.consumer.filter=tracing
```
重启服务
- 打开sop-book/sop-book-web/pom.xml
步骤同上
- 运行DubboDemoTest.java单元测试
运行完毕看控制台,找到日志信息
```text
2019-07-18 16:22:04.438 INFO [story-service,59dae98250b276bd,60828035658f175f,true] 90553 --- [:12345-thread-2] c.g.s.s.service.DefaultDemoService : dubbo provider, param: DemoParam(id=222)
```
日志内容多了`[story-service,59dae98250b276bd,60828035658f175f,true]`部分这些是zipkin加进去的说明如下
```text
story-service服务名称
59dae98250b276bdtraceId
60828035658f175fspanId
true是否上传到zipkin服务器
```
查看各个服务的控制台可以发现traceId是一致的。
- 浏览器打开http://127.0.0.1:9411/
将traceId复制黏贴到右上角文本框进行查询可看到服务调用链。
![预览](images/10109_1.png "10109_1.png")

View File

@@ -1,71 +0,0 @@
# 预发布灰度发布
从1.14.0开始支持预发布、灰度发布,可登陆`SOP-Admin`,然后选择`服务列表`进行操作。
## 使用预发布
SOP中预发布的思路如下
假设网关工程sop-gateway在阿里云负载均衡有两台服务器域名分别为
|域名|说明|
|:---- |:---- |
|open1.domain.com |网关服务器1 |
|openpre.domain.com | 网关服务器2作为预发布请求入口|
SLB对外域名为`open.domain.com`,即开放平台入口为:`http://open.domain.com`
访问`open.domain.com`会负载均衡到`open1.domain.com``openpre.domain.com`这两台实例
如果单独从`openpre.domain.com`访问,则会访问到预发布微服务。
SOP开启预发布步骤如下
修改网关工程配置文件,指定预发布域名
```properties
# 预发布网关域名,多个用英文逗号(,)隔开
pre.domain=openpre.domain.com
```
重启网关
微服务启动参数添加:`--spring.cloud.nacos.discovery.metadata.env=pre`eureka下是`--eureka.instance.metadata-map.env=pre`)。
建议线上配两套启动脚本,其中预发布启动脚本添加启动参数`--spring.cloud.nacos.discovery.metadata.env=pre`
登录SOP-Admin在服务列表中点击预发布。
`openpre.domain.com`请求进来的用户都会进预发布服务器从SLB域名进来请求路由到非预发服务器
## 使用灰度发布
- 灰度接口定义
接口名相同,版本号不同。比如接口`user.get`version:1.0
新建一个接口`user.get`version:2.0
那么这个2.0接口就是灰度接口
灰度发布可允许指定的用户访问灰度服务器,其它用户还是走正常流程。
登录SOP-Admin前往`服务列表`
- 先设置灰度参数指定灰度appId和灰度接口
- 服务器实例开启灰度
参考类:
- LoadBalanceServerChooser.java 预发布/灰度发布服务实例选择
### 自定义判断灰度用户
默认根据`appId``IP`来判断灰度用户如果要通过其它维度来判断是否是灰度用户可实现GrayUserBuilder接口
然后在springboot main方法中调用如下方法
```java
ApiConfig.getInstance().addGrayUserBuilder(new XXGrayUserBuilder());
```
参考com.gitee.sop.gatewaycommon.loadbalancer.builder.AppIdGrayUserBuilder.java

View File

@@ -1,33 +0,0 @@
# 动态修改请求参数
自1.14.0开始zuul网关支持动态修改请求参数。即在网关修改客户端传递过来的参数然后发送到微服务端。
```
客户端参数{"name": "jim"} --> zuul中修改为{"name": "Lucy"} --> 微服务端将收到{"name": "Lucy"}
```
使用场景:客户端请求参数经过加密,在网关解密后,再次发送明文参数给微服务端
- 如何使用
在网关springboot启动函数中添加如下代码
```java
public static void main(String[] args) {
ApiConfig.getInstance().setParameterFormatter(requestParams -> {
// 获取biz_content
JSONObject jsonObject = requestParams.getJSONObject(ParamNames.BIZ_CONTENT_NAME);
// 修改biz_content中的值
jsonObject.put("name", "name修改了111");
jsonObject.put("remark", "remark修改了222");
// 重新设置biz_content
requestParams.put(ParamNames.BIZ_CONTENT_NAME, jsonObject);
});
SpringApplication.run(SopGatewayApplication.class, args);
}
```
其中requestParams是客户端传递过来的参数可在此基础上添加修改参数。
更多参考com.gitee.sop.gatewaycommon.zuul.filter.PreParameterFormatterFilter.java

View File

@@ -1,3 +0,0 @@
# 使用eureka
切换到`eureka`分支

View File

@@ -1,14 +0,0 @@
# 超时设置
当微服务处理业务逻辑时间过长网关会报超时错误默认等待时间是5秒。
可在网关指定`spring.cloud.gateway.httpclient.response-timeout`参数设置超时时间,单位毫秒
```properties
# 设置响应超时10秒
spring.cloud.gateway.httpclient.response-timeout=10000
```
更多配置参见:`org.springframework.cloud.gateway.config.HttpClientProperties`
测试用例参见:`com.gitee.sop.test.TimeoutTest`

View File

@@ -1,149 +0,0 @@
# 网关性能测试
> 注意:记得关闭限流功能
**测试环境**
- 测试工具:[wrk](https://github.com/wg/wrk)[安装教程](https://www.cnblogs.com/quanxiaoha/p/10661650.html)
- 服务器CentOS7虚拟机宿主机macbookpro内存2GCPU:1核数2核
- 运行环境Java8、Mysql-5.7、Nacos-1.1.3
- 网关启动参数:
```
-verbose:gc -XX:+PrintGCDetails -XX:+PrintHeapAtGC -Xloggc:gc-zuul.log \
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn512m -Xss256k -XX:SurvivorRatio=8 \
-XX:+UseConcMarkSweepGC
```
- zuul配置仅针对zuulSpring Cloud Gateway没有做优化配置
```properties
# 不校验时间,这样一个链接可以一直进行测试
sop.api-config.timeout-seconds=0
sop.restful.enable=true
logging.level.com.gitee=info
# zuul调优
zuul.host.max-per-route-connections=5000
zuul.host.max-total-connections=5000
zuul.semaphore.max-semaphores=5000
ribbon.ReadTimeout=5000
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=13000
logging.file=sop-gateway.log
```
CentOS允许最大连接数
```
$ ulimit -n
65535
```
## 调用开放接口
- wrk命令
```
wrk -t8 -c200 -d30s "http://10.1.31.227:8081/?charset=utf-8&biz_content=%7B%22name%22%3A%22%E8%91%AB%E8%8A%A6%E5%A8%83%22%2C%22id%22%3A%221%22%7D&method=alipay.story.get&format=json&sign=RjK%2FThnzAJQOw%2BfoVLS18PgWZAR%2B25SI2XdONFhS%2BmS8vsv2jNT3rygFoh%2ByX1AJbMgIEfcBzkQyqrs29jjd5dcwHVkcf7vxXshyfcEgl0fbMF6Ihycnz7rqSqkW3lzAWx4NuWUfkPnTX8Ffuf%2BhYRaI0NCpNv%2FV300HvsvmUjS6ZzS4YHaT1peSq0agfUhwRPd97aYMnUwRZDzxNfc5wuXA7OQ1o%2FPYIuIb%2FajVfwNP5ysitc%2FKtYEqt9rNAuzkcFmsw71d2RRnrPLsDN%2BuBXnIEh482f%2FbMj2Rj4%2FMq%2B0PEtlTRbg3rYnxyfowymfX%2BNmI4gNRUt70D4a%2FL3Qiug%3D%3D&app_id=2019032617262200001&sign_type=RSA2&version=1.0&timestamp=2020-01-19+13%3A34%3A12"
```
- Spring Cloud Gateway测试结果
```
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 139.74ms 69.39ms 617.14ms 69.82%
Req/Sec 182.12 55.74 343.00 66.24%
43391 requests in 30.09s, 11.96MB read
Requests/sec: 1441.96
Transfer/sec: 406.96KB
```
结果说明,下同:
```
8 threads and 200 connections 共8个测试线程200个连接
(平均值) (标准差) (最大值)(正负一个标准差所占比例)
Thread Stats Avg Stdev Max +/- Stdev
(延迟)
Latency 139.74ms 69.39ms 617.14ms 69.82%
(每秒请求数)
Req/Sec 182.12 55.74 343.00 66.24%
43391 requests in 30.09s, 11.96MB read (30.09秒内处理了43391个请求耗费流量11.96MB)
Requests/sec: 1441.96 (QPS 1441.96,即平均每秒处理请求数为1441.96)
Transfer/sec: 406.96KB (平均每秒流量406.96KB)
```
- Spring Cloud Zuul测试结果
```
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 230.14ms 331.27ms 2.00s 86.98%
Req/Sec 141.69 51.04 323.00 66.99%
33945 requests in 30.09s, 9.88MB read
Socket errors: connect 0, read 0, write 0, timeout 385
Requests/sec: 1128.05
Transfer/sec: 336.15KB
```
## 调用restful请求
- wrk命令
```
wrk -t8 -c200 -d30s "http://10.1.31.227:8081/rest/story-service/food/getFoodById?id=2"
```
线程数为 8模拟 200 个并发请求,持续 30 秒
- Spring Cloud Gateway测试结果
```
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 120.14ms 58.30ms 513.85ms 67.47%
Req/Sec 210.47 54.26 770.00 69.37%
50301 requests in 30.10s, 7.53MB read
Requests/sec: 1670.97
Transfer/sec: 256.21KB
```
- Spring Cloud Zuul测试结果
```
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 185.86ms 285.65ms 1.99s 88.55%
Req/Sec 167.75 55.60 460.00 68.05%
40070 requests in 30.09s, 6.65MB read
Socket errors: connect 0, read 0, write 0, timeout 466
Requests/sec: 1331.81
Transfer/sec: 226.50KB
```
综上所述Spring Cloud Gateway在没有优化的情况下压测表现比zuul好但zuul的数据表现也不差但是出现超时现象总的来说还是Spring Cloud Gateway具有优势。
附启动脚本:
`restart.sh`
```bash
echo "Stopping sop-gateway-4.2.4-SNAPSHOT.jar"
pid=`ps -ef | grep sop-gateway-4.2.4-SNAPSHOT.jar | grep -v grep | awk '{print $2}'`
if [ -n "$pid" ]
then
echo "kill -9 的id:" $pid
kill -9 $pid
fi
nohup java -jar -verbose:gc -XX:+PrintGCDetails -XX:+PrintHeapAtGC -Xloggc:gc.log \
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio=8\
-XX:+UseConcMarkSweepGC sop-gateway-4.2.4-SNAPSHOT.jar\
--spring.profiles.active=dev --server.port=8081 &
tail -f nohup.out
```

View File

@@ -1,46 +0,0 @@
# 原理分析之如何存储路由
SOP基于spring cloud因此会涉及到网关路由。但是开发者不用去配置文件定义路由的隐射关系SOP帮你解决了这个问题。
## 获取路由信息
网关启动成后会触发一个事件,代码见:`com.gitee.sop.gatewaycommon.config.AbstractConfiguration.listenEvent`
这个事件会取拉取微服务中提供的路由信息
下面以nacos为例介绍拉取路由过程
1.从nacos中获取微服务实例
入口代码:`com.gitee.sop.bridge.route.NacosRegistryListener.onEvent`
2.拿到微服务信息,调用微服务提供的接口拉取路由数据
入口代码:`com.gitee.sop.gatewaycommon.route.BaseRegistryListener.pullRoutes`
最终落实到:`com.gitee.sop.gatewaycommon.route.ServiceRouteListener.onAddInstance`
微服务提供一个url`http://ip:port/sop/routes`对应Controller在`com.gitee.sop.servercommon.route.ServiceRouteController.listRoutes`
微服务找到被`@Open`注解的方法然后封装成一个路由对象放到List中最后返回给网关。
3.网关拿到路由信息,经过处理,转化成网关路由配置
关联方法:
`com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteCache.add`
`com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteCache.refresh`
路由的存储方式是一个Mapkey为路由id即接口名+版本号。
```java
/**
* keynameVersion
*/
private static final Map<String, GatewayTargetRoute> routes = synchronizedMap(new LinkedHashMap<>());
```
因为客户端调用接口都会传递一个接口名和版本号,因此通过这两个字段能够很快查询出路由信息,然后进行路由转发操作。

View File

@@ -1,33 +0,0 @@
# 原理分析之如何路由
Spring Cloud Gateway通过一系列的Filter来进行数据的传输如下图所示
![流程图](images/90012_1.png)
SOP网关在此基础上新增了几个Filter用来处理自己的逻辑前置校验、结果返回。
| 过滤器 | 类型 | Order | 功能 |
| ----- | ---- | ----------------------- | ---------------------------- |
|IndexFilter| `自定义` | -2147483648 | 入口过滤器,获取参数、签名校验 |
|ParameterFormatterFilter | `自定义` | -2147482647 | 格式化参数 |
|LimitFilter|`自定义`|-2147482447|限流|
|ForwardPathFilter|系统自带|0 |设置转发的path|
|RouteToRequestUrlFilter|系统自带|10000|设置转发host|
|SopLoadBalancerClientFilter|`自定义`|10100|LoadBalance获取转发实例|
|NettyRoutingFilter|系统自带|2147483647|获取httpclient发送请求|
|ForwardRoutingFilter|系统自带|2147483647|请求分发|
|GatewayModifyResponseGatewayFilter|`自定义`|-2|处理响应结果|
一个完整的请求会自上而下经过这些Filter下面讲解如何动态设置路由。
## 动态设置路由
网关启动后会从注册中心拉取微服务实例,然后请求微服务提供的一个接口(`/sop/routes`),获取开放接口信息(被`@Open`注解的接口)。
监听处理类在:`com.gitee.sop.bridge.route.NacosRegistryListener`
获取到路由信息后,将路由信息缓存到本地,并保存到数据库,代码在:`com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteCache.load`
然后动态设置Gateway路由代码在`com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteRepository.refresh`
当有微服务重新启动时,网关会监听到微服务实例有变更,会重复上述步骤,确保网关存有最新的路由。

View File

@@ -1,26 +0,0 @@
# 原理分析之文档归纳
作为开放平台必须要提供API文档。
SOP采用微服务架构实现因此文档应该由各个微服务各自实现。难点是如何归纳各个微服务端提供的文档信息并统一展示。
SOP的解决思路如下
- 各微服务使用swagger定义自己的接口信息
- sop-website项目在启动时向注册中心获取所有服务实例分别调用各个服务提供的swagger文档信息保存到本地
- sop-website前端页面负责展示swagger提供的文档信息
由于注册中心的存在可以很方便的获取每个微服务提供的接口因此可以获取到swagger提供的文档信息。
如此一来的好处是各微服务不用关心文档该怎么展示只需要写好swagger注解即可文档信息展示统一交给另外一个工程来维护各司其职。
SOP设计初衷亦是如此微服务只管写业务代码其它的都交给SOP来处理。
文档归纳原理图:
![文档归纳原理图](images/90013_1.png "10090_1.png")
- sop-website服务启动时向各微服务获取接口信息保存到本地
- 用户访问website页面website提供对应的接口文档并展示

View File

@@ -1,92 +0,0 @@
# 原理分析之预发布灰度发布
SOP网关采用`自定义负载均衡策略`来实现对预发布/灰度发布服务器实例的选择。
spring cloud gateway默认的负载均衡实现类在:`org.springframework.cloud.gateway.filter.LoadBalancerClientFilter.java`
这个类主要做了几件事情:
1. 解析出请求路径中的scheme
2. 如果scheme不是以`lb`协议开头直接跳过
3. 如果scheme以`lb`协议开头,则说明需要进行负载均衡,选出一台微服务实例
4.`lb`协议解析成`http://ip:port`,继续向下请求
其中第4步是由`org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient`来完成的,
我们只要分别继承`LoadBalancerClientFilter``RibbonLoadBalancerClient`,然后重写其中的方法就能完成自定义负载均衡。
SOP中的重写类是`SopLoadBalancerClientFilter``SopLoadBalancerClient`,核心代码委托给了`LoadBalanceServerChooser`处理,核心代码如下:
```java
/**
* 选择服务器
*
* @param serviceId serviceId仅gateway网关有作用
* @param exchange 请求上下文
* @param loadBalancer loadBalancer
* @param superChooser 父类默认的选择
* @param serverChooserFunction 执行选择操作
* @return 返回服务器实例没有选到则返回null
*/
public R choose(
String serviceId
, T exchange
, ILoadBalancer loadBalancer
, Supplier<R> superChooser
, Function<List<Server>, R> serverChooserFunction) {
// 获取所有服务实例
List<Server> servers = loadBalancer.getReachableServers();
// 存放预发服务器
List<Server> preServers = new ArrayList<>(4);
// 存放灰度发布服务器
List<Server> grayServers = new ArrayList<>(4);
// 存放非预发服务器
List<Server> notPreServers = new ArrayList<>(4);
for (Server server : servers) {
// 获取实例metadata
Map<String, String> metadata = getMetadata(serviceId, server);
// 是否开启了预发模式
if (this.isPreServer(metadata)) {
preServers.add(server);
} else if (this.isGrayServer(metadata)) {
grayServers.add(server);
} else {
notPreServers.add(server);
}
}
notPreServers.addAll(grayServers);
// 如果没有开启预发布服务和灰度发布,直接用默认的方式
if (preServers.isEmpty() && grayServers.isEmpty()) {
return superChooser.get();
}
// 如果是从预发布域名访问过来,则认为是预发布请求,选出预发服务器
if (this.isRequestFromPreDomain(exchange)) {
return serverChooserFunction.apply(preServers);
}
// 如果是灰度请求,则认为是灰度用户,选出灰度服务器
if (this.isRequestGrayServer(exchange)) {
return serverChooserFunction.apply(grayServers);
}
// 到这里说明不能访问预发/灰度服务器,则需要路由到非预发服务器
// 注意这里允许走灰度服务器如果不允许走注释notPreServers.addAll(grayServers);这行
return serverChooserFunction.apply(notPreServers);
}
```
其业务逻辑如下:
1. 选出`serviceId`对应的所有服务器实例
2. 将服务器实例进行分类,分别放进`预发布List``灰度List``非预发布List`
3. 如果`预发布List``灰度List`都为空,表示没有开启任何预发/灰度服务,直接使用父类的负载均衡策略
4. 如果是从预发布域名访问过来,则认为是预发布请求,选出预发服务器
5. 如果是灰度请求,则认为是灰度用户,选出灰度服务器
6. 最后剩下的是正常用户,正常用户不能走预发环境
## 参考类
- com.gitee.sop.gatewaycommon.gateway.filter.SopLoadBalancerClientFilter
- com.gitee.sop.gatewaycommon.gateway.loadbalancer.SopLoadBalancerClient
- com.gitee.sop.gatewaycommon.gateway.loadbalancer.GatewayLoadBalanceServerChooser

View File

@@ -1,125 +0,0 @@
# 常见问题
## Connection has been closed BEFORE send operation
压测时出现`Connection has been closed BEFORE send operation``java.nio.channels.ClosedChannelException`之类的错误
- 解决办法
尝试调大内存分配1G或2G`-Xms1g -Xmx1g`
## spring cloud gateway [DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144]
如果POST请求body内容太大可能会报这个错误
- 解决版办法
网关指定配置`spring.codec.max-in-memory-size=xx`xx默认是262144即256K262144=256*1024
## primordials is not defined
node版本太高导致与gulp版本不兼容解决方法node退回11版本。参考https://blog.csdn.net/zxxzxx23/article/details/103000393
## Nacos指定group
可在配置文件中添加:`spring.cloud.nacos.discovery.group=xxx`指定group不加默认是DEFAULT_GROUP
## 在SpringCloudGateway中获取请求参数
```java
ApiParam apiParam = ServerWebExchangeUtil.getApiParam(exchange);
```
## 微服务端如何获取appId等参数
```java
OpenContext openContext = ServiceContext.getCurrentContext().getOpenContext();
System.out.println("app_id:" + openContext.getAppId());
System.out.println("token:" + openContext.getAppAuthToken());
```
## 如何关闭签名验证
- 针对某一个接口关闭签名验证
`@Open(value = "alipay.story.get", ignoreValidate = true)`
## 注册到eureka显示hostname非ip
```properties
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
```
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
```
参考https://www.jianshu.com/p/5ad8317961b7
## 直接访问服务的swagger-ui.html 提示access forbidden
找到微服务的`OpenServiceConfig.java`重写内部类Swagger2中的swaggerAccessProtected()方法返回false。线上请设置成true
```java
// 开启文档
@Configuration
@EnableSwagger2
public static class Swagger2 extends SwaggerSupport {
@Override
protected String getDocTitle() {
return "故事API";
}
@Override
protected boolean swaggerAccessProtected() {
return false;
}
}
```
## 调试网关出现服务不可用
打断点调试网关出现Read Timeout
参考https://blog.csdn.net/qq_36872046/article/details/81058045
yml添加
```properties
# https://blog.csdn.net/qq_36872046/article/details/81058045
# 路由转发超时时间毫秒默认值1000详见RibbonClientConfiguration.DEFAULT_READ_TIMEOUT。
# 如果微服务端 处理时间过长会导致ribbon read超时解决办法将这个值调大一点
ribbon.ReadTimeout= 60000
```
## 其它微服务没有开放接口,需要排除
在sop-gateway项目中配置
```properties
# 排除服务,多个用,隔开
sop.service.exclude=your-serviceId1,your-serviceId2
```
或者使用正则:
```properties
# 排除以"test-"开头的
# 多个正则用英文分号(;)隔开
sop.service.exclude-regex=test\\-.*
```
## ISV公私钥 & 平台公私钥
```java
ISV私钥必须ISV保存用来生成签名 --> ISV公钥必须平台保存用来校验签名是否正确
平台私钥非必须平台保存对返回结果生成签名 --> 平台公钥非必须ISV保存用来校验签名是否正确
```
总结:私钥负责加密生成签名,公钥负责校验签名是否正确

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 853 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SOP开发文档</title>
<link rel="icon" href="_media/favicon.ico">
<meta name="google-site-verification" content="6t0LoIeFksrjF4c9sqUEsVXiQNxLp2hgoqo0KryT-sE" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="keywords" content="doc,docs,documentation,gitbook,creator,generator,github,jekyll,github-pages">
<meta name="description" content="A magical documentation generator.">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css" title="vue" >
<style>
nav.app-nav li ul {
min-width: 100px;
}
/* 显示区域宽度 */
.markdown-section{
max-width: 90%;
}
.sidebar li {
margin: 0px 0;
}
</style>
</head>
<body>
<div id="app">加载中 ...</div>
<script>
window.$docsify = {
alias: {
'/.*/_sidebar.md': '/_sidebar.md?t=' + new Date().getTime()
},
auto2top: true,
coverpage: false,
executeScript: true,
loadSidebar: true,
loadNavbar: false,
mergeNavbar: true,
maxLevel: 3,
subMaxLevel: 2,
ga: 'UA-106147152-1',
name: '',
search: {
noData: {
'/zh-cn/': '没有结果!',
'/': '没有结果!'
},
paths: 'auto',
placeholder: {
'/zh-cn/': '搜索',
'/': '搜索'
}
},
formatUpdated: '{MM}/{DD} {HH}:{mm}'
}
</script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/ga.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-bash.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-markdown.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-java.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-json.min.js"></script>
</body>
</html>

View File

@@ -1,49 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" 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>
<groupId>com.gitee.sop</groupId>
<artifactId>doc</artifactId>
<version>5.0.0-SNAPSHOT</version>
<properties>
<!-- Generic properties -->
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包时跳过测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1 +0,0 @@
docsify serve docs

View File

@@ -1,55 +0,0 @@
const liveServer = require('live-server')
const isSSR = !!process.env.SSR
const middleware = []
if (isSSR) {
const Renderer = require('./packages/docsify-server-renderer/build.js')
const renderer = new Renderer({
template: `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>docsify</title>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="/themes/vue.css" title="vue">
</head>
<body>
<!--inject-app-->
<!--inject-config-->
<script src="/lib/docsify.js"></script>
</body>
</html>`,
config: {
name: 'docsify',
repo: 'docsifyjs/docsify',
basePath: 'https://docsify.js.org/',
loadNavbar: true,
loadSidebar: true,
subMaxLevel: 3,
auto2top: true,
alias: {
'/de-de/changelog': '/changelog',
'/zh-cn/changelog': '/changelog',
'/changelog':
'https://raw.githubusercontent.com/docsifyjs/docsify/master/CHANGELOG'
}
},
path: './'
})
middleware.push(function(req, res, next) {
if (/\.(css|js)$/.test(req.url)) {
return next()
}
renderer.renderToString(req.url).then(html => res.end(html))
})
}
const params = {
port: 3000,
watch: ['lib', 'docs', 'themes'],
middleware
}
liveServer.start(params)

View File

@@ -1,99 +0,0 @@
package com.gitee.sop.doc;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 生成_sidebar.md文件直接运行即可
*
* @author 六如
*/
public class SidebarTest {
static String format = " * [%s](files/%s?t=%s)\r\n";
static Map<String, Menu> levelMap = new HashMap<>(8);
static {
int i = 0;
levelMap.put("1", new Menu("* 开发文档\n", i++));
levelMap.put("9", new Menu("* 原理分析\n", i++));
}
public static void main(String[] args) throws Exception {
String path = SidebarTest.class.getClassLoader().getResource("").getPath();
String root = path.substring(0, path.indexOf("doc")) + "doc";
String fileDir = root + "/docs/files";
File dir = new File(fileDir);
File[] files = dir.listFiles();
Stream<File> filesStream = Stream.of(files);
Map<String, List<FileExt>> menuMap = filesStream
.sorted(Comparator.comparing(File::getName))
.map(file -> {
if (file.isDirectory()) {
return null;
}
FileExt fileExt = new FileExt();
fileExt.menu = file.getName().substring(0, 1);
fileExt.file = file;
return fileExt;
})
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(FileExt::getMenu));
StringBuilder output = new StringBuilder();
output.append("* [首页](/?t=" + System.currentTimeMillis() + ")\n");
for (Map.Entry<String, List<FileExt>> entry : menuMap.entrySet()) {
Menu menu = levelMap.get(entry.getKey());
if (menu == null) {
continue;
}
output.append(menu.parentName);
for (FileExt fileExt : entry.getValue()) {
String filename = fileExt.file.getName();
String title = filename.substring(filename.indexOf("_") + 1, filename.length() - 3);
String line = String.format(format, title, filename, System.currentTimeMillis());
output.append(line);
}
}
System.out.println(output);
String sidebarFilepath = root + "/docs/_sidebar.md";
FileOutputStream out = new FileOutputStream(new File(sidebarFilepath));
out.write(output.toString().getBytes());
out.close();
}
static class Menu {
String parentName;
int order;
public Menu(String parentName, int order) {
this.parentName = parentName;
this.order = order;
}
}
static class FileExt {
File file;
String menu;
public File getFile() {
return file;
}
public String getMenu() {
return menu;
}
}
}

View File

@@ -3,11 +3,9 @@
JAVA_OPTS="-Xms128m -Xmx128m" JAVA_OPTS="-Xms128m -Xmx128m"
# mysql, nacos配置 # mysql, nacos配置
args="--mysql.host=10.1.30.110:3306 --mysql.username=root --mysql.password=root --register.url=10.1.30.110:8848" args="--mysql.host=10.1.30.110:3306 --mysql.username=root --mysql.password=root --dubbo.registry.address=10.1.30.110:8848"
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-gateway/sop-gateway.jar $args --logging.file.path=/sop/sop-gateway/log & java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-gateway/sop-gateway.jar $args --logging.file.path=/sop/sop-gateway/log &
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-admin/sop-admin.jar $args --logging.file.path=/sop/sop-admin/log & java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-admin/sop-admin.jar $args --logging.file.path=/sop/sop-admin/log &
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-website/sop-website.jar $args --logging.file.path=/sop/sop-website/log &
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-auth/sop-auth.jar $args --logging.file.path=/sop/sop-auth/log &
# 最后一条没有& # 最后一条没有&
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-story/sop-story.jar $args --logging.file.path=/sop/sop-story/log java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-story/sop-story.jar $args --logging.file.path=/sop/sop-story/log

View File

@@ -16,7 +16,6 @@
<description>一个开放平台解决方案项目基于Dubbo实现目标是能够让用户快速得搭建起自己的开放平台</description> <description>一个开放平台解决方案项目基于Dubbo实现目标是能够让用户快速得搭建起自己的开放平台</description>
<modules> <modules>
<module>doc</module>
<module>sop-example</module> <module>sop-example</module>
<module>sop-admin</module> <module>sop-admin</module>
<module>sop-test</module> <module>sop-test</module>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.1 323.1 0 0 1-107.769-242.852z"/></svg>

Before

Width:  |  Height:  |  Size: 706 B

View File

@@ -140,7 +140,7 @@ export function useNav() {
/** 获取`logo` */ /** 获取`logo` */
function getLogo() { function getLogo() {
return new URL("/logo.svg", import.meta.url).href; return new URL("/logo.png", import.meta.url).href;
} }
return { return {

View File

@@ -21,13 +21,13 @@ public class PayTradeWapPayRequest {
@ApiModelProperty(value = "商户网站唯一订单号", required = true, example = "70501111111S001111119") @ApiModelProperty(value = "商户网站唯一订单号", required = true, example = "70501111111S001111119")
@Length(max = 64) @Length(max = 64)
@NotBlank @NotBlank(message = "商户网站唯一订单号必填")
private String out_trade_no; private String outTradeNo;
@ApiModelProperty(value = "订单总金额.单位为元,精确到小数点后两位,取值范围:[0.01,100000000] ", @ApiModelProperty(value = "订单总金额.单位为元,精确到小数点后两位,取值范围:[0.01,100000000] ",
required = true, example = "9.00") required = true, example = "9.00")
@NotNull @NotNull(message = "订单总金额不能为空")
private BigDecimal total_amount; private BigDecimal totalAmount;
@ApiModelProperty( @ApiModelProperty(
@@ -36,7 +36,7 @@ public class PayTradeWapPayRequest {
example = "大乐透" example = "大乐透"
) )
@Length(max = 256) @Length(max = 256)
@NotBlank @NotBlank(message = "订单标题不能为空")
private String subject; private String subject;
@ApiModelProperty( @ApiModelProperty(
@@ -44,9 +44,9 @@ public class PayTradeWapPayRequest {
required = true, required = true,
example = "QUICK_WAP_WAY" example = "QUICK_WAP_WAY"
) )
@NotBlank @NotBlank(message = "销售产品码不能为空")
@Length(max = 64) @Length(max = 64)
private String product_code; private String productCode;
@ApiModelProperty( @ApiModelProperty(
@@ -54,7 +54,7 @@ public class PayTradeWapPayRequest {
example = "appopenBb64d181d0146481ab6a762c00714cC27" example = "appopenBb64d181d0146481ab6a762c00714cC27"
) )
@Length(max = 40) @Length(max = 40)
private String auth_token; private String authToken;
@ApiModelProperty( @ApiModelProperty(
value = "用户付款中途退出返回商户网站的地址", value = "用户付款中途退出返回商户网站的地址",
@@ -66,21 +66,21 @@ public class PayTradeWapPayRequest {
@ApiModelProperty( @ApiModelProperty(
value = "订单包含的商品列表信息json格式其它说明详见商品明细说明" value = "订单包含的商品列表信息json格式其它说明详见商品明细说明"
) )
private List<GoodsDetail> goods_detail; private List<GoodsDetail> goodsDetail;
@ApiModelProperty( @ApiModelProperty(
value = "绝对超时时间格式为yyyy-MM-dd HH:mm:ss。超时时间范围1m~15d。", value = "绝对超时时间格式为yyyy-MM-dd HH:mm:ss。超时时间范围1m~15d。",
example = "2016-12-31 10:05:00" example = "2016-12-31 10:05:00"
) )
@Length(max = 32) @Length(max = 32)
private String time_expire; private String timeExpire;
@ApiModelProperty( @ApiModelProperty(
value = "商户传入业务信息具体值要和支付平台约定应用于安全营销等参数直传场景格式为json格式", value = "商户传入业务信息具体值要和支付平台约定应用于安全营销等参数直传场景格式为json格式",
example = "{\"mc_create_trade_ip\":\"127.0.0.1\"}" example = "{\"mc_create_trade_ip\":\"127.0.0.1\"}"
) )
@Length(max = 512) @Length(max = 512)
private String business_params; private String businessParams;
@ApiModelProperty( @ApiModelProperty(
@@ -88,14 +88,14 @@ public class PayTradeWapPayRequest {
example = "merchantBizType%3d3C%26merchantBizNo%3d2016010101111" example = "merchantBizType%3d3C%26merchantBizNo%3d2016010101111"
) )
@Length(max = 512) @Length(max = 512)
private String passback_params; private String passbackParams;
@ApiModelProperty( @ApiModelProperty(
value = "商户原始订单号最大长度限制32位", value = "商户原始订单号最大长度限制32位",
example = "{\"mc_create_trade_ip\":\"127.0.0.1\"}" example = "{\"mc_create_trade_ip\":\"127.0.0.1\"}"
) )
@Length(max = 32) @Length(max = 32)
private String merchant_order_no; private String merchantOrderNo;
// --- // ---
@@ -108,7 +108,7 @@ public class PayTradeWapPayRequest {
) )
@NotBlank @NotBlank
@Length(max = 64) @Length(max = 64)
private String goods_id; private String goodsId;
@ApiModelProperty( @ApiModelProperty(
@@ -118,7 +118,7 @@ public class PayTradeWapPayRequest {
) )
@NotBlank @NotBlank
@Length(max = 256) @Length(max = 256)
private String goods_name; private String goodsName;
@ApiModelProperty( @ApiModelProperty(
value = "商品数量", value = "商品数量",
@@ -141,21 +141,21 @@ public class PayTradeWapPayRequest {
example = "20010001" example = "20010001"
) )
@Length(max = 32) @Length(max = 32)
private String alipay_goods_id; private String alipayGoodsId;
@ApiModelProperty( @ApiModelProperty(
value = "商品类目", value = "商品类目",
example = "34543238" example = "34543238"
) )
@Length(max = 24) @Length(max = 24)
private String goods_category; private String goodsCategory;
@ApiModelProperty( @ApiModelProperty(
value = "商品类目树从商品类目根节点到叶子节点的类目id组成类目id值使用|分割", value = "商品类目树从商品类目根节点到叶子节点的类目id组成类目id值使用|分割",
example = "124868003|126232002|126252004" example = "124868003|126232002|126252004"
) )
@Length(max = 128) @Length(max = 128)
private String categories_tree; private String categoriesTree;
@ApiModelProperty( @ApiModelProperty(
value = "商品描述信息", value = "商品描述信息",
@@ -169,7 +169,7 @@ public class PayTradeWapPayRequest {
example = "http://www.alipay.com/xxx.jpg" example = "http://www.alipay.com/xxx.jpg"
) )
@Length(max = 400) @Length(max = 400)
private String show_url; private String showUrl;
} }

View File

@@ -2,7 +2,7 @@ package com.gitee.sop.storyweb.impl;
import com.gitee.sop.storyweb.message.StoryMessageEnum; import com.gitee.sop.storyweb.message.StoryMessageEnum;
import com.gitee.sop.storyweb.open.OpenStory; import com.gitee.sop.storyweb.open.OpenStory;
import com.gitee.sop.storyweb.open.req.StorySaveDTO; import com.gitee.sop.storyweb.open.req.StorySaveRequest;
import com.gitee.sop.storyweb.open.resp.StoryResponse; import com.gitee.sop.storyweb.open.resp.StoryResponse;
import com.gitee.sop.support.context.OpenContext; import com.gitee.sop.support.context.OpenContext;
import com.gitee.sop.support.dto.CommonFileData; import com.gitee.sop.support.dto.CommonFileData;
@@ -29,13 +29,13 @@ public class OpenStoryImpl implements OpenStory {
@Override @Override
public Integer save(StorySaveDTO storySaveDTO) { public Integer save(StorySaveRequest storySaveDTO) {
return 1; return 1;
} }
@Override @Override
public Integer update(Integer id, StorySaveDTO storySaveDTO) { public Integer update(Integer id, StorySaveRequest storySaveDTO) {
System.out.println("update, id:" + id + ", storySaveDTO=" + storySaveDTO); System.out.println("update, id:" + id + ", storySaveDTO=" + storySaveDTO);
return 1; return 1;
} }
@@ -84,7 +84,7 @@ public class OpenStoryImpl implements OpenStory {
} }
@Override @Override
public StoryResponse upload(StorySaveDTO storySaveDTO, FileData file) { public StoryResponse upload(StorySaveRequest storySaveDTO, FileData file) {
System.out.println("getName:" + file.getName()); System.out.println("getName:" + file.getName());
System.out.println("getOriginalFilename:" + file.getOriginalFilename()); System.out.println("getOriginalFilename:" + file.getOriginalFilename());
checkFile(Arrays.asList(file)); checkFile(Arrays.asList(file));
@@ -97,7 +97,7 @@ public class OpenStoryImpl implements OpenStory {
@Override @Override
public StoryResponse upload2(StorySaveDTO storySaveDTO, FileData idCardFront, FileData idCardBack) { public StoryResponse upload2(StorySaveRequest storySaveDTO, FileData idCardFront, FileData idCardBack) {
List<String> list = new ArrayList<>(); List<String> list = new ArrayList<>();
System.out.println("upload:" + storySaveDTO); System.out.println("upload:" + storySaveDTO);
checkFile(Arrays.asList(idCardFront, idCardBack)); checkFile(Arrays.asList(idCardFront, idCardBack));
@@ -109,7 +109,7 @@ public class OpenStoryImpl implements OpenStory {
} }
@Override @Override
public StoryResponse upload3(StorySaveDTO storySaveDTO, List<FileData> files) { public StoryResponse upload3(StorySaveRequest storySaveDTO, List<FileData> files) {
List<String> list = new ArrayList<>(); List<String> list = new ArrayList<>();
list.add("upload:" + storySaveDTO); list.add("upload:" + storySaveDTO);
checkFile(files); checkFile(files);

View File

@@ -1,6 +1,6 @@
package com.gitee.sop.storyweb.open; package com.gitee.sop.storyweb.open;
import com.gitee.sop.storyweb.open.req.StorySaveDTO; import com.gitee.sop.storyweb.open.req.StorySaveRequest;
import com.gitee.sop.storyweb.open.resp.StoryResponse; import com.gitee.sop.storyweb.open.resp.StoryResponse;
import com.gitee.sop.support.annotation.Open; import com.gitee.sop.support.annotation.Open;
import com.gitee.sop.support.context.OpenContext; import com.gitee.sop.support.context.OpenContext;
@@ -18,10 +18,10 @@ import java.util.List;
public interface OpenStory { public interface OpenStory {
@Open("story.save") @Open("story.save")
Integer save(StorySaveDTO storySaveDTO); Integer save(StorySaveRequest storySaveRequest);
@Open("story.update") @Open("story.update")
Integer update(Integer id, StorySaveDTO storySaveDTO); Integer update(Integer id, StorySaveRequest storySaveRequest);
// 演示抛出异常 // 演示抛出异常
@Open("story.updateError") @Open("story.updateError")
@@ -48,12 +48,12 @@ public interface OpenStory {
// 演示单文件上传 // 演示单文件上传
@Open("story.upload") @Open("story.upload")
StoryResponse upload(StorySaveDTO storySaveDTO, FileData file); StoryResponse upload(StorySaveRequest storySaveRequest, FileData file);
// 演示多文件上传 // 演示多文件上传
@Open("story.upload.more") @Open("story.upload.more")
StoryResponse upload2( StoryResponse upload2(
StorySaveDTO storySaveDTO, StorySaveRequest storySaveRequest,
@NotNull(message = "身份证正面必填") FileData idCardFront, @NotNull(message = "身份证正面必填") FileData idCardFront,
@NotNull(message = "身份证背面必填") FileData idCardBack @NotNull(message = "身份证背面必填") FileData idCardBack
); );
@@ -61,7 +61,7 @@ public interface OpenStory {
// 演示多文件上传 // 演示多文件上传
@Open("story.upload.list") @Open("story.upload.list")
StoryResponse upload3( StoryResponse upload3(
StorySaveDTO storySaveDTO, StorySaveRequest storySaveRequest,
@Size(min = 2, message = "最少上传2个文件") @Size(min = 2, message = "最少上传2个文件")
List<FileData> files List<FileData> files
); );

View File

@@ -11,7 +11,7 @@ import java.util.Date;
* @author 六如 * @author 六如
*/ */
@Data @Data
public class StorySaveDTO implements Serializable { public class StorySaveRequest implements Serializable {
private static final long serialVersionUID = -1214422742659231037L; private static final long serialVersionUID = -1214422742659231037L;
@NotBlank(message = "故事名称必填") @NotBlank(message = "故事名称必填")

View File

@@ -88,7 +88,7 @@ public class ApiConfig {
private String zoneId = "Asia/Shanghai"; private String zoneId = "Asia/Shanghai";
/** /**
* 返回结果字段小写形式 * 字段下划线小写形式
*/ */
private Boolean fieldLowercase = false; private Boolean fieldSnakeCase = false;
} }

View File

@@ -1,13 +1,12 @@
package com.gitee.sop.gateway.config; package com.gitee.sop.gateway.config;
import com.gitee.sop.gateway.service.ParamExecutor; import com.gitee.sop.gateway.service.ParamExecutor;
import com.gitee.sop.gateway.service.impl.ParamExecutorImpl;
import com.gitee.sop.gateway.service.RouteService; import com.gitee.sop.gateway.service.RouteService;
import com.gitee.sop.gateway.service.impl.RouteServiceImpl;
import com.gitee.sop.gateway.service.Serde; import com.gitee.sop.gateway.service.Serde;
import com.gitee.sop.gateway.service.impl.ParamExecutorImpl;
import com.gitee.sop.gateway.service.impl.RouteServiceImpl;
import com.gitee.sop.gateway.service.impl.SerdeGsonImpl; import com.gitee.sop.gateway.service.impl.SerdeGsonImpl;
import com.gitee.sop.gateway.service.impl.SerdeImpl; import com.gitee.sop.gateway.service.impl.SerdeImpl;
import com.gitee.sop.gateway.service.interceptor.internal.ResultRouteInterceptor;
import com.gitee.sop.gateway.service.manager.ApiManager; import com.gitee.sop.gateway.service.manager.ApiManager;
import com.gitee.sop.gateway.service.manager.IsvApiPermissionManager; import com.gitee.sop.gateway.service.manager.IsvApiPermissionManager;
import com.gitee.sop.gateway.service.manager.IsvManager; import com.gitee.sop.gateway.service.manager.IsvManager;
@@ -99,13 +98,6 @@ public class GatewayConfig {
return new SerdeGsonImpl(); return new SerdeGsonImpl();
} }
// DEFAULT ROUTE INTERCEPTOR
@Bean
@ConditionalOnMissingBean
public ResultRouteInterceptor resultRouteInterceptor() {
return new ResultRouteInterceptor();
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public ParamExecutor paramExecutor() { public ParamExecutor paramExecutor() {

View File

@@ -1,4 +1,4 @@
package com.gitee.sop.gateway.service.interceptor; package com.gitee.sop.gateway.interceptor;
import com.gitee.sop.gateway.common.ApiInfoDTO; import com.gitee.sop.gateway.common.ApiInfoDTO;
import com.gitee.sop.gateway.request.ApiRequestContext; import com.gitee.sop.gateway.request.ApiRequestContext;
@@ -12,6 +12,9 @@ public interface RouteInterceptor {
/** /**
* 在路由转发前执行签名校验通过后会立即执行此方法 * 在路由转发前执行签名校验通过后会立即执行此方法
* <pre>
* 在这个方法中抛出异常会中断接口执行直接返回错误信息
* </pre>
* *
* @param context context * @param context context
* @param apiInfoDTO 接口信息 * @param apiInfoDTO 接口信息
@@ -24,7 +27,7 @@ public interface RouteInterceptor {
* *
* @param context context * @param context context
* @param apiInfoDTO 接口信息 * @param apiInfoDTO 接口信息
* @param result 返回结果,通常是HashMap * @param result 业务返回结果,通常是HashMap
* @return 返回格式化后的结果, 可对原结果进行修改 * @return 返回格式化后的结果, 可对原结果进行修改
*/ */
default Object afterRoute(ApiRequestContext context, ApiInfoDTO apiInfoDTO, Object result) { default Object afterRoute(ApiRequestContext context, ApiInfoDTO apiInfoDTO, Object result) {

View File

@@ -0,0 +1,10 @@
package com.gitee.sop.gateway.interceptor;
/**
* @author 六如
*/
public class RouteInterceptorOrders {
public static final int RESULT_INTERCEPTOR = -1000;
}

View File

@@ -1,11 +1,12 @@
package com.gitee.sop.gateway.service.interceptor.internal; package com.gitee.sop.gateway.interceptor.internal;
import com.gitee.sop.gateway.common.ApiInfoDTO; import com.gitee.sop.gateway.common.ApiInfoDTO;
import com.gitee.sop.gateway.request.ApiRequestContext; import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.service.interceptor.RouteInterceptor; import com.gitee.sop.gateway.interceptor.RouteInterceptor;
import com.gitee.sop.gateway.service.interceptor.RouteInterceptorOrders; import com.gitee.sop.gateway.interceptor.RouteInterceptorOrders;
import com.gitee.sop.support.dto.CommonFileData; import com.gitee.sop.support.dto.CommonFileData;
import com.gitee.sop.support.dto.FileData; import com.gitee.sop.support.dto.FileData;
import org.springframework.stereotype.Component;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -17,6 +18,7 @@ import java.util.Objects;
* *
* @author 六如 * @author 六如
*/ */
@Component
public class ResultRouteInterceptor implements RouteInterceptor { public class ResultRouteInterceptor implements RouteInterceptor {
private static final String CLASS = "class"; private static final String CLASS = "class";

View File

@@ -3,7 +3,6 @@ package com.gitee.sop.gateway.response;
import com.gitee.sop.gateway.message.ErrorEnum; import com.gitee.sop.gateway.message.ErrorEnum;
import com.gitee.sop.gateway.message.IError; import com.gitee.sop.gateway.message.IError;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Locale; import java.util.Locale;
@@ -53,19 +52,41 @@ import java.util.Locale;
* *
* @author 六如 * @author 六如
*/ */
@EqualsAndHashCode(callSuper = true)
@Data @Data
public class ApiResponse extends BaseResponse { public class ApiResponse implements Response {
public static final String SUCCESS_CODE = "0";
public static final String SUCCESS_MSG = "success";
/**
* 网关异常码范围0~100 成功返回"0"
*/
private String code = SUCCESS_CODE;
/**
* 网关异常信息
*/
private String msg = "";
/**
* 返回对象
*/
private Object data;
/** /**
* 业务异常码 * 业务异常码
*/ */
private String subCode = ""; private String sub_code = "";
/** /**
* 业务异常信息 * 业务异常信息
*/ */
private String subMsg = ""; private String sub_msg = "";
/**
* 解决方案
*/
private String solution;
public static ApiResponse success(Object data) { public static ApiResponse success(Object data) {
ApiResponse apiResponse = new ApiResponse(); ApiResponse apiResponse = new ApiResponse();
@@ -86,8 +107,8 @@ public class ApiResponse extends BaseResponse {
ApiResponse apiResponse = new ApiResponse(); ApiResponse apiResponse = new ApiResponse();
apiResponse.setCode(error.getCode()); apiResponse.setCode(error.getCode());
apiResponse.setMsg(error.getMsg()); apiResponse.setMsg(error.getMsg());
apiResponse.setSubCode(subCode); apiResponse.setSub_code(subCode);
apiResponse.setSubMsg(subMsg); apiResponse.setSub_msg(subMsg);
apiResponse.setSolution(solution); apiResponse.setSolution(solution);
return apiResponse; return apiResponse;
} }
@@ -99,8 +120,8 @@ public class ApiResponse extends BaseResponse {
public static ApiResponse error(IError error) { public static ApiResponse error(IError error) {
ApiResponse apiResponse = new ApiResponse(); ApiResponse apiResponse = new ApiResponse();
apiResponse.setSubCode(error.getSubCode()); apiResponse.setSub_code(error.getSubCode());
apiResponse.setSubMsg(error.getSubMsg()); apiResponse.setSub_msg(error.getSubMsg());
apiResponse.setCode(error.getCode()); apiResponse.setCode(error.getCode());
apiResponse.setMsg(error.getMsg()); apiResponse.setMsg(error.getMsg());
apiResponse.setSolution(error.getSolution()); apiResponse.setSolution(error.getSolution());
@@ -109,18 +130,7 @@ public class ApiResponse extends BaseResponse {
private static ApiResponse error(IError error, String subMsg) { private static ApiResponse error(IError error, String subMsg) {
ApiResponse response = error(error); ApiResponse response = error(error);
response.setSubMsg(subMsg); response.setSub_msg(subMsg);
return response; return response;
} }
public Response toLower() {
ApiResponseLower apiResponseLower = new ApiResponseLower();
apiResponseLower.setSub_code(this.subCode);
apiResponseLower.setSub_msg(this.subMsg);
apiResponseLower.setCode(this.subCode);
apiResponseLower.setMsg(this.subMsg);
apiResponseLower.setData(this.getData());
apiResponseLower.setSolution(this.getSolution());
return apiResponseLower;
}
} }

View File

@@ -1,108 +0,0 @@
package com.gitee.sop.gateway.response;
import com.gitee.sop.gateway.exception.ApiException;
import com.gitee.sop.gateway.message.ErrorEnum;
import com.gitee.sop.gateway.message.IError;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Locale;
/**
* 默认的结果封装类.
* <pre>
*
* xml返回结果:
* <response>
* <code>50</code>
* <msg>Remote service error</msg>
* <sub_code>isv.invalid-parameter</sub_code>
* <sub_msg>非法参数</sub_msg>
* </response>
* 成功情况:
* <response>
* <code>0</code>
* <msg>成功消息</msg>
* <data>
* ...返回内容
* </data>
* </response>
*
* json返回格式
* {
* "code":"50",
* "msg":"Remote service error",
* "sub_code":"isv.invalid-parameter",
* "sub_msg":"非法参数"
* }
* 成功情况:
* {
* "code":"0",
* "msg":"成功消息内容。。。",
* "data":{
* ...返回内容
* }
* }
* </pre>
* <p>
* 字段说明:
* code:网关异常码 <br>
* msg:网关异常信息 <br>
* sub_code:业务异常码 <br>
* sub_msg:业务异常信息 <br>
*
* @author 六如
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ApiResponseLower extends BaseResponse {
/**
* 业务异常码
*/
private String sub_code = "";
/**
* 业务异常信息
*/
private String sub_msg = "";
public static ApiResponseLower success(Object data) {
ApiResponseLower apiResponse = new ApiResponseLower();
apiResponse.setCode(SUCCESS_CODE);
apiResponse.setMsg(SUCCESS_MSG);
apiResponse.setData(data);
return apiResponse;
}
public static ApiResponseLower error(ApiException e) {
IError error = e.getError();
return error(error);
}
public static ApiResponseLower error(ErrorEnum errorEnum, Locale locale, String subMsg) {
IError error = errorEnum.getError(locale);
return error(error, subMsg);
}
public static ApiResponseLower error(ErrorEnum errorEnum, Locale locale) {
IError error = errorEnum.getError(locale);
return error(error);
}
public static ApiResponseLower error(IError error) {
return error(error, error.getSubMsg());
}
public static ApiResponseLower error(IError error, String subMsg) {
ApiResponseLower apiResponse = new ApiResponseLower();
apiResponse.setSub_code(error.getSubCode());
apiResponse.setSub_msg(error.getSubMsg());
apiResponse.setCode(error.getCode());
apiResponse.setMsg(subMsg);
return apiResponse;
}
}

View File

@@ -1,33 +0,0 @@
package com.gitee.sop.gateway.response;
import lombok.Data;
/**
* @author 六如
*/
@Data
public class BaseResponse implements Response {
public static final String SUCCESS_CODE = "0";
public static final String SUCCESS_MSG = "success";
/**
* 网关异常码范围0~100 成功返回"0"
*/
private String code = SUCCESS_CODE;
/**
* 网关异常信息
*/
private String msg = "";
/**
* 返回对象
*/
private Object data;
/**
* 解决方案
*/
private String solution;
}

View File

@@ -5,7 +5,7 @@ package com.gitee.sop.gateway.response;
* *
* @author 六如 * @author 六如
*/ */
public class NoCommonResponse extends BaseResponse { public class NoCommonResponse extends ApiResponse {
public static NoCommonResponse success(Object data) { public static NoCommonResponse success(Object data) {
NoCommonResponse apiResponse = new NoCommonResponse(); NoCommonResponse apiResponse = new NoCommonResponse();

View File

@@ -1,14 +1,25 @@
package com.gitee.sop.gateway.service; package com.gitee.sop.gateway.service;
import com.alibaba.fastjson2.JSONObject;
import java.util.Map;
/** /**
* 序列化 * 序列化/反序列化
* *
* @author 六如 * @author 六如
*/ */
public interface Serde { public interface Serde {
String toJSONString(Object object); String toJson(Object object);
String toXml(Object object); String toXml(Object object);
Map<String, Object> parseJson(String json);
default JSONObject parseObject(String json) {
Map<String, Object> jsonObj = parseJson(json);
return jsonObj instanceof JSONObject ? (JSONObject) jsonObj : new JSONObject(jsonObj);
}
} }

View File

@@ -8,6 +8,7 @@ import com.gitee.sop.gateway.request.ApiRequest;
import com.gitee.sop.gateway.request.ApiRequestContext; import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.request.RequestFormatEnum; import com.gitee.sop.gateway.request.RequestFormatEnum;
import com.gitee.sop.gateway.request.UploadContext; import com.gitee.sop.gateway.request.UploadContext;
import com.gitee.sop.gateway.response.NoCommonResponse;
import com.gitee.sop.gateway.response.Response; import com.gitee.sop.gateway.response.Response;
import com.gitee.sop.gateway.service.ParamExecutor; import com.gitee.sop.gateway.service.ParamExecutor;
import com.gitee.sop.gateway.service.Serde; import com.gitee.sop.gateway.service.Serde;
@@ -120,7 +121,7 @@ public class ParamExecutorImpl implements ParamExecutor<HttpServletRequest, Http
} else { } else {
Object responseData = apiResponse; Object responseData = apiResponse;
// 不需要公共参数 // 不需要公共参数
if (!apiResponse.needWrap()) { if (apiResponse instanceof NoCommonResponse || !apiResponse.needWrap()) {
responseData = data; responseData = data;
} }
this.writerText(apiRequestContext, responseData, response); this.writerText(apiRequestContext, responseData, response);
@@ -138,7 +139,7 @@ public class ParamExecutorImpl implements ParamExecutor<HttpServletRequest, Http
response.getWriter().write(xml); response.getWriter().write(xml);
} else { } else {
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentType(MediaType.APPLICATION_JSON_VALUE);
String json = serde.toJSONString(apiResponse); String json = serde.toJson(apiResponse);
response.getWriter().write(json); response.getWriter().write(json);
} }
} }

View File

@@ -5,7 +5,6 @@ import com.gitee.sop.gateway.common.enums.YesOrNoEnum;
import com.gitee.sop.gateway.config.ApiConfig; import com.gitee.sop.gateway.config.ApiConfig;
import com.gitee.sop.gateway.request.ApiRequestContext; import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.response.ApiResponse; import com.gitee.sop.gateway.response.ApiResponse;
import com.gitee.sop.gateway.response.ApiResponseLower;
import com.gitee.sop.gateway.response.NoCommonResponse; import com.gitee.sop.gateway.response.NoCommonResponse;
import com.gitee.sop.gateway.response.Response; import com.gitee.sop.gateway.response.Response;
import com.gitee.sop.gateway.service.ResultWrapper; import com.gitee.sop.gateway.service.ResultWrapper;
@@ -37,23 +36,15 @@ public class ResultWrapperImpl implements ResultWrapper {
if (needNotWrap) { if (needNotWrap) {
return NoCommonResponse.success(result); return NoCommonResponse.success(result);
} }
if (Objects.equals(apiConfig.getFieldLowercase(), true)) {
return ApiResponseLower.success(result);
} else {
return ApiResponse.success(result); return ApiResponse.success(result);
} }
}
private Response executeApiResponse(ApiResponse apiResponse, boolean needNotWrap) { private Response executeApiResponse(ApiResponse apiResponse, boolean needNotWrap) {
// 不需要公共返回参数 // 不需要公共返回参数
if (needNotWrap) { if (needNotWrap) {
return NoCommonResponse.success(apiResponse.getData()); return NoCommonResponse.success(apiResponse.getData());
} }
if (Objects.equals(apiConfig.getFieldLowercase(), true)) {
return apiResponse.toLower();
} else {
return apiResponse; return apiResponse;
} }
}
} }

View File

@@ -6,6 +6,7 @@ import com.gitee.sop.gateway.common.ApiInfoDTO;
import com.gitee.sop.gateway.common.ParamInfoDTO; import com.gitee.sop.gateway.common.ParamInfoDTO;
import com.gitee.sop.gateway.exception.ApiException; import com.gitee.sop.gateway.exception.ApiException;
import com.gitee.sop.gateway.exception.ExceptionExecutor; import com.gitee.sop.gateway.exception.ExceptionExecutor;
import com.gitee.sop.gateway.interceptor.RouteInterceptor;
import com.gitee.sop.gateway.message.ErrorEnum; import com.gitee.sop.gateway.message.ErrorEnum;
import com.gitee.sop.gateway.request.ApiRequest; import com.gitee.sop.gateway.request.ApiRequest;
import com.gitee.sop.gateway.request.ApiRequestContext; import com.gitee.sop.gateway.request.ApiRequestContext;
@@ -15,27 +16,28 @@ import com.gitee.sop.gateway.response.Response;
import com.gitee.sop.gateway.service.GenericServiceInvoker; import com.gitee.sop.gateway.service.GenericServiceInvoker;
import com.gitee.sop.gateway.service.ResultWrapper; import com.gitee.sop.gateway.service.ResultWrapper;
import com.gitee.sop.gateway.service.RouteService; import com.gitee.sop.gateway.service.RouteService;
import com.gitee.sop.gateway.service.interceptor.RouteInterceptor; import com.gitee.sop.gateway.service.Serde;
import com.gitee.sop.gateway.service.validate.Validator; import com.gitee.sop.gateway.service.validate.Validator;
import com.gitee.sop.gateway.util.ClassUtil; import com.gitee.sop.gateway.util.ClassUtil;
import com.gitee.sop.support.dto.CommonFileData;
import com.gitee.sop.support.context.DefaultOpenContext; import com.gitee.sop.support.context.DefaultOpenContext;
import com.gitee.sop.support.dto.FileData;
import com.gitee.sop.support.context.OpenContext; import com.gitee.sop.support.context.OpenContext;
import java.io.IOException; import com.gitee.sop.support.dto.CommonFileData;
import java.util.ArrayList; import com.gitee.sop.support.dto.FileData;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.utils.ClassUtils; import org.apache.dubbo.common.utils.ClassUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/** /**
@@ -61,6 +63,9 @@ public class RouteServiceImpl implements RouteService {
@Autowired @Autowired
private ResultWrapper resultWrapper; private ResultWrapper resultWrapper;
@Autowired
private Serde serde;
@Override @Override
public Response route(ApiRequestContext apiRequestContext) { public Response route(ApiRequestContext apiRequestContext) {
ApiRequest apiRequest = apiRequestContext.getApiRequest(); ApiRequest apiRequest = apiRequestContext.getApiRequest();
@@ -68,10 +73,13 @@ public class RouteServiceImpl implements RouteService {
try { try {
// 接口校验 // 接口校验
ApiInfoDTO apiInfoDTO = validator.validate(apiRequestContext); ApiInfoDTO apiInfoDTO = validator.validate(apiRequestContext);
// 执行拦截器前置动作
this.doPreRoute(apiRequestContext, apiInfoDTO); this.doPreRoute(apiRequestContext, apiInfoDTO);
// 微服务结果 // 微服务结果
Object result = doRoute(apiRequestContext, apiInfoDTO); Object result = doRoute(apiRequestContext, apiInfoDTO);
// 执行拦截器后置动作
result = this.doAfterRoute(apiRequestContext, apiInfoDTO, result); result = this.doAfterRoute(apiRequestContext, apiInfoDTO, result);
// 结果处理
return resultWrapper.wrap(apiRequestContext, apiInfoDTO, result); return resultWrapper.wrap(apiRequestContext, apiInfoDTO, result);
} catch (Exception e) { } catch (Exception e) {
log.error("接口请求报错, , ip={}, apiRequest={}", apiRequestContext.getIp(), apiRequest, e); log.error("接口请求报错, , ip={}, apiRequest={}", apiRequestContext.getIp(), apiRequest, e);
@@ -120,7 +128,7 @@ public class RouteServiceImpl implements RouteService {
} }
ApiRequest apiRequest = apiRequestContext.getApiRequest(); ApiRequest apiRequest = apiRequestContext.getApiRequest();
String bizContent = apiRequest.getBizContent(); String bizContent = apiRequest.getBizContent();
JSONObject jsonObject = JSON.parseObject(bizContent); JSONObject jsonObject = serde.parseObject(bizContent);
List<Object> params = new ArrayList<>(); List<Object> params = new ArrayList<>();
for (ParamInfoDTO paramInfoDTO : paramInfoList) { for (ParamInfoDTO paramInfoDTO : paramInfoList) {
String type = paramInfoDTO.getType(); String type = paramInfoDTO.getType();
@@ -141,10 +149,13 @@ public class RouteServiceImpl implements RouteService {
} else { } else {
if (ClassUtil.isPrimitive(type)) { if (ClassUtil.isPrimitive(type)) {
String paramName = paramInfoDTO.getName(); String paramName = paramInfoDTO.getName();
Object value = null;
try { try {
Object value = jsonObject.getObject(paramName, ClassUtils.forName(type)); if (jsonObject != null) {
params.add(value); value = jsonObject.getObject(paramName, ClassUtils.forName(type));
jsonObject.remove(paramName); jsonObject.remove(paramName);
}
params.add(value);
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
log.error("找不到参数class, paramInfoDTO={}, apiRequest={}", paramInfoDTO, apiRequest, e); log.error("找不到参数class, paramInfoDTO={}, apiRequest={}", paramInfoDTO, apiRequest, e);
throw new RuntimeException("找不到class:" + type, e); throw new RuntimeException("找不到class:" + type, e);

View File

@@ -3,7 +3,12 @@ package com.gitee.sop.gateway.service.impl;
import com.alibaba.nacos.shaded.com.google.gson.Gson; import com.alibaba.nacos.shaded.com.google.gson.Gson;
import com.alibaba.nacos.shaded.com.google.gson.GsonBuilder; import com.alibaba.nacos.shaded.com.google.gson.GsonBuilder;
import java.util.LinkedHashMap;
import java.util.Map;
/** /**
* 序列化/反序列化 gson实现
*
* @author 六如 * @author 六如
*/ */
public class SerdeGsonImpl extends SerdeImpl { public class SerdeGsonImpl extends SerdeImpl {
@@ -11,12 +16,22 @@ public class SerdeGsonImpl extends SerdeImpl {
Gson gson; Gson gson;
@Override @Override
public String toJSONString(Object object) { public String toJson(Object object) {
return gson.toJson(object); return gson.toJson(object);
} }
@Override @Override
protected void doInit() { public Map<String, Object> parseJson(String json) {
gson = new GsonBuilder().setDateFormat(dateFormat).create(); return gson.fromJson(json, LinkedHashMap.class);
} }
@Override
protected void doInit() {
gson = new GsonBuilder()
.setDateFormat(dateFormat)
.create();
}
} }

View File

@@ -1,27 +1,35 @@
package com.gitee.sop.gateway.service.impl; package com.gitee.sop.gateway.service.impl;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter; import com.alibaba.fastjson2.JSONWriter;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.gitee.sop.gateway.config.ApiConfig;
import com.gitee.sop.gateway.service.Serde; import com.gitee.sop.gateway.service.Serde;
import com.gitee.sop.gateway.util.XmlUtil; import com.gitee.sop.gateway.util.XmlUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.Map;
/** /**
* @author 六如 * @author 六如
*/ */
public class SerdeImpl implements Serde { public class SerdeImpl implements Serde {
static JSONWriter.Context context; static JSONWriter.Context WRITE_CONTEXT;
@Autowired
protected ApiConfig apiConfig;
@Value("${gateway.serialize.date-format}") @Value("${gateway.serialize.date-format}")
protected String dateFormat; protected String dateFormat;
@Override @Override
public String toJSONString(Object object) { public String toJson(Object object) {
return JSON.toJSONString(object, context); return JSON.toJSONString(object);
} }
@Override @Override
@@ -33,10 +41,15 @@ public class SerdeImpl implements Serde {
} }
} }
@Override
public Map<String, Object> parseJson(String json) {
return JSON.parseObject(json);
}
@PostConstruct @PostConstruct
public void init() { public void init() {
context = new JSONWriter.Context(); WRITE_CONTEXT = new JSONWriter.Context();
context.setDateFormat(dateFormat); WRITE_CONTEXT.setDateFormat(dateFormat);
this.doInit(); this.doInit();
} }

View File

@@ -1,11 +0,0 @@
package com.gitee.sop.gateway.service.interceptor;
/**
* @author 六如
*/
public class RouteInterceptorOrders {
public static final int RESULT_INTERCEPTOR = -1000;
public static final int RESULT_WRAPPER_INTERCEPTOR = RESULT_INTERCEPTOR + 1;
}

View File

@@ -1,38 +0,0 @@
package com.gitee.sop.gateway.service.interceptor.internal;
import com.gitee.sop.gateway.common.ApiInfoDTO;
import com.gitee.sop.gateway.config.ApiConfig;
import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.response.ApiResponse;
import com.gitee.sop.gateway.response.ApiResponseLower;
import com.gitee.sop.gateway.service.interceptor.RouteInterceptor;
import com.gitee.sop.gateway.service.interceptor.RouteInterceptorOrders;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Objects;
/**
* 对结果进行包裹
*
* @author 六如
*/
public class ResultWrapperInterceptor implements RouteInterceptor {
@Autowired
private ApiConfig apiConfig;
@Override
public Object afterRoute(ApiRequestContext context, ApiInfoDTO apiInfoDTO, Object result) {
if (Objects.equals(apiConfig.getFieldLowercase(), true)) {
return ApiResponseLower.success(result);
} else {
return ApiResponse.success(result);
}
}
@Override
public int getOrder() {
return RouteInterceptorOrders.RESULT_WRAPPER_INTERCEPTOR;
}
}

View File

@@ -0,0 +1,46 @@
package com.gitee.sop.gateway.util;
/**
* @author 六如
*/
public class FieldUtil {
private static final String REGEX = "([a-z])([A-Z])";
private static final String REGEX_VAL = "$1_$2";
private static final char UNDERLINE = '_';
/**
* 驼峰转下划线
*
* @return 返回下划线
*/
public static String camelCaseToSnakeCase(String name) {
return name.replaceAll(REGEX, REGEX_VAL).toLowerCase();
}
/**
* 下划线转驼峰
*
* @param param 内容
* @return 返回转换后的字符串
*/
public static String snakeCaseToCamelCase(String param) {
if (param == null || param.trim().isEmpty()) {
return "";
}
int len = param.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char c = param.charAt(i);
if (c == UNDERLINE) {
if (++i < len) {
sb.append(Character.toUpperCase(param.charAt(i)));
}
} else {
sb.append(c);
}
}
return sb.toString();
}
}

View File

@@ -34,4 +34,5 @@ public class JsonUtil {
return JSON.parseArray(value, clazz); return JSON.parseArray(value, clazz);
} }
} }

View File

@@ -1,5 +1,5 @@
spring.profiles.active=dev spring.profiles.active=dev
spring.application.name=sop-index spring.application.name=sop-gateway
server.port=8081 server.port=8081
####### gateway config ####### ####### gateway config #######
@@ -40,8 +40,6 @@ api.timeout-seconds=300
api.timestamp-pattern=yyyy-MM-dd HH:mm:ss api.timestamp-pattern=yyyy-MM-dd HH:mm:ss
# default zone # default zone
api.zone-id=Asia/Shanghai api.zone-id=Asia/Shanghai
# if true, response field name all lowercase, such as : sub_code, sub_msg
api.field-lowercase=false
####### dubbo config ####### ####### dubbo config #######
dubbo.protocol.name=dubbo dubbo.protocol.name=dubbo

View File

@@ -177,7 +177,7 @@ namespace SDKCSharp.Client
/// <typeparam name="T">返回的Response类</typeparam> /// <typeparam name="T">返回的Response类</typeparam>
/// <param name="request">请求对象</param> /// <param name="request">请求对象</param>
/// <returns>返回Response类</returns> /// <returns>返回Response类</returns>
public virtual T Execute<T>(BaseRequest<T> request) where T : BaseResponse public virtual Result<T> Execute<T>(BaseRequest<T> request)
{ {
return this.Execute<T>(request, null); return this.Execute<T>(request, null);
} }
@@ -189,7 +189,7 @@ namespace SDKCSharp.Client
/// <param name="request">请求对象</param> /// <param name="request">请求对象</param>
/// <param name="accessToken">accessToken</param> /// <param name="accessToken">accessToken</param>
/// <returns>返回Response类</returns> /// <returns>返回Response类</returns>
public virtual T Execute<T>(BaseRequest<T> request, string accessToken) where T : BaseResponse public virtual Result<T> Execute<T>(BaseRequest<T> request, string accessToken)
{ {
RequestForm requestForm = request.CreateRequestForm(this.openConfig); RequestForm requestForm = request.CreateRequestForm(this.openConfig);
Dictionary<string, string> form = requestForm.Form; Dictionary<string, string> form = requestForm.Form;
@@ -225,18 +225,12 @@ namespace SDKCSharp.Client
/// <param name="resp">服务器响应内容</param> /// <param name="resp">服务器响应内容</param>
/// <param name="request">请求Request</param> /// <param name="request">请求Request</param>
/// <returns>返回Response</returns> /// <returns>返回Response</returns>
protected virtual T ParseResponse<T>(string resp, BaseRequest<T> request) where T: BaseResponse protected virtual Result<T> ParseResponse<T>(string resp, BaseRequest<T> request)
{ {
string method = request.Method; string method = request.Method;
string rootNodeName = this.dataNameBuilder.Build(method); string rootNodeName = this.dataNameBuilder.Build(method);
string errorRootNode = openConfig.ErrorResponseName;
Dictionary<string, object> responseData = JsonUtil.ParseToDictionary(resp); Dictionary<string, object> responseData = JsonUtil.ParseToDictionary(resp);
bool errorResponse = responseData.ContainsKey(errorRootNode); object data = responseData.GetValueOrDefault(rootNodeName, null);
if (errorResponse)
{
rootNodeName = errorRootNode;
}
object data = responseData[rootNodeName];
responseData.TryGetValue(openConfig.SignName, out object sign); responseData.TryGetValue(openConfig.SignName, out object sign);
if (sign != null && !string.IsNullOrEmpty(publicKeyPlatform)) if (sign != null && !string.IsNullOrEmpty(publicKeyPlatform))
{ {
@@ -247,10 +241,8 @@ namespace SDKCSharp.Client
data = JsonUtil.ToJSONString(checkSignErrorResponse); data = JsonUtil.ToJSONString(checkSignErrorResponse);
} }
} }
string jsonData = data == null ? "{}" : data.ToString(); Result<T> result = JsonUtil.ParseObject<Result<T>>(resp);
T t = JsonUtil.ParseObject<T>(jsonData); return result;
t.Body = jsonData;
return t;
} }
/// <summary> /// <summary>

View File

@@ -7,7 +7,7 @@ namespace SDKCSharp.Common
public class CustomDataNameBuilder: DataNameBuilder public class CustomDataNameBuilder: DataNameBuilder
{ {
private string dataName = "result"; private string dataName = "data";
public CustomDataNameBuilder() public CustomDataNameBuilder()
{ {

View File

@@ -11,7 +11,7 @@ namespace SDKCSharp.Common
{ {
public class OpenConfig public class OpenConfig
{ {
public static DataNameBuilder DATA_NAME_BUILDER = new DefaultDataNameBuilder(); public static DataNameBuilder DATA_NAME_BUILDER = new CustomDataNameBuilder();
/// <summary> /// <summary>
/// 返回码成功值 /// 返回码成功值

View File

@@ -33,9 +33,7 @@ namespace SDKTest
{ {
TestGet(); TestGet();
Console.WriteLine("--------------------"); Console.WriteLine("--------------------");
TestCommon(); //TestUpload();
Console.WriteLine("--------------------");
TestUpload();
} }
// 标准用法 // 标准用法
@@ -49,54 +47,20 @@ namespace SDKTest
request.BizModel = model; request.BizModel = model;
// 发送请求 // 发送请求
GetStoryResponse response = client.Execute(request); Result<GetStoryResponse> result = client.Execute(request);
if (response.IsSuccess()) if (result.IsSuccess())
{ {
// 返回结果 // 返回结果
Console.WriteLine("成功response:{0}\n响应原始内容:{1}", JsonUtil.ToJSONString(response), response.Body); Console.WriteLine("成功response:{0}\n响应原始内容:{1}", JsonUtil.ToJSONString(result), result.Data);
} }
else else
{ {
Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}", Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}",
response.Code, response.Msg, response.SubCode, response.SubMsg); result.Code, result.Msg, result.SubCode, result.SubMsg);
} }
} }
// 懒人版如果不想添加Request,Response,Model。可以用这种方式返回Dictionary<string, object>,后续自己处理
private static void TestCommon()
{
// 创建请求对象
CommonRequest request = new CommonRequest("alipay.story.find");
// 请求参数
Dictionary<string, string> bizModel = new Dictionary<string, string>
{
["name"] = "白雪公主"
};
request.BizModel = bizModel;
// 发送请求
CommonResponse response = client.Execute(request);
if (response.IsSuccess())
{
// 返回结果
string body = response.Body;
Dictionary<string, object> dict = JsonUtil.ParseToDictionary(body);
Console.WriteLine("Dictionary内容:");
foreach (var item in dict)
{
Console.WriteLine("{0}:{1}", item.Key, item.Value);
}
}
else
{
Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}",
response.Code, response.Msg, response.SubCode, response.SubMsg);
}
}
private static void TestUpload() private static void TestUpload()
{ {
@@ -119,10 +83,10 @@ namespace SDKTest
request.AddFile(new UploadFile("file1", root + "/file1.txt")); request.AddFile(new UploadFile("file1", root + "/file1.txt"));
request.AddFile(new UploadFile("file2", root + "/file2.txt")); request.AddFile(new UploadFile("file2", root + "/file2.txt"));
DemoFileUploadResponse response = client.Execute(request); Result<DemoFileUploadResponse> result = client.Execute(request);
if (response.IsSuccess()) if (result.IsSuccess())
{ {
List<DemoFileUploadResponse.FileMeta> responseFiles = response.Files; List<DemoFileUploadResponse.FileMeta> responseFiles = result.Data.Files;
Console.WriteLine("您上传的文件信息:"); Console.WriteLine("您上传的文件信息:");
responseFiles.ForEach(file => responseFiles.ForEach(file =>
{ {
@@ -133,7 +97,7 @@ namespace SDKTest
else else
{ {
Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}", Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}",
response.Code, response.Msg, response.SubCode, response.SubMsg); result.Code, result.Msg, result.SubCode, result.SubMsg);
} }
} }
} }

View File

@@ -3,7 +3,7 @@ using Newtonsoft.Json;
namespace SDKCSharp.Response namespace SDKCSharp.Response
{ {
public class GetStoryResponse: BaseResponse public class GetStoryResponse
{ {
[JsonProperty("id")] [JsonProperty("id")]
public int Id { get; set; } public int Id { get; set; }

Some files were not shown because too many files have changed in this diff Show More