wenmo8 发布的文章

K,K线,Candle蜡烛图。

T,技术分析,工具平台

L,公式Language语言使用c++14,Lite小巧简易。

项目仓库:
https://github.com/bbqz007/KTL

国内仓库:
https://gitee.com/bbqz007/KTL

当前0.9.3。修正代码编辑器不能调整窗口大小的问题。

新功能以
patch.cpprest.7z
方式提供,包含源代码。需要手动解压。

关键字:pplx, cpprest, sina finance,  baidu gushitong.

新功能如下:

这个版本,加入pplx,cpprest编程模块。使用者可以在本工具使用pplx更简单地写异步代码, 使用cpprest更方便地写异步http,包括http请求数据还有http服务提供数据。

cpprest使用boost178,包含最小的asio头文件依赖。

然后提供应用工具,请求新浪财经数据,使用百度股市通opendata的api请求数据。

先来简单介绍pplx。是一个简化的
PPL
,相似地为java或javascript的Promise。用于简单方便地写异步应用。

将一个完整流程逻辑,拆分成一段一段的contination。每个contination借助pplx;:task投放到线程池。你可以wait()在当前控制的线程手动同步,或者then()让它在线程池事件循环异步等待结果。

auto task = create_task([]{
 return 1;
})
.then([](int){
 return 1.;
})
.then([](double){
 return std::string("end");
}); 

这里面安排了三个task,它们构成一个简单的filter-pipeline,前者的输出结果作为后者的输入参数。并且三个task在线程池自动完成分派。

下面是map-reduce形式

std::vector<task<int> > maps = {
		create_task([]{
			return 1;
		}),
		create_task([]{
			return 12;
		}),
		create_task([]{
			return 13;
		}),
		create_task([]{
			return 14;
		}),
	};
auto reduce = when_all(maps.begin(), maps.end())
	.then([=](auto){
		int sum = 0;
		for_each(maps.begin(), maps.end(), 
			[&](auto& task){
				sum += task.get();
			});
                return sum;
	});

reduce.get();

产生4个map任务并发, 然后reduce所有结果,一共5个task在线程池自动完成。

为了有更好的控制,这里有一个可等待事件,task_completion_event。我们可以用它作为一个事件源,借助一个task用它作为参数就可以进行同步或异步等待了,类似于future。

我们可以将这个future用作输入源,让安排好异步任务链等待输入才开始执行。

task_completion_event<int> future;
task<int> start(future);
auto pipeline = start.then([](int i){
 return i;
})
.then([](int i){
 return i;
})
.then([](double){
 return std::string("end");
}); 

start.set(0);
pipeline.wait();

或者利用这个future,让pplx线程池外的线程也参与到pplx异步任务链。让pplx异步任务等待外部线程的结果。

再次指出,then()异步等待,并不阻塞任何线程,它是由线程池的事件循环完成异步等待的。

本工具提供的pplx,其底层基于asio。所有可以用asio的timer进行async_wait。结合task_completion_event,可以实现delay。

pplx::task<void> delay_post(std::chrono::milliseconds delay) {
    boost::asio::io_service& io_service = pplx::threadpool::shared_instance().io_service();
    pplx::task_completion_event<void> tce;

    auto timer = std::make_shared<boost::asio::steady_timer>(io_service);
    timer->expires_after(delay);
    timer->async_wait([tce, timer](const boost::system::error_code& ec) mutable {
        if (!ec) {
            tce.set();
        } else {
            tce.set_exception(std::make_exception_ptr(std::runtime_error("Timer error: " + ec.message())));
        }
    });

    return pplx::create_task(tce);
}了

了解更多pplx,我写了另一篇《
浅析pplx库的设计与实现。

再来介绍cpprest。

cpprest就是cpp实现的rest开发库。线程模型使用pplx,http使用asio的实现。全程支持异步。

