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

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


  • 首页

  • 归档

  • 搜索

SpringMVC 参数解析器 和 Spring 类型转换

发表于 2021-11-30

前言

最近同事因为工作忙没时间面试候选人,偶尔会让我临时替补,就这样我作为替补面试官面了一个初级妹子,两个(自称)高级的开发。我面试候选人的习惯就是看他简历写啥我就问啥。他们的简历上都有写熟悉 SpringMVC ,于是我随便问了两个问题就回答的很不理想

  • SpringMVC 的执行流程
  • 前端传递 ?username=xx&password=xx ,controller 中就能自动用 User 对象来接收,这是如何实现的?

第一个问题应该比较简单,随便背背就好了,然而两个高级开发也是回答的很模糊……

第二个问题对于初级开发来说可能是相对有难度,但是对于 5-10 年的高级开发连边都答不上,我觉得这是很不应该的,我的要求很简单,并不是要候选人把完整的源码流程说出来,只要候选人能够答道 参数解析器/类型转换器,我就觉得这个问题可以过了。

PS 最让我伤心的是候选人税前年薪 40w +,期望年薪 50w + 都快到我三倍了……然后居然这些基础都答不上,消息队列和缓存中间件的应用场景也回答的模模糊糊,随便提个问题就答不上了。于是我只能安慰自己,他们肯定是来面管理岗的!薪资高正常……对,就是这样!!!

灵魂拷问

让我们先来看一段很常见的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@GetMapping("/example/{id}")
public void example(@PathVariable Long id,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime dateTime,
@RequestParam List<Long> ids,
@RequestParam Boolean enable,
@RequestParam ExampleEnum exampleEnum,
User user) {
log.info(id.toString());
log.info(dateTime.toString());
log.info(ids.toString());
log.info(enable.toString());
log.info(exampleEnum.toString());
log.info(user.toString());
}

然后我们写个测试模拟 HTTP 请求

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码@Test
void example() throws Exception {
mvc.perform(
get("/example/1")
.param("dateTime", "2020-09-02T16:00:01")
.param("ids", "1,2,3")
.param("enable", "no")
.param("exampleEnum", "ONE")
.param("username", "暮色妖娆丶")
)
.andExpect(status().isOk());
}

再来看打印的日志

1
2
3
4
5
6
ini复制代码: 1
: 2020-09-02T16:00:01
: [1, 2, 3]
: false
: ONE
: User(username=暮色妖娆丶)

这样的代码我们几乎每天都在写,但是你有没有想过

  • 为什么传的 1,2,3 能自动转为 List<Long>?
  • 为什么传的 no 能够被转为布尔类型的 false?
  • 为什么传的 username 就能被自动封装到 user 中?
  • 为什么传的 ONE 就能被自动封装到 ExampleEnum 中?

如果你提出疑问,我会用不就行了?为什么要理解它是怎么实现的?那么请你直接跳转到这里 自定义参数解析器优化分页代码

SpringMVC 执行流程

在搞清楚上述问题之前,我们先要知道 SpringMVC 在接收到一个请求,到这个请求进入控制器方法体之前这段时间都干了哪些事情。这个流程网上一搜到处都是,其实自己去 debug 一下源码就很清楚了,大致是以下几步

图中标颜色的,也是比较重要的一步,真正处理请求的 handle 方法,这里面包含了非常重要的一步操作,就是 参数解析 resolveArgument() 它的作用就是将我们原始请求的参数,解析成我们控制器方法上真正需要的参数类型。那么它是如何解析的呢?说到这就不得不提 SpringMVC 的参数解析器。

参数解析器

SpringMVC 提供了 20+ 种参数解析器来解析 @RequestParam、@RequestBody、@PathVariable...... 等注解的请求参数。

顶层接口是 HandlerMethodArgumentResolver ,所有的参数解析器都实现了这个接口,观察其源码

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码public interface HandlerMethodArgumentResolver {
/**
* ...
*/
boolean supportsParameter(MethodParameter parameter);
/**
* ...
*/
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

这是一个典型的策略模式接口,判断当前的方法形参类型是否支持,如果支持就使用该实现类的参数解析器对其解析。那么聪明的你可能已经想到了,这很明显我们可以实现该接口定制化自己的参数解析器。

这样一来上面我们提的几个问题就很显而易见了,是 SpringMVC 提供的一系列参数解析器帮我们实现这么智能化的转换。那么只要去看参数解析器里面的具体实现就行了。说到具体实现就不得不提 Spring 核心之类型转换器

类型转换器

Spring 3.0 引入了一个 core.convert 提供通用类型转换系统的包。系统定义了一个 SPI 来实现类型转换逻辑,并定义一个 API 来在运行时执行类型转换。这套类型转换系统主要有以下几个核心接口

Converter

顶层接口 Converter 观察其源码

1
2
3
4
5
java复制代码@FunctionalInterface
public interface Converter<S, T> {
@Nullable
T convert(S var1);
}

该接口的作用是将类型 S 转换为 T,在参数解析器中使用该接口的实现将前端请求参数转成控制器方法需要的参数类型。但是很多场景下,我们其实是要将一种类型转换为一组类型,比如刚刚上面的例子把 String 转换成 Enum ,为此 Spring 提供了另一个接口 ConverterFactory

ConverterFactory

观察其源码

1
2
3
csharp复制代码public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> var1);
}

该接口的作用是将类型 S 转换为 R 及其子类型(看源码的前提泛型必须要掌握,如果对泛型不太熟悉的同学可以看我写的 一文带你搞懂 Java 泛型 )。

ConversionService

ConversionService 定义了一个统一的 API 用来在运行时执行类型转换的逻辑,观察其源码

1
2
3
4
5
6
7
8
9
10
11
less复制代码public interface ConversionService {
boolean canConvert(@Nullable Class<?> var1, Class<?> var2);

boolean canConvert(@Nullable TypeDescriptor var1, TypeDescriptor var2);

@Nullable
<T> T convert(@Nullable Object var1, Class<T> var2);

@Nullable
Object convert(@Nullable Object var1, @Nullable TypeDescriptor var2, TypeDescriptor var3);
}

源码中可以说就两个方法,先判断能不能 convert,如果可以就执行真正的 convert 逻辑。所有的 Converter 都是在 ConversionService 后面默默的执行工作,所以这里是一个外观模式的体现 (Spring 家族的源码中设计模式多的一批……)

Spring 提供的 Converter

以示例代码为例,

  • ids -> 1,2,3 能够用 List<Long> 接收是因为 Spring 提供了 StringToCollectionConverter
  • enable -> no 能够用 Boolean 接收是因为 Spring 提供了 StringToBooleanConverter,源码中定义了以下值全部都能用 Boolean 来接收,有兴趣可以亲自尝试~
1
2
3
4
5
6
7
8
9
10
csharp复制代码static {
trueValues.add("true");
trueValues.add("on");
trueValues.add("yes");
trueValues.add("1");
falseValues.add("false");
falseValues.add("off");
falseValues.add("no");
falseValues.add("0");
}
  • exampleEnum -> ONE 能够用 ExampleEnum 接收是因为 Spring 提供了 StringToEnumConverterFactory 。这个 convert 方法体就更简单了
1
2
3
4
typescript复制代码@Nullable
public T convert(String source) {
return source.isEmpty() ? null : Enum.valueOf(this.enumType, source.trim());
}

Spring 提供的所有支持的类型转换器实现类都在 org.springframework.core.convert.support 包下,有兴趣可以阅读源码,很多转换器的 convert 方法体都非常简单~~

SpringBoot 提供的 Converter

在 Spring 提供的类型转换器之外,SpringBoot 又给我们提供了一些转换器的实现来简化我们的开发,这些转换器位于 org.springframework.boot.convert 包下,以最常见的 StringToDurationConverter 为例,你有没有想过为啥你在 application.yml 中配置 7d 这样的字符串,就能在配置类中用 Duration 映射?

1
2
yaml复制代码auto-review-loan:
effective-time: 7d
1
2
3
4
5
6
7
8
9
less复制代码@ConfigurationProperties(prefix = AutoReviewLoanProperties.PREFIX)
@Component
@Data
public class AutoReviewLoanProperties {
public static final String PREFIX = "auto-review-loan";

/** 风控给的额度有效时间,默认7天 */
private Duration effectiveTime = Duration.ofDays(7);
}

翻一翻 StringToDurationConverter 源码就知道啦~

自定义参数解析器优化分页代码

案例场景

试想现在有这么一个场景,你的公司最近引入了 MyBatisPlus 组件,在使用它自带的 IPage 分页的时候你们会如何写代码?大多数写法可能是这样

1
2
3
4
less复制代码public IPage<Response> page(@RequestParam Integer pageNum , @RequestParam Integer pageSize) {
IPage<Response> page = new Page<>(pageNum,pageSize);
return service.page(page);
}

或者也可能是这样

1
2
3
4
vbscript复制代码public IPage<Response> page(PageRequest request) {
IPage<Response> page = new Page<>(request.getPageNum(),request.getPageSize());
return service.page(page);
}

当然这两种写法本身没有什么问题,但是写多了之后你就会发现这行获取分页对象的代码是重复的,每一个分页接口都得写下面这行代码

1
ini复制代码IPage<Response> page = new Page<>(pageNum,pageSize)

本着 DRY(dont repeat yourself) 原则,我们有没有办法能省掉这行重复的代码呢?聪明的你可能已经想到了,我们可以尝试把 分页对象 IPage page; 定义到方法参数上去嘛,但是直接放上去的话,前端请求参数 pageNum、pageSize 是没法直接被转换成 IPage 类型的。想起上面我们说过的,反正它参数的解析规则都是参数解析器来做的,那么我实现 HandlerMethodArgumentResolver 自己写一个参数解析器来实现这个功能不就可以了嘛~~

自定义参数解析器

