CrazyAirhead

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

0%

说明

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

在 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

说明

在数据操作的章节中,我们只是讲解了对关系数据库和非关系数据库的简单的数据操作,在实际的业务当中,操作会更加复杂,因此不可避免的会涉及到数据库的事务和数据的缓存。

Solon 通过 Solon data 提供事务的管理和基础的缓存框架,具体的缓存实现还是通过插件的方式来实现的。

事务

Solon 的事务是通过 AOP 的方式实现的,因此在同一个类中的两个方法的调用,事务传播机制是不会生效的。

Solon 通过 Tran 注解来标记事务,主要有 policy (事务传导策略)和 isolation(事务隔离级别)

事务传导策略

传番策略 说明
TranPolicy.required 支持当前事务,如果没有则创建一个新的。这是最常见的选择。也是默认。
TranPolicy.requires_new 新建事务,如果当前存在事务,把当前事务挂起。
TranPolicy.nested 如果当前有事务,则在当前事务内部嵌套一个事务;否则新建事务。
TranPolicy.mandatory 支持当前事务,如果没有事务则报错。
TranPolicy.supports 支持当前事务,如果没有则不使用事务。
TranPolicy.not_supported 以无事务的方式执行,如果当前有事务则将其挂起。
TranPolicy.never 以无事务的方式执行,如果当前有事务则报错。

事务隔离级别

属性 说明
TranIsolation.unspecified 默认(JDBC默认)
TranIsolation.read_uncommitted 脏读:其它事务,可读取未提交数据
TranIsolation.read_committed 只读取提交数据:其它事务,只能读取已提交数据
TranIsolation.repeatable_read 可重复读:保证在同一个事务中多次读取同样数据的结果是一样的
TranIsolation.serializable 可串行化读:要求事务串行化执行,事务只能一个接着一个执行,不能并发执行

示例

虽然 Solon 支持手动的方式提供对事务的操作,但用注解的方式已经足够用,所以这里就不讲解手动使用的方式。如果需要可以查看官网文档 https://solon.noear.org/article/299 。官网也提供了事务的监听器,可能针对性的对事务的事件做些附加的处理。

基于 Web02 增加用户表(主表)和用户部门表(子表)来演示事务的三个常见:

  1. 回滚,无论添加主表方法异常,还是添加子表方法异常。在这个例子业务数据正常。
  2. 只回滚父事务,但不回滚子事务。也就是主表不会添加数据,子表会添加数据。在这个例子中表现为业务数据异常。
  3. 只回滚子事务,但不回滚父事务。也就是子表不会添加数据,父表会添加数据。在这个例子中表现为业务数据异常。

为了方便说明和查看方便,不同的回滚场景提供的代码会做删减,请注意到仓库获取完整代码。