对于http client。主要两步,异步等待response,然后异步读取content。当http_client接收读取出status跟header fields后,response完成等待,然后就可以对streambuf进行异步读取content数据。

对于http server。借助于pplx异步模型,灵活性大。一个request不再由一个线程从开头处理到结束。而是把处理逻辑拆分成异步片段。在线程池进行异步任务链处理。这样就可以将http请求实现成poll。

下面就用BlackJack例子说明。

客户端状态机如下:

0,准备开局,接受投注。

>0, 开局中。

1,  需要轮询,等待下一步。

2,  进行处理环节,可以操作。

0,  结束,得出结果。

开始时,大家都在状态0,并进行投注,服务器原子等待所有人都投注后才能开局。

服务器会阻塞所有投注请求,直到人齐数了,才会开局并发出response。

服务器不会将线程阻塞来等待其它投注进来,而是将request作为异步上下文缓存起来。当人数齐后,进行开局逻辑处理,并发出response通知开局信息,包含状态切换。

收到1的玩家,必须调用接口refresh阻塞轮询。收到2的玩家进入服务状态,进行牌局交互操作。结束后,服务器通知其切换状态1进入轮询等待。并选出一下玩家通知它的轮询切换状态2。

服务器通过缓存起全部轮询请求,以确保一桌人数同时在线。

全部玩家相继完成服务环节后,服务器向每人的轮询请求发出结果。并知会状态切换0。

BlackJack例子的实现就是利用pplx的异步优势。

本工具提供的BlackJack例子完全使用cpprest的BlackJack例程,逻辑没有任何修改。原例程应用于控制台标准输入输出,为了让其应用在界面,我只是将标准输入替换成QTextStream再指向pipe,标准输出替换成stringstream。

这样一来,原例程就可以原原本本地作为台后部分运行,界面作为前台部分运行。前后台之间用两条流进行数据交互。

要将cpprest线程池的数据更新到ui线程,我们需要将数据投递到ui线程执行。在win32界面我们可以PostMessage到线程队列,虽然qt没有线程队列,但是基于qt事件循环的线程,每个qt对象都可充当线程队列角色,我说的是signal队列。为方便,我们只需要借用任意一个qt对象的signal就可以了,不必新写一个qt对象只为了定义一个signal。在此,我就借用QFutureWatcher的signal finished(),用作投递方法。


struct MyQFutureWatcher : public QFutureWatcher<int>{~MyQFutureWatcher()
{
OutputDebugStringA(
"~MyQFutureWatcher");
}
QMutex mtx;
QList
<std::function<void()> >queue;
MyQFutureWatcher(QObject
*parent =nullptr)
: QFutureWatcher(parent)
{
QObject::connect(
this, &QFutureWatcher<int>::finished, [=](){
_sched();
});
}
void sched(std::function<void()>&functor)
{
mtx.
lock();
queue.push_back(functor);
mtx.unlock();
///Z#20241210, bug, Q_EMIT dispatch in caller's thread //Q_EMIT finished();//to sched QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection);
}
private:void_sched()
{
QList
<std::function<void()> >coll;
mtx.
lock();
coll
=std::move(queue);
mtx.unlock();
while (!coll.empty())
{
coll.front()();
coll.pop_front();
}
}
};

MyUIExecutor

于是有界面例程:

编写服务器也简单方便,虚函数handle_XXX对应于http请求方法的入口,只要override这些虚函数,再根据uri进行不同服务接口处理。

然后就是本工具的新增的
主要功能

通过两个例子,介绍如何使用cpprest写代码,请求新浪财经的数据,使用百度股市通的opendata接口请求数据。

对于新浪财务数据,就是页面的表格。我们可以用QTextEdit转换成MarkDown。

下面就是百度的opendata数据接口,它们在webpack://finance-pc/src/api/路径下api_prefix.js跟detail.js,有详细的注释说明。数据涵盖多个市场,A股,港股,美股,货币,有色金属等。