注意到分页字段在 IPage 的实现类 Page 中,所以我们的参数解析器策略要支持的是 Page.class,核心代码就是 HandlerMethodArgumentResolver 的两个方法实现。观察到项目中已经引入了 spring-data-common 依赖,它已经给我们提供了一个很好用的参数解析器实现类 PageableHandlerMethodArgumentResolver ,直接用它即可

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
java复制代码/** 自定义分页参数解析器 */
public class PageHandlerMethodArgumentResolver<T> implements HandlerMethodArgumentResolver {

private static final int DEFAULT_PAGE = 0;
private static final int DEFAULT_SIZE = 10;
private static final String DEFAULT_PAGE_PARAMETER = "pageNum";//参数名
private static final String DEFAULT_SIZE_PARAMETER = "pageSize";//参数名

private final PageableArgumentResolver pageableArgumentResolver;

//构造 pageableArgumentResolver 对象,设置分页字段名,默认大小等属性
public PageHandlerMethodArgumentResolver() {
PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver();
resolver.setFallbackPageable(PageRequest.of(DEFAULT_PAGE, DEFAULT_SIZE));
resolver.setSizeParameterName(DEFAULT_SIZE_PARAMETER);
resolver.setPageParameterName(DEFAULT_PAGE_PARAMETER);
resolver.setOneIndexedParameters(true);
this.pageableArgumentResolver = resolver;
}

//配置该参数解析器支持的方法参数类型是 Page
@Override
public boolean supportsParameter(MethodParameter parameter) {
return Page.class.equals(parameter.getParameterType());
}

//具体解析逻辑,返回 Page 对象
@Override
public Object resolveArgument(
@NonNull MethodParameter parameter,
ModelAndViewContainer mavContainer,
@NonNull NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
Pageable pageable =
pageableArgumentResolver.resolveArgument(
parameter, mavContainer, webRequest, binderFactory);
return new Page<T>(pageable.getPageNumber() + 1L, pageable.getPageSize());
}
}

定义好参数解析器之后我们需要把它添加到 SpringMVC 的解析器集合中,写一个实现 WebMvcConfigurer 接口的配置类覆盖 addArgumentResolvers() 即可

1
2
3
4
5
csharp复制代码/** 添加自定义分页参数解析器 */
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new PageHandlerMethodArgumentResolver<>());
}

准备工作完成,现在我们就可以在控制器方法上使用 (Page page) 来接收 pageNum 和 pageSize请求参数了,代码如下

1
2
3
4
kotlin复制代码@GetMapping("/xxx")
public IPage<Response> page(Page<Response> page) {
return service.page(page);
}

经过测试,的确达到了我们的效果,GET /xxx?pageNum=1&pageSize=10 请求的分页参数的确被解析成了 Page 对象,但是新的问题来了,由于 Page 类中还有很多其他属性导致 swagger 页面显示很多无用信息。

image.png

优化分页对象在 swagger 页面的展示

集成 swagger 的组件都提供了类型替换功能,我们可以将一个类型替换成另一个类型在 swagger 页面展示。新建一个分页参数实体类

1
2
3
4
5
6
7
8
9
10
less复制代码@Data
@ParameterObject
public class PageRequest {

@Parameter(description = "每页记录数", example = "10")
private Integer pageSize = 10;

@Parameter(description = "页数", example = "1")
private Integer pageNum = 1;
}

如果使用的是 springfox 组件,可以采用如下方法替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码@Bean
public AlternateTypeRuleConvention pageAlternateTypeRuleConvention(final TypeResolver resolver) {
return new AlternateTypeRuleConvention() {
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}

/** 在 swagger 页面把 Page 类型替换成 PageRequest,只显示 pageNum 和 pageSize 两个参数 */
@Override
public List<AlternateTypeRule> rules() {
return Collections.singletonList(
AlternateTypeRules.newRule(
resolver.resolve(Page.class, WildcardType.class),
resolver.resolve(PageRequest.class)));
}
};
}

如果使用的是 springdoc 组件,可以采用如下方法替换

1
2
3
4
5
6
7
arduino复制代码/*
注意 swagger页面 Page 类型被替换成 PageRequest,
所以分页接口返回值要使用 IPage 类型,不能用 Page,否则swagger页面不会正确显示
*/
static {
SpringDocUtils.getConfig().replaceWithClass(Page.class, PageRequest.class);
}

替换之后再看 swagger 页面已经达到了效果

image.png

值得注意的是由于 swagger 页面把 Page 换成了 PageRequest,所以分页接口返回值不能再使用 Page 而要使用 IPage

总结

经过以上案例可以发现,只有对开源组件的原理有一定理解,才能够对其进行扩展,解决问题~~更为重要的是 能够对现有项目进行不断优化迭代,才能让领导对你刮目相看啊,升职加薪就指日可待了!!!

结语

如果这篇文章对你有帮助,记得点赞加关注!你的支持就是我继续创作的动力!

  • 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。

本文转载自: 掘金

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

从双十一的物流大战,看全球通信网络的低延迟优化

发表于 2021-11-30

【融云全球互联网通信云】双十一刚刚过去,你的快递都收到了吗?好像曾经因流量激增,导致各地中转及收件点爆仓,快递迟迟不到,延迟甚至长达半个月的新闻几乎绝迹。当运输速度恒定,中转站点的多寡、分拣能力的强弱、是否丢包重发,决定了你的快递能否如期到达。

那么,如果 IM 消息是物,音视频内容是物,那么全球通信网就是负责传输的物流系统。在物理距离恒定的前提下,对于路由跳数、网络带宽、网络质量和缓存队列的设计和优化,决定了系统能否做到高质量、低延迟的传输。

这是融云首席架构师李淼在 WICC 广州“出海分论坛”中分享的话题引子。也因此,李淼关于《全球低延迟通信网络的设计与优化》的话题分享变得更加具象。

RTC 与 IM 全球网络的设计有所同,有所不同

融云全球通信网络分为 RTC 全球网络和 IM 全球通信络两个部分,这是由于 RTC 和 IM 在传输中不同的加速特点所决定。

图片

(RTC 网络与 IM 网络)

相同点在于:二者可在数据中心、节点等多项物理设施上进行复用,并且都必须保证高质量、低延迟的传输,从而为用户带来极佳的场景体验。

不同点在于:RTC 基于 UTP 协议运行,对于用户体验而言,允许有一定的丢包率,但对于延时要求苛刻;而 IM 基于 TCP 协议进行业务承载,在要求消息不能丢失的同时,需要消息的集中存储,不仅能为用户不在线时存储离线消息,还要根据业务类型,进行历史消息的存储。

因此,融云对于 RTC 的设计,是完全去中心化的分布式通信网络。好处是在后续进行网络优化时,可以随意增加媒体节点部署,而不影响用户的任何使用体验。

融云 IM 的网络设计采用的是将数据流量导入到数据中心的方式,已陆续在国内、北美和新加坡分别设立了数据中心,目前已迭代至基于 Anycast 的一体化加速网。特点在于多协议支持、多数据中心支持,并且,基于 SmartDNS & Anycast 的加速原理可以更高质量地保证在全球范围内,节点分配的准确度。此外,IM 的许多全球链路优化工作,都可以在 RTC 上复用。

了解完以上架构,重点来了:融云是如何进行延时优化的呢?这需要分别从 RTC 和 IM 两个方向进行解析。

如何降低 RTC 的网络延时

图片

(RTC 通信过程)

对于 RTC 而言,能降低延时最好的办法,就是提高 RTC 节点的覆盖率,目的在于缩短用户与边缘节点的物理距离,也就意味着以更少的跳数完成连接。

融云对于节点的选择先是要保证大洲级的全覆盖,再是对热门区域进行重点覆盖。所选节点基于一线 IaaS 厂商的公有云服务搭建,每个节点之间都可通过专线互联。不但可以提升链路传输的稳定性,还可以降低 RTC 节点的跳数,甚至可以做到 0 跳或者 1 跳。

优化的难点在于:如何让用户选择到质量最好的节点。 通常最直观的办法是通过智能 DNS 解析,但融云经过验证发现,准确度率只在 80% 左右。为此,融云在之后增加了 IP Anycast,它跟 DNS 原理完全不同,可直接通过 IP 的方式来进行分配,这个分配是运营商级的。

在链路探测方面,物理距离最近的 IDC 未必就是质量最好的节点,即便采用 smart DNS+IP Anycast,准确度依然无法达到 100%。为此,融云增加了客户端的探测能力,在用户连接时下发 N 个地址。客户端根据下发地址进行探测,择优选择链路连接。据日志分析,准确度达 99.5% 以上。

同云连接可以通过链路优化来保证,那么跨云又该怎么办呢?

融云的做法是通过二级级联,将数据中心之间的流量通过所采购的 SD-WAN 进行导入导出。这其中,级联优化至关重要。

比如,一个北美用户跟一个国内用户通信,融云会先在北美与香港之间进行专线互联,然后香港再与国内的节点进行专线互联。这种通过香港节点进行转发的方案,能够在保证质量的前提下,达到低延时的网络优化效果。

但难点在于:故障降级。传输过程中,同云的专线和 SD-WAN 都可能会出现故障。尽管故障的概率极低,但一旦故障发生,就必须有所取舍,为了保证用户能够正常接听互通,只能选择将整个通讯链路进行降级。比如当专线出问题时,会通过二级级联的方式,进行节点的跳转,或者直接通过互联网公网的方式进行数据的转发。

此外,要降延就要有完善的网络延时监控系统。融云在客户端建设了各种标准的 QoS 监测系统,包括数据实时上报和后台分析。

如何降低 IM 的网络延时

IM 的网络延时优化途径主要集中于节点间数据转发和证书计算前置两个方面。

在节点数据的转发方面:由于 IM 数据基于 TCP 协议传输,但 TCP 的拥塞控制和丢包重传策略并不友好,因此融云将部分 TCP 协议替换成 QUIC 协议,也就是说,从物理距离最远的边缘节点到路由节点数据的传输,融云都通过 QUIC 进行了优化。

图片

(IM 全球网络的历程)

通过 QUIC 优化,首先可以避免在边缘点跟路由节点之间,TCP 的三次握手,直接将 TLS RTT 降为 0;其次是当网络抖动时,QUIC 有更友好的丢包重传策略,可以做到丢哪个包就补哪个包,而不会像 TCP 那样,一旦丢包,后续所有的包都要进行重传。内测表明,这一优化,使整个网络延时降低了 15% 左右。

在证书计算前置方面:融云采取将 TLS 证书和 SSL 的证书,在边缘节点上直接进行交换的方式。这样一来,首先是减少了用户数据到数据中心之间的整体的 RTT,可将 RTT 直接降到 0。其次,IM 多有小包通讯的场景,例如一个信令包只有 10-20 个字节,通过在边缘点上将数据包进行解密,明文传递到融云的路由节点,再进行加密传到数据中心,大大降低了两个最远物理距端点间的数据传输量。

