开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Mybatis扩展之Interceptor

发表于 2021-06-01

Configuration和Interceptor是mybatis中最重要的两个类,Configuration是mybatis中所有数据的载体,包括mybatis配置信息、mapper.xml文件中sql等都存储在Configuration中。而Interceptor则是扩展mybatis功能最重要的接口。

Mybatis通过Interceptor为我们提供了扩展ParameterHandler、ResultSetHandler、StatementHandler、Executor的能力。本章我们介绍Interceptor。首先来看官方给出的example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Intercepts({
@Signature(type = Map.class, method = "get", args = {Object.class})})
public static class AlwaysMapPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return "Always";
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
}
}

上面就是Interceptor最简单的使用方法。

使用Interceptor需要了解的5个类:Interceptor、@Intercepts、@Signature、InterceptorChain、Plugin。

Interceptor

Interceptor是开发者需要去实现的接口,主要实现intercept方法,来定义对代理方法的代理逻辑。其接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class Invocation {

private final Object target;
private final Method method;
private final Object[] args;
}
public interface Interceptor {

//对代理方法进行逻辑扩展
Object intercept(Invocation invocation) throws Throwable;

//对指定对象生成代理类,一般直接使用org.apache.ibatis.plugin.Plugin#wrap,也可以自己实现代理逻辑
Object plugin(Object target);

//设置Properties参数
void setProperties(Properties properties);

}

@Intercepts、@Signature

注解在Interceptor实现类上,用来指定需要被代理的方法。

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
java复制代码@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
Signature[] value();
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
/**
* 需要对哪些类进行代理
* @return
*/
Class<?> type();

/**
* 需要对类的哪些方法进行代理
* @return
*/
String method();

/**
* 配合method()对需要代理的方法进行限定
* @return
*/
Class<?>[] args();
}

需要说明的是虽然上面例子实现了对Map类的代理,但是在Mybatis中一般只对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4个类进行代理。

Plugin

Mybatis提供的默认代理工厂类。

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
java复制代码public class Plugin implements InvocationHandler {

private final Object target;
private final Interceptor interceptor;
private final Map<Class<?>, Set<Method>> signatureMap;

private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}

public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}

InterceptorChain

用来存储多个Interceptor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class InterceptorChain {

private final List<Interceptor> interceptors = new ArrayList<>();

public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}

public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}

public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}

}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

什么是微服务网关?SpringCloud Gateway保姆

发表于 2021-06-01

本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接

在这里插入图片描述

什么是微服务网关

SpringCloud Gateway是Spring全家桶中一个比较新的项目,Spring社区是这么介绍它的:

该项目借助Spring WebFlux的能力,打造了一个API网关。旨在提供一种简单而有效的方法来作为API服务的路由,并为它们提供各种增强功能,例如:安全性,监控和可伸缩性。

而在真实的业务领域,我们经常用SpringCloud Gateway来做微服务网关,如果你不理解微服务网关和传统网关的区别,可以阅读此篇文章 Service Mesh和API Gateway关系深度探讨 来了解两者的定位区别。

以我粗浅的理解,传统的API网关,往往是独立于各个后端服务,请求先打到独立的网关层,再打到服务集群。而微服务网关,将流量从南北走向改为东西走向(见下图),微服务网关和后端服务是在同一个容器中的,所以也有个别名,叫做Gateway Sidecar。

img

为啥叫Sidecar,这个词应该怎么理解呢,吃鸡里的三蹦子见过没:

image-20210519001602403

摩托车是你的后端服务,而旁边挂着的额外座椅就是微服务网关,他是依附于后端服务的(一般是指两个进程在同一个容器中),是不是生动形象了一些。

由于本人才疏学浅,对于微服务相关概念理解上难免会有偏差。就不在此详细讲述原理性的文字了。

本文只探讨SpringCloud Gateway的入门搭建和实战踩坑。 如果小伙伴们对原理感兴趣,可以等后续原理分析文章。

注:本文网关项目在笔者公司已经上线运行,每天承担百万级别的请求,是经过实战验证的项目。

文章目录

  • 让我们造一个网关把
    • 引入pom依赖
    • 编写yml文件
    • 接口转义问题
    • 获取请求体(Request Body)
  • 踩坑实战
    • 获取客户端真实IP
    • 尾缀匹配
  • 总结

源代码

完整项目源代码已经收录到我的Github:

github.com/qqxx6661/sp…

让我们造一个网关把

引入pom依赖

我使用了spring-boot 2.2.5.RELEASE作为parent依赖:

1
2
3
4
5
6
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

在dependencyManagement中,我们需要指定sringcloud的版本,以便保证我们能够引入我们想要的SpringCloud Gateway版本,所以需要用到dependencyManagement:

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

最后,是在dependency中引入spring-cloud-starter-gateway:

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

如此一来,我们便引入了2.2.5.RELEASE版本的网关:

此外,请检查一下你的依赖中是否含有spring-boot-starter-web,如果有,请干掉它。因为我们的SpringCloud Gateway是一个netty+webflux实现的web服务器,和Springboot Web本身就是冲突的。

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

做到这里,实际上你的项目就已经可以启动了,运行SpringcloudGatewayApplication,得到结果如图:

image-20210518192253579

编写yml文件

SpringBoot的核心概念是约定优先于配置,在以前初学Spring时,一直不理解这句话的意思,在使用SpringCloud Gateway时,更加深入的理解了这句话。在默认情况下,你不需要任何的配置,就能够运行起来最基本的网关。针对你之后特定的需求,再去追加配置。

而SpringCloud Gateway更强大的一点就是内置了非常多的默认功能实现,你需要的大部分功能,比如在请求中添加一个header,添加一个参数,都只需要在yml中引入相应的内置过滤器即可。

可以说,yml是整个SpringCloud Gateway的灵魂。

一个网关最基本的功能,就是配置路由,在这方面,SpringCloud Gateway支持非常多方式。比如:

  • 通过时间匹配
  • 通过 Cookie 匹配
  • 通过 Header 属性匹配
  • 通过 Host 匹配
  • 通过请求方式匹配
  • 通过请求路径匹配
  • 通过请求参数匹配
  • 通过请求 ip 地址进行匹配

这些在官网教程中,都有详细的介绍,就算你百度下,也会有很多民间翻译的入门教程,我就不再赘述了,我只用一个请求路径做一个简单的例子。

在公司的项目中,由于有新老两套后台服务,我们使用不同的uri路径进行区分。

  • 老服务路径为:url/api/xxxxxx,服务端口号为8001
  • 新服务路径为:url/api/v2/xxxxx,服务端口号为8002

那么可以直接在yml里面配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码logging:
level:
org.springframework.cloud.gateway: DEBUG
reactor.netty.http.client: DEBUG

spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=gateway-env, springcloud-gateway
routes:
- id: "server_v2"
uri: "http://127.0.0.1:8002"
predicates:
- Path=/api/v2/**
- id: "server_v1"
uri: "http://127.0.0.1:8001"
predicates:
- Path=/api/**

上面的代码解释如下:

  • logging:由于文章需要,我们打开gateway和netty的Debug模式,可以看清楚请求进来后执行的流程,方便后续说明。
  • default-filters:我们可以方便的使用default-filters,在请求中加入一个自定义的header,我们加入一个KV为gateway-env:springcloud-gateway,来注明我们这个请求经过了此网关。这样做的好处是后续服务端也能够看到。
  • routes:路由是网关的重点,相信读者们看代码也能理解,我配置了两个路由,一个是server_v1的老服务,一个是server_v2的新服务。**请注意,一个请求满足多个路由的谓词条件时,请求只会被首个成功匹配的路由转发。**由于我们老服务的路由是/xx,所以需要将老服务放在后面,优先匹配词缀/v2的新服务,不满足的再匹配到/xx。

来看一下http://localhost:8080/api/xxxxx的结果:

image-20210519104944910

来看一下http://localhost:8080/api/v2/xxxxx的结果:

image-20210519105037416

可以看到两个请求被正确的路由了。由于我们真正并没有开启后端服务,所以最后一句error请忽略。

接口转义问题

在公司实际的项目中,我在搭建好网关后,遇到了一个接口转义问题,相信很多读者可能也会碰到,所以在这里我们最好是防患于未然,优先处理下。

问题是这样的,很多老项目在url上并没有进行转义,导致会出现如下接口请求,http://xxxxxxxxx/api/b3d56a6fa19975ba520189f3f55de7f6/140x140.jpg?t=1“

这样请求过来,网关会报错:

java.lang.IllegalArgumentException: Invalid character '=' for QUERY_PARAM in "http://pic1.ajkimg.com/display/anjuke/b3d56a6fa19975ba520189f3f55de7f6/140x140.jpg?t=1"

在不修改服务代码逻辑的前提下,网关其实已经可以解决这件事情,解决办法就是升级到2.1.1.RELEASE以上的版本。

The issue was fixed in version spring-cloud-gateway 2.1.1.RELEASE.

所以我们一开始就是用了高版本2.2.5.RELEASE,避免了这个问题,如果小伙伴发现之前使用的版本低于 2.1.1.RELEASE,请升级。

获取请求体(Request Body)

在网关的使用中,有时候会需要拿到请求body里面的数据,比如验证签名,body可能需要参与签名校验。

但是SpringCloud Gateway由于底层采用了webflux,其请求是流式响应的,即 Reactor 编程,要读取 Request Body 中的请求参数就没那么容易了。

网上谷歌了很久,很多解决方案要么是彻底过时,要么是版本不兼容,好在最后参考了这篇文章,终于有了思路:

www.jianshu.com/p/db3b15aec…

首先我们需要将body从请求中拿出来,由于是流式处理,Request的Body是只能读取一次的,如果直接通过在Filter中读取,会导致后面的服务无法读取数据。

SpringCloud Gateway 内部提供了一个断言工厂类ReadBodyPredicateFactory,这个类实现了读取Request的Body内容并放入缓存,我们可以通过从缓存中获取body内容来实现我们的目的。

首先新建一个CustomReadBodyRoutePredicateFactory类,这里只贴出关键代码,完整代码请看可运行的Github仓库:

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
java复制代码@Component
public class CustomReadBodyRoutePredicateFactory extends AbstractRoutePredicateFactory<CustomReadBodyRoutePredicateFactory.Config> {

protected static final Log log = LogFactory.getLog(CustomReadBodyRoutePredicateFactory.class);
private List<HttpMessageReader<?>> messageReaders;

@Value("${spring.codec.max-in-memory-size}")
private DataSize maxInMemory;

public CustomReadBodyRoutePredicateFactory() {
super(Config.class);
this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
}

public CustomReadBodyRoutePredicateFactory(List<HttpMessageReader<?>> messageReaders) {
super(Config.class);
this.messageReaders = messageReaders;
}

@PostConstruct
private void overrideMsgReaders() {
this.messageReaders = HandlerStrategies.builder().codecs((c) -> c.defaultCodecs().maxInMemorySize((int) maxInMemory.toBytes())).build().messageReaders();
}

@Override
public AsyncPredicate<ServerWebExchange> applyAsync(Config config) {
return new AsyncPredicate<ServerWebExchange>() {
@Override
public Publisher<Boolean> apply(ServerWebExchange exchange) {
Class inClass = config.getInClass();
Object cachedBody = exchange.getAttribute("cachedRequestBodyObject");
if (cachedBody != null) {
try {
boolean test = config.predicate.test(cachedBody);
exchange.getAttributes().put("read_body_predicate_test_attribute", test);
return Mono.just(test);
} catch (ClassCastException var6) {
if (CustomReadBodyRoutePredicateFactory.log.isDebugEnabled()) {
CustomReadBodyRoutePredicateFactory.log.debug("Predicate test failed because class in predicate does not match the cached body object", var6);
}
return Mono.just(false);
}
} else {
return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> {
return ServerRequest.create(exchange.mutate().request(serverHttpRequest).build(), CustomReadBodyRoutePredicateFactory.this.messageReaders).bodyToMono(inClass).doOnNext((objectValue) -> {
exchange.getAttributes().put("cachedRequestBodyObject", objectValue);
}).map((objectValue) -> {
return config.getPredicate().test(objectValue);
}).thenReturn(true);
});
}
}

@Override
public String toString() {
return String.format("ReadBody: %s", config.getInClass());
}
};
}

@Override
public Predicate<ServerWebExchange> apply(Config config) {
throw new UnsupportedOperationException("ReadBodyPredicateFactory is only async.");
}
}

代码主要作用:在有body的请求到来时,将body读取出来放到内存缓存中。若没有body,则不作任何操作。

这样我们便可以在拦截器里使用exchange.getAttribute(“cachedRequestBodyObject”)得到body体。

对了,我们还没有演示一个filter是如何写的,在这里就先写一个完整的demofilter。

让我们新建类DemoGatewayFilterFactory:

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
java复制代码@Component
public class DemoGatewayFilterFactory extends AbstractGatewayFilterFactory<DemoGatewayFilterFactory.Config> {

private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";

public DemoGatewayFilterFactory() {
super(Config.class);
log.info("Loaded GatewayFilterFactory [DemoFilter]");
}

@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("enabled");
}

@Override
public GatewayFilter apply(DemoGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
if (!config.isEnabled()) {
return chain.filter(exchange);
}
log.info("-----DemoGatewayFilterFactory start-----");
ServerHttpRequest request = exchange.getRequest();
log.info("RemoteAddress: [{}]", request.getRemoteAddress());
log.info("Path: [{}]", request.getURI().getPath());
log.info("Method: [{}]", request.getMethod());
log.info("Body: [{}]", (String) exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY));
log.info("-----DemoGatewayFilterFactory end-----");
return chain.filter(exchange);
};
}

public static class Config {

private boolean enabled;

public Config() {}

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}

这个filter里,我们拿到了新鲜的请求,并且打印出了他的path,method,body等。

我们发送一个post请求,body就写一个“我是body”,运行网关,得到结果:

image-20210520162659562

是不是非常清晰明了!

你以为这就结束了吗?这里有两个非常大的坑。

1. body为空时处理

上面贴出的CustomReadBodyRoutePredicateFactory类其实已经是我修复过的代码,里面有一行.thenReturn(true)是需要加上的。这才能保证当body为空时,不会报出异常。至于为啥一开始写的有问题,显然因为我偷懒了,直接copy网上的代码了,哈哈哈哈哈。

2. body大小超过了buffer的最大限制

这个情况是在公司项目上线后才发现的,我们的请求里body有时候会比较大,但是网关会有默认大小限制。所以上线后发现了频繁的报错:

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144

谷歌后,找到了解决方案,需要在配置中增加了如下配置

1
2
3
yaml复制代码spring: 
codec:
max-in-memory-size: 5MB

把buffer大小改到了5M。

你以为这就又双叕结束了,太天真了,你会发现可能没有生效。

问题的根源在这里:我们在spring配置了上面的参数,但是我们自定义的拦截器是会初始化ServerRequest,这个DefaultServerRequest中的HttpMessageReader会使用默认的262144

所以我们在此处需要从Spring中取出CodecConfigurer, 并将里面的Reader传给serverRequest。

详细的debug过程可以看这篇参考文献:

theclouds.io/tag/spring-…

OK,找到问题后,就可以修改我们的代码,在CustomReadBodyRoutePredicateFactory里,增加:

1
2
3
4
5
6
7
java复制代码@Value("${spring.codec.max-in-memory-size}")
private DataSize maxInMemory;

@PostConstruct
private void overrideMsgReaders() {
this.messageReaders = HandlerStrategies.builder().codecs((c) -> c.defaultCodecs().maxInMemorySize((int) maxInMemory.toBytes())).build().messageReaders();
}

这样每次就会使用我们的5MB来作为最大缓存限制了。

依然提醒一下,完整的代码可以请看可运行的Github仓库

讲到这里,入门实战就差不多了,你的网关已经可以上线使用了,你要做的就是加上你需要的业务功能,比如日志,延签,统计等。

踩坑实战

获取客户端真实IP

很多时候,我们的后端服务会去通过host拿到用户的真实IP,但是通过外层反向代理nginx的转发,很可能就需要从header里拿X-Forward-XXX类似这样的参数,才能拿到真实IP。

在我们加入了微服务网关后,这个复杂的链路中又增加了一环。

这不,如果你不做任何设置,由于你的网关和后端服务在同一个容器中,你的后端服务很有可能就会拿到localhost:8080(你的网关端口)这样的IP。

这时候,你需要在yml里配置PreserveHostHeader,这是SpringCloud Gateway自带的实现:

1
2
makefile复制代码filters:
- PreserveHostHeader # 防止host被修改为localhost

字面意思,就是将Host的Header保留起来,透传给后端服务。

filter里面的源码贴出来给大家:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public GatewayFilter apply(Object config) {
return new GatewayFilter() {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(ServerWebExchangeUtils.PRESERVE_HOST_HEADER_ATTRIBUTE, true);
return chain.filter(exchange);
}

public String toString() {
return GatewayToStringStyler.filterToStringCreator(PreserveHostHeaderGatewayFilterFactory.this).toString();
}
};
}

尾缀匹配

公司的项目中,老的后端仓库api都以.json结尾(/api/xxxxxx.json),这就催生了一个需求,当我们对老接口进行了重构后,希望其打到我们的新服务,我们就要将.json这个尾缀切除。可以在filters里设置:

1
2
ini复制代码filters:
- RewritePath=(?<segment>/?.*).json, $\{segment} # 重构接口抹去.json尾缀

这样就可以实现打到后端的接口去除了.json后缀。

总结

本文带领读者一步步完成了一个微服务网关的搭建,并且将许多可能隐藏的坑进行了解决。最后的成品项目在笔者公司已经上线运行,并且增加了签名验证,日志记录等业务,每天承担百万级别的请求,是经过实战验证过的项目。

最后再发一次项目源码仓库:

github.com/qqxx6661/sp…

感谢大家的支持,如果文章对你起到了一丁点帮助,请点赞转发支持一下!

你们的反馈是我持续更新的动力,谢谢~

参考

cloud.tencent.com/developer/a…

juejin.cn/post/684490…

segmentfault.com/a/119000001…

cloud.spring.io/spring-clou…

www.cnblogs.com/savorboard/…

www.servicemesher.com/blog/servic…

www.cnblogs.com/hyf-huangyo…

www.codercto.com/a/52970.htm…

github.com/spring-clou…

blog.csdn.net/zhangzhen02…

关注我

我是一名奋斗在一线的互联网后端开发工程师。

平时主要关注后端开发,数据安全,边缘计算等方向,欢迎交流。

个人公众号:后端技术漫谈

如果文章对你有帮助,请各位老板点赞在看转发支持一下,你的支持对我非常重要~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot 2x系列:RestTemplate

发表于 2021-06-01

概览

上一篇介绍了如何使用Spring Boot构建RESTful风格Web服务的实现方法,juejin.cn/post/695494…,完成了Web服务构建后,大部分情况下将接口提供给前端的小伙伴去调用就可以了,实际开发过程中也会涉及到后端去调用另外一个后端写的接口,我们需要做的事情就是如何对服务进行消费,目前常用有三种方式

  • JDK自带的HttpConnection
  • Apache的HttpClient
  • Spring封装的RestTemplate

严格意思是RestTemplate底层并没有自己去实现Http协议栈,RestTemplate只是Spring定义一个模板,类似JDBCTemplate,底层实际发送Http请求还是通过HttpConnetion或者HttpClient,具体使用哪个类库,可以在自定义RestTemplate的时候来指定。当然相较传统的 HttpClient 客户端工具类库,RestTemplate 在编码的简便性以及异常的处理等方面都做了很多改进。

RestTemplate常用方法介绍

在远程访问上,RestTemplate内置了一批常用的工具方法,我们可以根据HTTP的语义以及RESTful的设计原则对这些方法进行归类:

HTTP方法 RestTemplate方法组
GET getForObject/getForEntity
POST postForLocation/postForObject/postForEntity
PUT put
DELETE delete
Header headForHeaders
不限 Exchange/execute

好,常用方法介绍完后,接下来我们通过一个简单的入门,把这些方法实际走一边,注意:本节的入门Demo需要依赖上一节介绍构建RESTful Web服务提供的接口服务

简单入门

实例代码对应的仓库地址:

github.com/dragon8844/…

gitee.com/drag0n/spri…

引入依赖

在pom.xml的文件中引入相关依赖:

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
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--简化代码-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!--使用httpclient实现http客户端-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>

<!-- 方便等会写单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>

添加配置

在resources目录下创建应用的配置文件application.yml,我们这里使用8081,8080端口留给RESTful web服务,添加如下配置内容:

1
2
3
yaml复制代码# 用来修改web服务的端口
server:
port: 8081

编写代码

自定义RestTemplate

1
2
3
4
5
6
7
8
9
java复制代码@Bean
public RestTemplate customRestTemplate(){
        HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
        httpRequestFactory.setConnectionRequestTimeout(3000);
        httpRequestFactory.setConnectTimeout(3000);
        httpRequestFactory.setReadTimeout(3000);
 
        return new RestTemplate(httpRequestFactory);
}

定义统一响应VO R

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
java复制代码@Data
public class R<T> implements Serializable {
private static final String DEFAULT_SUCCESS_MSG = "请求成功";
private static final int DEFAULT_SUCCESS_CODE = 0;
public static final int SUCCESS = DEFAULT_SUCCESS_CODE;
private Integer code;
private String msg;
private T data;
public static<T> R ok(T data){
R jsonResult = new R();
jsonResult.setCode(DEFAULT_SUCCESS_CODE);
jsonResult.setMsg(DEFAULT_SUCCESS_MSG);
jsonResult.setData(data);
return jsonResult;
}
public static<T> R err(int code, String msg , T data){
R jsonResult = new R();
jsonResult.setCode(code);
jsonResult.setMsg(msg);
jsonResult.setData(data);
return jsonResult;
}
public static<T> R err(int code, String msg){
R jsonResult = new R();
jsonResult.setCode(code);
jsonResult.setMsg(msg);
return jsonResult;
}
}

定义User请求VO

1
2
3
4
5
6
7
java复制代码@Data
public class UserReqVO implements Serializable {

private String username;
private String password;

}

定义User响应VO

1
2
3
4
5
6
7
java复制代码@Data
public class UserRespVO implements Serializable {

private Integer id;
private String username;
private Date createTime;
}

编写UserClient使用restTemplate访问接口

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
java复制代码@Component
@Slf4j
public class UserClient {

@Resource
RestTemplate restTemplate;

public R<UserRespVO> getForObject(Integer id){
Map<String, Object> uriVariables = new HashMap<>(1);
uriVariables.put("id", id);
R r = restTemplate.getForObject("http://localhost:8080/v1/user/{id}", R.class, uriVariables);
return r;
}

public R<UserRespVO> getForEntity(Integer id){
Map<String, Object> uriVariables = new HashMap<>(1);
uriVariables.put("id", id);
ResponseEntity<R> r = restTemplate.getForEntity("http://localhost:8080/v1/user/{id}", R.class,uriVariables);
log.info(r.getHeaders().toString());
return r.getBody();
}

public R<Boolean> postForObject(UserReqVO userReqVO){
R r = restTemplate.postForObject("http://localhost:8080/v1/user", userReqVO, R.class);
return r;
}

public R<Boolean> postForEntity(UserReqVO userReqVO){
ResponseEntity<R> r = restTemplate.postForEntity("http://localhost:8080/v1/user", userReqVO, R.class);
log.info(r.getHeaders().toString());
return r.getBody();
}

/**
* exchange 是一个通用且统一的方法,它既能发送 GET 和 POST 请求,也能用于发送其他各种类型的请求。
* @return
*/
public R<UserRespVO> exchange(Integer id){
Map<String, Object> uriVariables = new HashMap<>(1);
uriVariables.put("id", id);
ResponseEntity<R> r = restTemplate.exchange("http://localhost:8080/v1/user/{id}", HttpMethod.GET, null, R.class, uriVariables);
log.info(r.getHeaders().toString());
return r.getBody();
}