本功能使用了两个接口,第一个是码表搜索。第二个是基本行情。所有接口皆用JSON承载数据。

本功能帮助大家调试分析这些接口的数据。每次请求结果会在下方树型表缓冲。右键树型节点打开不同功能。根节点预览并转换格式保存文件。对于JSON的对象节点,右键打开属性路径搜索。叶子节点则是复制值。

除了上面两个固定网站的接口应用例子,还可以使用通用请求。

现就用通用请求跟BlackJack例子一同测试一下。

本功能的源代码, 已经完整展示了如何使用cpprest调用opendata接口,如何使用cpprest写界面交互。使用者可以根据需要修改裁剪扩展,满足自己需要的功能。 使用cpprest从其它股票相关网站获取数据也是十分方便。有cpprest借助pplx加承,不必费心多线程跟io并发,轻轻松松写异步http请求。

还有一点注意, cpprest默认使用unicode,qt字符串底层也是使用unicode。其它json11,pugihtml需要utf8。

本篇结束。

在.NET中,
CancellationTokenSource

CancellationToken

Task
是处理异步操作和取消任务的重要工具。本文将通过一些简单的例子,帮助你理解它们的用法和协作方式。


CancellationTokenSource

CancellationTokenSource
是一个取消操作的触发器。它用于生成和管理
CancellationToken
,并控制取消信号的发出。

常用属性和方法
  • Token
    : 返回一个与此源关联的
    CancellationToken
  • Cancel()
    : 触发取消操作。
  • CancelAfter(milliseconds)
    : 指定时间后触发取消操作。
  • Dispose()
    : 释放资源。
示例
var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

Task.Run(() => {
    for (int i = 0; i < 10; i++)
    {
        if (token.IsCancellationRequested)
        {
            Console.WriteLine("Task canceled");
            break;
        }
        Console.WriteLine($"Task running: {i}");
        Thread.Sleep(500);
    }
});

Thread.Sleep(2000);
cts.Cancel();

CancellationToken

CancellationToken
是用于传播取消请求的轻量级结构。它由
CancellationTokenSource
生成。

常用属性和方法
  • IsCancellationRequested
    : 是否收到取消请求。
  • ThrowIfCancellationRequested()
    : 如果已请求取消,抛出
    OperationCanceledException
  • Register(Action)
    : 注册一个取消时触发的回调。
示例
var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

Task.Run(() => {
    token.Register(() => Console.WriteLine("Cancellation registered"));

    try
    {
        for (int i = 0; i < 10; i++)
        {
            token.ThrowIfCancellationRequested();
            Console.WriteLine($"Task running: {i}");
            Thread.Sleep(500);
        }
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Task was canceled");
    }
});

Thread.Sleep(2000);
cts.Cancel();

Task与CancellationToken

Task
是.NET中的异步操作单元。结合
CancellationToken
可以在任务运行时取消它。

示例:取消任务
var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

Task task = Task.Run(() => {
    for (int i = 0; i < 10; i++)
    {
        if (token.IsCancellationRequested)
        {
            Console.WriteLine("Task canceled");
            break;
        }
        Console.WriteLine($"Task running: {i}");
        Thread.Sleep(500);
    }
}, token);

Thread.Sleep(2000);
cts.Cancel();

try
{
    task.Wait();
}
catch (AggregateException ex)
{
    foreach (var inner in ex.InnerExceptions)
    {
        if (inner is TaskCanceledException)
        {
            Console.WriteLine("Task cancellation exception caught");
        }
    }
}
示例:带超时的任务
var cts = new CancellationTokenSource(3000); // 3秒后自动取消
CancellationToken token = cts.Token;

Task.Run(() => {
    try
    {
        for (int i = 0; i < 10; i++)
        {
            token.ThrowIfCancellationRequested();
            Console.WriteLine($"Task running: {i}");
            Thread.Sleep(1000);
        }
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Task canceled due to timeout");
    }
});