需要说明的是,用户完全无需担心数据的安全问题。因为融云的边缘节点和路由节点全部由融云控制,均为受信网络。但如果是必须要在公网完成数据传输,融云仍然会通过传统 TLS 方式来进行数据链路加密。

当然,融云对 IM 的优化策略远不止于此,更多表现在客户端及服务端日志的收集、zero copy、多路复用、IP 直连和 QoS 保证等多个方面。

比如对日志的收集,融云每发一个 SDK 版本,都会增加新的日志埋点,用于分析业务、分析网络等,以此进行一些定向或定点区域的优化。

在谈及未来计划时,李淼指出,融云将不计成本,不遗余力地继续加大网络建设力度,为开发者提供更加优质的服务。就研发而言,将持续提升软件本身的处理能力,不断丰富数据收集的手段,同时提升数据预估的准确性。

本文转载自: 掘金

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

SpringBoot项目引入logback进行项目日志管理

发表于 2021-11-30

序

这是我参与11月更文挑战的第7天,活动详情查看:[2021最后一次更文挑战]

嘿,线上系统出BUG了

0f42f793b893bd1e5eed37c57e86e3e.jpg
哇,出现bug了,咋办咋办呀…….挠头.img……
cfe55edea7fc9b1a330648c4185ae3a.jpg
哦,去看看日志吧!

对于已经上线的线上系统一旦出现bug最快速的方式只能是查看系统的错误日志,进行问题定位,然后进行解决问题。不能说像我们平时 debug 等模式进行问题定位,先通过日志找到问题,然后进行复现,从而解决后面在客户允许的情况先才能够重写部署新的包!

系统日志概述

对于目前的情况来说,对于Java系统中系统日志,使用比较广泛的日志框架:
log4j,logback….

log4j logback 关系

介绍它们的话我slf4j肯定得提一嘴了。

1
vbnet复制代码Simple Logging Facade for Java (SLF4J) serves as a simple facade or abstraction for various logging frameworks, such as java.util.logging, logback and log4j. SLF4J allows the end-user to plug in the desired logging framework at *deployment* time. Note that SLF4J-enabling your library/application implies the addition of only a single mandatory dependency, namely *slf4j-api-${project.version}.jar*.

上面的意思其实就是:slf4j是一系列的日志接口,简单日志门面.
log4j和logback是众多日志框架中的几种,它们实现了部分slf4j的一些接口,下面的图应该更直观一点:

log4j

image.png

logback

image.png

关于详细的我就不多解释了有趣的是:

logback同样是由log4j的作者设计完成的,拥有更好的特性,用来取代log4j的一个日志框架,是slf4j的原生实现(即直接实现了slf4j的接口,而log4j并没有直接实现,所以就需要一个适配器slf4j-log4j12.jar)

总结

1、slf4j是java的一个日志门面,实现了日志框架一些通用的api,log4j和logback是具体的日志框架。

2、他们可以单独的使用,也可以绑定slf4j一起使用。

单独使用,分别调用框架自己的方法来输出日志信息。绑定slf4j一起使用。调用slf4j的api来输入日志信息,具体使用与底层日志框架无关(需要底层框架的配置文件)。显然不推荐单独使用日志框架。假设项目中已经使用了log4j,而我们此时加载了一个类库,而这个类库依赖另一个日志框架。这个时候我们就需要维护两个日志框架,这是一个非常麻烦的事情。而使用了slf4j就不同了,由于应用调用的抽象层的api,与底层日志框架是无关的,因此可以任意更换日志框架。

logback在项目中的使用

springBoot项目拿来即用

首先一般来说我们不是应该会需要去应用一个logback的pom嘛,eg:

1
2
3
4
5
6
7
xml复制代码<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>

但是Spring2.x过后自带日志工具就是Slf4j,所以系统中spring-boot-statter 中已经引用了logback相关的依赖了,不需要去引用特殊的依赖了

image.png
所以一般springboot项目中 logback 或者 logf4j 如果你想要使用的话一般就直接配置一些相关的xml配置文件直接使用就行啦!!!

logback-spring.xml

在项目resources中创建 logback-spring.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
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别 TRACE < DEBUG < INFO < WARN < ERROR < FATAL 级别低的会打印比它高的日志,反向不会 -->
<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration scan="true" scanPeriod="60 seconds">
<contextName>logback</contextName>

<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
<!-- 从 Spring Boot 配置文件中,读取 spring.application.name 应用名 -->
<springProperty name="applicationName" scope="context" source="spring.application.name"/>
<!-- 定义在服务器端打印的日志路径-->
<property name="logPath" value="/var/log/${applicationName}"/>

<!--0. 日志格式和颜色渲染 -->
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex"
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx"
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

<!--1. 输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>debug</level>
</filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>

<!--2. 输出到文档-->
<!-- 2.1 level为 DEBUG 日志,时间滚动输出 -->
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文档的路径及文档名 -->
<file>${logPath}/debug.log</file>
<!--日志文档输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志归档 -->
<fileNamePattern>${logPath}/debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文档保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文档只记录debug级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>debug</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- 2.2 level为 INFO 日志,时间滚动输出 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文档的路径及文档名 -->
<file>${logPath}/info.log</file>
<!--日志文档输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${logPath}/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文档保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文档只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- 2.3 level为 WARN 日志,时间滚动输出 -->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文档的路径及文档名 -->
<file>${logPath}/warn.log</file>
<!--日志文档输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${logPath}/warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文档保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文档只记录warn级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- 2.4 level为 ERROR 日志,时间滚动输出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文档的路径及文档名 -->
<file>${logPath}/error.log</file>
<!--日志文档输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${logPath}/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文档保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文档只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- 2.5 所有 除了DEBUG级别的其它高于DEBUG的 日志,记录到一个文件 -->
<appender name="ALL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文档的路径及文档名 -->
<file>${logPath}/all.log</file>
<!--日志文档输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${logPath}/all-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文档保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文档记录除了DEBUG级别的其它高于DEBUG的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
</appender>

<!--
<logger>用来设置某一个包或者具体的某一个类的日志打印级别、
以及指定<appender>。<logger>仅有一个name属性,
一个可选的level和一个可选的addtivity属性。
name:用来指定受此logger约束的某一个包或者具体的某一个类。
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
还有一个特殊值INHERITED或者同义词NULL,代表强制执行上级的级别。
如果未设置此属性,那么当前logger将会继承上级的级别。
addtivity:是否向上级logger传递打印信息。默认是true。
<logger name="org.springframework.web" level="info"/>
<logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
-->

<!--
使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
【logging.level.org.mybatis=debug logging.level.dao=debug】
-->

<!--
root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
不能设置为INHERITED或者同义词NULL。默认是DEBUG
可以包含零个或多个元素,标识这个appender将会添加到这个logger。
-->

<springProfile name="dev">
<root level="info">
<appender-ref ref="ALL_FILE"/>
</root>
<!-- 开发环境, 指定某包日志为debug级 -->
<logger name="com.smile.ssm" level="debug"/>
</springProfile>

<springProfile name="test">
<root level="info">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
<appender-ref ref="ALL_FILE"/>
</root>
<!-- 测试环境, 指定某包日志为info级 -->
<logger name="com.smile.ssm" level="info"/>
</springProfile>

<springProfile name="pro">
<root level="info">
<!-- 生产环境最好不配置console写文件 -->
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
<appender-ref ref="ALL_FILE"/>
</root>
<!-- 生产环境, 指定某包日志为warn级 -->
<logger name="com.smile.ssm" level="warn"/>
<!-- 特定某个类打印info日志, 比如application启动成功后的提示语 -->
<logger name="com.smile.ssm.SsmApplication" level="info"/>
</springProfile>

</configuration>

小知识

今天遇见一个maven问题:
由于某些包应用导致引用的包出现了重复应用或者包冲突问题可以通过idea的maven视图工具进行处理

image.png
点击之后可以去看包之间的关联关系,如果出现上图中红色循环的话可以自己优化处理一下

image.png

处理方式:< exclusions></ exclusions> 这标签排除一些应用项目

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.2</version>
<exclusions>
<exclusion>
<groupId>org.apiguardian</groupId>
<artifactId>apiguardian-api</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>

END

源码地址:gitee.com/smile_lx/ss…

这个月最后一天啦,这个也是这个月参加这个活动的最后一篇了,说实话都是自己在自娱自乐挺花时间的也有意义吧,写点东西一步步的逼着自己去弄代码,希望这个项目能按照自己的想法走下去吧!

本文转载自: 掘金

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

音视频系列五:ffmpeg之rtmp推流阿里云转发vlc拉流

发表于 2021-11-30

title: 音视频系列五:ffmpeg之rtmp推流阿里云转发vlc拉流播放

categories:[ffmpeg]

tags:[音视频编程]

date: 2021/11/30

作者:hackett

微信公众号:加班猿

在前两篇 阿里云服务器搭建Nginx+rtmp推流服务器中,我们已经配置把阿里云的rtmp推流服务搭建好了,用的是PC软件OBS来进行推流到阿里云服务器,接下来就用雷神的最简单的基于ffmpeg的推流器,rtmp方式推流,阿里云服务器转发流,VLC拉流的流程走一遍。

链接地址:最简单的基于FFmpeg的推流器(以推送RTMP为例) blog.csdn.net/leixiaohua1…

一、RTMP简介

RTMP是Real Time Messaging Protocol(实时消息传输协议)的首字母缩写。该协议基于TCP,是一个协议族,包括RTMP基本协议及RTMPT/RTMPS/RTMPE等多种变种。RTMP是一种设计用来进行实时数据通信的网络协议,主要用来在Flash/AIR平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。支持该协议的软件包括Adobe Media Server/Ultrant Media Server/red5等。RTMP与HTTP一样,都属于TCP/IP四层模型的应用层。– 百度百科

RTMP推流器(Streamer)的在流媒体系统中的作用可以用下图表示。首先将视频数据以RTMP的形式发送到流媒体服务器端(Server,比如FMS,Red5,Wowza等),然后客户端(一般为Flash Player)通过访问流媒体服务器就可以收看实时流了。

二、程序流程图

RTMP采用的封装格式是FLV。因此在指定输出流媒体的时候需要指定其封装格式为“flv”。同理,其他流媒体协议也需要指定其封装格式。例如采用UDP推送流媒体的时候,可以指定其封装格式为“mpegts”。

av_register_all():注册FFmpeg所有编解码器。

avformat_network_init():初始化Network。

avformat_open_input():输入(Input)。

avformat_find_stream_info():查找流信息。