public R<Boolean> put(Integer id, UserReqVO userReqVO){
restTemplate.put("http://localhost:8080/v1/user/{id}", userReqVO, id);
return R.ok(true);
}

public R<Boolean> delete(Integer id){
restTemplate.delete("http://localhost:8080/v1/user/{id}", id);
return R.ok(true);
}
}
  • getForObject() 发送一个HTTP GET请求,返回的请求体将映射为业务对象
  • getForEntity() 发送一个HTTP GET请求,返回的ResponseEntity包含了响应体所映射成的业务对象,在这个对象中还包含了 HTTP 消息头等信息,而且getForObject方法返回的只是业务对象
  • postForObject() POST 数据到一个URL,返回根据响应体匹配形成的对象
  • postForEntity()
    POST 数据到一个URL,返回包含一个对象的ResponseEntity,这个对象是从响应体中映射得
    到的,和getForEntity类似,ResponseEntity对象中还包含了HTTP消息头等信息。
  • put() PUT 资源到特定的URL
  • delete() 在特定的URL上对资源执行HTTP DELETE操作
  • exchange()
    在URL上执行特定的HTTP方法,是一个通用且统一的方法,它既能发送 GET 和 POST 请求,也能用于发送其他各种类型的请求。返回包含对象的ResponseEntity,这个对象是从响应体中映射得到的,同时包含了HTTP消息头等信息。

单元测试

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
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class UserClientTest {

@Resource
UserClient userClient;

@Test
public void getForObject() {

R r = userClient.getForObject(9991);
log.info("result : {}", r);
}
@Test
public void getForEntity() {
R r = userClient.getForEntity(9991);
log.info("result : {}", r);
}
@Test
public void postForObject() {
UserReqVO userReqVO = new UserReqVO();
userReqVO.setUsername("张三");
userReqVO.setPassword("123456");
R r = userClient.postForObject(userReqVO);
log.info("result : {}", r);
}
@Test
public void postForEntity() {
UserReqVO userReqVO = new UserReqVO();
userReqVO.setUsername("张三");
userReqVO.setPassword("123456");
R r = userClient.postForEntity(userReqVO);
log.info("result : {}", r);
}
@Test
public void exchange() {
R r = userClient.exchange(9991);
log.info("result : {}", r);
}
@Test
public void put() {
UserReqVO userReqVO = new UserReqVO();
userReqVO.setUsername("张三");
userReqVO.setPassword("123456");
R r = userClient.put(1,userReqVO);
log.info("result : {}", r);
}
@Test
public void delete() {
R r = userClient.delete(1);
log.info("result : {}",r);
}
}

小结

我们在Spring Boot构建RESTful风格Web服务的基础,通过引入RestTemplate模板实现了对远程服务接口的访问,RestTemplate 为开发人员提供了一大批有用的工具方法来实现 HTTP 请求的发送以及响应的获取。同时,该模板类还开发了一些定制化的入口供开发人员嵌入,用来实现对 HTTP 请求过程进行精细化管理的处理逻辑。和 JdbcTemplate 一样,RestTemplate 在设计和实现上也是一款非常有效的工具类。希望这个简单的入门对您有帮忙~

最后说一句

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力,多谢支持。

此外,关注公众号:黑色的灯塔,专注Java后端技术分享,涵盖Spring,Spring Boot,SpringCloud,Docker,Kubernetes中间件等技术。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

三年了,总结一下,一起加油~

发表于 2021-06-01

前言

有工作在合肥的小伙伴吗?作为一名Java后端开发,3年工作经验的你在合肥的薪资是多少呀?3年工作经验我们应该具备哪些技能呢?你们对自己的职业生涯有规划吗?从事 java开发工作,现在也三年多了,都说程序员3年是一个阶段,5年一个阶段,10年一个阶段,嗯,我觉得说的很有道理。

程序员的几个阶段

  • 三年

这个阶段将会淘汰掉一批不适合写代码的人。这一阶段,我们走出校园,迈入社会,成为一名程序员,正式从书本上的内容迈向真正的企业级开发。我们知道如何团队协作、如何使用项目管理工具、项目版本如何控制、我们写的代码如何测试如何在线上运行等等,积累了一定的开发经验,也对代码有了一定深入的认识,是一个比较纯粹的Coder的阶段;

  • 五年

有些人在三年里,除了完成工作,在空余时间基本不会研究别的东西,这些人永远就是个Coder,年纪大一些势必被更年轻的人给顶替;有些人在三年里,除了写代码之外,还热衷于研究各种技术实现细节、看了N多好书、写一些博客、在Github上分享技术,这些人在五年后必然具备在技术上独当一面的能力并且清楚自己未来的发展方向,从一个Coder逐步走向系统分析师或是架构师,成为项目组中不可或缺的人物;

  • 十年

十年又是另一个门槛了,转行或是继续做一名程序员就在这个节点上。如果在前几年就抱定不转行的思路并且为之努力的话,那么在十年的这个节点上,有些人必然成长为一名对行业有着深入认识、对技术有着深入认识、能从零开始对一个产品进行分析的程序员,这样的人在公司基本担任的都是CTO、技术专家、首席架构师等最关键的职位,这对于自己绝对是一件荣耀的事,当然老板在经济上也绝不会亏待你。

需要具备的技能

3年工作经验的Java后端应该具备的技能,这里要说明一下,以下列举的内容只是我自己的一些见解~

基础知识

这部分不用说,肯定要不断的巩固了,有点东西需要我们细细推敲,基础知识打牢了,学习其他的框架什么的就容易多了,重点是集合、多线程、io等这些知识。

B站上有很多很好的视频可以去看,例如 尚硅谷 系列的文章

对于Java开发刚入门的同学,可以看看下面的这本书,还是很不错的。

链接:pan.baidu.com/s/1J16FkCLh…

提取码:ishy

image-20210929130308493

设计模式

经典设计模式总共有23种(现在远不止23种了,还有一些变种),全部掌握难度太大了,我们只需要掌握一些常用的就好了,必须要掌握的我用小红旗已经标出来了。

img

设计模式在工作中还是非常重要、非常有用的,项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。这里我推荐小傅哥《重学 Java 设计模式》,我看完了,写的非常通俗易懂,pdf和源码我都下载了,可以从我的百度云盘下载:

链接:https://pan.baidu.com/s/1bMri7SgHPkwnyy1AzYSjMw

提取码:bdbu

JVM

我觉得还是有必要了解JVM的知识的,网上也有好多视频的讲解,分享几个链接吧

尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机)

框架

最常见的spring、springboot、springcloud、mybatis、netty、dubbo等框架,还有好多,不一一列举了,我们不仅要会用,还要知道其原理。

关于基础框架这部分,大神们的学习方法是:使用框架 -> 懂框架 -> 造轮子。

相关资料

《Spring技术内幕》和《Spring实战》下载地址:

链接:pan.baidu.com/s/19J3xeuJq…

提取码:zc3z

数据库

MYSQL是必须要会的,还有缓存数据库啊,像Redis,也很重要,一般面试java后端的时候都会问。

尚硅谷MySQL数据库高级,mysql优化,数据库优化

下面这个是我自己收藏的关于MYSQL的一个视频,我感觉还是挺不错的,感兴趣的可以看看。

链接:pan.baidu.com/s/1Q2kN8S3j…

提取码:e8vg

image-20210929150750542

Linux知识

目前大多数的互联网项目,都是部署在Linux上,所以Linux的基本的一些命令,一些软件在Linux的怎么安装部署你都要会。面试的时候一般面试官都会问Linux的相关知识,回答的越好越详细,越加分。

【狂神说Java】Linux最通俗易懂的教程阿里云真实环境学习

数据结构和算法

这部分我们也要不断的去学习,我们不能只当一个小小的码农,什么是码农啊?讲通俗点就是写代码的时候,业务逻辑+框架,就可以搞定我们工作中的一些需求了。但是如果你是一个有追求的程序员,还是要学习算法和数据结构,数组、链表是基础,栈和队列深入一些但也不难,树挺重要的,比较重要的树AVL树、红黑树,可以不了解它们的具体实现,但是要知道什么是二叉查找树、什么是平衡树,AVL树和红黑树的区别。学习算法我们可以去力扣上学习,官网:https://leetcode-cn.com/

相关资料

【算法图解】该书语言风趣,有比较多的插图,入门很合适。电子书籍网盘链接如下:

image-20210929125254950

链接:pan.baidu.com/s/1c9g1CK8P…

提取码:g5vz

学习资源网站列表汇总

(1)视频网站

  • B站(推荐):www.bilibili.com/
  • 网易云课堂:study.163.com/
  • 极客学院:www.jikexueyuan.com/
  • 慕课网:www.imooc.com/

(2)专栏

  • 极客时间(推荐):time.geekbang.org/
  • Gitchat gitbook.cn/

(3)Github

  • Java 知识地图(推荐):github.com/smileArchit…

(4)技术博客:

  • CSDN 博客:blog.csdn.net/
  • 博客园:www.cnblogs.com/
  • 掘金社区(推荐):juejin.cn/
  • InfoQ:xie.infoq.cn/
  • 思否:segmentfault.com/
  • 开源中国:www.oschina.net/blog

(5)搜索引擎:

  • 百度:www.baidu.com/
  • 谷歌:www.google.com/

(6)知识问答:

  • 知乎(推荐):www.zhihu.com/
  • stackoverflow(推荐):stackoverflow.com/

(7)刷题:

  • 力扣(推荐):leetcode-cn.com/
  • 牛客:www.nowcoder.com/

(8)云笔记:

  • 石墨:shimo.im/
  • 语雀:www.yuque.com/
  • 有道云笔记:note.youdao.com/
  • 印象笔记:www.yinxiang.com/ 看个人习惯去选择,不推荐了。