小结

  1. 使用
    CancellationTokenSource
    来控制取消。
  2. 通过
    CancellationToken
    将取消信号传递给任务或方法。
  3. 任务中可以通过
    ThrowIfCancellationRequested
    或检查
    IsCancellationRequested
    响应取消请求。
  4. 合理使用
    Register
    可以处理取消时的回调逻辑。

通过灵活运用这些工具,你可以编写更高效、可控的异步程序。

本篇介绍
Manim
中的两个旋转类的动画,名称差不多,分别是
Rotate

Rotating

Rotate
类主要用于对图形对象进行指定角度、围绕特定点的
精确旋转
,适用于几何图形演示、物理模拟和机械运动展示等场景;

Rotating
类则侧重于创建让对象围绕指定轴或点
持续旋转
的动画,用于动态图标、天体运动模拟和装饰性动态元素等场景。

1. 动画概述

1.1. Rotate

Rotate
是一个用于对
Mobject
进行旋转的动画类。

它通过指定旋转角度、旋转轴等参数来实现精确的旋转效果,例如,它可以围绕特定点(如对象的中心或者自定义的点)进行旋转。

此外,还可以设置旋转的起始角度和终止角度,并且可以指定旋转的速率,通过控制动画运行的时间来调整旋转的快慢。

它的主要参数有:

参数名称 类型 说明
mobject Mobject 要旋转的
Mobject
对象
angle float 旋转角度,以弧度为单位指定
axis np.ndarray 旋转轴,用向量表示
about_point [float] 旋转中心
about_edge [float] 指定边界框点的方向作为旋转中心

about_point

about_edge
只有一个有效,当
about_point

None
时,
about_edge
才生效。

1.2. Rotating

Rotating
更侧重于创建一个持续旋转的动画效果。

它会使
Mobject
一直围绕指定的轴或点进行旋转,通常用于创建动态的、循环的旋转场景。

它可以设置旋转的方向(顺时针或逆时针),并且可以方便地控制旋转的速度。

它的主要参数有:

参数名称 类型 说明
mobject Mobject 要旋转的
Mobject
对象
radians np.ndarray 旋转的弧度值
axis np.ndarray 旋转轴
about_point [float] 旋转中心
about_edge [float] 指定边界框点的方向作为旋转中心
rate_func func 速率函数,用于控制动画在时间上的进展速度

1.3. 两种旋转的区别与联系

Rotate

Rotating
有明显的区别和联系。


区别
方面,
Rotate
重点在于精确控制旋转,它有明确的起始和终止角度设定,可以精准地让
Mobject
从一个角度旋转到另一个角度,适合一次性的、角度明确的旋转操作。

例如,将一个三角形从初始位置旋转 30 度来展示特定的几何关系。


Rotating
是让
Mobject
持续旋转,没有终止角度的限制,只要动画持续,就会一直围绕指定轴或点循环旋转,更适合用于需要持续动态旋转的场景,如一个不停旋转的风车。

它们的
联系之处
在于都用于实现旋转动画,都依赖于
图形对象

Mobject
)和
旋转轴

旋转中心
这些基本要素。

而在一些复杂的动画场景中,还可以配合使用,比如先用
Rotate
将物体旋转到一个特定位置,再用
Rotating
让其在该位置持续旋转。

2. 使用示例

下面通过示例来演示
Rotate

Rotating
动画的使用。

2.1. Rotate 时钟指针的旋转

该示例用箭头线段代表时钟指针,通过
Rotate
动画,围绕原点将指针从
12点
位置旋转到
3点
位置(即旋转
90
度,对应
PI/2
弧度)。

以此简单模拟时钟指针的转动情况,直观展现 Rotate 在模拟有明确角度变化场景下的应用。

d = Dot(color=BLUE)
c = Circle(color=YELLOW, radius=1)
self.add(d, c)
# 创建时钟指针(简单用线段表示)
hand = Arrow(ORIGIN, [0, 1, 0])
# 模拟时钟指针从 12 点位置旋转到 3 点位置
r_anim = Rotate(
    hand,
    angle=PI / 2,
    axis=IN,
    about_point=ORIGIN,
)
self.play(r_anim)