avformat_alloc_output_context2():分配输出 AVFormatContext。

avcodec_copy_context():复制AVCodecContext的设置(Copy the settings of AVCodecContext)。

avformat_write_header():写文件头(Write file header)。

av_read_frame():获取一个AVPacket(Get an AVPacket)。

av_rescale_q_rnd():转换PTS/DTS(Convert PTS/DTS)

av_interleaved_write_frame():写文件尾。

三、代码

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
cpp复制代码#include <iostream>

extern "C" {//包含C头文件
#include "libavformat/avformat.h" 
#include "libavutil/time.h"
};

int main(int argc, char* argv[]) {
 AVOutputFormat* ofmt = NULL;
 //输入对应一个AVFormatContext,输出对应一个AVFormatContext
 //(Input AVFormatContext and Output AVFormatContext)
 AVFormatContext* ifmt_ctx = NULL, * ofmt_ctx = NULL;
 AVPacket pkt;
 const char* in_filename, * out_filename;
 int ret, i;
 int videoindex = -1;
 int frame_index = 0;
 int64_t start_time = 0;
 //in_filename  = "cuc_ieschool.mov";
 //in_filename  = "cuc_ieschool.mkv";
 //in_filename  = "cuc_ieschool.ts";
 //in_filename  = "cuc_ieschool.mp4";
 //in_filename  = "cuc_ieschool.h264";
 in_filename = "./ouput_1min.flv";//输入URL(Input file URL)
 //in_filename  = "shanghai03_p.h264";

 //out_filename = "rtmp://localhost/publishlive/livestream";//输出 URL(Output URL)[RTMP]
 out_filename = "rtmp://阿里云服务器IP:1935/live";//输出 URL(Output URL)[RTMP]
 //out_filename = "rtp://233.233.233.233:6666";//输出 URL(Output URL)[UDP]

 //注册FFmpeg所有编解码器
 av_register_all();
 //Network
 avformat_network_init();
 //输入(Input)
 if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
  printf("Could not open input file.");
  goto end;
 }
 if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
  printf("Failed to retrieve input stream information");
  goto end;
 }

 for (i = 0; i < ifmt_ctx->nb_streams; i++)
  if (ifmt_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
   videoindex = i;
   break;
  }

 av_dump_format(ifmt_ctx, 0, in_filename, 0);

 //输出(Output)

 avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_filename); //RTMP
 //avformat_alloc_output_context2(&ofmt_ctx, NULL, "mpegts", out_filename);//UDP

 if (!ofmt_ctx) {
  printf("Could not create output context\n");
  ret = AVERROR_UNKNOWN;
  goto end;
 }
 ofmt = ofmt_ctx->oformat;
 for (i = 0; i < ifmt_ctx->nb_streams; i++) {
  //根据输入流创建输出流(Create output AVStream according to input AVStream)
  AVStream* in_stream = ifmt_ctx->streams[i];
  AVStream* out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec);
  if (!out_stream) {
   printf("Failed allocating output stream\n");
   ret = AVERROR_UNKNOWN;
   goto end;
  }
  //复制AVCodecContext的设置(Copy the settings of AVCodecContext)
  ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
  if (ret < 0) {
   printf("Failed to copy context from input to output stream codec context\n");
   goto end;
  }
  out_stream->codec->codec_tag = 0;
  if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
   out_stream->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
 }
 //Dump Format
 av_dump_format(ofmt_ctx, 0, out_filename, 1);
 //打开输出URL(Open output URL)
 if (!(ofmt->flags & AVFMT_NOFILE)) {
  ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
  if (ret < 0) {
   printf("Could not open output URL '%s'", out_filename);
   goto end;
  }
 }
 //写文件头(Write file header)
 ret = avformat_write_header(ofmt_ctx, NULL);
 if (ret < 0) {
  printf("Error occurred when opening output URL\n");
  goto end;
 }

 start_time = av_gettime();
 while (1) {
  AVStream* in_stream, * out_stream;
  //获取一个AVPacket(Get an AVPacket)
  ret = av_read_frame(ifmt_ctx, &pkt);
  if (ret < 0)
   break;
  //FIX:No PTS (Example: Raw H.264)
  //Simple Write PTS
  if (pkt.pts == AV_NOPTS_VALUE) {
   //Write PTS
   AVRational time_base1 = ifmt_ctx->streams[videoindex]->time_base;
   //Duration between 2 frames (us)
   int64_t calc_duration = (double)AV_TIME_BASE / av_q2d(ifmt_ctx->streams[videoindex]->r_frame_rate);
   //Parameters
   pkt.pts = (double)(frame_index * calc_duration) / (double)(av_q2d(time_base1) * AV_TIME_BASE);
   pkt.dts = pkt.pts;
   pkt.duration = (double)calc_duration / (double)(av_q2d(time_base1) * AV_TIME_BASE);
  }
  //Important:Delay 延时
  if (pkt.stream_index == videoindex) {
   AVRational time_base = ifmt_ctx->streams[videoindex]->time_base;
   AVRational time_base_q = { 1,AV_TIME_BASE };
   int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
   int64_t now_time = av_gettime() - start_time;
   if (pts_time > now_time)
    av_usleep(pts_time - now_time);

  }

  in_stream = ifmt_ctx->streams[pkt.stream_index];
  out_stream = ofmt_ctx->streams[pkt.stream_index];
  /* copy packet */
  //转换PTS/DTS(Convert PTS/DTS)
  pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
  pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
  pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
  pkt.pos = -1;
  //Print to Screen
  if (pkt.stream_index == videoindex) {
   printf("Send %8d video frames to output URL\n", frame_index);
   frame_index++;
  }
  //ret = av_write_frame(ofmt_ctx, &pkt);
  ret = av_interleaved_write_frame(ofmt_ctx, &pkt);

  if (ret < 0) {
   printf("Error muxing packet\n");
   break;
  }

  av_free_packet(&pkt);

 }
 //写文件尾(Write file trailer)
 av_write_trailer(ofmt_ctx);
end:
 avformat_close_input(&ifmt_ctx);
 /* close output */
 if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
  avio_close(ofmt_ctx->pb);
 avformat_free_context(ofmt_ctx);
 if (ret < 0 && ret != AVERROR_EOF) {
  printf("Error occurred.\n");
  return -1;
 }
 return 0;
}

四、运行结果

如果你觉得文章还不错,可以给个”三连“,文章同步到以下个人微信公众号[加班猿]

我是hackett,我们下期见

本文转载自: 掘金

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

微服务项目中的网关

发表于 2021-11-30

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」。

前言

在微服务场景下,我们为什么要引入网关 ? 或者说微服务架构下,网关起到了什么作用 ?

面临的问题

微服务架构下我们项目的承载能力提升了,但是相对的维护多个服务协同工作,以及集群的管理也给我们带来了一些新的问题。

  • 客户端调用不同服务时需要知道每个服务的端口号,比如调用 A 服务 http://127.0.0.1:8000/addproduct ,调用 B 服务时 http://127.0.0.1:8001/adduser, 每个服务端口号都不同,这就使我们的前端代码不好维护。
  • 当同一个服务部署了多个节点,前端代码指定了特定端口后,将无法实现负载均衡
  • 客户端与服务端通过具体端口访问,这种强耦合性导致后期我们对某个现有服务进行拆分,必须通知客户端,前端对应的代码也得做出更改。
  • 服务间的通信,客户端调用 A 服务需要认证 调用 B 服务又需要认证 重复开发认证授权相关代码。
  • 对外暴露信息过多,系统安全性降低

微服务网关

基于上述所面临的问题,我们可以引入微服务网关,网关的作用就是对外暴露一个统一端口,来维护后端各个服务的路由信息,对应用户而言,所有的请求都直接发向网关,然后网关处理,路由到对应的服务上,这个时候我们可以做负载均衡,统一授权拦截处理,等等。

Spring Cloud Gateway

Spring Cloud Gateway:

Spring Cloud Gateway 是 Spring Clould 第二代网关,第一代 Netflix zuul已经停止维护了。

基于 Netty(NewlO)、Reactor (响应式编程)及 WebFlux (新的 WebMVC 框架)构建

Spring Cloud Gateway 的优点:

  • 性能强劲,执行效率是传统基于 Servlet 同步通信 Zuul 效率的 1.6 倍
  • 功能强大,与 Spring Cloud 体系天然整合,不存在兼容问题,内置转发/限流/监控
  • 设计优雅,使用简单,易于扩展
  • Spring Cloud Gateway的缺点:*
  • 基于 NIO,非 J2EE 体系,学习源码是由一定门槛
  • 不兼容 Servlet,应用不兼容 Tomcat,无法打 War 包
  • 只支持 Spring Boot 2.x 版本,对于不熟悉 SpringBoot 的小伙伴有学习门槛

Spring Cloud Gateway 快速整合

  • 创建一个基础的 SpringBoot 应用
  • 引入依赖
1
2
3
4
5
6
7
8
xml复制代码<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
  • 编写配置信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yml复制代码spring:
application:
name: gateway
cloud:
# 让gateway通过nacos实现自动路由转发
gateway:
discovery:
locator:
enabled: true
nacos:
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
server:
port: 9001
  • 启动项目

查看 nacos 中已经注册了我们的网关服务,接下来我们就可以通过网关访问之前已经注册好的两个测试服务了。
image.png

  • 访问测试服务

erp 服务 9002

system 服务 9000
image.png

  • 成功响应
    image.png

本文转载自: 掘金

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

数据仓库和数据集市:ODS、DW、DWD、DWM、DWS、A

发表于 2021-11-30

@TOC

数据流向

img

应用示例
在这里插入图片描述

何为数仓DW

Data warehouse(可简写为DW或者DWH)数据仓库,是在数据库已经大量存在的情况下,它是一整套包括了etl、调度、建模在内的完整的理论体系。

数据仓库的方案建设的目的,是为前端查询和分析作为基础,主要应用于OLAP(on-line Analytical Processing),支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果。目前行业比较流行的有:AWS Redshift,Greenplum,Hive等。

数据仓库并不是数据的最终目的地,而是为数据最终的目的地做好准备,这些准备包含:清洗、转义、分类、重组、合并、拆分、统计等