(9)在线画图:

  • processOn:www.processon.com/
  • drawio:app.diagrams.net/ 各有特色,都推荐。

计算机网络

计算机网络知识对于我们java后端开发来说是尤为重要的,推荐几个好的学习计算机网络的视频给大家:

计算机网络微课堂(有字幕无背景音乐版)(陆续更新中……)_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili

2019 王道考研 计算机网络_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili

计算机网络(谢希仁第七版)-方老师_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili

总结

作为一名程序员,其他任何行业都是一样,都是要不断的学习,接受新的知识,才能不被社会淘汰,作为一名程序员,我想,随着我们工作年限的增长以及对生活对生命认识的深入,应当不断思考三个问题:

  1. 我到底适不适合当一名程序员?
  2. 我到底应不应该一辈子以程序员为职业?
  3. 我对编程到底持有的是一种什么样的态度,是够用就好呢还是不断研究?

最终,明确自己的职业规划,对自己的规划负责并为之努力。一起加油~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Nginx系列教程(一) 手把手教你在Linux环境下搭建

发表于 2021-06-01

作者:JackTian

微信公众号:杰哥的IT之旅(ID:Jake_Internet)

这是我参与更文挑战的第 1 天,活动详情查看:更文挑战

一、什么是 Nginx?

Nginx是一个高性能的HTTP和反向代理Web服务器,也提供了IMAP / POP3 / SMTP服务,由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru站点而开发,第一个公开版本0.1.0发布于2004年10月4日,特点是占有内存少,并发能力强,专为性能优化而开发,稳定性和低系统资源消耗,以及对HTTP并发连接的高处理能力,可支持单台高达50000个并发连接数。

那么,在事实上Nginx的并发能力在同类型的网页服务器中表现较好,实际环境中,如果我们采用Nginx的话,可能是如下该场景的架构图。其实,在如下这种架构图中,Nginx服务器你可以直接理解为是一台负载均衡服务器或者反向代理服务器,所以当客户端发出请求到Nginx服务器时,Nginx服务器需将它配置好的规则由客户端发来的请求并转发到后端的LAMP、Tomcat、LNMP上。

图片

二、为什么要用 Nginx?

作为 Web 服务器

相比 Apache,Nginx 使用资源更少,支持更多的并发连接数。在高并发的情况下,Nginx 是 Apache 服务器的替代品,Nginx 作为负载均衡服务器,内部是支持Rails和PHP程序对外服务,也支持作为HTTP代理服务器对外服务,采用C语言编写,无论在系统资源开销还是CPU使用效率要比Perlbal好很多。

Nginx 配置简单,Apache 复杂

Nginx 启动容易,几乎可以做到7*24小时不间断运行,即便很长时间未重新启动,也能够在不间断服务的情况下对软件版本进行升级,静态处理性能要比 Apache 高 3倍以上,Nginx 需要配合其他后端来进行使用,而 Apache 对 PHP 支持较简单,组件同时也比 Nginx 多。

核心点

Nginx 是异步的,多连接可对应一个进程;

Apache 是同步多进程模型,一个连接对应一个进程;

擅长领域

Nginx 适用于前端处理静态请求;

Apache 适用于后端处理动态请求;

三、Nginx 安装

安装支持软件

Nginx 的配置及运行需要pcre、zlib软件包的支持,需先安装这些软件的开发包供相应的库和头文件,以此来确保 Nginx 的顺利安装。

1
arduino复制代码# yum -y install pcre-devel zlib-devel

创建运行用户、组

Nginx 服务程序默认以noboby身份运行,这里建议大家创建新的用户账号,更准确的控制访问权限、增加灵活性、降低安全风险;

1
shell复制代码# useradd -M -s /sbin/nologin nginx

下载编译安装

配置 Nginx 的编译选项时,将安装目录设为/usr/local/nginx,运行用户和组均设为nginx;启用http_stub_status_module模块以支持状态统计,便于查看服务器的连接信息。

1
2
3
4
5
shell复制代码# wget http://nginx.org/download/nginx-1.17.0.tar.gz
# tar zxf nginx-1.17.0.tar.gz
# cd nginx-1.17.0
# ./configure --prefix=/usr/local/nginx --user=nginx --group=nginx --with-http_stub_status_module
# make && make install

为主程序 Nginx 创建链接文件

1
2
3
shell复制代码# ln -s /usr/local/sbin/nginx /usr/local/sbin/
# ls -l /usr/local/sbin/nginx
lrwxrwxrwx. 1 root root 21 6月 4 07:31 /usr/local/sbin/nginx -> /usr/local/sbin/nginx

安装好之后,进入默认安装路径到sbin目录下,执行nginx即可启动;

1
2
3
4
5
shell复制代码# cd /usr/local/nginx/sbin/
# pwd
/usr/local/nginx/sbin
# ./nginx
# nginx

监听 Nginx 程序的状态

1
2
bash复制代码# netstat -anpt | grep nginx
tcp        0      0 0.0.0.0:80                  0.0.0.0:*                   LISTEN      53816/nginx

监听到端口后,在浏览器中直接访问Nginx 地址,当浏览器看到如下页面时,说明Nginx已经安装成功了。

图片

使用 Nginx 服务脚本

可编写Nginx服务脚本,使用chkconfig和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
bash复制代码#!/bin/bash
# chkconfig: 2345 99 20
# description: Nginx Server Control Scripts shell
PROG="/usr/local/nginx/sbin/nginx"
PIDF="/usr/local/nginx/logs/nginx.pid"
case "$1" in
start)
if [ -f $PIDF ]; then
echo "Nginx is running.. Start it is error"
else
$PROG
fi
;;
stop)
if [ -f $PIDF ]; then
kill -s QUIT $(cat $PIDF)
rm -rf $PIDF
else
echo "Nginx is stopping .. Stop it is error"
fi
;;
restart)
$0 stop
$0 start
;;
reload)
if [ -f $PIDF ]; then
kill -s HUP $(cat $PIDF)
else
echo "Nginx is stopping . reload it is error"
fi
;;
status)
if [ -f $PIDF ]; then
echo "Nginx is running"
else
echo "Nginx is stopping"
fi
;;
*)
echo "Usage: $0 (start|stop|restart|reload|status)"
exit 1
esac
exit 0
# chmod +x /etc/init.d/nginx
# chkconfig --add nginx

如果修改了Nginx的配置文件,可通过./nginx -s reload命令加载Nginx配置文件。

1
shell复制代码# ./nginx -s reload

总结

以上我们介绍了Nginx的基本概念、为什么要用Nginx、Nginx 的安装有个初步的认识,待后续的文章继续介绍正向代理、反向代理、负载均衡以及构建 LNMP 架构等;今天我们就介绍到这里,有问题大家随时留言讨论哦。

原创不易,如果你觉得这篇文章对你有点用的话,麻烦你为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

绝了!基于SpringBoot的可视化接口开发工具,不再需要

发表于 2021-06-01

本文正在参加「Java主题月 - Java 开发实战」,详情查看活动链接。

摘要

作为Java后端开发,平时开发API接口的时候经常需要定义Controller、Service、Dao、Mapper、XML、VO等Java对象。我们甚至使用代码生成器来通过数据库生成这些代码!有没有什么办法可以让我们不写这些代码,直接操作数据库生成API接口呢?今天给大家推荐一款工具magic-api,来帮我们实现这个小目标!

SpringBoot实战电商项目mall(40k+star)地址:github.com/macrozheng/…

magic-api简介

magic-api是一个基于Java的接口快速开发框架,编写接口将通过magic-api提供的UI界面完成,自动映射为HTTP接口,无需定义Controller、Service、Dao、Mapper、XML、VO等Java对象。

使用

下面我们来波实战,熟悉下使用magic-api来开发API接口。

在SpringBoot中使用

magic-api原生支持SpringBoot,可与SpringBoot无缝整合。

  • 首先在pom.xml中添加magic-api相关依赖;
1
2
3
4
5
6
xml复制代码<!--接口快速开发框架 magic-api-->
<dependency>
<groupId>org.ssssssss</groupId>
<artifactId>magic-api-spring-boot-starter</artifactId>
<version>1.0.2</version>
</dependency>
  • 在配置文件application.yml中添加数据源及magic-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
yaml复制代码spring:
datasource:
url: jdbc:mysql://localhost:3306/magic_api?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root

magic-api:
# 配置api管理页面入口
web: /magic/web
# 配置存储方式
resource:
# 配置接口资源存储位置,可选file、database、redis
type: database
# 存储表名
tableName: magic_api_file
# 使用database、redis存储时的key前缀
prefix: /magic-api
# 是否是只读模式
readonly: false
# 启用驼峰命名转换
sql-column-case: camel
# 分页配置
page-config:
# 页大小的请求参数名称
size: size
# 页码的请求参数名称
page: page
# 未传页码时的默认页码
default-page: 1
# 未传页大小时的默认页大小
default-size: 10
  • 在MySQL中创建数据库magic_api,由于我们配置了使用数据库存储接口资源,所以需要先创建magic_api_file表;
1
2
3
4
5
6
7
sql复制代码CREATE TABLE `magic_api_file`
(
`id` bigint(255) NOT NULL AUTO_INCREMENT,
`file_path` varchar(255) DEFAULT NULL,
`file_content` text,
PRIMARY KEY (`id`)
)
  • 再创建pms_brand表,用于测试;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码CREATE TABLE `pms_brand` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`big_pic` varchar(255) DEFAULT NULL,
`brand_story` varchar(255) DEFAULT NULL,
`factory_status` bit(1) DEFAULT NULL,
`first_letter` varchar(255) DEFAULT NULL,
`logo` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`product_comment_count` int(11) DEFAULT NULL,
`product_count` int(11) DEFAULT NULL,
`show_status` bit(1) DEFAULT NULL,
`sort` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4;
  • 最后启动项目,访问magic-api的UI界面,访问地址:http://localhost:8080/magic/web

增删改查

接下来我们将以商品品牌管理为例,体验下使用magic-api开发接口的快感!使用magic-api开发API接口,仅需在界面中使用magic-script脚本即可。

  • 首先我们来写个新增接口,先创建一个分组,然后在分组中创建一个新增接口,在编辑框中输入如下脚本;
1
2
javascript复制代码// 使用body对象可以直接获取请求body中的参数
return db.table('pms_brand').insert(body);
  • 在底部的接口信息中进行如下配置,POST请求,请求路径为/create,请求参数放在请求body中;

  • 再来个根据ID查询的接口,在编辑框中输入如下脚本;
1
2
3
4
5
javascript复制代码// 路径变量从path对象中获取
return db.table('pms_brand')
.where()
.eq('id',path.id)
.selectOne();
  • 在底部的接口信息中进行如下配置,GET请求,请求路径为/detail/{id},请求参数放在路径变量中;

  • 再来个修改的接口,在编辑框中输入如下脚本;
1
javascript复制代码return db.table('pms_brand').primary('id',body.id).update(body);
  • 在底部的接口信息中进行如下配置,POST请求,请求路径为/update,请求参数放在请求body中;

  • 再来个分页查询查询的接口,在编辑框中输入如下脚本;
1
javascript复制代码return db.table('pms_brand').page();
  • 在底部的接口信息中进行如下配置,GET请求,请求路径为/page,请求参数放在请求参数中(由于已经在application.yml中配置好了分页参数,可直接使用);

  • 再来个根据ID删除的接口,在编辑框中输入如下脚本,删除只能使用update,这设计有点…