2.2. Rotating 风扇叶片旋转

以两个矩形表示风扇叶片,借助
Rotating
动画类,让叶片围绕其中心,沿着垂直屏幕向外的轴持续旋转,一圈又一圈,设定旋转一圈的时长为 4 秒。

很好地体现了
Rotating
用于创建持续旋转效果的功能,模拟风扇叶片不停转动的场景。

# 创建风扇叶片(用矩形表示)
b1 = Rectangle(height=0.2, width=2, color=BLUE_B)
b2 = Rectangle(height=2, width=0.2, color=BLUE_D)
vg = VGroup(b1, b2)
# 使用 Rotating 让风扇叶片持续绕中心旋转
r_anim = Rotating(
    vg,
    axis=OUT,
    radians=TAU,
    about_point=vg.get_center(),
    run_time=4,
)
self.play(r_anim)

2.3. Rotate 和 Rotating 旋转门

用矩形作为旋转门的一扇门叶,先通过
Rotate
将门叶从关闭位置旋转到一个位置,

随后使用
Rotating
让门叶在打开位置以小角度围绕其左下角持续摆动,还借助
wiggle
速率函数营造摆动效果。

以此模拟旋转门打开后受外界因素影响产生摆动的场景,展示了两个动画类结合使用的灵活性。

# 创建旋转门的一扇门叶(用矩形表示)
door = Rectangle(
    height=3,
    width=2,
    color=BLUE,
    fill_opacity=0.2,
    fill_color=YELLOW,
)
l = Line(UP * 2, DOWN * 2, stroke_width=1, color=GREEN)
self.add(door, l)
# 先使用 Rotate 将门叶从关闭位置旋转到打开 90 度位置
rotate_to_open = Rotate(
    door,
    axis=UP,
    angle=PI / 4 + PI,
    about_point=door.get_bottom(),
    rate_func=rush_into,
)
# 然后使用 Rotating 让门叶在打开位置持续小角度摆动(模拟风或其他因素影响)
rotating_at_open = Rotating(
    door,
    axis=UP,
    radians=PI / 8,
    about_point=door.get_bottom(),
    run_time=3,
    rate_func=wiggle,
)
self.play(rotate_to_open)
self.play(rotating_at_open)

3. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
rotate.py
),

下载地址:
完整代码
(访问密码: 6872)

《整洁架构之道》的最后一章《细节决定成败》又在讨论 Javaer 永恒的问题:分层后 DAO Service Controller 应该按功能分包还是按层分包。 按功能分包的人认为这些文件在业务上是一起的,应该放在同一个包。按层分包的人认为每个层代表了不同的技术,应该按层分包。

可以想象,按层分包的人主张,DAO 层应该打包成一个 jar,对 PG 数据库用户提供 PG 的 jar,对 mysql 的提供 mysql 的 jar,到时换库很灵活。

而按功能分包的人现在有一个更好的理由,未来可以按功能切分成微服务!

《细节决定成败》这一章似乎是国内的译者写的,Bob 应当不太可能关注这种问题。但是问题的确存在。

假如我们把系统拆分来看,在一个古老的 CS 系统,分为服务器和客户端两大部分。客户端是 .net 开发的,服务端是 java 开发的。业务运行在 Java 服务器,客户端同样也表达一些业务知识,当然,主要是呈现性的知识。在包的组织上,客户端基本和服务器会采取一一对应的包设计。

这种系统里一个功能分在两个项目中,上面讨论的问题也就消失了。

从这个类比看,按层拆分似乎是最正确的选择。假如开发语言不同,同一个功能分成两个工程就很合理了。

那么,为什么大家还要纠结拆分呢?如果从一开始就分为三个工程,dao,service,controller 各一个工程,问题不是解决了?