主要特点

  • 面向主题*[附录]*
    • 操作型数据库组织面向事务处理任务,而数据仓库中的数据是按照一定的主题域进行组织。
    • 主题是指用户使用数据仓库进行决策时所关心的重点方面,一个主题通过与多个操作型信息系统相关。
  • 集成
    • 需要对源数据进行加工与融合,统一与综合
    • 在加工的过程中必须消除源数据的不一致性,以保证数据仓库内的信息时关于整个企业的一致的全局信息。(关联关系)
  • 不可修改
    • DW中的数据并不是最新的,而是来源于其他数据源
    • 数据仓库主要是为决策分析提供数据,涉及的操作主要是数据的查询
  • 与时间相关
    • 处于决策的需要数据仓库中的数据都需要标明时间属性

与数据库的对比

  • DW:专门为数据分析设计的,涉及读取大量数据以了解数据之间的关系和趋势
  • 数据库:用于捕获和存储数据
特性 数据仓库 事务数据库
适合的工作负载 分析、报告、大数据 事务处理
数据源 从多个来源收集和标准化的数据 从单个来源(例如事务系统)捕获的数据
数据捕获 批量写入操作通过按照预定的批处理计划执行 针对连续写入操作进行了优化,因为新数据能够最大程度地提高事务吞吐量
数据标准化 非标准化schema,例如星型Schema或雪花型schema 高度标准化的静态schema
数据存储 使用列式存储进行了优化,可实现轻松访问和高速查询性能 针对在单行型物理块中执行高吞吐量写入操作进行了优化
数据访问 为最小化I/O并最大化数据吞吐量进行了优化 大量小型读取操作

为何要分层

数据仓库中涉及到的问题:

  1. 为什么要做数据仓库?
  2. 为什么要做数据质量管理?
  3. 为什么要做元数据管理?
  4. 数仓分层中每个层的作用是什么?
  5. ……

在实际的工作中,我们都希望自己的数据能够有顺序地流转,设计者和使用者能够清晰地知道数据的整个声明周期,比如下面左图。

但是,实际情况下,我们所面临的数据状况很有可能是复杂性高、且层级混乱的,我们可能会做出一套表依赖结构混乱,且出现循环依赖的数据体系,比如下面的右图。

img

为了解决我们可能面临的问题,需要一套行之有效的数据组织、管理和处理方法,来让我们的数据体系更加有序,这就是数据分层。数据分层的好处:

  • 清晰数据结构:让每个数据层都有自己的作用和职责,在使用和维护的时候能够更方便和理解
  • 复杂问题简化:将一个复杂的任务拆解成多个步骤来分步骤完成,每个层只解决特定的问题
  • 统一数据口径:通过数据分层,提供统一的数据出口,统一输出口径
  • 减少重复开发:规范数据分层,开发通用的中间层,可以极大地减少重复计算的工作

数据分层

每个公司的业务都可以根据自己的业务需求分层不同的层次;目前比较成熟的数据分层:数据运营层ODS、数据仓库层DW、数据服务层ADS(APP)。

数据运营层ODS

数据运营层:Operation Data Store 数据准备区,也称为贴源层。数据源中的数据,经过抽取、洗净、传输,也就是ETL过程之后进入本层。该层的主要功能:

  • ODS是后面数据仓库层的准备区
  • 为DWD层提供原始数据
  • 减少对业务系统的影响

在源数据装入这一层时,要进行诸如去噪(例如有一条数据中人的年龄是 300 岁,这种属于异常数据,就需要提前做一些处理)、去重(例如在个人资料表中,同一 ID 却有两条重复数据,在接入的时候需要做一步去重)、字段命名规范等一系列操作。

但是为了考虑后续可能需要追溯数据问题,因此对于这一层就不建议做过多的数据清洗工作,原封不动地接入原始数据也可以,根据业务具体分层的需求来做。

这层的数据是后续数据仓库加工数据的来源。数据来源的方式:

  • 业务库
    • 经常会使用sqoop来抽取,例如每天定时抽取一次。
    • 实时方面,可以考虑用canal监听mysql的binlog,实时接入即可。
  • 埋点日志
    • 日志一般以文件的形式保存,可以选择用flume定时同步
    • 可以用spark streaming或者Flink来实时接入
    • kafka也OK
  • 消息队列:即来自ActiveMQ、Kafka的数据等。

数据仓库层

数据仓库层从上到下,又可以分为3个层:数据细节层DWD、数据中间层DWM、数据服务层DWS。

数据细节层DWD

数据细节层:data warehouse details,DWD(数据清洗/DWI)

该层是业务层和数据仓库的隔离层,保持和ODS层一样的数据颗粒度;主要是对ODS数据层做一些数据的清洗和规范化的操作,比如去除空数据、脏数据、离群值等。

为了提高数据明细层的易用性,该层通常会才采用一些维度退化方法,将维度退化至事实表中,减少事实表和维表的关联。

数据中间层DWM

数据中间层:Data Warehouse Middle,DWM

该层是在DWD层的数据基础上,对数据做一些轻微的聚合操作,生成一些列的中间结果表,提升公共指标的复用性,减少重复加工的工作。

简答来说,对通用的核心维度进行聚合操作,算出相应的统计指标

数据服务层DWS

数据服务层:Data Warehouse Service,DWS(宽表-用户行为,轻度聚合)

该层是基于DWM上的基础数据,整合汇总成分析某一个主题域的数据服务层,一般是宽表,用于提供后续的业务查询,OLAP分析,数据分发等。

一般来说,该层的数据表会相对较少;一张表会涵盖比较多的业务内容,由于其字段较多,因此一般也会称该层的表为宽表。

  • 用户行为,轻度聚合对DWD
  • 主要对ODS/DWD层数据做一些轻度的汇总。

数据应用层ADS

数据应用层:Application Data Service,ADS(APP/DAL/DF)-出报表结果

该层主要是提供给数据产品和数据分析使用的数据,一般会存放在ES、Redis、PostgreSql等系统中供线上系统使用;也可能存放在hive或者Druid中,供数据分析和数据挖掘使用,比如常用的数据报表就是存在这里的。

事实表 Fact Table

事实表是指存储有事实记录的表,比如系统日志、销售记录等。事实表的记录在不断地增长,比如电商的商品订单表,就是类似的情况,所以事实表的体积通常是远大于其他表。

维表层Dimension(DIM)

维度表(Dimension Table)或维表,有时也称查找表(Lookup Table),是与事实表相对应的一种表;它保存了维度的属性值,可以跟事实表做关联,相当于将事实表上经常重复出现的属性抽取、规范出来用一张表进行管理。维度表主要是包含两个部分:

  • 高基数维度数据:一般是用户资料表、商品资料表类似的资料表,数据量可能是千万级或者上亿级别
  • 低基数维度数据:一般是配置表,比如枚举字段对应的中文含义,或者日期维表等;数据量可能就是个位数或者几千几万。

临时表TMP

每一层的计算都会有很多临时表,专设一个DWTMP层来存储我们数据仓库的临时表

数据集市

狭义ADS层; 广义上指hadoop从DWD DWS ADS 同步到RDS的数据

数据集市(Data Mart),也叫数据市场,数据集市就是满足特定的部门或者用户的需求,按照多维的方式进行存储,包括定义维度、需要计算的指标、维度的层次等,生成面向决策分析需求的数据立方体。

从范围上来说,数据是从企业范围的数据库、数据仓库,或者是更加专业的数据仓库中抽取出来的。数据中心的重点就在于它迎合了专业用户群体的特殊需求,在分析、内容、表现,以及易用方面。数据中心的用户希望数据是由他们熟悉的术语表现的。

带有数据集市的数据仓储结构
在这里插入图片描述

区别数据仓库

数据集市就是企业级数据仓库的一个子集,它主要面向部门级业务,并且只面向某个特定的主题。为了解决灵活性与性能之间的矛盾,数据集市就是数据仓库体系结构中增加的一种小型的部门或工作组级别的数据仓库。数据集市存储为特定用户预先计算好的数据,从而满足用户对性能的需求。数据集市可以在一定程度上缓解访问数据仓库的瓶颈。

理论上讲,应该有一个总的数据仓库的概念,然后才有数据集市。实际建设数据集市的时候,国内很少这么做。国内一般会先从数据集市入手,就某一个特定的主题(比如企业的客户信息)先做数据集市,再建设数据仓库。数据仓库和数据集市建立的先后次序之分,是和设计方法紧密相关的。而数据仓库作为工程学科,并没有对错之分。

在数据结构上,数据仓库是面向主题的、集成的数据的集合。而数据集市通常被定义为星型结构或者雪花型数据结构,数据集市一般是由一张事实表和几张维表组成的。
在这里插入图片描述

问题总结

ODS与DWD区别?

**问:**还是不太明白 ods 和 dwd 层的区别,有了 ods 层后感觉 dwd 没有什么用了。

**答:**嗯,我是这样理解的,站在一个理想的角度来讲,如果 ods 层的数据就非常规整,基本能满足我们绝大部分的需求,这当然是好的,这时候 dwd 层其实也没太大必要。 但是现实中接触的情况是 ods 层的数据很难保证质量,毕竟数据的来源多种多样,推送方也会有自己的推送逻辑,在这种情况下,我们就需要通过额外的一层 dwd 来屏蔽一些底层的差异。

**问:**我大概明白了,是不是说 dwd 主要是对 ods 层做一些数据清洗和规范化的操作,dws 主要是对 ods 层数据做一些轻度的汇总?

**答:**对的,可以大致这样理解。

APP层干什么的?

问答三:app 层是干什么的?

**问:**感觉DWS层是不是没地方放了,各个业务的DWS表是应该在 DWD还是在 app?

**答:**这个问题不太好回答,我感觉主要就是明确一下DWS层是干什么的,如果你的DWS层放的就是一些可以供业务方使用的宽表表,放在 app 层就行。如果你说的数据集市是一个比较泛一点的概念,那么其实 dws、dwd、app 这些合起来都算是数据集市的内容。

**问:**那存到 Redis、ES 中的数据算是 app层吗?

**答:**算是的,我个人的理解,app 层主要存放一些相对成熟的表,能供业务侧使用的。这些表可以在 Hive 中,也可以是从 Hive 导入 Redis 或者 ES 这种查询性能比较好的系统中。

附录

ETL

ETL :Extract-Transform-Load,用于描述将数据从来源端经过抽取、转换、加载到目的端的过程。

宽表

  • 含义:指字段比较多的数据库表。通常是指业务主体相关的指标、纬度、属性关联在一起的一张数据库表。
  • 特点:
    • 宽表由于把不同的内容都放在同一张表,宽表已经不符合三范式的模型设计规范:
      • 坏处:数据有大量冗余
      • 好处:查询性能的提高和便捷
    • 宽表的设计广泛应用于数据挖掘模型训练前的数据准备,通过把相关字段放在同一张表中,可以大大提供数据挖掘模型训练过程中迭代计算的消息问题。

