CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

Web 开发 —— 高阶 本地网关

说明

这里说本地网关,主要是为了区分分布式网关。

在 Web 进阶中,我们讲解了如何定制过滤器和拦截器,提到 Action 本质上是 Handler 和 Class Method 的结合通过,今天我们要讲解一种特殊的Handler —— Gateway(本地网关),通过本地网关更统一的定制系统功能,比如二级路由、拦截、过滤、熔断、异常处理等,虽然直接使用过滤器,也可以做统一的鉴权和异常处理,但网关还可以把一批接口编排进多个网关中,从而实现不同的协议。

示例

在 IDEA 中通过 File->New->Modules... 可以创建新的模块 demo-web04,基础代码和配置可以从demo-web02中拷贝过来。

在这里,我们将继续调整demo-web02,通过 Gateway 来统一的控制逻辑。

  • 增加路由分组,/admin-api/** 对应管理端接口,/app-api/**对应应用端接口
  • 不同的路由分组使用不同的过滤器,从而实现不同的协议等。

通用

BaseGateway

通过 Component 的标签来添加过滤器的通用逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.demo.web.common.gateway;

import java.util.function.Predicate;
import org.noear.solon.Solon;
import org.noear.solon.core.BeanWrap;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.Gateway;

/**
* @author airhead
*/
public abstract class BaseGateway extends Gateway {
public void addFilters(Predicate<BeanWrap> where) {
Solon.context()
.lifecycle(
() ->
Solon.context()
.beanForeach(
(bw) -> {
if (where.test(bw)) {
if (bw.raw() instanceof Filter) {
filter(bw.index(), bw.raw());
}
}
}));
}
}

SwaggerConfig

对 /admin-api 和 /app-api 的文档进行分组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package com.example.demo.web.common.config;

import com.github.xiaoymin.knife4j.solon.extension.OpenApiExtensionResolver;
import io.swagger.models.Scheme;
import io.swagger.models.auth.ApiKeyAuthDefinition;
import io.swagger.models.auth.In;
import java.util.function.Predicate;
import org.dromara.hutool.core.text.StrUtil;
import org.noear.solon.Solon;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
import org.noear.solon.core.BeanWrap;
import org.noear.solon.docs.DocDocket;
import org.noear.solon.docs.models.ApiContact;
import org.noear.solon.docs.models.ApiInfo;

/**
* @author airhead
*/
@Configuration
public class SwaggerConfig {
@Inject private OpenApiExtensionResolver openApiExtensionResolver;

@Bean("adminApi")
public DocDocket adminApi() {
DocDocket docDocket =
new DocDocket()
.groupName("Admin 端接口")
.info(
new ApiInfo()
.title("porpoise-demo")
.description("在线API文档")
.contact(new ApiContact().name("CrazyAirhead").email("l4qiang@hotmail.com"))
.version("1.0"))
.schemes(Scheme.HTTP.toValue())
.globalResponseInData(true)
.basePath(getBasePath())
.vendorExtensions(openApiExtensionResolver.buildExtensions())
.securityExtensions("token", new ApiKeyAuthDefinition().name("token").in(In.HEADER));

addApis(docDocket, bw -> "adminApi".equals(bw.tag()));

return docDocket;
}

@Bean("appApi")
public DocDocket appApi() {
DocDocket docDocket =
new DocDocket()
.groupName("app 端接口")
.info(
new ApiInfo()
.title("Ficus")
.description("在线App API文档")
.termsOfService("https://jishunetwork.com")
.contact(
new ApiContact()
.name("Ficus")
.url("https://jishunetwork.com")
.email("l4qiang@gmail.com"))
.version("1.0"))
.schemes(Scheme.HTTP.toValue())
.globalResponseInData(true)
.basePath(getBasePath());

addApis(docDocket, bw -> "appApi".equals(bw.tag()));

return docDocket;
}

private String getBasePath() {
String basePath = Solon.cfg().serverContextPath();
if (StrUtil.isBlank(basePath)) {
return "/";
}

if (basePath.endsWith("/")) {
basePath = basePath.substring(0, basePath.length() - 1);
}

return basePath;
}

private void addApis(DocDocket docDocket, Predicate<BeanWrap> where) {
Solon.context()
.lifecycle(
() ->
Solon.context()
.beanForeach(
bw -> {
if (where.test(bw)) {
String name = bw.clz().getName();
docDocket.apis(name);
}
}));
}
}

管理端

AdminGateway

路由注册,添加 adminApi 标签的过滤器,添加 adminApi 标签的请求方法。除了按批添加接口,还可以单独添加,这里未单独列出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demo.web.common.gateway;

import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Mapping;

/**
* @author airhead
*/
@Component
@Mapping("/admin-api/**")
public class AdminGateway extends BaseGateway {
@Override
protected void register() {
// 添加过滤器
addFilters(beanWrap -> "adminApi".equals(beanWrap.tag()));

// 添加接口
addBeans(beanWrap -> "adminApi".equals(beanWrap.tag()));
}
}

AdminExceptionFilter

增加 adminApi 的标签,delivered设置为false,这样不会被加载为全局的路由,index设置为-1,提供有限级。为了区分与 App 端的不同,这里的的错误会返回异常的具体信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.example.demo.web.common.filter;

import com.example.demo.web.common.model.Ret;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.noear.solon.annotation.Component;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.FilterChain;
import org.noear.solon.validation.ValidatorException;