创建表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `demo_user` (
`id` int NOT NULL AUTO_INCREMENT,
`nick_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '昵称',
`user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户名',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
`create_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creator` varchar(255) DEFAULT NULL COMMENT '创建人',
`updater` varchar(255) DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';

CREATE TABLE `demo_user_dept` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` int DEFAULT NULL COMMENT '用户id',
`dept_id` int DEFAULT NULL COMMENT '部门id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

回滚主子表

回滚主子表的时候,可能出现两种情况,一种是子表方法出现错误,一种是主表方法出现问题。

UserController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller
@Mapping("/user")
@Api("用户管理")
public class UserController {
@Inject private UserService service;

@Mapping("rollbackAll")
@Post
@ApiOperation("回滚全部")
public Boolean rollbackAll(@Body UserDto userDto) {
service.rollbackAll(userDto);

return true;
}

@Mapping("rollbackAll1")
@Post
@ApiOperation("回滚全部1")
public Boolean rollbackAll1(@Body UserDto userDto) {
return service.rollbackAll1(userDto);
}
}
UserService
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
/**
* @author airhead
*/
@Component
@Slf4j
public class UserService {
@Db("db1")
private EasyEntityQuery entityQuery;

@Inject private UserDeptService userDeptService;

@Tran
public void rollbackAll(UserDto userDto) {
UserEntity user = UserConvert.INSTANCE.convert(userDto);

entityQuery.insertable(user).executeRows(true);

UserDeptDto userDeptDto = new UserDeptDto();
userDeptDto.setUserId(user.getId());
userDeptDto.setDeptId(userDto.getDeptId());
// 子表方法异常
userDeptService.rollbackUserDept(userDeptDto);
}

@Tran
public Boolean rollbackAll1(UserDto userDto) {
UserEntity user = UserConvert.INSTANCE.convert(userDto);

entityQuery.insertable(user).executeRows(true);

UserDeptDto userDeptDto = new UserDeptDto();
userDeptDto.setUserId(user.getId());
userDeptDto.setDeptId(userDto.getDeptId());
userDeptService.addUserDept(userDeptDto);

// 主表方法异常
throw new RuntimeException("不能添加主表");
}
}
UserDeptService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author airhead
*/
@Component
@Slf4j
public class UserDeptService {
@Db("db1")
private EasyEntityQuery entityQuery;

@Tran
public void addUserDept(UserDeptDto userDeptDto) {
UserDeptEntity userDept = UserDeptConvert.INSTANCE.convert(userDeptDto);
entityQuery.insertable(userDept).executeRows(true);
}

@Tran
public Boolean rollbackUserDept(UserDeptDto userDeptDto) {
UserDeptEntity userDept = UserDeptConvert.INSTANCE.convert(userDeptDto);
entityQuery.insertable(userDept).executeRows(true);

throw new RuntimeException("不能添加子表");
}
}
效果

这里就不截图了,可以从日志和看数据库数据,可以看到 user 表和 user_dept 表都不会增加数据。

只回滚主表,但不回滚子表

UserController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
@Mapping("/user")
@Api("用户管理")
public class UserController {
@Inject private UserService service;

@Mapping("rollbackMaster")
@Post
@ApiOperation("回滚主表")
public Boolean rollbackMaster(@Body UserDto userDto) {
service.rollbackMaster(userDto);
return true;
}
}
UserService
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
/**
* @author airhead
*/
@Component
@Slf4j
public class UserService {
@Db("db1")
private EasyEntityQuery entityQuery;

@Inject private UserDeptService userDeptService;

@Tran
public void rollbackMaster(UserDto userDto) {
UserEntity user = UserConvert.INSTANCE.convert(userDto);

entityQuery.insertable(user).executeRows(true);

UserDeptDto userDeptDto = new UserDeptDto();
userDeptDto.setUserId(user.getId());
userDeptDto.setDeptId(userDto.getDeptId());

userDeptService.addUserDept1(userDeptDto);

throw new RuntimeException("不能添加主表");
}
}
UserDeptService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author airhead
*/
@Component
@Slf4j
public class UserDeptService {
@Db("db1")
private EasyEntityQuery entityQuery;

@Tran(policy = TranPolicy.requires_new)
public void addUserDept1(UserDeptDto userDeptDto) {
UserDeptEntity userDept = UserDeptConvert.INSTANCE.convert(userDeptDto);
entityQuery.insertable(userDept).executeRows(true);
}
}
效果

这里就不截图了,可以从日志和看数据库数据,可以看到 user 表不会增加数据,但 user_dept 表会增加数据。

只回滚子表,但不回滚主表

UserController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
@Mapping("/user")
@Api("用户管理")
public class UserController {
@Inject private UserService service;

@Mapping("rollbackSub")
@Post
@ApiOperation("回滚子表")
public Boolean rollbackSub(@Body UserDto userDto) {
service.rollbackSub(userDto);

return true;
}
}
UserService
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
/**
* @author airhead
*/
@Component
@Slf4j
public class UserService {
@Db("db1")
private EasyEntityQuery entityQuery;

@Inject private UserDeptService userDeptService;

@Tran
public void rollbackSub(UserDto userDto) {
UserEntity user = UserConvert.INSTANCE.convert(userDto);

entityQuery.insertable(user).executeRows(true);

UserDeptDto userDeptDto = new UserDeptDto();
userDeptDto.setUserId(user.getId());
userDeptDto.setDeptId(userDto.getDeptId());

try {
userDeptService.rollbackUserDept1(userDeptDto);
} catch (Exception ignore) {
// 忽略子表的异常
}
}
}
UserDeptService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author airhead
*/
@Component
@Slf4j
public class UserDeptService {
@Db("db1")
private EasyEntityQuery entityQuery;

@Tran(policy = TranPolicy.nested)
public Boolean rollbackUserDept1(UserDeptDto userDeptDto) {
UserDeptEntity userDept = UserDeptConvert.INSTANCE.convert(userDeptDto);
entityQuery.insertable(userDept).executeRows(true);

throw new RuntimeException("不能添加子表");
}
}
效果

这里就不截图了,可以从日志和看数据库数据,可以看到 user 表会添加数据,但 user_dept 表不会添加数据。

缓存

Solon 通过 solon-data 插件提供缓存的基础框架,然后通过不同的缓存插件实现具体的缓存逻辑,使得应用层可以快速的切换缓存的存储或者实现多级缓存。

Solon 的缓存框架使用 key(唯一标识) 和 tags(标签)两个维度来管理缓存。

  • key :缓存唯一标识,没有指定时会自动生成。无论自己指定还是自动生成都需要注意避免 key 重复。
  • tags:缓存标签,可以用于标签的批量操作,通常为批量删除。

注解说明

@Cache 注解:

属性 说明
service() 缓存服务
seconds() 缓存时间
key() 缓存唯一标识,支持字符串模版
tags() 缓存标签,多个以逗号隔开(为当前缓存块添加标签,用于清除)

@CachePut 注解:

属性 说明
service() 缓存服务
seconds() 缓存时间
key() 缓存唯一标识,支持字符串模版
tags() 缓存标签,多个以逗号隔开(为当前缓存块添加标签,用于清除)

@CacheRemove 注解:

属性 说明
service() 缓存服务
keys() 缓存唯一标识,多个以逗号隔开,支持字符串模版
tags() 缓存标签,多个以逗号隔开(方便清除一批key)

示例

我们继续通过部门管理的例子来演示数据的缓存。

RedisConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author airhead
*/
@Configuration
public class RedisConfig {
@Bean(typed = true)
public CacheService defaultCache(@Inject RedisClient redisClient) {
RedisCacheService cacheService = new RedisCacheService(redisClient, 30);
cacheService.enableMd5key(false);
return cacheService;
}

@Bean(typed = true)
public RedisClient defaultClient(
@Inject("${demo.redis}") RedisClientSupplier redisClientSupplier) {
return redisClientSupplier.get();
}
}

DeptDto

需要实现 Serializable 这样才能给缓存序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author airhead
*/
@ApiModel
@Data
public class DeptDto implements Serializable {
private Long id;

/** 部门名称 */
private String name;

/** 部门编码 */
private String code;
}

DeptService

保存、更新、删除的时候通过tags清理缓存,获取id和list的时候通过key增加缓存,同时增加tags标签。

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
/**
* @author airhead
*/
@Component
@Slf4j
public class DeptService {
@Db("db1")
private EasyEntityQuery entityQuery;

@CachePut(key = "dept:list", tags = "dept")
public List<DeptDto> list() {
log.info("search list from db");

return entityQuery.queryable(DeptEntity.class).select(DeptDto.class).toList();
}

@CacheRemove(tags = "dept")
public Boolean add(DeptDto deptDto) {
DeptEntity dept = DeptConvert.INSTANCE.convert(deptDto);

return entityQuery.insertable(dept).executeRows(true) > 0L;
}

@CacheRemove(tags = "dept")
public Boolean update(DeptDto deptDto) {
DeptEntity dept = DeptConvert.INSTANCE.convert(deptDto);
return entityQuery.updatable(dept).executeRows() > 0L;
}

@CachePut(key = "dept:${id}", tags = "dept")
public DeptDto get(Long id) {
log.info("search from db by id");

return entityQuery
.queryable(DeptEntity.class)
.whereById(id)
.select(DeptDto.class)
.firstOrNull();
}

@CacheRemove(tags = "dept")
public Boolean delete(Long id) {
return entityQuery
.getEasyQueryClient()
.deletable(DeptEntity.class)
.allowDeleteStatement(true)
.whereById(id)
.executeRows()
> 0L;
}
}

效果

调用 list 或 get 接口时缓存,如果存在缓存时不再从数据库中获取数据。

img

小结

要在 Solon 应用中使用事务和缓存是比较容易的,增加对应的注解即可实现相关的功能。有了事务和缓存的支持,数据的可靠性和应用的性能可以进一步得到保障,通过以上内容的学习,基本上可以起飞,做业务开发了。

说明

在基础篇当中,我们讲究了 Solon 对 MVC 的支持和提供的基础的Web 能力。在这个章节我们会讲解 Solon 提供了哪些能力可以供我们进行控制。

首先我们来看看 Solon Web 的请求过程,理解 Solon 的基础概念,这样方便我们后续的定制。

请求过程

img

从图中,我们可以看到在Web 的请求过程中,会经过几个环节:Filter(全局过滤器)->

RouterInterCeptor(路由拦截器)-> Handler(处理器)-> Interceptor(拦截器)。

Context

Solon 采用 Handler + Context 架构,Context 是请求的上下文。 在 Context 中可以获取请求和响应相关的数据,包括请求相关的对象与接口,会话相关的对象与接口,响应相关的对象与接口等。Solon 提供了多种方式获取Context,也为Context 其他了一些常用的接口,更详细的内容可以查看 https://solon.noear.org/article/216

Filter

过滤器,可以对所有请求(无论动态还是静态的请求)进行全局的控制。通常用于全局的 Web 请求异常处理,全局的性能记录,全局的响应状态调整,全局的上下文日志记录,全局的链路跟踪等等,当然也可以作用在局部上。

RouterInterceptor

路由拦截器,可以对动态请求进行全局的控制,功能基本与 Filter 一致。主要的区别是:1. 只能对动态请求(路由器范围)进行拦截;2. 可以修改参数和返回结果。

Interceptor

拦截器,拦截器使用切面(AOP)的方式实现,通过定义注解、设置切点、注册拦截器,Solon 将对方法进行拦截,从而增加拦截器方法的执行。拦截器可以用于实现缓存、事务等等功能。

Action

实际的请求方法,注册到路由器的方法(通常是@Mapping 函数,可以通过 Controller 的方式定义,也可以通过 Handler 的方式定义,等等)。Action 本质上是 Handler 和 Class Method 的结合,可以请求方法进行局部的控制。

示例 demo-web02

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

这里我们继续补充demo-web01 ,增加定制化的处理。

  • 增加简单的鉴权用于演示的Filter。
  • 增加RouterInterceptor,用于演示Filter和RouterInterceptor的区别。
  • 增加日志审计的功能用于演示 Interceptor 拦截器。

全局过滤器

使用全局的Filter做简单的 token 验证。这里需要指定Component的注解,也就是统一有Solon 来管理。如果有多个过滤器是可以指定Component的 index 参数,参数越小,越外层,可以理解为优先级越高。

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
package com.example.demo.web.common.filter;

import com.jfinal.kit.Kv;
import lombok.extern.slf4j.Slf4j;
import org.dromara.hutool.core.text.StrUtil;
import org.noear.snack.ONode;
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
public class GlobalFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
String path = ctx.path();
log.info("1. GlobalFilter, path: {}", path);
// 简单逻辑,非静态的路径都需要token
if (!path.contains(".")) {
String token = ctx.param("token");
if (StrUtil.isBlank(token)) {
ctx.outputAsJson(ONode.stringify(Kv.of("state", false)));
return;
}
}

chain.doFilter(ctx);
}
}

路由拦截器

这边只是为了对比和Filter的不同,如果是静态资源请求时,不会进入路由拦截器,也就不会打印对应的日志。

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

import lombok.extern.slf4j.Slf4j;
import org.noear.solon.annotation.Component;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Handler;
import org.noear.solon.core.route.RouterInterceptor;
import org.noear.solon.core.route.RouterInterceptorChain;

/**
* @author airhead
*/
@Slf4j
@Component
public class DemoRouteInterceptor implements RouterInterceptor {
@Override
public void doIntercept(Context ctx, Handler mainHandler, RouterInterceptorChain chain)
throws Throwable {
String path = ctx.path();
log.info("2. DemoRouteInterceptor, path: {}", path);

chain.doIntercept(ctx, mainHandler);
}
}

局部过滤器

局部过滤器可以通过 Addition 注解直接使用,可以通过注解继承的方式使用。

WhitelistFilter

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

import lombok.extern.slf4j.Slf4j;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.FilterChain;

/**
* @author airhead
*/
@Slf4j
public class WhitelistFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
String path = ctx.path();
log.info("3. WhitelistFilter, path: {}", path);
chain.doFilter(ctx);
}
}

Whitelist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.demo.web.common.annotation;

import com.example.demo.web.common.filter.WhitelistFilter;
import java.lang.annotation.*;
import org.noear.solon.annotation.Addition;

/**
* @author airhead
*/
@Addition(WhitelistFilter.class)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Whitelist {}

拦截器

拦截器可以通过 Around 注解直接使用,可以通过注解继承的方式使用。

LogInterceptor

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

import lombok.extern.slf4j.Slf4j;
import org.noear.solon.core.aspect.Interceptor;
import org.noear.solon.core.aspect.Invocation;
import org.noear.solon.core.handle.Context;

/**
* @author airhead
*/
@Slf4j
public class LogInterceptor implements Interceptor {
@Override
public Object doIntercept(Invocation inv) throws Throwable {
Context context = Context.current();
String path = context.path();
log.info("4. LogInterceptor, path: {}", path);

return inv.invoke();
}
}

OperateLog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.demo.web.common.annotation;

import com.example.demo.web.common.interceptor.LogInterceptor;
import java.lang.annotation.*;
import org.noear.solon.annotation.Around;

/**
* @author airhead
*/
@Around(LogInterceptor.class)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {}

Controller的调整

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
package com.example.demo.web.manage.controller;

import com.example.demo.web.common.annotation.OperateLog;
import com.example.demo.web.common.annotation.Whitelist;
import com.example.demo.web.common.filter.WhitelistFilter;
import com.example.demo.web.common.interceptor.LogInterceptor;
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.Context;
import org.noear.solon.core.handle.ModelAndView;
import org.noear.solon.core.handle.UploadedFile;

@Controller
@Mapping("/dept")
@Api("部门管理")
public class DeptController {
@Inject private DeptService service;

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

return mv;
}

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

验证

  • 请求 /dept/index,不带token得情况,演示全局的过滤器效果

img

  • 请求 /dept/list?tokent=1,带token得情况,验证请求过程,使用的是 Addition 和 Around 的方式添加局部过滤器和拦截器。

img

img

  • 请求 /dept/index?tokent=1,带token得情况,验证请求过程,使用注解继承的方式添加局部过滤器和拦截器。

img

img

  • 请求 /img/wechat.png,验证静态请求不会通过RouterInterceptor。如果静态资源不存在的情况,依然会继续查找动态路由,此时会进入RouterInterceptor。

img

小结

本章节通过示意图的方式简单描述Solon web的请求过程,并对其中的重要概念做了讲解,通过实例的方式正式了对过滤器,路由拦截器和拦截器的定制。

官网对这些内容做了多篇文章的介绍,可以通过官网进一步的了解这些概念和使用方法。

说明

Web 基础主要讲解 Solon 对 Web 开发提供的基础能力,支持 HTML 文档和其他静态资源的获取,提供数据交换的能力。

MVC

Web 开发中最常使用的 MVC 架构,MVC 全称是 “模型 (Model)- 视图 (View)- 控制器 (Controller)”,是一种广泛使用的设计模式,用于将应用程序的逻辑、数据和界面进行分离。

img

在具体对 MVC 的划分中可能存在一些边界的不同,有的人认为 Modle是输入,View 是输出,Controller是处理;有的人认为 Model 是数据,View 是数据的展示与用户的交互,Controller是处理,包含了输入和输出;有的人认为 Model 就是模型,即数据+接口,View 是模型的裁剪,Controller 是数据交互。随着前后端分离的架构的发展,前端也有自己的MVC 的结构,这个时候后端的接口的就成了 Model 层。根据自己对 MVC 的实际理解去用就好了,适合自己就好。

在 Solon 中可以这样理解:solon-data 对应处理 Model 层,提供数据处理、事务、缓存等支持;solon-core/solon-boot 对应处理 Controller 层,提供 Handler + Context 操作接口,提供Http信号接入支持;solon-view 对应处理 View 层(单体结构),提供不同的模板渲染框架,solon-serialization 对应处理 View 层(前后端分离结构),提供JSON、XML、PROTOSTUFF 等不同格式的序列化。

常用注解

@Controller 控制器注解(只有一个注解,会自动通过不同的返回值做不同的处理)
@Param 注入请求参数(包括:QueryString、Form、Path)。起到指定名字、默认值等作用
@Header 注入请求 header
@Cookie 注入请求 cookie
@Path 注入请求 path 变量(因为框架会自动处理,所以这个只是标识下方便文档生成用)
@Body 注入请求体(一般会自动处理。仅在主体的 String, Steam, Map 时才需要)
@Mapping 路由关系映射注解
@Get @Mapping 的辅助注解,便于 RESTful 开发
@Post @Mapping 的辅助注解,便于 RESTful 开发
@Put @Mapping 的辅助注解,便于 RESTful 开发
@Delete @Mapping 的辅助注解,便于 RESTful 开发
@Patch @Mapping 的辅助注解,便于 RESTful 开发
@Produces 输出内容类型申明
@Consumes 输入内容类型申明(当输出内容类型未包函 @Consumes,则响应为 415 状态码)
@Multipart 显式申明支持 Multipart 输入

参数注入

关于 MVC 参数注入,主要尊守以下规则:

  • 参数名与请求数据名一一对应(使用 -parameters)。
  • 当对映不上时,会采用整体数据注入(如果接收的是实体)
  • 参数名与请求数据同名时,又想整体注入(如果接收的是实体),可使用 @Body 注解强制标注。

个人偏好

不同的 MVC 的理解,会有不一样的项目结构,下面是阿里巴巴《Java 开发手册(黄山版)》的截图,可供参考。

img

img

以下属于个人偏好的目录结构,后续的示例会按这个方式处理。

1
2
3
4
5
6
7
domain(业务领域)
- controller(对应Contoller 层,提供用户交互接口)
- admin(管理端接口)
- app(应用端接)
- dto (对应 View 层的数据)
- model(对应 Model 层的数据)
- service(对应 Model 层的接口)

进一步补充,在 controller 中我不会放业务逻辑,只是调用 service进行转发,这样能更大程度的复用Service;在service中通常不采用接口实现的分离方式(虽然这种方式更好,但在编写、查看代码的过程中跳转太多,而且对应业务来说,通常不会有多种实现,有多种实现的逻辑再进行接口实现分离的方式);尽可能少的分层领域模型,避免过多的数据转换操作。

demo-web01

在 IDEA 中通过 File->New->Modules... 可以创建新的模块 demo-web01,基础代码和配置可以从demo-orm中拷贝过来,或者自己手动创建,之后调整包名。

这里我们继续补充demo-orm的部门管理的例子。实现基本的增删改查(CURD)接口,实现数据的导入导出展示文件的上传与下载,实现 index 接口用于展示模版的渲染,同时在模版中访问静态资源。提供 list 展示 json 的序列化,提供 listXml 展示 XML 的序列化。

为了在接口中尽可能多的展示不同的参数注入的使用,接口的定义不会是完全的 RESTful 风格。

修改依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
dependencies {
// enjoy 模版渲染
implementation("org.noear:solon-view-enjoy")

// xml 序列化
implementation("org.noear:solon-serialization-jackson-xml")

// json 序列化,默认就是snack3 可以不配置
implementation("org.noear:solon-serialization-snack3")

// 静态资源访问
implementation("org.noear:solon-web-staticfiles")
}

创建model

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
package com.example.demo.web.manage.model;

import com.easy.query.core.annotation.Column;
import com.easy.query.core.annotation.EntityProxy;
import com.easy.query.core.annotation.Table;
import com.easy.query.core.proxy.ProxyEntityAvailable;
import com.example.demo.web.manage.model.proxy.DeptEntityProxy;
import java.time.LocalDateTime;
import lombok.Data;

/**
* 部门表 实体类。
*
* @author Airhead
* @since 1.0
*/
@Data
@Table(value = "demo_dept")
@EntityProxy
public class DeptEntity implements ProxyEntityAvailable<DeptEntity, DeptEntityProxy> {

/** id */
@Column(primaryKey = true, value = "id", generatedKey = true)
private Long id;

/** 部门名称 */
private String name;

/** 部门编码 */
private String code;

/** 时间时间 */
private LocalDateTime createAt;

/** 更新时间 */
private LocalDateTime updateAt;
}

创建service

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
package com.example.demo.web.manage.service;

import com.easy.query.api.proxy.client.EasyEntityQuery;
import com.easy.query.solon.annotation.Db;
import com.example.demo.web.manage.convert.DeptConvert;
import com.example.demo.web.manage.dto.DeptDto;
import com.example.demo.web.manage.model.DeptEntity;
import java.util.List;
import org.noear.solon.annotation.*;

/**
* @author airhead
*/
@Component
public class DeptService {
@Db("db1")
private EasyEntityQuery entityQuery;

public List<DeptDto> list() {
return entityQuery.queryable(DeptEntity.class).select(DeptDto.class).toList();
}

public Boolean add(DeptDto deptDto) {
DeptEntity dept = DeptConvert.INSTANCE.convert(deptDto);

return entityQuery.insertable(dept).executeRows(true) > 0L;
}

public Boolean update(DeptDto deptDto) {
DeptEntity dept = DeptConvert.INSTANCE.convert(deptDto);
return entityQuery.updatable(dept).executeRows() > 0L;
}

public DeptDto get(Long id) {
return entityQuery
.queryable(DeptEntity.class)
.whereById(id)
.select(DeptDto.class)
.firstOrNull();
}

public Boolean delete(Long id) {
return entityQuery
.getEasyQueryClient()
.deletable(DeptEntity.class)
.allowDeleteStatement(true)
.whereById(id)
.executeRows()
> 0L;
}
}

创建dto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.demo.web.manage.dto;

import lombok.Data;

/**
* @author airhead
*/
@Data
public class DeptDto {
private Long id;

/** 部门名称 */
private String name;

/** 部门编码 */
private String code;
}

创建controller

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
package com.example.demo.web.manage.controller;

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.Context;
import org.noear.solon.core.handle.ModelAndView;
import org.noear.solon.core.handle.UploadedFile;

@Controller
@Mapping("/dept")
@Api("部门管理")
public class DeptController {
@Inject private DeptService service;

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

@Mapping(value = "listXml", produces = "application/xml")
@Get
@ApiOperation("获取列表Xml")
public List<DeptDto> listXml() {
return service.list();
}

@Mapping
@Post
@ApiOperation("新增")
public Boolean add(@Body DeptDto deptDto) {
return service.add(deptDto);
}

@Mapping
@Put
@ApiOperation("更新")
public Boolean update(@Body DeptDto deptDto) {
return service.update(deptDto);
}

@Mapping("/{id}")
@Get
@ApiOperation("获取")
public DeptDto get(@Path Long id) {
return service.get(id);
}

@Mapping
@Delete
@ApiOperation("删除")
public Boolean delete(@Param Long id) {
return service.delete(id);
}

@Mapping("/importData")
@Post
@ApiOperation("上传")
@Multipart
public Boolean importData(UploadedFile file) {
return service.importData(file);
}

@Mapping("/exportData")
@Get
@ApiOperation("下载")
public void exportData(Context context) {
service.exportData(context);
}

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

return mv;
}
}

上传下载

Solon 对上传下载的支持也是比较丰富的,这里只是使用了最常用的方式进行展示。更多的使用方法,参看 https://solon.noear.org/article/313

静态资源

引入solon-web-staticfiles可以提供公共的静态文件服务,默认静态文件目录是resources/static/ 。可以通过配置的的方式调整目录。

1
2
3
4
5
solon:
staticfiles:
mappings:
- path: "/img/"
repository: "classpath:img"

还可以通过代码的方式调整目录,更多的配置参看 https://solon.noear.org/article/268

验证

View

微信号的展示和logo,访问的是静态资源,且使用了不同的目录。

img

Json

img

Xml

img

上传

img

上传文件后,可以在日志中看到上传的文件名,可以在代码根目录看到对应上传的文件。

下载

下载的内容 swagger 工具显示不了,这里只展示返回的头信息。可以在浏览器中直接输入http://localhost:8080/dept/exportData 进行下载测试。

img

问题

在 Solon 3.0.7 版本测试的时候发现上述 controller 的路由定义,可能存在 index 接口无法访问,而路由到的 get 接口的情况,但 3.1.0-SNAPSHOT已经修复这个问题,当前代码实用了 Solon 3.1.0-SNAPSHOT,后续 3.1.0 发布时,教程也会统一更新版本号。

img

小结

MVC 虽然广为人知,但是在细节处还是存在理解的不同。Solon 对 MVC 用完整的支持,可以放心食用。不喜欢示例中的项目结构划分的可以参考阿里或者其他的目录结构进行项目结构的组织。

补充说明

春节期间的囤货已经用完了,也是第一次写比较完整的教程,后续教程的更新可能会比较慢。

说明

一般的 Web 应用都会使用到 Redis 做缓存处理,也可能会用 ElasticSearch 做全文检索。所以这里主要介绍对这个两个数据中间件的使用。Monogo方面也有对应的插件,可以自行尝试。

创建模块

在 IDEA 中通过 File->New->Modules... 可以创建新的模块 demo-nosql,基础代码和配置可以从demo01中拷贝过来,或者自己手动创建。

修改依赖

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
plugins {
id 'java'
id "io.freefair.lombok"
}

group = "com.example"
// 统一版本号
version "${demoVersion}"
description = "orm demo"

dependencies {
implementation platform(project(":demo-parent"))

implementation("org.noear:solon-web")
implementation("org.noear:solon-logging-logback")
implementation("org.noear:solon-openapi2-knife4j")
implementation("org.noear:solon-cache-jedis")
implementation("org.noear:esearchx")

annotationProcessor("org.mapstruct:mapstruct-processor:${mapstructVersion}")

testImplementation("org.noear:solon-test-junit5")
}


compileJava {
options.encoding = "UTF-8"
options.compilerArgs << "-parameters"
}

jar {
manifest {
attributes "Main-Class": "com.example.demo.orm.DemoOrmApp"
}

dependsOn(configurations.runtimeClasspath)

duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(configurations.runtimeClasspath.collect {
if (it.isDirectory()) it else zipTree(it)
}) {
exclude 'META-INF/MANIFEST.MF'
exclude 'META-INF/*.SF'
exclude 'META-INF/*.DSA'
exclude 'META-INF/*.RSA'
}

def sourcesMain = sourceSets.main
sourcesMain.allSource.forEach { println("add from sources: ${it.name}") }
from(sourcesMain.output)
}

使用 Redis

增加依赖

build.gradle

1
2
3
dependencies {
implementation("org.noear:solon-cache-jedis")
}

修改配置

app.yml

1
2
3
4
5
6
7
8
9
10
11
12
# 配置数据源
demo:
redis:
driverType: redis #驱动类型
server: 127.0.0.1:6379
db: 0 #默认为 0,可不配置
password: porpoise-demo
keyHeader: porpoise-demo
enableMd5key: false
defSeconds: 600 #默认为 30,可不配置
idleConnectionTimeout: 10000
connectTimeout: 10000

增加配置类

RedisConfig

1
2
3
4
5
6
7
8
9
10
11
/**
* @author airhead
*/
@Configuration
public class RedisConfig {
@Bean(typed = true)
public RedisClient defaultClient(
@Inject("${demo.redis}") RedisClientSupplier redisClientSupplier) {
return redisClientSupplier.get();
}
}

增加 controller

Controller 非必须,只是方便测试。

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
/**
* @author airhead
*/
@Controller
@Mapping("/redis")
@Api("redis")
public class RedisController {
@Inject private RedisClient redisClient;

@Mapping
@Get
@ApiOperation("获取key的值")
public String get(String key) {
return redisClient.openAndGet(session -> session.key(key).get());
}

@Mapping
@Post
@ApiOperation("设置key的值")
public Boolean set(String key, String value) {
redisClient.open(
session -> {
session.key(key).set(value);
});

return true;
}
}

验证

验证有很多种方式,这里通过 swagger 文档进行调用。浏览器输入 http://localhost:8080/doc.html

设置

img

获取

img

使用 ElasticSearch

增加依赖

build.gradle

1
2
3
4
dependencies {
// es 没有对应的插件,需要自己引入 esearchx
implementation("org.noear:esearchx")
}

修改配置

app.yml

1
2
3
4
5
6
# 配置数据源
demo:
es:
url: http://127.0.0.1:9200
username: root
password: 123456

增加配置类

EsConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author airhead
*/
@Configuration
public class EsConfig {
/**
* url,username,password
*
* @param properties
* @return
*/
@Bean(name = "esContext", typed = true)
public EsContext esContext(@Inject("${demo.es}") Properties properties) {
return new EsContext(properties);
}
}

增加 entity

增加DemoEntity方便操作数据

1
2
3
4
5
6
7
8
/**
* @author airhead
*/
@Data
public class DemoEntity {
private String name;
private String code;
}

增加 controller

Controller 非必须,只是方便测试。

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
/**
* @author airhead
*/
@Controller
@Mapping("/es")
@Api("es")
@Slf4j
public class EsController {
@Inject private EsContext esContext;

@Mapping
@Get
@ApiOperation("获取列表")
public List<DemoEntity> list() {
try {
EsData<DemoEntity> result = null;
result = esContext.indice("demo").selectList(DemoEntity.class);
return result.getList();
} catch (IOException e) {
log.error("add error", e);
throw new RuntimeException(e);
}
}

@Mapping
@Post
@ApiOperation("保存文档")
public Boolean add(@Body DemoEntity entity) {
try {
esContext.indice("demo").insert(entity);
} catch (IOException e) {
log.error("add error", e);
throw new RuntimeException(e);
}

return true;
}
}

验证

验证有很多种方式,这里通过 swagger 文档进行调用。浏览器输入 http://localhost:8080/doc.html

保存文档

img

获取文档列表

img

小结

Solon 通过简单的配置就可以操作 Redis 和 ElasticSearch ,对 NoSql 的支持还是挺完整的,更多的对数据的操作需要看具体的类库提供的接口。

完整代码可以从 https://gitee.com/CrazyAirhead/porpoise-demo 获取。

说明

Solon 不像 Jfinal 提供了基础的数据库能力,是通过集成第三方的 ORM 来实现的,当然 Solon 家也有自己的 ORM(wood)。结合自己使用了多个 ORM 的情况,选择了无 XML 的 easy-query。虽然 Solon 也有 activerecord 的插件,但是 Jfinal 框架内,使用还是不够趁手。

个人确实比较喜欢 activerecord,后续也会补充对 easy-query 的扩展,比如继承 activerecord 的 sql 模版管理,基于 map 的 Model Bean。

这里使用的是 MySQL 数据库,其他数据库是类似的,切换对应的驱动即可。

创建模块

在 IDEA 中通过 File->New->Modules... 可以创建新的模块 demo-orm,基础代码和配置可以从demo01中拷贝过来,或者自己手动创建。

修改依赖

使用初始器创建项目,增加 web 和 easy-query 依赖,同时增加 swagger 依赖,并非必须,只是为了方便测试接口。

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
plugins {
id 'java'
id "io.freefair.lombok"
}

group = "com.example"
// 统一版本号
version "${demoVersion}"
description = "orm demo"

dependencies {
implementation platform(project(":demo-parent"))

implementation("org.noear:solon-web")
implementation("org.noear:solon-logging-logback")
implementation("org.noear:solon-openapi2-knife4j")

implementation("com.zaxxer:HikariCP")
implementation("mysql:mysql-connector-java")
implementation("com.easy-query:sql-solon-plugin")

annotationProcessor("org.mapstruct:mapstruct-processor:${mapstructVersion}")
// 生成 entityProxy
annotationProcessor("com.easy-query:sql-processor:${easyQueryVersion}")

testImplementation("org.noear:solon-test-junit5")
}


compileJava {
options.encoding = "UTF-8"
options.compilerArgs << "-parameters"
}

jar {
manifest {
attributes "Main-Class": "com.example.demo.orm.DemoOrmApp"
}

dependsOn(configurations.runtimeClasspath)

duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(configurations.runtimeClasspath.collect {
if (it.isDirectory()) it else zipTree(it)
}) {
exclude 'META-INF/MANIFEST.MF'
exclude 'META-INF/*.SF'
exclude 'META-INF/*.DSA'
exclude 'META-INF/*.RSA'
}

def sourcesMain = sourceSets.main
sourcesMain.allSource.forEach { println("add from sources: ${it.name}") }
from(sourcesMain.output)
}

创建数据库

1
2
3
4
5
6
7
8
CREATE TABLE `demo_dept` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '部门名称',
`code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '部门编码',
`create_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '时间时间',
`update_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='部门表';

修改配置

app.yml

1
2
3
4
5
6
7
8
# 配置数据源
solon.dataSource:
db1:
jdbcUrl: jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=true
driverClassName: com.mysql.cj.jdbc.Driver
username: demo
password: 123456

配置数据源

1
2
3
4
5
6
7
@Configuration
public class EasyQueryConfig {
@Bean(name = "db1", typed = true)
public DataSource db1(@Inject("${demo.db1}") HikariDataSource ds) {
return ds;
}
}

配置文档

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
/**
* @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)
.vendorExtensions(openApiExtensionResolver.buildExtensions())
.apis("com.example.demo");

return docDocket;
}
}

增加 model

easy-query 提供了插件可以生成 model,通过IDEA 的database 连接数据库,选择表,右键选择TableEntityGenerate,填写对应的信息,点击 ok。

img

img

生成代码后需要使用Ctrl+enter,呼出IDEA的代码生成菜单,点击 EntityQueryImplement。

img

如果没有使用插件,自己创建实体类即可。

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
package com.example.demo.orm.model;

import com.easy.query.core.annotation.Column;
import com.easy.query.core.annotation.EntityProxy;
import com.easy.query.core.annotation.Table;
import com.easy.query.core.proxy.ProxyEntityAvailable;
import com.example.demo.orm.model.proxy.DeptEntityProxy;
import lombok.Data;

import java.time.LocalDateTime;

/**
* 部门表 实体类。
*
* @author Airhead
* @since 1.0
*/
@Data
@Table(value = "demo_dept")
@EntityProxy
public class DeptEntity implements ProxyEntityAvailable<DeptEntity, DeptEntityProxy> {

/**
* id
*/
@Column(primaryKey = true, value = "id", generatedKey = true)
private Long id;

/**
* 部门名称
*/
private String name;

/**
* 部门编码
*/
private String code;

/**
* 时间时间
*/
private LocalDateTime createAt;

/**
* 更新时间
*/
private LocalDateTime updateAt;


}

增加Controller

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
@Controller
@Mapping("/dept")
@Api("部门管理")
public class DeptController {
@Db("db1")
private EasyEntityQuery entityQuery;

@Mapping
@Get
@ApiOperation("获取列表")
public List<DeptEntity> list() {
return entityQuery.queryable(DeptEntity.class).toList();
}

@Mapping
@Post
@ApiOperation("新增")
public Boolean add(@Body DeptEntity dept) {
return entityQuery.insertable(dept).executeRows(true) > 0L;
}

@Mapping
@Put
@ApiOperation("更新")
public Boolean update(@Body DeptEntity dept) {
return entityQuery.updatable(dept).executeRows() > 0L;
}

@Mapping("/{id}")
@Get
@ApiOperation("获取")
public DeptEntity get(Long id) {
return entityQuery.queryable(DeptEntity.class).whereById(id).firstOrNull();
}

@Mapping("/{id}")
@Delete
@ApiOperation("删除")
public Boolean delete(Long id) {
return entityQuery
.getEasyQueryClient()
.deletable(DeptEntity.class)
.allowDeleteStatement(true)
.whereById(id)
.executeRows()
> 0L;
}

验证

验证有很多种方式,这里通过 swagger 文档进行调用。浏览器输入 http://localhost:8080/doc.html

创建

img

查询

img

其他的接口可以自行尝试。

小结

通过以上步骤已经,可以实现对 dept 表的基础增删改查了,可以自己继续添加一些功能进行尝试性的修改。Solon 第三方的 orm 插件非常的多,如果不喜欢使用 easy-query,也可以选择其他 orm 框架进行尝试。

完整代码可以从 https://gitee.com/CrazyAirhead/porpoise-demo 获取。

说明

为了简化和统一后续的开发,我们采用多模块的项目结构,引入 BOM 来统一管理依赖。本人并不擅长前端,前端不会过多讲解,主要基于 Sysbean Admin,后续的例子也将以对接 Sysbean Admin的接口作为例子进行讲解。

多模块

多模块的配置,可以参看 https://mp.weixin.qq.com/s/qXaZ7BCL_0-1hkqTGNxFmA。

在 IDEA 中通过 File->New->Modules... 可以创建新的模块。

先创建一个demo01的模块,然后把第一应用的文件移动到 demo01中(第一个应用的玩转例子保留在 start-up 的分支)。

之后再创建demo-parent模块,该模块用于统一管理依赖。

修改settings.gradle

补充说明写在注释中。

1
2
3
4
5
6
// 将demo,修改名称为porpoise-demo
rootProject.name = "porpoise-demo"

// 创建新模版后自动增加的内容可以不用调整
include 'demo01'
include 'demo-parent'

配置 build.gradle

补充说明写在注释中。

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
plugins {
id 'java'
id "io.freefair.lombok" version "8.11"
}

// 统一 jdk 版本号
def jdkVersion = 17
def demoVersion = "1.0.0"
// processor 的依赖需要指定版本号
def mapstructVersion = "1.6.3"
// processor 的依赖需要指定版本号
def easyQueryVersion = "2.4.14"

group = "com.example.demo"
version "${demoVersion}"

allprojects {
repositories {
mavenLocal()
mavenCentral()
// 增加腾讯云 maven 仓库镜像,提速。
maven { url "https://mirrors.cloud.tencent.com/nexus/repository/maven-public/" }
}

// 版本号传递
ext["jdkVersion"] = "${jdkVersion}"
ext["demoVersion"] = "${demoVersion}"
ext["mapstructVersion"] = "${mapstructVersion}"
ext["easyQueryVersion"] = "${easyQueryVersion}"

tasks.withType(JavaCompile).tap {
configureEach {
options.encoding = "UTF-8"
// 统一指定编译时保留参数名
options.compilerArgs << "-parameters" << "-Xlint:unchecked" << "-Xlint:deprecation"
}
}

}

配置 demo-parant

增加dependencies.gradle

用于统一管理版本号。

1
2
3
4
5
6
7
8
ext {
set("solonVersion", "3.0.7")
set("esearchxVersion", "1.0.22")
set("hikaricpVersion", "5.1.0")
set("mysqlVersion", "8.0.28")
set("hutoolVersion", "6.0.0-M18")
set("transmittableThreadLocalVersion", "2.14.2")
}

配置 build.gradle

补充说明写在注释中。

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
plugins {
// 使用 java-platform 可以统一管理 bom
id 'java-platform'
id 'maven-publish'
}

// 引入版本管理文件
apply from: "dependencies.gradle"

group 'com.example.demo'
version "${demoVersion}"

javaPlatform {
allowDependencies()
}

dependencies {
api platform("org.noear:solon-parent:${solonVersion}")

constraints {
api("com.zaxxer:HikariCP:${hikaricpVersion}")
api("mysql:mysql-connector-java:${mysqlVersion}")
api("com.easy-query:sql-solon-plugin:${easyQueryVersion}")
api("org.noear:esearchx:${esearchxVersion}")
api("org.dromara.hutool:hutool-all:${hutoolVersion}")
api("org.mapstruct:mapstruct:${mapstructVersion}")
api("com.alibaba:transmittable-thread-local:${transmittableThreadLocalVersion}")
}
}

publishing {
publications {
//以下maven并非关键字,可自定义
maven(MavenPublication) {
//通过java-platform生成BOM
from components.javaPlatform
}
}

repositories {
// 根据需要发布到不同的仓库,此处只是发布到本地
mavenLocal()
}
}

配置 demo01

配置 build.gradle

补充说明写在注释中。

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
plugins {
id 'java'
id "io.freefair.lombok"
}

group = "com.example"
// 统一版本号
version "${demoVersion}"
description = "Demo project for Solon"

java {
toolchain {
languageVersion = JavaLanguageVersion.of("${jdkVersion}")
}
}

dependencies {
// 使用 demo-parent,依赖调整的最主要的位置
implementation platform(project(":demo-parent"))

implementation("org.noear:solon-web")
implementation("org.noear:solon-view-freemarker")
implementation("org.noear:solon-logging-logback")

// 删除,已经用插件的方式引入了
// compileOnly("org.projectlombok:lombok")
testImplementation("org.noear:solon-test")
}


compileJava {
options.encoding = "UTF-8"
options.compilerArgs << "-parameters"
}

jar {
manifest {
attributes "Main-Class": "com.example.demo.App"
}

dependsOn(configurations.runtimeClasspath)

duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(configurations.runtimeClasspath.collect {
if (it.isDirectory()) it else zipTree(it)
}) {
exclude 'META-INF/MANIFEST.MF'
exclude 'META-INF/*.SF'
exclude 'META-INF/*.DSA'
exclude 'META-INF/*.RSA'
}

def sourcesMain = sourceSets.main
sourcesMain.allSource.forEach { println("add from sources: ${it.name}") }
from(sourcesMain.output)
}

重新运行demo01

日志中出现如下字样,表示项目结构调整成功,可通过浏览器调用进行测试。

1
App: End loading elapsed=258ms pid=32912 v=3.0.7

img

技术栈

后端

  • JDK 17
  • Gradle 8+
  • Solon 3.0.7
  • Easy-query 2.4.14
  • lombok 8.11
  • Mapstruct 1.6.3
  • Hutool 6.0.0-M18
  • MySQL 8+
  • Redis 6+

前端

具体参看其官方文档,https://docs.soybeanjs.cn/zh/guide/quick-start

  • Node 18.19.0+
  • pnpm 8.7.0+
  • Vue 3
  • Vite 4
  • TypeScript

小结

将项目调整为多模块结构,增加父BOM,并测试 demo01 可以正常运行,为后续项目的开发做好准备。

完整代码可以从 https://gitee.com/CrazyAirhead/porpoise-demo 获取。

创建项目

使用Solon Initializr(https://solon.noear.org/start/),创建基础项目。也可以使用 IDEA 的 Solon 插件,https://plugins.jetbrains.com/plugin/21380-solon,功能类似,用其中一种方式就可以了。

img

这里选择Gradle-Groovey,Java, 3.0.6,jar,jdk17,Solon Web。

运行

解压下载的demo.zip,用 IDEA 打开目录的 build.gradle 文件,弹窗选择open as project。

生成的代码已经包含了一个服务接口,无须额外编写代码。

等 IDEA 加载依赖完毕,即可直接运行 App.java的main函数。

打开浏览器,输入http://localhost:8080/hello

你的第一个 Solon 的 web 应用就完成了。

img

测试

默认生成的Demo 已经提供测试,可以点击后看到如下效果。

img

img

如果提示不存在任何测试的错误,调整 IDEA 的配置。

1
2
3
No matching tests found in any candidate test task.
Requested tests:
Test pattern features.HelloTest.hello in task :demo:test

img

项目结构

img

代码

默认生成的代码可以从下面的分支获取。

https://gitee.com/CrazyAirhead/porpoise-demo/tree/start-up/

性能

Solon 作者针对的提供了多个性能的测试对比,具体看链接 https://solon.noear.org/article/737 的文章。这里不逻辑所有的性能对比,只例出Spring VS Javalin VS Solon的对比,另外测试只是做个参考。不同的环境、场景,效果不同。

测试记录

项目 SpringBoot2 SpringBoot3 Javalin Solon
运行时 java 17 java 17 java 17 java 17
测试前状态/内存 101.1Mb 112.9Mb 66.1Mb 45.6Mb
测试后状态/内存 996.3Mb 326.9Mb 457.3Mb 369.2Mb
测试后状态/并发 2万 2.6万 12万 17万

测试视频

https://www.bilibili.com/video/BV1nJ4m1h79P/

小结

通过 Solon Initializr 可以直接创建基础可运行的应用。

说明

本部分内容建议直接看官网 https://solon.noear.org,以便能看到最新的内容。这里只是简单的罗列。

Solon 体系

img

Solon Core

Solon 核心库,主要分成,容器、插件、上下文处理。

  • Ioc / Aop 容器,提供基于注入依赖的自动装配体系
  • 插件扩展机制,提供“编码风格”的扩展体系
  • 通用上下文处理接口,提供开放式网络协议对接适配体系(俗称,三元合一)

Solon Boot

服务启动器,处理网络协议,三元合一。

Solon Serialization

序列化体系及相关适配插件。

Solon View

Solon Web 的后端视图体系及相关适配插件。

Solon Data

Solon 数据处理,包括事务、缓存、Orm 框架、NoSQL相关的插件。

Solon Doc

Solon 文档相关的插件。

Solon Log

Solon 日志相关的插件。

Solon Test

Solon 单元测试等开发辅助相关的插件。

Solon Security

Solon 安全,鉴权、校验、加密等安全相关的插件。

Solon I18n

Solon 国际化相关的插件。

Solon Native

Solon Native 是可以让 Solon 应用程序以 GraalVM 原生镜像的方式运行的技术方案。

Solon AI

Solon 使用 AI 的相关插件。

Solon Remoting

Solon 远程服务相关的插件及其应用。

Solon Web

Solon Web 一个虚拟的项目,是相关依赖项目的组合。

Solon Scheduling

Solon 任务调度相关的插件。

Solon FaaS

Solon 即时运行函数(一个文件,即为一个函数)的技术方案。

Solon Flow

Solon 流程引擎、规则引擎 等相关的插件。

Solon Cloud

Solon 分布式与微服务开发相关的插件,主要包括:

  • Solon Cloud Gateway 分布式网关相关插件
  • Solon Cloud Config 分布式配置相关插件
  • Solon Cloud Discovery 分布式注册与发现相关插件
  • Solon Cloud Event 分布式时间总线相关插件
  • Solon Cloud Job 分布式任务调度插件
  • Solon Cloud File 分布式文件插件
  • Solon Cloud Log 分布式日志插件
  • Solon Cloud Trace 分布式跟踪插件
  • Solon Cloud Metrics 分布式监控插件
  • Solon Cloud Breaker 分布式熔断插件
  • Solon Cloud Id 分布式 ID 插件
  • Solon Cloud I18n 分布式国际化配置插件
  • Solon Cloud List 分布式名单,白名单、黑名单等
  • Solon Cloud Lock 分布式锁插件

技术趋势

我们关注 Spring Boot3 的话,当前已经只支持 JDK17 以上版本,对 WebFlux,异步编程的支持越来越好。而且因为云原生的发展,Native 项目。随着大语言模型的发展,对构建 AI 应用的需求也会增加。在这些方面 Solon 都是有完整支持的,跟进 Solon 也能保持自己在技术方面的更新迭代。

说明

一次在 Jfinal 的群聊中,无意中发现 Solon 的,说「 Solon只需要几M」。于是搜索到官网,看了官网内容。抱着试一试的态度,于是按着官方文档写例子,发现挺好用的,生态也很完整,自己想要的东西基本都有。

目前自己在尝试构建 Solon 版的快速开发框架,算是学习环境。在公司使用 Solon 和 Solon Cloud 开发安全运营平台,算是实践环境。

在学习实践 Solon 的过程中,也是会碰到问题的,我就会把问题和处理过程记录下来。本着「教是最好的学」的原则,于是整理本教程《Solon 实用教程》,以便自己能更好的理解和使用 Solon。

由于我还在学习和实践 Solon 的过程中,因此本教程会持续更新。同时,由于我的学习可能不够深入或使用不当,教程中难免会出现错误,欢迎大家指正。

Solon 是什么

以下是Solon在开放原子开源基金会的介绍https://www.openatom.cn/project/YNjOOt50Swn2)。

项目简介

Solon 一个是新的Java “生态型”应用开发框架。相对目前主流解决方案。Solon并发高 2~ 3 倍;内存省 50%;启动快 5 ~ 10 倍;打包小 50% ~ 90%;同时兼容 java8 ~ java22 运行时。

项目特点

技术价值

Solon 提供了更高的计算性价比;更好的软件开发效率;快的生产与部署体验;更灵活的兼容选择。

业务价值

Solon解决了Spring 的历史包袱重,架构臃肿,计算资源浪费,学习曲线复杂,以及不再支持java17之前版本等问题。

生态价值

Solon 给Java 生态整合提供了更先进的底座支持。灵活的架构,即可适配传统的java-ee体系,又可以支持最新的技术发展成果。

img

为什么要学 Solon

  • 更快、更小、更简单。Solon并发高 2~ 3 倍;启动快 5 ~ 10 倍;打包小 50% ~ 90%;内存省 50%;最小 Web 完整开发单位 1Mb。除了注解模式之外,还可以按需手动,生态丰富,选择自由。同时兼容 java8 ~ java22 运行时。
  • 国产框架,生态完整。随着国产信创的推荐,使用国产框架将更有优势。
  • 交流方便,容易理解。有官方的沟通交流群和使用 Gitee,沟通交流方面,文档使用中文编写,容易理解。

教程内容

在学习和实践 Solon 的过程中,会发现自己一开始更关注怎么用?接着才会关注为什么要这么用?之后才会想这样设计的好处?如何用 Solon 进行 Web 开发是本教程的主线,本教程不会讲解 Solon 的源码。本教程的主要来源为 Solon 官网Solon 源码和自己的实践(https://gitee.com/CrazyAirhead/projects?sort=&scope=&state=public&search=solonhttps://gitee.com/CrazyAirhead/porpoise-demo)。

本教程主要以下几个部分:

  • 初识 Solon
  • 数据操作
  • Web 开发
  • Solon Boot
  • Solon Cloud

本教程使用 IDEA、JDK17 和 Gradle,需要有这些基础知识。本教程于官网资料的主要不同在于,官网面向的读者更宽泛,会涉及各种不同版本 JDK,Maven 和 Gradle 的配置,不同的编程习惯,比如注解或手动获取 Bean 等,不同的技术偏好,比如多种 ORM 组件等。本教程则更面向实践,并限定了选择,这样可以比较快速简单的入门,实际使用 Solon 进行开发工作。