主题(Subject)

是在较高层次上将企业信息系统中的数据进行综合、归类和分析利用的一个抽象概念,每一个主题基本对应一个宏观的分析领域。在逻辑意义上,它是对应企业中某一宏观分析领域所涉及的分析对象。例如“销售分析”就是一个分析领域,因此这个数据仓库应用的主题就是“销售分析”。

参考:
www.cnblogs.com/amyzhu

www.jianshu.com/p/1dd894e5b…

blog.csdn.net/pmdream/art…

百度百科

本文由博客一文多发平台 OpenWrite 发布!

本文转载自: 掘金

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

SQL分类和命名规范 1 SQL 分类 2 SQL语言的

发表于 2021-11-30
  • 这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战
  1. SQL 分类

SQL语言在功能上主要分为如下3大类:

  • DDL(Data Definition Languages、数据定义语言),这些语句定义了不同的数据库、表、视图、索引等数据库对象,还可以用来创建、删除、修改数据库和数据表的结构。主要的语句关键字包括 CREATE 、 DROP 、 ALTER 等。
  • DML(Data Manipulation Language、数据操作语言),用于添加、删除、更新和查询数据库记录,并检查数据完整性。主要的语句关键字包括 INSERT 、 DELETE 、 UPDATE 、 SELECT 等。SELECT是SQL语言的基础,最为重要。
  • DCL(Data Control Language、数据控制语言),用于定义数据库、表、字段、用户的访问权限和安全级别。主要的语句关键字包括 GRANT 、 REVOKE 、 COMMIT 、 ROLLBACK 、 SAVEPOINT 等。

因为查询语句使用的非常的频繁,所以很多人把查询语句单拎出来一类:DQL(数据查询语言)。
还有单独将 COMMIT 、 ROLLBACK 取出来称为TCL (Transaction Control Language,事务控制语言)。

  1. SQL语言的规则与规范

  • SQL 可以写在一行或者多行。为了提高可读性,各子句分行写,必要时使用缩进
  • 每条命令以 ; 或 \g 或 \G 结束
  • 关键字不能被缩写也不能分行
  • 关于标点符号
+ 必须保证所有的`()、单引号、双引号`是成对结束的
+ 必须使用英文状态下的半角输入方式
+ 字符串型和日期时间类型的数据可以使用单引号(`' '`)表示
+ 列的别名,尽量使用双引号(`" "`),而且不建议省略`as`
  • SQL大小写规范 (建议遵守)
  • MySQL 在 Windows 环境下是大小写不敏感的
  • MySQL 在 Linux 环境下是大小写敏感的
+ 数据库名、表名、表的别名、变量名是严格区分大小写的
+ 关键字、函数名、列名(或字段名)、列的别名(字段的别名) 是忽略大小写的。
  • 推荐采用统一的书写规范:
+ 数据库名、表名、表别名、字段名、字段别名等都小写
+ SQL 关键字、函数名、绑定变量等都大写
  1. 注 释

可以使用如下格式的注释结构