1
javascript复制代码return db.update('delete from pms_brand where id=#{id}');
  • 在底部的接口信息中进行如下配置,POST请求,请求路径为/delete/{id},请求参数放在路径变量中;

参数验证

我们可以通过断言模块assert来进行参数验证。

  • 比如新增品牌的时候名称和首字母不能为空,在编辑框中输入如下脚本;
1
2
3
4
5
javascript复制代码import assert;  //导入断言模块
//验证不通过时,会终止运行
assert.notEmpty(body.name,400,'名称不能为空!');
assert.notEmpty(body.firstLetter,400,'首字母不能为空!');
return db.table('pms_brand').insert(body);
  • 在底部的接口信息中进行如下配置,POST请求,请求路径为/test,请求参数放在请求body中;

  • 当我们不添加name字段时,调用接口会返回我们自己定义的错误信息和状态码。

结果转换

我们可以使用map方法对查询数据进行转换,返回我们想要的数据。

  • 比如我们想将showStatus转换为中文说明,并只返回三个需要的字段,在编辑框中输入如下脚本;
1
2
3
4
5
6
javascript复制代码var list = db.table('pms_brand').select();
return list.map((item)=>{
name : item.name,
firstLetter : item.firstLetter,
showStatus : item.showStatus? '不显示' : '显示'
});
  • 访问该接口,在执行结果中可以发现,返回结果已经转换。

使用事务

在我们使用Java开发接口的时候,少不了用到事务,当然magic-api也是支持事务的。使用db.transaction()方法即可,支持自动事务和手动事务。

  • 还是以修改品牌为例,先查询是否存在,如果存在则更新;
1
2
3
4
5
6
7
8
javascript复制代码import assert; 
var val = db.transaction(()=>{
var exist = db.table('pms_brand').where().eq('id',body.id).selectOne();
assert.notNull(exist,404,'找不到该品牌!');
db.table('pms_brand').primary('id',body.id).update(body);
return v2;
});
return val;
  • 在底部的接口信息中进行如下配置,POST请求,请求路径为/test,请求参数放在请求body中;

集成Swagger

写了那么多接口,都是在magic-api的界面中进行调试的。如果你习惯使用Swagger,magic-api也可以和Swagger进行无缝整合。

  • 首先在pom.xml中添加Swagger相关依赖;
1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<dependencies>
<!--Swagger-UI API文档生产工具-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
  • 在配置文件application.yml中添加Swagger相关配置;
1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码magic-api:
# 集成Swagger配置
swagger-config:
# 文档名称
name: MagicAPI 测试接口
# 文档标题
title: MagicAPI Swagger Docs
# 文档描述
description: MagicAPI 测试接口信息
# 文档版本号
version: 1.0
# 文档资源位置
location: /v2/api-docs/magic-api/swagger2.json
  • 访问Swagger界面即可查看我们在magic-api中写的接口了,访问地址:http://localhost:8080/swagger-ui.html

总结

magic-api是个很有意思的框架,可以通过在UI界面中使用简单的脚本,进行API接口的开发。不过作为一款小众框架,magic-api还有很长一段路要走!

参考资料

官方文档:ssssssss.org/

项目源码地址

github.com/macrozheng/…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点 SpringBoot Listener

发表于 2021-06-01

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 .前言

本篇文档来完善 Spring 体系中 Listener 的相关概念 :

主要内容 :

  • Listener 的处理流程
  • Listener 的同步和异步处理
  • 常见的 Listener 处理

Spring-listener.jpg

二 . Listener 的基础使用

基础使用中包括四个步骤 :

  • 构建一个 TranTO 用于承载数据
  • 构建一个 Event 用于发布
  • 构建一个 Listener 接受事件及处理
  • 主业务中发布事件

2.1 数据承载体

数据承载体用于在发布事件的同事携带数据给 Listener

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
java复制代码public class ListenerTranTO {

private String eventName;

private Map<String, String> eventInfo;

public String getEventName() {
return eventName;
}

public void setEventName(String eventName) {
this.eventName = eventName;
}

public Map<String, String> getEventInfo() {
return eventInfo;
}

public void setEventInfo(Map<String, String> eventInfo) {
this.eventInfo = eventInfo;
}


@Override
public String toString() {
return "ListenerTranTO{" +
"eventName='" + eventName + '\'' +
", eventInfo=" + eventInfo +
'}';
}
}

2.2 构建发布 Event

可以看到 , Event 是基于 ApplicationEvent 构建了一个对象

1
2
3
4
5
6
7
8
java复制代码  public class DefaultEvent extends ApplicationEvent {

private Logger logger = LoggerFactory.getLogger(this.getClass());

public DefaultEvent(ListenerTranTO tranTO) {
super(tranTO);
}
}

2.3 构建一个 Listener 接受事件及处理

1
2
3
4
5
6
7
8
9
10
java复制代码@Component
public class DefaultListener implements ApplicationListener<DefaultEvent> {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public void onApplicationEvent(DefaultEvent event) {
logger.info("------> DefaultEvent Listner , Properties [{}] <-------", String.valueOf(event.getSource()));
}
}

2.4 主业务中发布事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码 @Autowired
private ApplicationContext context;

@Override
public void run(ApplicationArguments args) throws Exception {

logger.info("------> Default Event Publish Start >>>>> <-------");

ListenerTranTO tranTO = new ListenerTranTO();

Map<String, String> infoMap = new HashMap<>();
infoMap.put("info", "This is in Info");
infoMap.put("message", "Listener Success");

tranTO.setEventInfo(infoMap);
tranTO.setEventName("DefaultListener");

context.publishEvent(new DefaultEvent(tranTO));

logger.info("------> Default Event Publish End >>>>> [{}] <-------", tranTO.toString());
}
}

三 . Listener 流程解析

这部分主要是说明 Listener 在 整个体系中的运用 , 先来看一下常见的几个 Listener , 首先看一下官方对 ApplicationListener 的定义 : 由应用程序事件侦听器实现的接口 , 基于观察者设计模式的标准java.util.EventListener接口。

这里回顾一下 观察者模式 的相关概念 :

观察者模式意图 : 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

参考 @ blog.csdn.net/zzg19950824…

image.png

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
java复制代码public interface Observer {  
public void update();
}
//两个实现类:

public class Observer1 implements Observer {

@Override
public void update() {
System.out.println("observer1 has received!");
}
}

public class Observer2 implements Observer {

@Override
public void update() {
System.out.println("observer2 has received!");
}

}
//Subject接口及实现类:

public interface Subject {

/*增加观察者*/
public void add(Observer observer);

/*删除观察者*/
public void del(Observer observer);

/*通知所有的观察者*/
public void notifyObservers();

/*自身的操作*/
public void operation();
}

public abstract class AbstractSubject implements Subject {

private Vector<Observer> vector = new Vector<Observer>();
@Override
public void add(Observer observer) {
vector.add(observer);
}

@Override
public void del(Observer observer) {
vector.remove(observer);
}

@Override
public void notifyObservers() {
Enumeration<Observer> enumo = vector.elements();
while(enumo.hasMoreElements()){
enumo.nextElement().update();
}
}
}

public class MySubject extends AbstractSubject {

@Override
public void operation() {
System.out.println("update self!");
notifyObservers();
}

}

//测试类:

public class ObserverTest {

public static void main(String[] args) {
Subject sub = new MySubject();
sub.add(new Observer1());
sub.add(new Observer2());

sub.operation();
}

}
//输出:
update self!
observer1 has received!
observer2 has received!

3.1 Listener 的触发方式

Listerner的触发主要有以下几种 :

  • 容器启动时自动触发
  • 手动发布 publishEvent

类型一 : 容器自动触发 , 触发流程

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
java复制代码C- AbstractApplicationContext # refresh()

其中大部分是在这个流程中完成触发 , 通常都是通过 publish 进行发布
this.applicationContext.publishEvent(new ServletWebServerInitializedEvent(this.webServer, this.applicationContext));
C50- AbstractApplicationContext
M50_10- publishEvent(ApplicationEvent event)
M50_11- publishEvent(Object event, @Nullable ResolvableType eventType)

// Step 1 : 发布 Event 事件
public void publishEvent(ApplicationEvent event) {
publishEvent(event, null);
}


protected void publishEvent(Object event, @Nullable ResolvableType eventType) {

// Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent) event;
} else {
// 此处如果不是 ApplicationEvent , 则构建 PayloadApplicationEvent -> PS:M50_11_01
applicationEvent = new PayloadApplicationEvent<>(this, event);
if (eventType == null) {
// 此处获取事件内 Object 类型
eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
}
}

// 如果可能,立即进行多播,或者在多播器初始化后延迟多播 -> PS:M50_11_02
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
} else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}

// 也可以通过父上下文发布事件
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
}else {
this.parent.publishEvent(event);
}
}
}


// PS:M50_11_01 PayloadApplicationEvent 是什么 ?
C- PayloadApplicationEvent
?- 携带任意有效负载的ApplicationEvent
I- ResolvableTypeProvider
F- private final T payload;
M- getResolvableType() : 返回描述此实例的ResolvableType
- return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getPayload()));

PS:M50_11_02 Listener 的 多播是什么?

Listener 的 多播是什么 ?

将所有事件多播给所有注册的侦听器,让侦听器忽略它们不感兴趣的事件.
监听器通常会对传入的事件对象执行相应的instanceof检查。

同时 , 可以异步处理 , 指定一个替代任务执行器,使侦听器在不同的线程中执行,例如在线程池中执行

总结 : 可以把其当成Listener 的主要发布者

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
java复制代码
// Step 1 : 事件多播的发布方式
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
} else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}


// Step 2 : 对象的创建地方
C- AbstractApplicationContext
F- Set<ApplicationEvent> earlyApplicationEvents : 在多播器设置之前发布的ApplicationEvents

// 事件的多播器创建的地方是在 AbstractApplicationContext :
C- AbstractApplicationContext
M- refush
- initApplicationEventMulticaster()

protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
// 如果 APPLICATION_EVENT_MULTICASTER_BEAN_NAME (applicationEventMulticaster) 中存在 , 则直接从Bean 工厂获取
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
} else {
// 构建一个新 SimpleApplicationEventMulticaster , 并且注册
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
}
}


// Step 3 : 来看一下 earlyApplicationEvents 的使用
C- AbstractApplicationContext
protected void registerListeners() {
// 首先注册静态指定的侦听器
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
}

// 获得所有的 ApplicationListener , 此处会根据 type 获取所有的 ApplicationListener
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
// 往 ApplicationEventMulticaster 中添加 Listener
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}

// 发布早期应用事件
// 此处首先获取原本的 earlyEventsToProcess 对象 , 用于缓存 ,同时清空之前的镀锡
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (!CollectionUtils.isEmpty(earlyEventsToProcess)) {
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}

// 可以看到 , 最后仍然会调用 multicastEvent 进行消息的发布

3.2 Listener 的循环处理

此处调用 multicastEvent 进行最后的操作 , 主要有几个步骤 :

  • resolveDefaultEventType 获得 事件的类型
  • getTaskExecutor 获得 Executor -> PS:M50_12_1
  • 获得所有的 Listenr
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
java复制代码// Step 2 : 循环所有的 Listener 
C50- AbstractApplicationContext
M50_12- multicastEvent(ApplicationEvent event)
- getApplicationListeners 获取所有的 Listeners -> M50_12_01