/**
* @author airhead
*/
@Slf4j
@Component(tag = "adminApi", delivered = false, index = -1)
public class AdminExceptionFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
try {
chain.doFilter(ctx);
} catch (ValidatorException e) {
log.error("validator exception", e);

ctx.status(e.getCode());
ctx.outputAsJson(Ret.fail(e.getMessage()).toJson());
} catch (Throwable e) {
log.error("other throwable", e);

ctx.status(500);
ctx.outputAsJson(Ret.fail(e.getMessage()).toJson());
} finally {
// 文件清理放此处也算合理,异常时需要清理和尝试恢复
if (ctx.isMultipartFormData()) {
try {
ctx.filesDelete();
} catch (IOException e) {
log.error("clean temp file exception", e);
}
}
}
}
}

AdminTokenFilter

管理端需要 token 校验,这里也是为了演示通过不同的 Gateway 可以有不同的过滤器的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.demo.web.common.filter;

import lombok.extern.slf4j.Slf4j;
import org.dromara.hutool.core.text.StrUtil;
import org.noear.solon.annotation.Component;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.FilterChain;

/**
* @author airhead
*/
@Slf4j
@Component(tag = "adminApi", delivered = false, index = 0)
public class AdminTokenFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
String token = ctx.header("token");
if (StrUtil.isBlank(token)) {
throw new RuntimeException("token 不能为空");
}

chain.doFilter(ctx);
}
}

DeptController

这里的 Controller 去除了 @Controller 的注解和 @Mapping 的注解,使用的是 @Component 的注解,并设置了adminApi的标签,以便 AdminGateway 能进行批量的添加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.demo.web.manage.controller.admin;

import com.example.demo.web.common.annotation.OperateLog;
import com.example.demo.web.common.model.Ret;
import com.example.demo.web.manage.dto.DeptDto;
import com.example.demo.web.manage.service.DeptService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.util.List;
import org.noear.solon.annotation.*;

/**
* @author airhead
*/
@Component(tag = "adminApi")
@Api("管理端 部门管理")
public class DeptController {
@Inject private DeptService service;

@Mapping(value = "listWithException")
@Get
@ApiOperation("获取列表但返回异常")
public Ret<List<DeptDto>> listWithException() {
return Ret.ok(service.listWithException());
}

@Mapping(value = "list")
@Get
@ApiOperation("获取列表")
@OperateLog
public Ret<List<DeptDto>> list() {
return Ret.ok(service.list());
}
}

应用端

AppGateway

路由注册,添加 appApi 标签的过滤器,添加 appApi 标签的请求方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demo.web.common.gateway;

import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Mapping;

/**
* @author airhead
*/
@Component
@Mapping("/app-api/**")
public class AppGateway extends BaseGateway {
@Override
protected void register() {
// 添加过滤器
addFilters(beanWrap -> "appApi".equals(beanWrap.tag()));

// 添加接口
addBeans(beanWrap -> "appApi".equals(beanWrap.tag()));
}
}

AppExceptionFilter

Component增加 appApi 的标签,delivered设置为false,这样不会被加载为全局的路由,index设置为-1,提高优先级。为了区分与 Admin 端的不同,这里的的错误只返回「服务器错误,请重试」这样的错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.example.demo.web.common.filter;

import com.example.demo.web.common.model.Ret;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.noear.solon.annotation.Component;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.FilterChain;
import org.noear.solon.validation.ValidatorException;

/**
* App
*
* @author airhead
*/
@Slf4j
@Component(tag = "appApi", delivered = false, index = -1)
public class AppExceptionFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
try {
chain.doFilter(ctx);
} catch (ValidatorException e) {
log.error("validator exception", e);

ctx.status(e.getCode());
ctx.outputAsJson(Ret.fail("服务器错误,请重试").toJson());
} catch (Throwable e) {
log.error("other throwable", e);

ctx.status(500);
ctx.outputAsJson(Ret.fail("服务器错误,请重试").toJson());
} finally {
// 文件清理放此处也算合理,异常时需要清理和尝试恢复
if (ctx.isMultipartFormData()) {
try {
ctx.filesDelete();
} catch (IOException e) {
log.error("clean temp file exception", e);
}
}
}
}
}

AppDeptController

这里的 Controller 去除了 @Controller 的注解和 @Mapping 的注解,使用的是 @Component 的注解,并设置了appApi的标签,以便 AdminGateway 能进行批量的添加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.example.demo.web.manage.controller.app;

import com.example.demo.web.manage.dto.DeptDto;
import com.example.demo.web.manage.service.DeptService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.util.List;
import org.noear.solon.annotation.*;
import org.noear.solon.core.handle.ModelAndView;

/**
* @author airhead
*/
@Component(tag = "appApi")
@Api("App 端部门服务")
public class AppDeptController {
@Inject private DeptService service;

@Mapping(value = "listWithException")
@Get
@ApiOperation("获取列表但返回异常")
public List<DeptDto> listWithException() {
return service.listWithException();
}

@Mapping(value = "list")
@ApiOperation("获取列表")
public List<DeptDto> list() {
return service.list();
}

@Mapping("/index")
@Get
@ApiOperation("浏览部门")
public ModelAndView index() {
ModelAndView mv = new ModelAndView();
mv.put("depts", service.list());
mv.view("dept.shtm");

return mv;
}
}

验证

管理端接口请求

img

应用端接口请求

img

小结

本章通过本地网关对路由分组注册的方式演示了统一的异常处理和鉴权的逻辑,其实 Gateway 还能进一步的定制,可以参考官网的文档 https://solon.noear.org/article/214

欢迎关注我的其它发布渠道