1
2
3
sql复制代码单行注释:#注释文字(MySQL特有的方式)
单行注释:-- 注释文字(--后面必须包含一个空格。)
多行注释:/* 注释文字 */
  1. 命名规则

  • 数据库、表名不得超过30个字符,变量名限制为29个
  • 必须只能包含 A–Z, a–z, 0–9, _共63个字符
  • 数据库名、表名、字段名等对象名中间不要包含空格
  • 同一个MySQL软件中,数据库不能同名;同一个库中,表不能重名;同一个表中,字段不能重名
  • 必须保证你的字段没有和保留字、数据库系统或常用方法冲突。如果坚持使用,请在SQL语句中使用` (着重号)引起来
  • 保持字段名和类型的一致性,在命名字段并为其指定数据类型的时候一定要保证一致性。假如数据类型在一个表里是整数,那在另一个表里可就别变成字符型了

本文转载自: 掘金

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

统一异常处理和统一返回格式RestControllerAd

发表于 2021-11-30

闲来无事,自己的模块写完了,就看了看项目框架。之前有个大佬说很多公司都没做统一异常处理和统一返回处理,我现在公司做了,所以就看看。
首先,咱们了解一下什么是统一返回,统一返回就是一种规范,咱们自定义这种规范,从后端返回的数据都按这种规范来,这样前端更容易处理。一般都是用类,这种类都命名为Result

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
csharp复制代码	//主要属性:
@ApiModelProperty(notes = "状态")
private int code;
@ApiModelProperty(notes = "提示信息")
private String message;
@ApiModelProperty(notes = "返回内容")
private T data;

//然后自己定义一些构造方法如下:
public Result() {
this(SUCCESS, "success!");
}

public Result(int code, String message, T content) {
this.code = code;
this.message = message;
this.data = content;
}

public Result(int code, String message) {
this(code, message, null);
}

public static <T> Result<T> error() {
return error(ERROR, "error!");
}

public static <T> Result<T> error(String msg) {
return error(ERROR, msg);
}

public static <T> Result<T> error(int code, String msg) {
return new Result<>(code, msg);
}

public static <T> Result<T> error(String msg, T content) {
return new Result<>(ERROR, msg, content);
}

public static <T> Result<T> error(int code, String msg, T content) {
return new Result<>(code, msg, content);
}

public static <T> Result<T> ok() {
return new Result<>();
}

public static <T> Result<T> ok(String message) {
return new Result<>(SUCCESS, message);
}

public static <T> Result<T> ok(T content) {
return new Result<>(SUCCESS, "success!", content);
}

public static <T> Result<T> ok(int status, T content) {
return new Result<>(status, "success!", content);
}

public static <T> Result<T> ok(int status, String message, T content) {
return new Result<>(status, message, content);
}

@Override
public boolean matching() {
return this.code == 0;
}

定义好返回的类后,就可以开始编写统一返回处理了,在编写统一返回处理之前我们得了解一个接口类ResponseBodyAdvice,这个接口类如其名,主要是对ResponseBody的一个处理,也是就controller的响应进行一个处理。
它有两个方法,第一个方法,这里有两个参数,大家可以自行去了解一下哈,我只用到了returnType.getGenericParameterType(),这个方法是得到返回的数据类型,因此我们就可以判断返回的类型是不是我们规定的了。

1
arduino复制代码 boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

这个方法主要是判断需要不需要第二个方法beforeBodyWrite。

如果supports返回的true,就会执行第二个方法,如果返回false就不会执行第二个方法。所以第一个supports,能在定位到包的基础上更精确的处理一些返回值。

至于对返回值的统一处理就靠我们第二方法了。
方法中的body就是相应的返回值了,而returnType.getGenericParameterType()可以获取返回值的类型

1
vbscript复制代码  T beforeBodyWrite(T body, MethodParameter returnType, MediaType selectedContentType,Class<? extends HttpMessageConverter<?>> selectedConverterType,ServerHttpRequest request, ServerHttpResponse response);

我们实现这个接口类,重写这两个方法,并且类上打上注解@RestControllerAdvice(basePackages = “XXX”)
这个注解是拦截controller的,输入包路径后,就是指定的包路径了。
第一个方法,我们实现主要是判断返回类型是不是我们的规范类型,如果不是就返回true,就会执行第二个方法。
第二个方法就可以对数据进行一个处理了,我们就进行一个封装,这样返回类型就是统一规范了。如下:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码   if (body == null) {
return Result.ok();
}
if (body instanceof Result) {
return body;
}
if (returnType.getGenericParameterType().equals(String.class)) {
return body;
}
return Result.ok(body);

完成上述步骤后,即使返回的为空,只要没报错都会有个状态码,和状态,以上就是返回统一格式。

接下来是统一异常处理,统一异常处理,也要用到@RestControllerAdvice注解,拦截所有controller,还有 @ExceptionHandler(),参数是指定异常类型,主要是用这两个注解。代码如下:

1
2
3
4
less复制代码    public ResponseEntity<Object> customExceptionHandler(BaseCustomException exception) {
log.error(exception.getLogMessage());
return new ResponseEntity<>(Result.error(exception.getErrorCode(), exception.getMessage()), exception.getHttpStatus());
}
1
复制代码完成上述代码,这样遇到异常就能按我们的格式抛来了。自定义一个异常,这样就完成自定义异常处理了。

本文转载自: 掘金

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

【JS 逆向百例】反混淆入门,某鹏教育 JS 混淆还原

发表于 2021-11-30

关注微信公众号:K哥爬虫,持续分享爬虫进阶、JS/安卓逆向等技术干货!

声明

本文章中所有内容仅供学习交流,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除!

逆向目标

  • 目标:某鹏教育登录接口加密,含有简单的 JS 混淆
  • 主页:aHR0cHM6Ly9sZWFybi5vcGVuLmNvbS5jbi8=
  • 接口:aHR0cHM6Ly9sZWFybi5vcGVuLmNvbS5jbi9BY2NvdW50L1VuaXRMb2dpbg==
  • 逆向参数:Form Data:black_box: eyJ2IjoiR01KM0VWWkVxMG0ydVh4WUd...

逆向过程

本次逆向的目标同样是一个登录接口,其中的加密 JS 使用了简单的混淆,可作为混淆还原的入门级教程,来到登录页面,随便输入账号密码进行登录,其中登录的 POST 请求里, Form Data 有个加密参数 black_box,也就是本次逆向的目标,抓包如下:

01.png

直接搜索 black_box,在 login.js 里可以很容易找到加密的地方,如下图所示:

02.png

看一下 _fmOpt.getinfo() 这个方法,是调用了 fm.js 里的 OO0O0() 方法,看这个又是 0 又是 O 的,多半是混淆了,如下图所示:

03.png

点进去看一下,整个 fm.js 都是混淆代码,我们选中类似 OQoOo[251] 的代码,可以看到实际上是一个字符串对象,也可以直接在 Console 里输出看到其实际值,这个 OO0O0 方法返回的 oOoo0[OQoOo[448]](JSON[OQoOo[35]](O0oOo[OQoOo[460]])),就是 black_box 的值,如下图所示:

04.png

仔细观察,可以发现 OQoOo 应该是一个类似数组的东西,通过传入元素下标来依次取其真实值,随便搜索一个值,可以在代码最后面找到一个数组,这个数组其实就是 OQoOo,可以传入下标来验证一下,如下图所示:

05.png

到这里其实就知道了其大致混淆原理,我们可以把这个JS 拿下来,到本地写个小脚本,将这些值替换一下:

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
python复制代码# ==================================
# --*-- coding: utf-8 --*--
# @Time : 2021-11-09
# @Author : 微信公众号:K哥爬虫
# @FileName: replace_js.py
# @Software: PyCharm
# @describe: 混淆还原小脚本
# ==================================


# 待替换的值(太多了,仅列出少部分)
# 以实际列表为准,要和 fm_old.js 里的列表一致
item = ['referrer', 'absolute', 'replace',...]

# 混淆后的 JS
with open("fm_old.js", "r", encoding="utf-8") as f:
js_lines = f.readlines()

js = ""
for j in js_lines:
js += j

for i in item:
# Qo00o 需要根据你 fm_old.js 具体的字符串进行替换
str_old = "Qo00o[{}]".format(item.index(i))
js = js.replace(str_old, '"' + i + '"')

# 还原后的 JS
with open("fm_new.js", "w", encoding="utf-8") as f:
f.write(js)

使用此脚本替换后,可能会发现 JS 会报错,原因是一些换行符、斜杠解析错误,以及双引号重复使用的问题,可以自己手动修改一下。

这里需要注意的一点,fm.js 后面还有个后缀,类似 t=454594,t=454570 等,不同的后缀得到的 JS 内容也有差异,各种函数变量名和那个列表元素顺序不同,实际上调用的方法是同一个,所以影响不大,只需要注意替换时列表内容、需要替换的那个字符串和你下载的 JS 文件里的一致即可。

将 JS 还原后,我们可以将还原后的 JS 替换掉网站本身经过混淆后的 JS,这里替换方法有很多,比如使用 Fiddler 等抓包工具替换响应、使用 ReRes 之类的插件进行替换、使用浏览器开发者工具自带的 Overrides 功能进行替换(Chrome 64 之后才有的功能)等,这里我们使用 Fiddler 的 Autoresponder 功能来替换。

实测这个 fm.js 的后缀短时间内不会改变,所以可以直接复制其完整地址来替换,要严谨一点的话,我们可以用正则表达式来匹配这个 t 值,在 Fiddler 里面选择 AutoResponder,点击 Add Rule,添加替换规则,正则表达式的方法写法如下:regex:https:\/\/static\.tongdun\.net\/v3\/fm\.js\?t=\d+,注意 regex 前缀必不可少,上方依次选中 Enable rules(应用规则)、Accept all CONNECTs(接受所有连接)、Unmatched requests passthrough(不匹配规则的就按照之前的请求地址发送过去),Enable Latency 是设置延迟生效时间,不用勾选,如下图所示:

06.png

替换后再次登录,下断点,可以看到现在的 JS 已经清晰了不少,再看看这个函数最后的 return 语句,oQOQ0["blackBox"] 包含了 it、os、t、v 三个参数,使用 JSON 的 stringify 方法将其转换成字符串,然后调用 QQo0 方法进行加密,如下图所示:

07.png

我们先来看看 oQOQ0["blackBox"] 里的四个参数,其中 it、os、v 三个参数在这个函数开始就已经有定义,v 就是 Q0oQQ["version"],是定值,直接搜索可以发现这个值是在最开始的那个大列表里,os 为定值,it 是两个时间戳相减的值,O000o 这个方法就是两个值进行相减,oQOQo 这个时间戳可以搜索 var oQOQo,是一开始加载就生成的时间戳,JS 一开始加载到点击登陆进入加密函数,也就一分钟左右,所以这里我们可以直接生成一个五位随机数(一分钟左右在毫秒上的差值在五位数左右)。

08.png

现在就剩下一个 t 参数了,往下看 t 其实就是 Q0oQQ["tokens"],中间经过了一个 if-else 语句,可以埋下断点进行调试,发现其实只执行了 else 语句,对 t 赋值也就这一句,所以剩下的代码其实在扣的时候都可以删掉。

09.png

这个 tokens 多次测试发现是不变的,尝试直接搜索一下 token 关键字,可以发现其赋值的地方,对 id 按照 | 符号进行分割,取其第 1 个索引值就是 tokens,再看看 id 的值,并没有找到明显的生成逻辑,复制其值搜索一下,发现是通过一个接口返回的,可以直接写死,也可以自己先去请求一下这个接口,取其返回的值,如下图所示:

10.png

11.png

自此所有参数都找完了,回到原来的 return 位置,还差一个加密函数,即 ooOoO["encode"](),直接跟进去,将这个方法扣下来即可,本地调试缺啥补啥,将用到的函数补全就行了。

12.png

完整代码

GitHub 关注 K 哥爬虫,持续分享爬虫相关代码!欢迎 star !github.com/kgepachong/

以下只演示部分关键代码,不能直接运行! 完整代码仓库地址:github.com/kgepachong/…

JavaScript 加密关键代码架构

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
javascript复制代码function oQ0OQ(Q0o0, o0OQ) {
return Q0o0 < o0OQ;
}

function O000O(Q0o0, o0OQ) {
return Q0o0 >> o0OQ;
}

function Qo0oo(Q0o0, o0OQ) {
return Q0o0 | o0OQ;
}

function OOO0Q(Q0o0, o0OQ) {
return Q0o0 << o0OQ;
}

function OooQo(Q0o0, o0OQ) {
return Q0o0 & o0OQ;
}

function Oo0OO(Q0o0, o0OQ) {
return Q0o0 + o0OQ;
}

var oQoo0 = {};
oQoo0["_keyStr"] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
oQoo0["encode"] = function QQQ0(Q0o0) {
var o0OQ = 62;
while (o0OQ) {
switch (o0OQ) {
case 116 + 13 - 65: {}
case 118 + 8 - 63: {}
case 94 + 8 - 40: {}
case 122 + 6 - 63: {}
}
}
};
oQoo0["_utf8_encode"] = function oOQ0(Q0o0) {}

function OOoO0() {
var tokens = "e0ia+fB5zvGuTjFDgcKahQwg2UEH8b0k7EK/Ukt4KwzyCbpm11jjy8Au64MC6s7HvLRacUxd7ka4AdDidJmYAA==";
var version = "+X+3JWoUVBc12xtmgMpwzjAone3cp6/4QuFj7oWKNk+C4tqy4un/e29cODlhRmDy";
var Oo0O0 = {};
Oo0O0["blackBox"] = {};
Oo0O0["blackBox"]["v"] = version;
Oo0O0["blackBox"]["os"] = "web";
Oo0O0["blackBox"]["it"] = parseInt(Math.random() * 100000);
Oo0O0["blackBox"]["t"] = tokens;
return oQoo0["encode"](JSON.stringify(Oo0O0["blackBox"]));
}

// 测试样例
console.log(OOoO0())

Python 登录关键代码

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
python复制代码# ==================================
# --*-- coding: utf-8 --*--
# @Time : 2021-11-10
# @Author : 微信公众号:K哥爬虫
# @FileName: open_login.py
# @Software: PyCharm
# ==================================


import time
import execjs
import requests


login_url = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"


def get_black_box():
with open('get_black_box.js', 'r', encoding='utf-8') as f:
exec_js = f.read()
black_box = execjs.compile(exec_js).call('OOoO0')
return black_box


def login(black_box, username, password):
params = {"bust": str(int(time.time() * 1000))}
data = {
"loginName": username,
"passWord": password,
"validateNum": "",
"black_box": black_box
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36"
}
response = requests.post(url=login_url, params=params, data=data, headers=headers)
print(response.json())


def main():
username = input("请输入登录账号: ")
password = input("请输入登录密码: ")
black_box = get_black_box()
login(black_box, username, password)


if __name__ == '__main__':
main()

本文转载自: 掘金

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

构建一个PHP工程化项目 (杂谈篇)

发表于 2021-11-30

前言

本篇文章是开启我写作之路的第一篇技术文章,有一些东西觉得可以和大家分享

如果文中有描述不合理的地方,望大家海量指出。

本系列文章我会根据我的实际项目经验谈谈以下内容

  • 为什么要工程化
  • 如何更好的组织业务代码
  • 用 PHP 来构建一个基于微服务 的商城API

其中 实战 一节我将放在下一篇文章中去叙述 (Show Code)

为什么工程化

本篇谈及的工程化仅在代码组织和项目组织上的一些见解

我相信大家在多年的工作中一定接手过不少项目代码

从 搭建项目 到 维护项目 再到 开发新功能 这个过程中,我相信原先写代码的兄弟已经被问候多次。

我认为一个值得花时间去学习和研究的项目应该具有以下几点特征

项目结构组织明确

没有严格的按照工程层次划分目录, 随心所欲,导致冗余目录和相似功能目录增多,让人心里抵触。

我觉得一个良好的项目工程应该是经过系统的设计,并且他人从项目结构中可以轻易看出系统层次。

image-20211128155850776.png

以一个微服务中的工程标准,我一般会按以下结构组织代码

1
2
3
4
5
6
7
8
9
10
11
markdown复制代码├─app           // 服务APP
│ ├─api // API层 对外提供 HTTP/JSON RPC
│ ├─model // 模型层 存放对数据模型的定义(包括表数据模型和业务输出输入模型)
│ └─service // 服务层 业务逻辑封装管理,特定的业务逻辑实现和封装。
│ └─constants // 存放常量及消息的映射
├─deploy // 部署脚本目录
├─config // 配置目录
├─tests // 测试目录
├─Dockerfile // 构建镜像
├─composer.json // 管理第三方包的依赖
├─.env // 项目的环境配置文件

工程化组织代码

代码组织混乱最常见的几种情况

  • 变量定义不规范, 常量使用一堆幻数
  • 代码可复用性低
  • 业务核心逻辑职责不明确

下面我会通过几段代码来说明一下工程化 组织带来的好处,希望借此抛砖引玉。

统一管理常量

项目中使用大量的数字来标识状态,会使他人接手项目的理解成本上线。

将常量的定义统一放到 类似 constants 的文件夹中统一管理, 我们可以轻易从项目角度去快速理解业务的上下文。

例如我需要一个类来专门管理这些订单的状态

根据上文中组织的项目结构,我们可以在 app/constants 中建立一个 OrderStatus.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
php复制代码<?php
/**
* 订单状态常量
*/
namespace app\constants;


class OrderStatus
{
/**
* @Message('待付款')
*/
const TO_BE_PAY = 10;

/**
* @Message("待发货")
*/
const TO_BE_DELIVERY = 11;

...
}

?>
用设计模式消除重复代码

重复的代码会让系统变得臃肿,难以维护,增加接手项目人员的负担

消除重复的方式无非是 封装 和抽象

我们站在工程的角度说明一下如何让一个充值的业务模块更直观从而减少重复。

笔者之前接手过一个项目,其中充值部分包括 充值平台币 和 开通VIP 他们都需要支持 微信 支付宝 支付。

以下是项目中此业务的控制层代码 (部分代码经过处理)

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
php复制代码<?php
namespace app\controller;

class OrderController
{
/**
* Route('GET', '/recharge/vip')
*/
public function rechargeVip()
{
$payType = $this->request->get('pay_type');
$vipRuleId = $this->request->get('rule_id');

$vipProductInfo = (new VipService())->getVipProduct($vipRuleId);

if( $payType == PayType::WECHAT) {
$payHandler = new WechatPay();
$result = $payHandler->doPay(...);
return $this->responseSuccess($result);
} else if ($payType === PayType::ALIPAY ) {
$payHandler = new AliPay();
$payHandler->doPay(....)
return $this->responseSuccess($result);
}
...
return $this->responseFail('支付方式不存在');

}

/**
* Route('POST', '/recharge/coin')
*/
public function rechargeCoin()
{
$payType = $this->request->get('pay_type');
$coinRuleId = $this->request->get('coin_rule_id');

$coinProductInfo = (new CoinService())->getCoinProduct($coinTuleId);

// 和 rechargeVip 一样的重复代码
...
}
}
?>

我相信大家已经看出了这段代码存在重复并且不太符合 面向对象 的思想。

The goal of software architecture is to minimize the human resources required to build and maintain the required system.

译: 软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求

站在工程化的角度思考,就是我们该以何种方式去组织这一模块代码,让它 减少重复 并且符合 面向对象的思想。

通过对业务的一些抽象, 为业务定义一个规范,并且遵守它。 形成一个上下游的概念。

这样我们就可以服务好我们上游的两个C Client 和 Controller

作为 Client 我们可以只用一个 充值 的接口就能调用我们所有下游业务提供的充值服务

image.png
首先我们需要一个 RechargeInterface.php 让充值业务都按照系统的标准去实现逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
php复制代码<?php
namespace app\contract;

interface RechargeInterface
{ /* 充值方法 */
public function recharge(int $userId, string $ruleId, int $payType);
/* 完成充值后 */
public function whenFinishRecharge(NotifyResponse $response);
/* 获取充值Title */
public function getTitle(): string;
/* 获取充值完成通知的URL */
public function getNotifyUrl(): string
}
?>

接着我们根据业务建立两个类,分别是 VipRecharge.php 和 CoinRecharge.php

  • VIP充值实现类 VipRecharge.php
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
php复制代码<?php
namespace app\service\recharge;

class VipRecharge implements RechargeInterface
{
/**
* 充值规则的实体
* @var RechargeRuleModel
*/
protected $vipRechargeRule;

protected $vipRechargeLog;

public function recharge(int $userId, string $ruleId, int $payType)
{
$vipRuleProduct = $this->vipRuleModel->getVipRuleById($ruleId);
if($vipRuleProduct) {
return $this->vipRuleRechargeLog->create([
'rule_id' => $ruleId,
'price' => $vipRuleProduct->price,
'pay_type' => $payType,
'user_id' => $userId,
'vip_key' => $vipRuleProduct->vip_key
]);
}
// 抛出充值规则不存在异常
}
public function getTitle():string
{
// 返回充值Title
}
public function getNotifyUrl():
{
// 返回充值回调接口地址
}
public function whenFinishRecharge(NotifyRecharge $response)
{
// 处理充值完成后的内容,主要用于接收到回调后的处理
}
}
?>
  • 金币充值实现类 CoinRecharge.php
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
php复制代码<?php
namespace app\service\recharge;

class CoinRecharge implements RechargeInterface
{
protected $coinRechargeRule;

protected $coinRechargeLog;

public function recharge(int $userId, string $ruleId, int $payType)
{
$coinRuleProduct = $this->coinRechargeRule->getCoinRuleById($ruleId);
if($coinRuleProduct) {
return $this->coinRechargeLog->create([
'rule_id' => $ruleId,
'price' => $coinRuleProduct->price,
'coin' => $coinRuleProduct->coin,
'user_id' => $userId,
'pay_type' => $payType
])
}

// 抛出规则不存在异常
}

...
}
?>

最后我们需要一个充值服务对下游的业务进行管理。

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
php复制代码<?php
namespace app\service\recharge;

class RechargeService
{
/* 允许支付的方法 */
protected $allowPay = [
PayType::WECHAT,
PayType::ALIPAY
];

/* 充值服务提供者 */
protected $rechargeProviders = [
RechargeType::VIP_RECHARGE : VipRecharge::class,
RechargeType::COIN_RECHARGE: CoinRecharge::class
];

/**
* 充值接口
* @var RechargeInterface
*/
protected $rechargeHandle;

public function __construct(string $rechargeType)
{
$this->rechargeHandle = new $this->rechargeProviders[$rechargeType]();
}

public function payRecharge(int $payType, $payPrice, $payOptions)
{
// 调用支付服务(支付类型,支付价格, 支付信息)
// 返回调用支付结果
}

/**
* 充值业务入口
*/
public function doRecharge(int $userId, $ruleId, int $payType)
{
$recharge = $this->rechargeHandle->recharge($userId, $ruleId, $payType);

// 使用支付
$payOptions = [
'title' => $this->rechargeHandle->getTitle(),
'notify_url' => $this->rechargeHandle->getNotifyUrl(),
'order_num' => $recharge->order_num
]
return $this->payRecharge($payType, $recharge->price, $payOptions);
}
}
?>

抽象永远是软件工程领域中最难的命题,因为他没有规则没有标准,甚至没有对错,只分好坏,只分是否适合。

这里仅以个人实际工作经验总结出来对代码工程化的一些思考 。

如何更好的组织业务代码

Programs are meant to be read by humans and only incidentally for computers to execute
译: 代码始终是写给人看的,只是恰好能被计算机执行。

我相信,局部干净,核心逻辑简洁的代码一定是好的代码。

为什么提倡简洁? 对于我而言就是容易 单元测试 代码可控性高 试想一下一个业务逻辑里的代码冗余非常多的与该业务无关的代码,维护起来简直就是想开喷。

写到这一段时我首先需要感谢我的启蒙恩师 PIPO, 是他在我初入这行时为我提供了不少宝贵经验,其中第一课就给我指导了代码工程的重要性,所以后期除了注重框架技术的学习,也更加注重代码工程的质量。

如何组织一个业务,让他能最小化的达到快速验收的目的?

分离业务中的主线和支线

在业务代码中,每个业务的主要逻辑都是一条主线,我们在编写每个业务逻辑时,应该要突出主线,分离支线,这样按照我们日常的思维才更容易去理解代码,如果你的支线代码变多,那么就会有种喧宾夺主的感觉,让我们无法轻易了解业务的内容。

分离业务主次方面我们可以通过

  • 框架提供的中间件(Middleware)
  • 事件(Event)
  • AOP (面向切面去创建业务的连接点)

例如上面的充值服务 的充值方法中,我们就应该是 检查充值规则, 抽取支付内容, 发起支付 那么这个充值方法就应该只有简单的几行代码,而不应该在有诸如权限判断, 支付方式实例判断, 性能记录 等无关主线的代码。

分离业务代码和其他代码

业务代码通常是和业务逻辑相关的, 而诸如基础工具类代码, 日志记录代码, 这些应该和业务逻辑分开。

相同业务高内聚,不同业务低耦合

耦合是一种摩擦力, 太高的偶尔会使摩擦力变强,不易行走, 太低的摩擦力又无法正常行走, 所以根据你的业务控制耦合的高低也是做好业务代码组织的一种手段

就拿上面的充值模块 举例,充值服务是依赖于与支付服务的,因为我们完成充值规则的校验和支付参数装配后,我们需要调用充值支付。

这时候我们可以通过依赖注入的方式,将 支付服务 注入到 充值服务 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
php复制代码<?php

class RechargeService
{
/**
* @Inject
* @var payServiceInterface
*/
protected $payService;

/**
* 通过依赖来组合业务之间的关系
*/
public function payRecharge($payType, $payPrice, $payOptions)
{
reutrn $this->payService->pay($payType, $payPrice, $payOptions);
}

}
?>

这样做的好处是整个 RechargeService 我们可以轻易的做单元测试, 我们只需在单元测试的时候 Mock 支付服务类使其按要求返回结果即可。

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
php复制代码<?php
namespace ProjectTest\Services;

class RechargeServiceTest extends TestCase
{
protected $container;

protected $rechargeService;

public function setUp():void
{
$payService = Mockery::mock(PayService::class);
$payService->shoudReceive('pay')->andReturn([
'pay_type' => PayType::WECHAT,
'pay_data' => []
]);

$this->container = ApplicationContext();

// 替换RechargeService中依赖的PayService
$this->container->getDefinitionSource()->addDefinition(PayService::class , function use ($payService) {
// 我们之前Mock好的PayService
return $payService;
})

$this->rechargeService = new RechargeService();
}

public function testRechargeService()
{
$result = $this->reachargeService->doRecharge();
$this->assertIsArray($result);
}
}
?>

image.png

写在最后

一直没有时间好好输出一篇文章,之前一直考虑的问题是自己的表达能力和写作能力不到位,但是回过头来想想, 也只是输出自己的一些实际经验之谈, 算不上什么大作。

下一篇文章我会以这篇文章叙述的为基础,谈谈我是如何使用 PHP 来构建一个微服务。

用PHP来谈微服务 总觉得有些格格不入, 主要是为了说明 PHP是世界上最好的语言

最好在写这篇文章的时候参考了

  • 阿里技术号关于重拾面向对象的文章
  • 腾讯技术关于业务代码实践的文章

两个业界标杆出品写的技术文章总是能让我从中学习到新的知识。

文章中的示例代码使用的是 Hyperf 框架,看了原作者做的Hyperf教程视频,受益良多。

一杯咖啡,洋洋洒洒写了上千字, 好久没这么舒畅!

本文转载自: 掘金

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

1…104105106…956

开发者博客

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