/**
* 循环 Listener
**/
C- SimpleApplicationEventMulticaster
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();

// 获取 Listener 对象
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}else {
invokeListener(listener, event);
}
}
}


// PS : ApplicationEventMulticaster 接口 , 主要实现类是 SimpleApplicationEventMulticaster
C- ApplicationEventMulticaster
M- void addApplicationListener(ApplicationListener<?> listener) : 添加一个侦听器,用于通知所有事
M- void addApplicationListenerBean(String listenerBeanName) : 添加一个侦听器bean,用于通知所有事件
M- void removeApplicationListener(ApplicationListener<?> listener) : 从通知列表中删除侦听器
M- void removeApplicationListenerBean(String listenerBeanName) : 从通知列表中删除侦听器bean
M- void removeAllListeners() : 删除所有注册到这个多播广播的监听器
M- void multicastEvent(ApplicationEvent event) : 将给定的应用程序事件组播到适当的侦听器
M- void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) : 将给定的应用程序事件组播到适当的侦听器


// PS : 补充 SimpleApplicationEventMulticaster 详见下文

以下通过 eventType 获得 ApplicationListener 类

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
java复制代码C- AbstractApplicationEventMulticaster
// M50_12_01 getApplicationListeners 主流程
protected Collection<ApplicationListener<?>> getApplicationListeners(
ApplicationEvent event, ResolvableType eventType) {

Object source = event.getSource();
Class<?> sourceType = (source != null ? source.getClass() : null);
ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);

// 快速检查ConcurrentHashMap上的现有条目...
ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
if (retriever != null) {
return retriever.getApplicationListeners();
}

if (this.beanClassLoader == null ||(ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
(sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
// listener检索器的完全同步构建和缓存
synchronized (this.retrievalMutex) {
retriever = this.retrieverCache.get(cacheKey);
if (retriever != null) {
return retriever.getApplicationListeners();
}
retriever = new ListenerRetriever(true);
// 获取 ApplicationListener 集合
Collection<ApplicationListener<?>> listeners =retrieveApplicationListeners(eventType, sourceType, retriever);
this.retrieverCache.put(cacheKey, retriever);
return listeners;
}
} else {
// 没有监听检索缓存 则 没有必要同步
return retrieveApplicationListeners(eventType, sourceType, null);
}
}


C- AbstractApplicationContext
// 实际检索给定事件和源类型的应用程序监听器
private Collection<ApplicationListener<?>> retrieveApplicationListeners(
ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable ListenerRetriever retriever) {

List<ApplicationListener<?>> allListeners = new ArrayList<>();
Set<ApplicationListener<?>> listeners;
Set<String> listenerBeans;

// 通常为 ListenerRetriever , 这是一个Helper类,它封装一组特定的目标侦听器,允许高效检索预筛选的侦听器
// 每个事件类型和源类型都会缓存此Helper类的实例
synchronized (this.retrievalMutex) {
// 获得 Listener 对象
listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners);
listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);
}

// 添加以编程方式注册的侦听器,包括来自ApplicationListenerDetector的侦听器(单例bean和内部bean)ans).
for (ApplicationListener<?> listener : listeners) {
// 判断是否支持该 Event
if (supportsEvent(listener, eventType, sourceType)) {
if (retriever != null) {
retriever.applicationListeners.add(listener);
}
allListeners.add(listener);
}
}

// 按bean名添加侦听器,可能与上面以编程方式注册的侦听器重叠
if (!listenerBeans.isEmpty()) {

// 因为此处时Bean 名称 , 所以需要通过BeanFactory 构建
ConfigurableBeanFactory beanFactory = getBeanFactory();

for (String listenerBeanName : listenerBeans) {
try {
// 校验是否支持当前 Event
if (supportsEvent(beanFactory, listenerBeanName, eventType)) {
ApplicationListener<?> listener =
beanFactory.getBean(listenerBeanName, ApplicationListener.class);
if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) {
if (retriever != null) {
if (beanFactory.isSingleton(listenerBeanName)) {
retriever.applicationListeners.add(listener);
}
else {
retriever.applicationListenerBeans.add(listenerBeanName);
}
}
allListeners.add(listener);
}
}
else {
Object listener = beanFactory.getSingleton(listenerBeanName);
if (retriever != null) {
retriever.applicationListeners.remove(listener);
}
allListeners.remove(listener);
}
}
catch (NoSuchBeanDefinitionException ex) {
}
}
}

// 对 allListeners 进行排序
AnnotationAwareOrderComparator.sort(allListeners);
if (retriever != null && retriever.applicationListenerBeans.isEmpty()) {
retriever.applicationListeners.clear();
retriever.applicationListeners.addAll(allListeners);
}
return allListeners;
}


// 获取 Listener
C- AbstractApplicationEventMulticaster
// Helper类,它封装一组特定的目标Listener,允许高效检索预筛选的Listener
PVC- ListenerRetriever
F- Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>()
F- Set<String> applicationListenerBeans = new LinkedHashSet<>()

PS:M50_12_1 : getTaskExecutor 获取 Executor

返回此多播器的当前任务执行器

3.3 Invoke 代理

此处进行实际的调用

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
java复制代码
C- SimpleApplicationEventMulticaster
M- invokeListener(ApplicationListener<?> listener, ApplicationEvent event)
- 获取 ErrorHandler
- doInvokeListener 代理 Listener
- 如果出现异常 , errorHandler.handleError(err)
M- doInvokeListener(ApplicationListener listener, ApplicationEvent event)
- listener.onApplicationEvent(event) 发起Event 处理



// Step 3 : invoke Listener 类
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
doInvokeListener(listener, event);
}catch (Throwable err) {
errorHandler.handleError(err);
}
}else {
doInvokeListener(listener, event);
}
}

// Event 处理
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
listener.onApplicationEvent(event);
}

补充 存放 Listener 的时机

1
2
3
4
5
6
7
java复制代码
//存在四个地方往 Set 中添加 Listener 对象

C- AbstractApplicationEventMulticaster # addApplicationListener(ApplicationListener<?> listener)
?- 手动添加 Listener , 具体哪些地方会调用就不详细说了
C- AbstractApplicationEventMulticaster # retrieveApplicationListeners
?- 这里是从 defaultRetriever 添加到传入的 ListenerRetriever 中

四. SpringListener 异步处理

在不开启异步的情况下 , Listener 是非异步的

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
java复制代码@Configuration
@EnableAsync
public class AsyncListenerConfiguration implements AsyncConfigurer {

/**
* Spring Async 配置信息
*
* @return
*/
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
// 设置线程池的数目
threadPoolTaskExecutor.setCorePoolSize(10);
threadPoolTaskExecutor.setMaxPoolSize(20);
threadPoolTaskExecutor.setQueueCapacity(50);
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}

/**
* 异步监听
**/
@Component
public class AsyncListener {

private Logger logger = LoggerFactory.getLogger(this.getClass());

/**
* 此处使用 Async 异步处理
*
* @param defaultEvent
*/
@Async
@EventListener
public void doAsyncEvent(DefaultEvent defaultEvent) {
logger.info("------> 通过异步监听 :[{}] , Thread is :[{}]<-------", defaultEvent.getSource(), Thread.currentThread().getId());
}

}


// 发布事件
context.publishEvent(new DefaultEvent(tranTO));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}



2021-05-31 15:02:53.284 INFO 23640 --- [ main] c.g.s.s.demo.listener.DefaultListener : ------> DefaultEvent Listner , Properties [ListenerTranTO{eventName='DefaultListener', eventInfo={message=Listener Success, info=This is in Info}}] <-------
2021-05-31 15:02:53.287 INFO 23640 --- [ main] c.g.s.s.demo.listener.DefaultListener : ------> Listener Thread 情况 :[1] <-------
2021-05-31 15:02:53.297 INFO 23640 --- [lTaskExecutor-1] c.g.s.s.demo.listener.AsyncListener : ------> 通过异步监听 :[ListenerTranTO{eventName='DefaultListener', eventInfo={message=Listener Success, info=This is in Info}}] , Thread is :[330]<-------
2021-05-31 15:03:03.288 INFO 23640 --- [ main] c.g.s.source.demo.listener.TestListener : ------> Default Event Publish End >>>>> [ListenerTranTO{eventName='DefaultListener', eventInfo={message=Listener Success, info=This is in Info}}] -- [1] <-------



// 可以看到 , 此处发布后 , 同步监听停了10秒 ,而异步监听成功处理

// 原理 :
AsyncExecutionInterceptor : 主要是方法进行了异步代理

总结

这文章完善了 Spring 体系中的 Listener 模块 , 总体可以分为2种情况 :

同步情况 : AbstractApplicationContext 中循环所有的 Listener 进行处理

异步情况 : 首先对方法进行代理 ,在调用时 ,通过 Async 配置的线程池中开启一个新线程进行处理

附录

# Listener 的注册方式

转载自 @ www.cnblogs.com/linlf03/p/1…

方式一 : spring.factories 中配置监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/

public class FirstListener implements ApplicationListener<ApplicationStartedEvent> {

@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
System.out.println("hello, first listener");
}
}

// 在spring.factories文件中增加配置监听器
// 原理可以看看这一篇文章 : https://juejin.cn/post/6955489109225930789

org.springframework.context.ApplicationListener=com.example.demo.listener.FirstListener

方式二 : Main 函数中添加 Listener

1
2
3
4
5
6
java复制代码public static void main(String[] args) {

SpringApplication springApplication = new SpringApplication(Sb2Application.class);
springApplication.addListeners(new SecondListener());
springApplication.run(args);
}

方式三 : application.propeties 文件中配置

1
2
java复制代码
context.listener.classes=....listener.class

方式四 : 相关模块中添加 listener

在部分场景中 , 可以往 Web 容器中通过 AddListener 的方式添加

  • JPA 的 @EntityListeners

方式五 : 注解添加

1
2
3
4
java复制代码@EventListener
public void event(Object event){
System.out.println("MyEventHandle 接收到事件:" + event.getClass());
}

方式五 : WebListener

1
2
3
4
5
6
java复制代码@WebListener
public class DefaultHttpSessionListener implements HttpSessionListener {
//...........
}

// PS : Application 方法上需要标注注解 @ServletComponentScan

# WebListener 使用 (与此相同的还有 ServletContextListener )

这里说一下 WebListener 的使用方式 , HttpSessionListener 也是 EventListener 的子接口之一 , 通过实现该 Listener 可以实现对 Session 的操作

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
java复制代码@WebListener
public class DefaultHttpSessionListener implements HttpSessionListener {

private static final Logger LOG = LoggerFactory.getLogger(DefaultHttpSessionListener.class);

private final AtomicInteger counter = new AtomicInteger();

@Override
public void sessionCreated(HttpSessionEvent se) {

LOG.info("New session is created. Adding Session to the counter.");
counter.incrementAndGet(); //incrementing the counter
updateSessionCounter(se);
}

@Override
public void sessionDestroyed(HttpSessionEvent se) {
LOG.info("Session destroyed. Removing the Session from the counter.");
counter.decrementAndGet(); //decrementing counter
updateSessionCounter(se);
}

private void updateSessionCounter(HttpSessionEvent httpSessionEvent) {
//Let's set in the context
httpSessionEvent.getSession().getServletContext()
.setAttribute("activeSession", counter.get());
LOG.info("Total active session are {} ", counter.get());
}
}