问题出在那儿?

首先最直接的感受是效率的损耗,一个功能分前后端可以说是无奈的选择,现在一个后端还要分三个工程,老板就要问一问有没有价值了。

DAO 层要独立,原因是所谓的“数据库是细节”,不能绑定太深,未来可能要换数据库。

Controller 要独立,因为它是属于 Web 的,不是业务。

随后,更多的分层来了,Service 要分 ServiceInterface 和 ServiceImpl,DAO 也要分 ServiceInterface 和 ServiceImpl。

Service 这么分的理由是 Controller 没有资格访问具体的 ServiceImpl,应当通过依赖反转由容器对 Controller 赋予 ServiceImpl。

而 DAO 拆分的理由是,Service 直接触碰 DAO 太脏,DAO 可能更换,甚至可以说,DAO 接口是由 Service 出具的,只有那点可怜的 DAOImpl 才属于 DAO 工程。

也就是说,如果切分为 3 个工程,Service 这层应当出具一个 Service 接口工程给 Controller 工程,并且出具一个 DAO 接口工程给 DAO 工程。

以上就是完美的划分。

但是,在实践中,

  1. Controller 代码太少了,Controller 是不能改变业务逻辑的,所以 Controller 仅仅是将 Service 的一些函数映射出去而已。专门搞一个 Controller 工程非常傻。
  2. 数据库没有想象的那么烂————Uncle Bob 踩过早期数据库的坑,他固执的认为数据库仅仅是一个存储,带着这种错误的认知他甚至没有把关系运算当作一个范式。但是在实际项目中,DAO并不是简单的存取,往往用到关系运算或如 mongodb 的 map-reduce,换数据库难度是非常大的。试想,Model 最终都在数据库里,Service 要取用数据都要走数据库,而号称实现业务的 Service,使用的却是普通的运行于内存的编程语言,随时可能因为 JVM 宕机导致业务逻辑中断,并且还不支持事务,由此,一个业务件竟然不敢持有业务状态。并且
  3. Service DAO Controller 全部位于同一个 Spring 容器,这使得这种切分显得很荒谬。Controller 是 Web 服务器层的东西,带有高强度的 IO,随时会被冲垮,Service 这么宝贵的东西竟然和 Controller 混在一起,是不是很奇葩。
  4. 这就导致很多业务最终并不是靠 Service.java 或 ServiceImp.java,而是通过规则引擎、消息队列、工作队列等工作。

这时有人会说, 照你这个说法,Java 根本就不适合写业务?

不是没有可能。我可以理直气壮的说,
如果一个 Service 把持久扔掉,它根本没有资格做 Service
。此外, Controller 不过是 Service 的 RPC 入口。

最严重的问题是,它很蹩脚,这么蹩脚的设计不禁让我们思考:
什么是理想的划层?

看三个例子:

网络 7 层,包括应用层传输层等,HTTP 是建立在 TCP 上的,TCP 这一层不需要为 HTTP 做任何调整。
上一层对下一层是完全无知无感的
,当层划分好后,甚至能做到下一层的变动不会影响上一层。

又如操作系统的层,我们日用的进程文件等等,属于操作系统提供的层。这一层上的应用例如视频播放器完全不需要考虑我的进程有没有被调度到CPU,磁盘磁道怎么安排,磁盘改成 NVME 怎么办等等,视频播放器绝不会说我要加一个功能需要操作系统改一改升个级。

像 JVM 这种跨平台虚拟机进一步对操作系统做了抽象,我们甚至可以编写与目标操作系统完全无关的程序。

上述这些分层都是完全解耦的,上一层依赖下一层,但是下一层对上一层无感,不需要考虑上一层的心情。

套用一下让子弹飞体:
什么叫分层?这 TM 才叫分层!