// PS : Application 方法上需要标注注解 @ServletComponentScan

该方案的实现实现流程 :

public void tellNew() {

// Notify interested session event listeners
fireSessionEvent(Session.SESSION_CREATED_EVENT, null);

// Notify interested application event listeners
Context context = manager.getContext();
Object listeners[] = context.getApplicationLifecycleListeners();
if (listeners != null && listeners.length > 0) {
HttpSessionEvent event =
new HttpSessionEvent(getSession());
for (int i = 0; i < listeners.length; i++) {
if (!(listeners[i] instanceof HttpSessionListener))
continue;
HttpSessionListener listener =
(HttpSessionListener) listeners[i];
try {
context.fireContainerEvent("beforeSessionCreated",
listener);
listener.sessionCreated(event);
context.fireContainerEvent("afterSessionCreated", listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
context.fireContainerEvent("afterSessionCreated",
listener);
} catch (Exception e) {
// Ignore
}
manager.getContext().getLogger().error
(sm.getString("standardSession.sessionEvent"), t);
}
}
}

}

// Context 的处理逻辑
ServletListenerRegistrationBean 中进行注册

更新日志 :

  • V20210804 : 补充流程图

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

你必须懂也可以懂的Transactional原理

发表于 2021-05-31

1.前言

系统应用开发过程中通常需要使用事务来保证业务数据的一致性,实现方式如:开启事务、执行数据库写操作、提交或者回滚事务,这种标准实现方式适用于少量一致性业务,如果存在大量需要保证数据一致性的业务,不仅会让开发人员重复编码,还会给系统造成冗余代码。基于这些问题,伟大的Spring框架为我们提供了@Transactional注解,那么它是如何使用一个注解就解决了我们的烦恼呢?我们该如何着手进行分析呢?

SpringBoot集成的功能往往要从一个xxxAutoConfiguration开始说起

2.自动配置

打开TransactionAutoConfiguration自动配置类可以看到一个比较重要的注解@EnableTransactionManagement用于开启事务管理功能,@EnableTransactionManagement注解又导入了AutoProxyRegistrar和ProxyTransactionManagementConfiguration

2.1 事务配置

ProxyTransactionManagementConfiguration中声明了一个切面BeanFactoryTransactionAttributeSourceAdvisor,看到切面必定会有相对应的切点TransactionAttributeSourcePointcut(用于声明切入的范围)和通知TransactionInterceptor(用于实现切入目标的后续操作)。

2.2 声明@Transactional注解处理器

1
2
3
4
5
java复制代码@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource();
}

AnnotationTransactionAttributeSource实例化指定了注解解析器为SpringTransactionAnnotationParser

可以看到该解析器主要用来处理@Transactional注解

2.3 注入自动代理注册器

在2.自动配置提到@EnableTransactionManagement还引入了AutoProxyRegistrar,向IOC容器中注入InfrastructureAdvisorAutoProxyCreator

1
2
3
4
5
java复制代码@Nullable
public static BeanDefinition registerAutoProxyCreatorIfNecessary(
BeanDefinitionRegistry registry, @Nullable Object source) {
return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source);
}

InfrastructureAdvisorAutoProxyCreator实现了BeanPostProcessor接口,具有拦截并处理Bean的能力

2.3.1 Bean后置处理

1
2
3
4
5
6
7
8
9
10
java复制代码@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}

2.3.2 从容器中获取所有的Advisor

1
2
3
4
5
6
7
8
java复制代码/**
* Find all candidate Advisors to use in auto-proxying.
* @return the List of candidate Advisors
*/
protected List<Advisor> findCandidateAdvisors() {
Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available");
return this.advisorRetrievalHelper.findAdvisorBeans();
}

2.3.3 筛选出符合条件的Advisor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
// 1.获取切点对应的MethodMatcher
MethodMatcher methodMatcher = pc.getMethodMatcher();
for (Class<?> clazz : classes) {
// 2.获取当前类中的所有方法
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
for (Method method : methods) {
// 3.判断是否符合切点要求,此处的methodMatcher为TransactionAttributeSourcePointcut
if (methodMatcher.matches(method, targetClass)) {
return true;
}
}
}

return false;
}

判断方法上是否有@Transactional注解,如果有则使用SpringTransactionAnnotationParser进行解析并生成TransactionAttribute

2.3.4 Advisor排序

1
java复制代码sortAdvisors(eligibleAdvisors);

2.3.4 小结

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
// 1.从容器中获取所有类型为Advisor的切面
List<Advisor> candidateAdvisors = findCandidateAdvisors();
// 2.筛选出符合条件的切面(也就是类或方法上被@Transactional注解标注)
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
// 3.对符合条件的切面进行升序排序
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

2.4 选择代理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (!IN_NATIVE_IMAGE &&
(config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
Class<?> targetClass = config.getTargetClass();
// 1.如果实现接口则选择jdk代理
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 2.选择cglib代理
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

2.4.1 生成代理

此处以cglib为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
try {
Class<?> rootClass = this.advised.getTargetClass();
Class<?> proxySuperClass = rootClass;

// Configure CGLIB Enhancer...
Enhancer enhancer = createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
}
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));

Callback[] callbacks = getCallbacks(rootClass);
}
}
1
2
3
4
java复制代码private Callback[] getCallbacks(Class<?> rootClass) throws Exception {
// Choose an "aop" interceptor (used for AOP calls).
Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);
}

创建Enhancer并指定回调为DynamicAdvisedInterceptor

2.5 调用代理

执行被代理对象目标方法userService.saveUser(user);时会调用DynamicAdvisedInterceptor的intercept()方法

2.5.1 筛选满足条件的Advice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Override
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
Advised config, Method method, @Nullable Class<?> targetClass) {
// 1.遍历所有满足条件的Advisor,也就是2.3.3章节返回的Advisor
for (Advisor advisor : advisors) {
if (advisor instanceof PointcutAdvisor) {
// Add it conditionally.
PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
// 2. 判断是否满足切点要求
boolean match = mm.matches(method, actualClass);
// 3.满足切点要求
if (match) {
// 3. 获取切面对应的通知,也就是TransactionInterceptor
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
}
}
// 4.返回满足条件的通知
return interceptorList;
}

2.5.2 执行满足条件的Advice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Override
@Nullable
public Object proceed() throws Throwable {
// 1.如果没有advice可以执行,则执行目标方法
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}

// 2.从advice列表中取出一个advice
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
// 3.执行advice的invoke方法,也就是TransactionInterceptor的invoke
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
  1. 事务调用

3.1 获取事务配置属性

1
2
java复制代码TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);

也就是@Transactional注解声明的属性

3.2 获取事务管理器

1
java复制代码final TransactionManager tm = determineTransactionManager(txAttr);

从容器中获取DataSourceTransactionManagerAutoConfiguration自动配置类中声明的事务管理器JdbcTransactionManager

1
2
3
4
5
6
7
8
9
10
java复制代码public class DataSourceTransactionManagerAutoConfiguration {
@Bean
@ConditionalOnMissingBean(TransactionManager.class)
JdbcTransactionManager transactionManager(DataSource dataSource,
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
JdbcTransactionManager transactionManager = new JdbcTransactionManager(dataSource);
transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
return transactionManager;
}
}

3.3 执行事务

  1. 总结

@Transactional实现原理三要素切面、切点、通知

  • InfrastructureAdvisorAutoProxyCreator后置处理器拦截所有Bean
  • 遍历所有类型为Advisor的切面
  • 返回满足切点条件的切面列表
  • 选择代理方法
  • 生成代理
  • 调用通知的invoke()方法
    • 开启事务
    • 调用其它通知的invoke()方法,如果没有执行目标方法
    • 执行异常,回滚事务
    • 执行成功,提交事务
  • 执行目标方法

了解@Transactional注解实现原理,不仅可以让我们对切面、切点、通知有一个清晰的认识,还可以让我们通过其思想实现类似功能,如@Cache注解实现应用缓存,@Async注解实现业务异步执行

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