不怕不识货就怕货比货,现在我们可以发现 Java 项目这种所谓的分层有多么荒谬。既然已经分层还要考虑横切还是竖切,那就说明根本没有实现正交,Service 层不是建立在 DAO 基础上,DAO 也不建立在 Service 上,它们互相缠绕,把一个现实的功能凌迟的到处都是。

为什么 Java 要把持久化到数据库搞成一个层呢?究其原因乃是 Uncle Bob 等人踩过的一些坑使其一朝被蛇咬十年怕井绳,Uncle Bob 豪迈的宣称:ORM 只是细节,只应当放在 Dao 层;对象数据库害了我们,我们把宝押在某某数据库、对象数据库,犯了大错,诸如此类。

客户要求换数据库固然会导致重写很多代码,但是哪天客户要求换程序语言呢?假如客户要把 Java 换成 C#,rust,python 呢?那我们是不是该庆幸当初写的全是 SQL,只要把数据库连接方式改一下把SQL抄过来就 OK?据此推理下去,为了随时替换编程语言,我们把系统搞成 nginx + 存储过程?

如果我们不像 Uncle Bob 等人一样,带着畏惧情绪去看待数据库,而是把关系运算也当作一种范式,OOP + 关系运算结合才是业务的实际状态,换数据库和换 Java 一样,是绝对偶发的情形,问题就会简单不少。重复一遍,
业务是由数据库和 OOP 共同完成的

同样的,Service 不可能是一个闷葫芦,它要对外提供服务,就必然有对外服务的手段,而这根本不足以形成一个层。为什么呢?Controller 没有资格自行决定暴露哪些 Service 接口,暴露哪些是业务规定的,因此,暴露 Service 这个工作就应当由 Service 自己说明。这可以是 Annotation 也可以是配置文件之类的,但是这并不足以构成一个层。在现实代码中 Controller 主要工作就是做输入校验,把 request 转化为 Service 的查询对象,如果 controller 只做这么一点工作,它应该是通用的,这部分工作应当变成一种协议,也就是 Tcp 层 Http 层之后的一个对象路由层,用过 mina 的人都知道我在说什么。

该层输出的对象大体是这样的

class UserQuery extends Query{
  @Required
  String name
  GenderEnum gender
}
abstract class Query{
  abstract void validate() throws ValidationError;
  HttpRequest request;
}

至于向该层投放的对象,由于是 Restful 接口,输出的查询结果用普通的 POJO 甚至 Map 均可。

有兴趣还可以继续推演。如需要将 Service 保持参数形态,如
UserService.search(String name, Gender gender)
而不是
UserServer.search(UserQuery query)
)那么这种层应当怎么设计?

把完整的框架发明出来超出了本文讨论范围。

综上,
第一:Service 理应和 DAO 合并,业务本就是透过 Java 和关系运算共同实现的。
第二:Controller 可能没有资格存在,应当处理为一个对象路由层。

其它讨论:

  • 有人会说,那么其它的中间件呢?为什么只有数据库有这种地位?消息队列呢?

看完上述分析我们可以理解,消息队列对于 Service 同样是一个对象路由层。

  • 那么 ES 呢?

ES 之类其它带有运算的存储,必然是业务共同体。假如 ES 是替换数据库的,那么,根据需求,我们可以从原来的 Service 派生一个 ESService,覆盖部分方法(部分替换),也可以将原 Service 抽象为 Service + PGService,然后另外派生一个 ESService(完全替换)。

背景

需要在异步任务中中断任务的执行,故选择通过调用
interrupt
方法对线程设置中断信号。
在比较耗时的业务代码前增加判断
Thread.currentThread().isInterrupted()
抛出异常停止任务执行,并回退任务。

问题

中断信号发出后,任务线程一直未检测到中断信号状态。
以下为测试过程。
最终结论:线程 interrupt 方法不适用实际业务编码,应自定义线程中断状态。

第一次测试

无任何其他操作,只在各个阶段打印日志时间和线程中断状态。
观察信号中断时间和报表内日志的打印时间,判断任务中在哪个节点获取到的中断信号。

日志示例

中断信号发起时间

2024-12-20
09:52:23
:011 erp-gl [XNIO-1 task-1] INFO c.u.c.c.c.ErpGlobalController[71] TxId=6664fc8e-0596-4012-8d23-ebd40de9977f SpanId= GLOBAL-终止线程=[ERP-REQ-10342620]

任务内各阶段中断信号校验时间

2024-12-20 09:52:17:253 erp-gl [ERP-REQ-10342620] INFO c.u.gl.program.GlReportTotalTypeAc[104] TxId= SpanId= 中断信号01 2024-12-20
09:52:17
,false

2024-12-20 09:52:17:539 erp-gl [ERP-REQ-10342620] INFO c.u.gl.program.GlReportTotalTypeAc[138] TxId= SpanId= 中断信号02 2024-12-20
09:52:17
,false

中断信号03 2024-12-20
09:52:41,false

中断信号04 2024-12-20
09:52:55,false

中断信号05 2024-12-20
09:53:12,false

中断信号06 2024-12-20
09:53:13,false

中断信号07 2024-12-20
09:53:14,false

结论:
一直到程序执行完并未获取到中断信号,或者中断信号被某段代码重置。

第二次测试

在第一次测试的基础上,添加while代码
观察在未执行业务代码之前是否能获取到中断信号

中断信号发起时间

2024-12-20
10:08:34
:978 erp-gl [XNIO-1 task-1] INFO c.u.c.c.c.ErpGlobalController[71] TxId=666a7b26-7f52-4ac4-b45a-c7c0e04ec75e SpanId= GLOBAL-终止线程=[ERP-REQ-10342620]

任务内各阶段中断信号校验时间

中断信号01 2024-12-20
10:08:31,false

2024-12-20 10:08:34:978 erp-gl [ERP-REQ-10342620] INFO c.u.gl.program.GlReportTotalTypeAc[163] TxId= SpanId=
捕获到中断信号时间: 2024-12-20 10:08:34

中断信号02 2024-12-20
10:08:34,true

中间有数据批量插入操作,中断信号被重置

中断信号03 2024-12-20
10:08:57,false

以下各阶段均为 false

结论:
未执行业务代码前可以收到中断信号

第三次测试

调整while 位置,观察是否是因为数据库插入操作将中断信号重置

中断信号发起时间

2024-12-20
10:17:10
:759 erp-gl [XNIO-1 task-1] INFO c.u.c.c.c.ErpGlobalController[71] TxId=66632106-d9f7-4210-afe3-95794f2ff6bc SpanId= GLOBAL-终止线程=[ERP-REQ-10342620]

任务内各阶段中断信号校验时间

中断信号01 2024-12-20
10:17:06,false

中断信号02 2024-12-20
10:17:06,false

while 循环操作,未获取到中断信号日志

中断信号03 2024-12-20
10:28:19,false

以下各阶段均为 false

结论:
数据库插入操作导致中断信号被重置。

第四次测试

在第三次测试的基础上,注释 所有数据库插入操作,观察中断信号是否被重置

中断信号发起时间

2024-12-20
10:39:16
:373 erp-gl [XNIO-1 task-1] INFO c.u.c.c.c.ErpGlobalController[71] TxId=6663bc98-bd9d-499e-aee4-28932e4c468f SpanId= GLOBAL-终止线程=[ERP-REQ-10342620]

任务内各阶段中断信号校验时间

中断信号01 2024-12-20
10:39:12,false

中断信号02 2024-12-20
10:39:12,false

捕获到中断信号时间: 2024-12-20 10:39:16

中断信号03 2024-12-20
10:39:16,true

中断信号04 2024-12-20
10:39:16,true

中间为双重for循环数据处理,没有任何数据插入操作,中断信号被重置

中断信号05 2024-12-20
10:39:32,false

结论:
未确定哪行代码将中断信号重置。