推荐一个完善的停车管理系统(停车收费、物业管理、物联网、自助

发表于 2021-05-31

功能介绍

停车场系统源码,停车场小程序,智能停车,Parking system

(1)兼容市面上主流的多家相机,理论上兼容所有硬件,可灵活扩展,

(2)相机识别后数据自动上传到云端并记录,校验相机唯一id和硬件序列号,防止非法数据录入,

(3)用户手机查询停车记录详情可自主缴费(支持微信,支付宝,银行接口支付,支持每个停车场指定不同的商户进行收款),支付后出场在免费时间内会自动抬杆。

(4)支持app上查询附近停车场(导航,可用车位数,停车场费用,优惠券,评分,评论等),可预约车位。

(5)断电断网支持岗亭人员使用app可接管硬件进行停车记录的录入。

技术架构

后端开发语言java,框架oauth2+springboot2+doubble2.7.3,数据库mysql/mongodb/redis,即时通讯底层框架netty4,安卓和ios均为原生开发,后台管理模板vue-typescript-admin-template,文件服务fastDFS,短信目前仅集成阿里云短信服务。为千万级数据而生,千万级用户无忧,目前真实用户40w无压力,大数据时代物联网必备。100+Java项目教程+源码+笔记

部署环境

目前仅测试linux环境一切正常,win环境没部署过,演示地址在本文章末尾

功能介绍

(1)兼容市面上主流的多家相机,理论上兼容所有硬件,可灵活扩展。

(2)相机识别后数据自动上传到云端并记录,校验相机唯一id和硬件序列号,防止非法数据录入。

(3)用户手机查询停车记录详情可自主缴费(支持微信,支付宝,银行接口支付,支持每个停车场指定不同的商户进行收款),支付后出场在免费时间内会自动抬杆。

(4)支持app上查询附近停车场(导航,可用车位数,停车场费用,优惠券,评分,评论等),可预约车位。

(5)断电断网支持岗亭人员使用app可接管硬件进行停车记录的录入。

开源情况

代码完全开源,不存在授权问题,完全自主原创,不存在任何后门,不使用任何第三方私有jar包,性能和安全完全自主可控,想怎么耍就这么耍,就是这么任性,后续更新的话本人会持续更新部署教程。代码专业规范,新手看得懂,高手喜欢用。本系统完全免费 。

软件架构

一、技术构成简述

编程语言与架构简述

1.开发语言

(1)服务端 服务端语言目前均采用java语言开发,jdk版本要求1.8+。开发框架为springboot2+dubbo,鉴权采用oauth2,DB操作框架Mybaits,即时通讯底层框架与协议netty4

(2)客户端 目前我们主要客户端分为三个场景,分别为安卓,ios,微信公众号。安卓与ios均为原生开发,H5页面web端框架为vue

(3)后台管理 后台管理前端框架采用的是主流的vue element admin(TypeScript版本),分层清晰,官方文档完整,社区活跃

2.数据存储

(1)重要数据存储 重要数据均采用mysql进行存储,支持部署主从,大部分数据尽可能进行事务处理,确保数据容灾性

(2)一般数据存储 非重要性数据例如聊天内容,系统消息通知,广告等数据均存储于mongodb数据库中

(3)缓存数据存储 微小量缓存会存在mysql中,例如评论的前N条评论快照会超小量进行字段适当冗余,在提高存储性价比情况下大大提高数据的查询能力。其它大部分数据缓存均存储于redis数据中

3.性能与安全

(1)性能解决方案 架构与技术解决方案均为本团队一线5年开发经验总结,目前我们正在接触的项目真实用户40w+,毫无压力,我们系统采用的架构与技术均在仔细多方面综合考虑后多次调整,采用更加合理,性能更佳的模式与解决方案

(2)安全解决方案 所有请求均需携带jwt串token进行访问,每个接口服务和管理服务均需配置公钥文件且具有jwt串token合法性校验能力,用户权限服务携带私钥文件负责密钥生成

4.架构与生命力

(1)采用架构 本系统采用阿里巴巴微服务框架dubbo来进行实现微服务提供能力,追求高性能,高可用,超细粒度独立微服务,相同服务可以动态灵活增加与减少,支持不停机发布新版本服务。每个服务之间均为独立存在,互不影响。例如短信发送,支付,订单,停车场系统接口,停车场后台管理,停车场提供者服务等均为独立的服务。

(2)架构潜力 整个系统众多服务分工明确,细粒度微服务,实现真正的插拔服务,功能的删减或停用,新增等均可在不破坏和入侵原来系统的前提下满足新的开发需求

5.二次开发说明

(1)适用客户对象 ①本身有互联网it编程技术和经验或者拥有技术团队的。 ②不具备第一个条件但是费用预算比较充足,二次开发需求少或者愿意支付高额定制费的

(2)团队要求 服务器运维,安卓与ios开发者,web前端开发者,java实际开发经验2年+开发者

(3)技术要求 过硬的java编程能力,网络编程能力,数据库设计与优化能力,架构设计能力,微服务思维能力,成熟的前端技术开发能力,中大型系统部署与运营能力

(4)硬件要求 Linux操作系统,4核8G(最低)5M带宽,可多台服务器中的微服务指向统一微服务调度中心(本系统微服务调度中心管理平台zookeeper)

软件与硬件数据交互简述

1.硬件端

(1)目前解决方案 封装工具类,兼容市场主流硬件设备,只负责各类硬件数据封装为统一数据结构。硬件发包目前多为http主动推送数据,被动接受服务端返回指令

(2)未来解决方案 改造主流厂商硬件底层服务系统,新增硬件规范的合法身份数据,采用长连接进行数据交互,保证数据与指令的实时性与可靠性得到更好的保障

2.服务端

(1)被动处理硬件数据 中间件处理各类前端数据,接收硬件推送数据,解析,计算,做出相应反馈

(2)主动通知硬件发生事件行为 长连接推送指令,例如开闸,实时动态配置硬件数据等,

二、常规功能简述

基础功能

1.硬件管理 支持单个硬件管理与记录,硬件在线状态,维修与进度记录等。与指定停车场出入口进行绑定,均有记录GPS位置

2.停车场管理 不同时段费用配置,每日封顶因素综合参与动态计费,也支持静态+每日上限计费。支持查询附近停车场功能

3.停车记录管理 详细记录产生时间,地点,进出口位置,进出时间,异常数据实时推送与快速处理

4.支付机构管理 每个停车场的支付账号均可以独立配置,支持同一个停车场使用多家支付机构进行支付,例如支付宝,微信,银联等。

5.支付与优惠活动管理 支付宝与微信,银联都均支持免密支付(无感支付)。本系统自带优惠券功能,支持支持多种套餐自定义与用户进行快捷手机上下单随时购买。100+Java项目教程+源码+笔记

特色功能

1.异常数据实时推送,汇报,及时处理,提前预知与通知

2.即时通讯功能(IM聊天沟通) 性能,架构,优化等均参考微信聊天功能机制进行开发

3.行业好友与圈子 让该应用不止只能停车,还能交到志同道合的行业知音,让应用更有温度

4.商城与营销功能 此功能主要考虑到使用者有运营周边的兴趣和能力,在商城和广告营销上进行盈利

安装教程

  1. 安装JDK1.8+
  2. 安装MySQL5.6+ 安装MongoDB 安装Redis 安装FastDFS 安装Zookeeper
  3. 将打包好的代码上传到服务器上,直接运行jar包即可

地址:

项目地址:github.com/981011512/-…

演示地址:http://139.9.155.149 admin 123456

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go 面试官:GMP 模型,为什么要有 P?

发表于 2021-05-31

微信搜索【脑子进煎鱼了】关注这一只爆肝煎鱼。本文 GitHub github.com/eddycjy/blo… 已收录,有我的系列文章、资料和开源 Go 图书。

大家好,我是煎鱼。

最近金三银四,是面试的季节。在我的 Go 读者交流群里出现了许多小伙伴在讨论自己面试过程中所遇到的一些 Go 面试题。

今天的主角,是 Go 面试的万能题 GMP 模型的延伸题(疑问),那就是 ”GMP 模型,为什么要有 P?“

进一步推敲问题的背后,其实这个面试题本质是想问:”GMP 模型,为什么不是 G 和 M 直接绑定就完了,还要搞多一个 P 出来,那么麻烦,为的是什么,是要解决什么问题吗?“

这篇文章煎鱼就带你一同探索,GM、GMP 模型的变迁是因为什么原因。

GM 模型

在 Go1.1 之前 Go 的调度模型其实就是 GM 模型,也就是没有 P。

今天带大家一起回顾过去的设计。

解密 Go1.0 源码

我们了解一个东西的办法之一就是看源码,和煎鱼一起看看 Go1.0.1 的调度器源码的核心关键步骤:

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
c复制代码static void
schedule(G *gp)
{
...
schedlock();
if(gp != nil) {
...
switch(gp->status){
case Grunnable:
case Gdead:
// Shouldn't have been running!
runtime·throw("bad gp->status in sched");
case Grunning:
gp->status = Grunnable;
gput(gp);
break;
}

gp = nextgandunlock();
gp->readyonstop = 0;
gp->status = Grunning;
m->curg = gp;
gp->m = m;
...
runtime·gogo(&gp->sched, 0);
}
  • 调用 schedlock 方法来获取全局锁。
  • 获取全局锁成功后,将当前 Goroutine 状态从 Running(正在被调度) 状态修改为 Runnable(可以被调度)状态。
  • 调用 gput 方法来保存当前 Goroutine 的运行状态等信息,以便于后续的使用;
  • 调用 nextgandunlock 方法来寻找下一个可运行 Goroutine,并且释放全局锁给其他调度使用。
  • 获取到下一个待运行的 Goroutine 后,将其的运行状态修改为 Running。
  • 调用 runtime·gogo 方法,将刚刚所获取到的下一个待执行的 Goroutine 运行起来。

思考 GM 模型

通过对 Go1.0.1 的调度器源码剖析,我们可以发现一个比较有趣的点。那就是调度器本身(schedule 方法),在正常流程下,是不会返回的,也就是不会结束主流程。

G-M模型简图

他会不断地运行调度流程,GoroutineA 完成了,就开始寻找 GoroutineB,寻找到 B 了,就把已经完成的 A 的调度权交给 B,让 GoroutineB 开始被调度,也就是运行。

当然了,也有被正在阻塞(Blocked)的 G。假设 G 正在做一些系统、网络调用,那么就会导致 G 停滞。这时候 M(系统线程)就会被会重新放内核队列中,等待新的一轮唤醒。

GM 模型的缺点

这么表面的看起来,GM 模型似乎牢不可破,毫无缺陷。但为什么要改呢?

在 2012 年时 Dmitry Vyukov 发表了文章《Scalable Go Scheduler Design Doc》,目前也依然成为各大研究 Go 调度器文章的主要对象,其在文章内讲述了整体的原因和考虑,下述内容将引用该文章。

当前(代指 Go1.0 的 GM 模型)的 Goroutine 调度器限制了用 Go 编写的并发程序的可扩展性,尤其是高吞吐量服务器和并行计算程序。

实现有如下的问题:

  • 存在单一的全局 mutex(Sched.Lock)和集中状态管理:
    • mutex 需要保护所有与 goroutine 相关的操作(创建、完成、重排等),导致锁竞争严重。
  • Goroutine 传递的问题:
    • goroutine(G)交接(G.nextg):工作者线程(M’s)之间会经常交接可运行的 goroutine。
    • 上述可能会导致延迟增加和额外的开销。每个 M 必须能够执行任何可运行的 G,特别是刚刚创建 G 的 M。
  • 每个 M 都需要做内存缓存(M.mcache):
    • 会导致资源消耗过大(每个 mcache 可以吸纳到 2M 的内存缓存和其他缓存),数据局部性差。
  • 频繁的线程阻塞/解阻塞:
    • 在存在 syscalls 的情况下,线程经常被阻塞和解阻塞。这增加了很多额外的性能开销。

GMP 模型

为了解决 GM 模型的以上诸多问题,在 Go1.1 时,Dmitry Vyukov 在 GM 模型的基础上,新增了一个 P(Processor)组件。并且实现了 Work Stealing 算法来解决一些新产生的问题。

GMP 模型,在上一篇文章《Go 群友提问:Goroutine 数量控制在多少合适,会影响 GC 和调度?》中已经讲解过了。

觉得不错的小伙伴可以关注一下,这里就不再复述了。

带来什么改变

加了 P 之后会带来什么改变呢?我们再更显式的讲一下。

  • 每个 P 有自己的本地队列,大幅度的减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减少。而 GM 模型的性能开销大头就是锁竞争。
  • 每个 P 相对的平衡上,在 GMP 模型中也实现了 Work Stealing 算法,如果 P 的本地队列为空,则会从全局队列或其他 P 的本地队列中窃取可运行的 G 来运行,减少空转,提高了资源利用率。

为什么要有 P

这时候就有小伙伴会疑惑了,如果是想实现本地队列、Work Stealing 算法,那为什么不直接在 M 上加呢,M 也照样可以实现类似的组件。为什么又再加多一个 P 组件?

结合 M(系统线程) 的定位来看,若这么做,有以下问题:

  • 一般来讲,M 的数量都会多于 P。像在 Go 中,M 的数量默认是 10000,P 的默认数量的 CPU 核数。另外由于 M 的属性,也就是如果存在系统阻塞调用,阻塞了 M,又不够用的情况下,M 会不断增加。
  • M 不断增加的话,如果本地队列挂载在 M 上,那就意味着本地队列也会随之增加。这显然是不合理的,因为本地队列的管理会变得复杂,且 Work Stealing 性能会大幅度下降。
  • M 被系统调用阻塞后,我们是期望把他既有未执行的任务分配给其他继续运行的,而不是一阻塞就导致全部停止。

因此使用 M 是不合理的,那么引入新的组件 P,把本地队列关联到 P 上,就能很好的解决这个问题。

总结

今天这篇文章结合了整个 Go 语言调度器的一些历史情况、原因分析以及解决方案说明。

”GMP 模型,为什么要有 P“ 这个问题就像是一道系统设计了解,因为现在很多人为了应对面试,会硬背 GMP 模型,或者是泡面式过了一遍。而理解其中真正背后的原因,才是我们要去学的要去理解。

知其然知其所以然,才可破局。

若有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创作的最大动力,感谢支持。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,回复【000】有我准备的一线大厂面试算法题解和资料;本文 GitHub github.com/eddycjy/blo… 已收录,欢迎 Star 催更。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…656657658…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%