2024年11月

比赛在这里呢

填算符

下发题解说的神马东西,赛时根本想不到

讲一个赛时想得到的
\(O(n\log 值域)\)
的思路,很好理解

我们处理出二进制下每一位上的 1 的最后一次出现的位置,
将第
\(i\ (i\in[0,60])\)
位上的 1 最后一次出现的位置记作
\(pos_i\)

同时我们设
\(H=n-k-1\)
为总共有的
bitor
的操作数

有以下结论:由于
\(pos_i\)

\(i\)
位上最后一个 1,所以一旦它后面放了一个 与,这一位上就是 0 了;若我们想要这一位为 1,必须至少满足从
\(pos_i\)
到最后的运算符全是
bitor

发现有以下情况:


  • \(n-pos_i>H\)
    ,即
    \(pos_i\)
    之后需要放的运算符的数量比
    bitor
    的总操作数多,也就是说在
    \(pos_i\)
    之后我一定需要放
    bitand
    操作,所以这种情况下这一位一定不对答案有贡献


  • \(n-pos_i<H\)
    ,也就是说我可以从
    \(pos_i\)
    的前一个位置开始到最后全放
    bitor
    操作,那么这样第
    \(i\)
    位上可以是 1,为了使值最大,所以第
    \(i\)
    位上一定要是 1,所以从第
    \(pos_i\)
    位到最后必须全是
    bitor
    操作,
    对于这种情况的
    \(i\)
    我们记为合法位


  • \(n-pos_i=H\)
    ,也就是说从第
    \(pos_i\)
    到最后的运算符可以全是
    bitor
    操作,但
    \(pos_i\)
    的前一位只能是
    bitand
    所以我们
    特判从第 1 个位置到
    \(pos_i\)
    的前一位全放
    bitand
    能不能让到第
    \(pos_i\)
    个数时得到的值第 $\forall $
    \(j 满足 [pos_j=pos_i]\)
    位为 1,若能则该位也为合法位,否则不合法

对于所有合法位的
\(pos\)
取最小值设为
\(end\)
,因为已经保证
\(end\)
到最后的预算符全是
bitor
,此时有一下两种可能,而我们想尽量构成第二种可能:

  1. \(end\)
    的前一位预算符也为
    bitor
    ,这样我们一定能达到答案最大了

    ,想使答案最优直接让从
    \(end-2\)
    开始的
    \(k\)
    个运算符为
    bitor
    就好了

  2. \(end\)
    的前一位在某些情况为
    bitand
    也是可以使答案最大的,所以我们
    判断能不能让
    \(end\)
    的前一位为
    bitand
    同样使答案最大;

    发现可以的条件相当于从第
    \(end-1\)
    个数到最前面用仅剩的
    bitor
    操作得到一个答案,使得这个答案第 $\forall $
    \(i 满足 [pos_i=end]\)
    位为 1,若能满足条件则第
    \(end-1\)
    个操作符为
    bitand

满足条件的判断又和上述的第三个情况判断一致了,相当于以
\(end-1\)
为下界,再做一次求
\(min(合法的\ pos)\)
,实质上是不断的递归。

形式化如下:

所以一个递归
\(dfs(end, H)\)
表示下界为
\(end\)
,还剩
\(H\)

bitor
操作,判断能不能得到我想要的答案:

若不能则直接从第
\(end-2\)
开始的
\(k-res\)
个运算符全为
bitand
就是答案(
\(res\)
为在之前的递归中已经确定的
bitand
的个数)

若能
则第
\(end-1\)
个位置可以为
bitand

,并设
\(end'=min(这一层中合法的\ pos)\)

继续递归
\(dfs(end',H-(end-end'))\)
判断第
\(end'-1\)
个位置能不能为
bitand

code:

#include<bits/stdc++.h>
#define Aqrfre(x, y) freopen(#x ".in", "r", stdin),freopen(#y ".out", "w", stdout)
#define mp make_pair
#define Type ll
#define qr(x) x=read()
typedef __int128 INT;
typedef long long ll;
using namespace std;

inline ll read(){
    char c=getchar(); ll x=0, f=1;
    while(!isdigit(c)) (c=='-'?f=-1:f=1), c=getchar();
    while(isdigit(c)) x=(x<<1)+(x<<3)+(c^48), c=getchar();
    return x*f;
}

const int N = 1e6 + 5; 
const int maxn = 1e8;

int n, k, K; ll a[N], b[N];

int la[62], pre[62][N], zh[62], X;
vector<int>v[N], ans, tem, num;

inline bool check(int pos, int op){ // 判断从第一个运算符到第 pos 个全为 & 能不能使得到的值满足条件
    int now = pos + 1; ll x = 0;
    for(int i : v[now]) x += (1 << i);
    if(~X) x += (1 << X);
    int y = a[1];
    for(int i=2; i<=now; i++)
        y = y & a[i];
    if(y & x == x) return true;
    return false;
}

inline void dfs(int pos, int H){ // 递归函数
    if(pos <= 0 and H <= 0) return;
    int now = pos + 1, end = 2e9;
    bool f = true; X = -1;
    for(int x : tem) v[pre[x][now]].clear(); //为方便更新新的一层的 V ,先清空
    for(int x : tem){
        if(pos - pre[x][now] > H or !pre[x][now]){
            f = false; break;
        }
        else if(pos - pre[x][now] < H) // 合法则更新 end 并加入 V
            end = min(end, pre[x][now] - 1), v[pre[x][now]].emplace_back(x);
        else{
            X = x;
            if(pre[x][now] == 1 or check(pre[x][now] - 1, 1))
                end = min(end, pre[x][now] - 1), v[pre[x][now]].emplace_back(x);
            else f = false;
        }
    }
    if(f) ans.emplace_back(pos), k--; // pos 位可以为 &,加到答案中
    if(!k) return;
    if(f and end >= k){
        tem.clear(); for(int x : v[end+1]) tem.emplace_back(x);
        dfs(end, H-(pos-end-1)); //继续递归判断 end 位可否为 &
    }
    else{
        int cnt = k; // pos 位不可以为 &,则最优方案为从 pos-1 到 pos-cnt 全为 &
        for(int i=pos-cnt; i<pos; i++)
            k--, cout<<i<<" ";
        return;
    }
}

signed main(){ // bitop
    Aqrfre(bitop, bitop);

    qr(n); qr(k); K = k;
    for(int i=1; i<=n; i++){
        qr(a[i]);
        for(int j=0; j<62; j++){
            if(a[i] & (1ll << j)) pre[j][i] = la[j], la[j] = i;
            else pre[j][i] = pre[j-ans.size()][i-1]; // 二进制下第 j 位为 1 在第 i 个数之前一次出现的位置
        }
    }

    if(k == n - 1){
        for(int i=1; i<=k; i++) cout<<i<<" ";
        return 0;
    }

    for(int j=0; j<62; j++) //  V 存当前这一层递归的下界包含的 最后一个 1 出现在这个下界的 二进制位
        if(la[j]) zh[j] = la[j], v[zh[j]].emplace_back(j);

    int H = n - 1 - k, endi = 1e9; bool go = false;
    for(int i=0; i<62; i++){ // 把第一次递归剖出来单独做
        if(!zh[i]) continue; 
        if(n - zh[i] > H) continue; 
        if(n - zh[i] < H){
            endi = min(endi, zh[i] - 1);
            continue;
        }
        if(go) continue;
        if(n - zh[i] == H){
            if(check(zh[i] - 1, 0)){ //特殊的:合法直接输出
                for(int i=1; i<=k; i++)
                    cout<<i<<" ";
                return 0;
            }
            go = true;
        }
    }

    for(int x : v[endi+1]) tem.emplace_back(x); //tem 暂存下界这个数的 V

    H -= (n - endi - 1);

    dfs(endi, H);
    sort(ans.begin(), ans.end());
    
    for(int x : ans) cout<<x<<" ";



    return 0;
}

我们在开发桌面应用的时候,不管是之前C#开发Winform的时候,还是现在使用wxpython来开发跨平台应用的时候,都需要了解布局的处理,wxpython的常用布局Sizer类,包括BoxSizer,FlexGridSizer,GridBagSizer都是我们需要经常打交道的,因此有必要对它们进行一些了解,这样开发界面起来才能得心应手。本篇随笔介绍一下这几种布局Sizer的不同以及对它们进行测试和封装使用。

1、BoxSizer,FlexGridSizer,GridBagSizer的布局介绍和差异

在 wxPython 中,布局管理是通过 Sizer 类来实现的。常用的 Sizer 类包括
BoxSizer

FlexGridSizer

GridBagSizer
。下面是这些 Sizer 的介绍及其之间的差异:

1. BoxSizer

  • 描述
    :
    BoxSizer
    是最简单的 Sizer 类型,允许你将控件沿一个方向(水平或垂直)排列。
  • 用法
    : 适用于简单的线性布局。你可以指定方向(
    wx.HORIZONTAL

    wx.VERTICAL
    )并控制每个控件的边距和比例。
  • 特点
    :
    • 所有子控件按顺序排列。
    • 可以设置比例,控制控件的伸缩行为。
    • 较适合创建简单的、单一方向的布局。

2. FlexGridSizer

  • 描述
    :
    FlexGridSizer
    是一个可以在行和列中进行灵活布局的 Sizer。它会自动调整每个单元格的大小,以适应控件的内容。
  • 用法
    : 适用于需要均匀分配空间的网格布局。
  • 特点
    :
    • 行和列的大小可以根据内容自动调整。
    • 所有控件都放置在独立的单元格中,不能跨越多个单元格。
    • 适合需要规则网格的情况,比如表单或简单的网格布局。

3. GridBagSizer

  • 描述
    :
    GridBagSizer
    是一种更复杂的布局管理器,它允许你在一个网格中放置控件,并支持控件的大小、位置以及跨越多个行和列。
  • 用法
    : 适用于需要高度自定义布局的情况,比如复杂的用户界面。
  • 特点
    :
    • 支持控件在网格中的精确控制。
    • 可以设置控件的对齐方式和边距。
    • 功能强大,适合复杂布局。

总结

  • BoxSizer
    : 最简单,适合线性布局。
  • FlexGridSizer
    : 灵活的网格,适合不规则网格布局。
  • GridBagSizer
    : 功能最强,支持复杂的自定义布局。

选择合适的 Sizer 取决于你的具体布局需求。对于简单场景,使用
BoxSizer
就足够了;而对于更复杂的布局,
FlexGridSizer

GridBagSizer
提供了更多的灵活性和控制能力。

2、使用布局Sizer控件创建不同的界面案例

为了直观的了解集中Sizer布局控件的不同,我们使用几个例子来测试它们的界面效果和区别。

1)BoxSizer的界面及代码

它是使用BoxeSizer来垂直放置几个部分的内容的,其中底部的两个按钮又是创建一个新的Panel进行维护,如下代码所示。

classMyForm(wx.Frame):def __init__(self):
wx.Frame.
__init__(self, None, wx.ID_ANY, title='Boxesizer 测试')#Add a panel so it looks correct on all platforms self.panel =wx.Panel(self, wx.ID_ANY)#使用内置图标 bmp = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_OTHER, (48, 48))
titleIco
=wx.StaticBitmap(self.panel, wx.ID_ANY, bmp)
title
= wx.StaticText(self.panel, wx.ID_ANY, '测试内容')#设置标题的字体大小 font =title.GetFont()
font.SetPointSize(
28)
title.SetFont(font)
#使用内置图标 bmp = wx.ArtProvider.GetBitmap(wx.ART_TIP, wx.ART_OTHER, (16, 16))
inputOneIco
=wx.StaticBitmap(self.panel, wx.ID_ANY, bmp)
labelOne
= wx.StaticText(self.panel, wx.ID_ANY, 'Input 1')
inputTxtOne
= wx.TextCtrl(self.panel, wx.ID_ANY, '')

inputTwoIco
=wx.StaticBitmap(self.panel, wx.ID_ANY, bmp)
labelTwo
= wx.StaticText(self.panel, wx.ID_ANY, 'Input 2')
inputTxtTwo
= wx.TextCtrl(self.panel, wx.ID_ANY,'')

inputThreeIco
=wx.StaticBitmap(self.panel, wx.ID_ANY, bmp)
labelThree
= wx.StaticText(self.panel, wx.ID_ANY, 'Input 3')
inputTxtThree
= wx.TextCtrl(self.panel, wx.ID_ANY, '')

okBtn
= wx.Button(self.panel, wx.ID_ANY, '确定')
cancelBtn
= wx.Button(self.panel, wx.ID_ANY, '取消')
self.Bind(wx.EVT_BUTTON, self.onOK, okBtn)
self.Bind(wx.EVT_BUTTON, self.onCancel, cancelBtn)

topSizer
=wx.BoxSizer(wx.VERTICAL)
titleSizer
=wx.BoxSizer(wx.HORIZONTAL)
inputOneSizer
=wx.BoxSizer(wx.HORIZONTAL)
inputTwoSizer
=wx.BoxSizer(wx.HORIZONTAL)
inputThreeSizer
=wx.BoxSizer(wx.HORIZONTAL)
btnSizer
=wx.BoxSizer(wx.HORIZONTAL)

titleSizer.Add(titleIco, 0, wx.ALL,
5)
titleSizer.Add(title, 0, wx.ALL,
5)

inputOneSizer.Add(inputOneIco, 0, wx.ALL,
5)
inputOneSizer.Add(labelOne, 0, wx.ALL,
5)

inputOneSizer.Add(inputTxtOne,
1, wx.ALL|wx.EXPAND, 5)

inputTwoSizer.Add(inputTwoIco, 0, wx.ALL,
5)
inputTwoSizer.Add(labelTwo, 0, wx.ALL,
5)
inputTwoSizer.Add(inputTxtTwo,
1, wx.ALL|wx.EXPAND, 5)

inputThreeSizer.Add(inputThreeIco, 0, wx.ALL,
5)
inputThreeSizer.Add(labelThree, 0, wx.ALL,
5)
inputThreeSizer.Add(inputTxtThree,
1, wx.ALL|wx.EXPAND, 5)

btnSizer.Add(okBtn, 0, wx.ALL,
5)
btnSizer.Add(cancelBtn, 0, wx.ALL,
5)

topSizer.Add(titleSizer, 0, wx.CENTER)
topSizer.Add(wx.StaticLine(self.panel,), 0, wx.ALL
|wx.EXPAND, 5)
topSizer.Add(inputOneSizer, 0, wx.ALL
|wx.EXPAND, 5)
topSizer.Add(inputTwoSizer, 0, wx.ALL
|wx.EXPAND, 5)
topSizer.Add(inputThreeSizer, 0, wx.ALL
|wx.EXPAND, 5)
topSizer.Add(wx.StaticLine(self.panel), 0, wx.ALL
|wx.EXPAND, 5)
topSizer.Add(btnSizer, 0, wx.ALL
|wx.ALIGN_RIGHT, 5)

self.panel.SetSizer(topSizer)
topSizer.Fit(self)

上面可以看到,BoxSizer比较僵硬,它只能垂直或者水平放置内容,如果具有水平和垂直方向的处理,就需要分别创建多个不同的BoxSizer进行管理,因此代码会相对多一些。

2)FlexGridSizer的界面及代码

同样使用FlexGridSizer也可以做到很好的控制,通过设置指定行或者列的拉伸效果,可以很好的实现自动拉伸功能,类似Winform里面的Dock的方向处理。

该布局例子的代码如下所示。

classFrame(wx.Frame):def __init__(self):
super().
__init__(None, title = '测试FlexGridSizer', size=(600, 400))#self.SetBackgroundColour("#4f5049") self.SetMinSize((300, 350))

main_sizer
= wx.BoxSizer( wx.VERTICAL ) #主面板布局使用垂直方向 sizer= wx.FlexGridSizer(2, (10,20))
sizer.Add(wx.StaticText(self, label
='昵称'))
sizer.Add(wx.TextCtrl(self), flag
=wx.EXPAND)
sizer.Add(wx.StaticText(self, label
='留言'))
sizer.Add(wx.TextCtrl(self, style
=wx.TE_MULTILINE), flag=wx.EXPAND, proportion=1)

sizer.Add(wx.StaticText(self, label
='测试'))
sizer.Add(wx.TextCtrl(self), flag
=wx.EXPAND)

sizer.Add(wx.StaticText(self, label
='测试其他'))
sizer.Add(wx.TextCtrl(self), flag
=wx.EXPAND)#添加一行,使其占用两列 sizer.AddSpacer(0) #占位符,保持布局 sizer.Add(wx.TextCtrl(self, value='测试占用两列'), flag=wx.EXPAND, proportion=1)

self.sampleList
= ['friends', 'advertising', 'web search', 'Yellow Pages']
self.edithear
= wx.ComboBox(self, size=(95, -1), style=wx.CB_DROPDOWN)
self.Bind(wx.EVT_COMBOBOX, self.EvtComboBox, self.edithear)
self.edithear.AppendItems(self.sampleList)

sizer.Add(wx.StaticText(self, label
='下拉列表', size=(100, -1)))
sizer.Add(self.edithear, flag
=wx.EXPAND)

sizer.AddGrowableRow(
1)
sizer.AddGrowableCol(
1)

main_sizer.Add(sizer, flag
=wx.EXPAND|wx.ALL, proportion=1, border=10)
self.SetSizer(main_sizer)

okBtn
= wx.Button(self, wx.ID_ANY, "确定")
cancelBtn
= wx.Button(self, wx.ID_ANY, '取消')
btnSizer
=wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add(okBtn, 0, wx.ALL,
5)
btnSizer.Add(cancelBtn, 0, wx.ALL,
5)
main_sizer.Add(btnSizer, flag
=wx.ALIGN_RIGHT|wx.ALL, border=10)

前面我们提到了,该布局控件的
所有控件都放置在独立的单元格中,不能跨越多个单元格

如果您需要实现控件跨越多行或多列的布局,应该使用
wx.GridBagSizer

wx.GridBagSizer
是一个更灵活的布局管理器,允许控件在网格中跨越多个单元格。

3)GridBagSizer的界面及代码

这是一个简单的案例,主要来介绍
GridBagSizer 的缩放效果及其跨行的实现的。

它的代码如下所示

classMyFrame(wx.Frame):def __init__(self):
super().
__init__(None, title="GridBagSizer 示例")#创建一个 BoxSizer 作为外层 sizer outer_sizer =wx.BoxSizer(wx.VERTICAL)#创建一个 GridBagSizer grid_sizer = wx.GridBagSizer(5, 5) #行间距和列间距为 5 #添加控件到 sizer grid_sizer.Add(wx.StaticText(self, label="姓名"), pos=(0, 0), flag=wx.ALIGN_CENTER_VERTICAL, border=10)
grid_sizer.Add(wx.TextCtrl(self, size
= (100, -1)), pos=(0, 1), flag=wx.EXPAND|wx.ALL, border=10)#添加一个控件,占用 1 行 2 列 grid_sizer.Add(wx.StaticText(self, label="占用两列的控件,测试内容很长很长很长很长"), pos=(1, 0), span=(1, 2), flag=wx.EXPAND)#添加更多控件 grid_sizer.Add(wx.StaticText(self, label="介绍内容"), pos=(2, 0), flag=wx.ALIGN_CENTER_VERTICAL, border=10)
grid_sizer.Add(wx.TextCtrl(self, style
=wx.TE_MULTILINE), pos=(2, 1), flag=wx.EXPAND|wx.ALL, border=10)#让控件跟随窗口拉伸 grid_sizer.AddGrowableCol(1) #允许第二列拉伸 grid_sizer.AddGrowableRow(2) #允许第三行拉伸 #将 grid_sizer 添加到 outer_sizer,并设置顶部边距 outer_sizer.Add(grid_sizer, flag=wx.EXPAND | wx.ALL, border=10) #设置顶部、底部、左侧和右侧边距 #设置外层 sizer self.SetSizer(outer_sizer)
self.SetSize((
400, 300)) #设置初始大小 outer_sizer.Fit(self)#调整窗口大小以适应控件 self.Layout()

注意,我们如果要使得控件能够拉伸,通过设置指定布局的行或者列可以拉伸即可,如上面代码介绍。

        #让控件跟随窗口拉伸
        grid_sizer.AddGrowableCol(1)  #允许第二列拉伸
        grid_sizer.AddGrowableRow(2)  #允许第三行拉伸

注意上面的代码中的位置,以及跨行的设置代码

pos=(1, 0), span=(1, 2)


wx.GridBagSizer
中,
pos

span
参数用于控制控件在网格中的位置和跨越的行列数。

1. pos 参数

  • 描述
    :
    pos
    用于指定控件在网格中的位置。它是一个二元组,格式为
    (行索引, 列索引)
  • 示例
    :
    pos=(1, 0)
    表示将控件放置在第 2 行(索引从 0 开始)第 1 列。

2. span 参数

  • 描述
    :
    span
    用于指定控件跨越的行数和列数。它也是一个二元组,格式为
    (行数, 列数)
  • 示例
    :
    span=(1, 2)
    表示该控件在垂直方向上跨越 1 行,在水平方向上跨越 2 列。

结合使用:
当将 pos span 一起使用时,可以创建复杂的布局。例如:

sizer.Add(wx.StaticText(self, label="占用两列的控件"), pos=(1, 0), span=(1, 2), flag=wx.EXPAND)

在这个示例中,控件的位置是
(1, 0)
,意味着它放置在第二行第一列;而
span=(1, 2)
意味着它占用整整 1 行和 2 列的空间。这使得该控件可以在横向上扩展到第二列,形成一个跨越的效果。

wx.GridBagSizer
是 wxPython 中一个非常灵活和强大的布局管理器,适用于需要复杂布局的用户界面。对于动态添加或删除控件的情况,
GridBagSizer
能够很好地处理控件的重新排列和调整。

复杂布局的简化

  • 组合使用
    :
    GridBagSizer
    可以与其他 Sizer(如
    BoxSizer

    FlexGridSizer
    )组合使用,以满足更复杂的布局需求。
  • 层次结构
    : 可以在
    GridBagSizer
    中嵌套其他 Sizer,从而实现多层次的布局管理。

wx.GridBagSizer
提供了强大的功能和灵活性,使得开发者可以创建复杂和响应式的用户界面。通过合理使用
pos

span
、边距设置及可扩展性选项,可以有效提升应用程序的用户体验。

如下面界面效果,就是基于该布局创建的。

导航

  • 前言
  • 火线告警,CPU飚了
  • 服务重启,迅速救火
  • 黑盒:无尽的猜测和不安
  • Arthas:锋利的Java诊断工具
  • 在线追踪Cpu占比高的代码段
  • 代码重构,星夜上线,稳了
  • 结语
  • 参考

肮脏的代码必须重构,但漂亮的代码也需要很多重构。

前言

有些代码在当初编写的时候是非常稳健的,但是随着数据量的不断增加,有些代码的“性能瓶颈”逐渐暴露出来。

这就可能会导致一些不可预知的线上事故。

那么,如何快速定位问题和处置问题就变得极其重要。

火线告警,CPU飚了

运维三板斧,重启、重装、重新买!

在多年的职业历练中,我养成了一个习惯——随时关注群里用户的反馈。

在一个阳光很好的午后,我和同事们正在加班加点的赶一个版本。

突然,群里有人反馈,线上的一个功能出现了问题,需要紧急处理。



随即便是更多的业务对接群开始炸锅。

上个月因为数据库性能问题,已经出现了几次线上宕机的情况,被用户吐槽。
为此,我们做了大量的优化工作:

  • 慢sql优化
  • 去高频接口
  • 数据冷热分离
  • ...

今天再次遇到这样的问题,我们惊讶了几秒,然后很快恢复了镇定。

服务重启,迅速救火

我和业务团队的同事一边安抚用户的情绪,一边查看报警日志。

紧急着查看了报警日志,发现部署该业务接口的两台ecs CPU飙高了...



再看数据库的CPU使用率并未报警。

当机立断,先重启一下服务。(PS:不要慌,不要慌,不要慌!)

大约两分钟之后,我们验证了可用性,并查看ecs和数据库各项指标,正常。

于是大家一一回复了用户群,对接群终于安静了。

黑盒:无尽的猜测和不安

路漫漫其修远兮,吾将上下而求索。

在这个时候,我已经开始了我的思考——是哪个功能或者哪句代码引发了ecs cpu标高呢?

过去,我们的思路总是先去查看网关日志,从时间点上排查可能导致性能问题的接口,然后逐渐深入。

然而,这个项目已经迭代3年多了,接口繁多,想快速定位无疑是大海捞针。

所以,对于这种黑盒般的问题,因为缺乏诊断工具,往往让我们陷入无尽的猜测和不安中。

是否有这样的工具帮助我快速定位到问题的代码呢?

Arthas:锋利的Java诊断工具

在这次的问题诊断中,我使用了Arthas来进行线上问题的诊断。

Arthas
(阿尔萨斯)(是
Alibaba
开源的Java诊断工具,深受开发者喜爱。
在线排查问题,无需重启、动态跟踪Java代码、实时监控 JVM 状态。

Arthas
支持JDK 6+,支持
Linux/Mac/Windows
,采用命令行交互模式,同时提供丰富的
Tab
自动补全功能,进一步方便进行问题的定位和诊断。

当你遇到以下类似问题而束手无策时,Arthas 可以帮助你解决:

  • 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  • 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  • 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  • 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  • 是否有一个全局视角来查看系统的运行状况?
  • 有什么办法可以监控到 JVM 的实时运行状态?
  • 怎么快速定位应用的热点,生成火焰图?

官方教程

使用arthas-boot(推荐)

下载arthas-boot.jar,然后用java -jar的方式启动:

  • 执行该程序的用户需要和目标进程具有相同的权限。比如以admin用户来执行:sudo su admin && java -jar arthas-boot.jar 或 sudo -u admin -EH java -jar arthas-boot.jar。
  • 如果 attach 不上目标进程,可以查看~/logs/arthas/ 目录下的日志。
  • 如果下载速度比较慢,可以使用 aliyun 的镜像:java -jar arthas-boot.jar --repo-mirror aliyun --use-http
  • java -jar arthas-boot.jar -h 打印更多参数信息。
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar





选择应用java进程:

blog-webapp-0.0.1-SNAPSHOT.jar
进程是第1个,则输入1,再输入回车/enter。Arthas 会 attach 到目标进程上,并输出日志:

[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 27575 blog-webapp-0.0.1-SNAPSHOT.jar
1
[INFO] local lastest version: 3.7.2, remote lastest version: 4.0.2, try to download from remote.
[INFO] Start download arthas from remote server: https://arthas.aliyun.com/download/4.0.2?mirror=aliyun
[INFO] Download arthas success.
[INFO] arthas home: /root/.arthas/lib/4.0.2/arthas
[INFO] Try to attach process 27575
[INFO] Attach process 27575 success.
[INFO] arthas-client connect 127.0.0.1 3658
  ,---.  ,------. ,--------.,--.  ,--.  ,---.   ,---.
 /  O  \ |  .--. ''--.  .--'|  '--'  | /  O  \ '   .-'
|  .-.  ||  '--'.'   |  |   |  .--.  ||  .-.  |`.  `-.
|  | |  ||  |\  \    |  |   |  |  |  ||  | |  |.-'    |
`--' `--'`--' '--'   `--'   `--'  `--'`--' `--'`-----'

wiki       https://arthas.aliyun.com/doc
tutorials  https://arthas.aliyun.com/doc/arthas-tutorials.html
version    4.0.2
main_class
pid        27575
time       2024-11-02 22:28:37.037

在线追踪CPU占比高的代码段

从官方文档可以看到Arthas可以帮助定位到cpu飙高的代码段。

具体如何操作呢?

可以关注一下这个命令:
thread

展示当前最忙的前 N 个线程并打印堆栈(
https://arthas.aliyun.com/doc/thread.html
)

$ thread -n 3
"C1 CompilerThread0" [Internal] cpuUsage=1.63% deltaTime=3ms time=1170ms

"arthas-command-execute" Id=23 cpuUsage=0.11% deltaTime=0ms time=401ms RUNNABLE
    at java.management@11.0.7/sun.management.ThreadImpl.dumpThreads0(Native Method)
    at java.management@11.0.7/sun.management.ThreadImpl.getThreadInfo(ThreadImpl.java:466)
    at com.taobao.arthas.core.command.monitor200.ThreadCommand.processTopBusyThreads(ThreadCommand.java:199)
    at com.taobao.arthas.core.command.monitor200.ThreadCommand.process(ThreadCommand.java:122)
    at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl.process(AnnotatedCommandImpl.java:82)
    at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl.access$100(AnnotatedCommandImpl.java:18)
    at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl$ProcessHandler.handle(AnnotatedCommandImpl.java:111)
    at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl$ProcessHandler.handle(AnnotatedCommandImpl.java:108)
    at com.taobao.arthas.core.shell.system.impl.ProcessImpl$CommandProcessTask.run(ProcessImpl.java:385)
    at java.base@11.0.7/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
    at java.base@11.0.7/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base@11.0.7/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
    at java.base@11.0.7/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base@11.0.7/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base@11.0.7/java.lang.Thread.run(Thread.java:834)


"VM Periodic Task Thread" [Internal] cpuUsage=0.07% deltaTime=0ms time=584ms

上面展示了cpu最高的三个线程。

通过这种方式我们就可以定位到到cpu飙高的代码段。(这里是示例,具体项目案例这里就不粘贴了~)

代码重构,星夜上线,稳了

通过这个工具相对比较精准的定到了导致cpu飙高的代码片段。

进一步进入代码发现,是因为这里有一个接口,包含了一个分页查询,在返回数据的时候,需要对数据进行了包装。
这里的代码逻辑如下:
遍历循环,查询数据库,然后计算了一个数据赋值给某个扩展字段。

如果是普通接口,数据量不大,也不会有什么问题。

但是,这里是IM群里会话接口,在某一个瞬间(比如,大量用户同时登录软件),拉去IM群里的会话列表,所以这里的代码逻辑就会导致cpu飙高。

Note: 本项目类似企业微信的IM群聊,但是没有使用本地数据库,聊天数据从接口实时拉取。

于是,快速重构了这段代码,星夜上线。

至此,该问题就解决了。

结语

哪有什么岁月静好,总有人在看不到地方为你负重前行。

所谓的"技术好",不是单纯的卖弄技术,而是能够针对灵活多变的场景,恰到好处的运用技术。

活到老,学到老。

在这个过程中,我们要保持对技术的敬畏,不断学习,不断进步。

善于使用工具来解决问题,让我们的生活更加美好。

这里笔者只根据个人多年的工作经验,一点点思考和分享,抛砖引玉,欢迎大家怕批评和斧正。

参考



探头与变送器

前面的文章已经实现了ModBus客户端与服务端和他们之间的通信。但只是软件不够,毕竟传感器是硬件。
经过我的了解,一个完整的实现了Modbus协议的,并且通过RS485电缆与电脑交换
ModBusRTU
报文的Modbus设备,一般由两个部分组成。

  • 探头
    探头就是将物理量转化为电信号的东西。比如一个测温电阻,温度变化时电阻会改变。这个
    电阻/电压
    的变化情况可以测得。

  • 变送器
    变送器根据探头传过来的电信号,进行转换和存储,并且给响应电脑
    ModBusRTU
    报文。所以变送器左边通过
    火线与零线
    连接探头,右边通过
    RS485电缆
    连接电脑。内部有一个微型处理器,负责处理探头电信号,查表或者通过曲线得到物理量的值,存储在自身的寄存器中,然后响应报文。

购买设备

RS485转USB转换器

由于我的电脑只有USB接口,没有RS485接口,所以我需要买一个转换器。该设备大概30元,右边是RS485接口,中间是一个芯片,左边是一个USB接口,可以直插电脑。比较令我意外的是,USB接口的访问也是通过串口进行的。

image

TH10S-B_RS485通讯型温湿度变送器

该设备40元,是一个探头、变送器一体化集成的设备。上面的金属片是温湿度探头,具体的物理原理我没有了解。紧连着金属片后边导线扁平部分就是变送器。服务端程序就在那里。

image

探头大概几块钱,但变送器贵一点。因为探头的物理特性不同,肯定要和特定型号的变送器适配,才能保证物理量与电信号量的值一一对应,要么就要变送器可以配置这种对应关系。一体化的设备减去了这种麻烦,只需要直接接到转换器上即可。

  • 使用说明书
    image

设备连接

其中
绿色

黄色
线是双工的485信号线,用来传输ModbusRTU数据包。

image

  • 驱动安装
    这个地址是店铺客服发送的。USB转485转换器需要安装驱动才能使用,插上后电脑设备管理器中增加了一个串口
    驱动地址
    image

image

测试

把转换器插上电脑后,打开客户端软件,多了一个
COM7
串口。

image

这个变送器和客户端里面还支持设置设备地址。比如我们买了好几个这个设备时,每个设备的初试地址都是默认的
1
,因此当我们把这些设备接在一条485总线上时,就无法区分不同设备了。所以就需要我们到一个设备一个设备的连接电脑,设置不同的地址,之后再统一连到总线上,实现多个设备的访问。
我询问了我们部门的嵌入式工程师,他说公司做的还不能动态设置地址,采取的是重新编译程序再刷到设备上。

然后开始测试设备。点击连续读取后,我再用双手把传感器捂住,温度和湿度都上升很快很快。

image

没有读取在读取数据时,转换器上面的指示灯熄灭。每读取一次,闪烁一次。点击客户端软件的连续读取时,指示灯会连续闪烁。

image

监控主机

实际上还存在一种叫监控主机的东西,可以上面有一到两个网口、多个485串口,以此实现电脑远程通过光纤网络,经过监控主机,访问现场ModBus设备。在井工煤矿中这个设备大量使用。但是对于这个设备,我还缺乏了解。

本章目标

  • 使用Blazor WebAssembly实现管理“贴纸”页面
  • 集成认证与授权机制

如果你对Blazor WebAssembly的使用不感兴趣,可以跳过本章的阅读。你也可以使用自己熟悉的前端技术完成案例的界面部分,之前我们开发的后端API比较简单,所以自己实现一套前端界面并不会是一个困难的事情。

完成本章内容后,我们会得到下面的效果(点击查看大图),是不是跟第一章中所画的概念图已经很接近了?

我们到哪里了?

在进一步介绍后续内容之前,先看看目前实现了哪些内容。回顾之前的一张架构简图(其实也不算是架构图),彩色部分是目前我们已经实现的部分,虽然目前有些地方还并不完善,只是在开发环境能够正常运行起来,并且我们开发的RESTful API都还没有容器化。

本章会完成“Sticker前端应用”这个部分,在完成这部分内容后,我们就可以在开发环境中调试运行整个应用程序了,由于还没有引入基于nginx的API网关,所以,整个系统的结构跟上图相比还是会有些差异。

Blazor WebAssembly是什么?

如果问ChatGPT,它的回答是这样的:Blazor WebAssembly是一个基于WebAssembly的现代Web应用程序框架,由微软开发。它允许开发人员使用C#和.NET技术构建客户端Web应用程序,而无需使用JavaScript。Blazor WebAssembly利用WebAssembly的性能优势,将C#代码编译为WebAssembly字节码,从而在浏览器中运行高性能的客户端应用程序。开发人员可以使用Blazor组件模型构建交互式和动态的用户界面,同时利用.NET的强大功能和生态系统。Blazor WebAssembly还支持与服务器端Blazor应用程序的通信,以及与现有JavaScript库的集成,为开发人员提供了灵活和强大的工具来构建现代的Web应用程序。

Blazor应用程序基本上可以分为两种:

  • Blazor服务端应用:它基于ASP.NET Core基础设施实现服务端Hosting,并通过一种通信方式(比如SignalR)实现用户交互
  • Blazor WebAssembly:它是在客户端浏览器中运行的Web应用程序,它将C#代码编译为WebAssembly字节码,直接在浏览器中执行。Blazor WebAssembly应用程序完全在客户端执行,可以实现更快的加载速度和更高的性能,适用于需要在客户端独立运行的应用程序,以及对实时性要求较高的应用

从.NET 8开始,Visual Studio引入新的Blazor应用程序构建模板:Blazor Web App,它整合了Blazor服务端和Blazor WebAssembly的优势,并且利用了.NET 8中新引入的Blazor相关功能,比如静态服务端渲染(static SSR)、流式渲染(Streaming Rendering)等。原有的Blazor Server App和Blazor WebAssembly Standalone App在.NET 8 中仍然支持,只不过可以考虑将这些类型的应用迁移到Blazor Web App上。详见:
https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-7.0#new-blazor-web-app-template

本系列文章案例代码选用Blazor WebAssembly项目模板作为基础进行开发。

为什么选择Blazor WebAssembly?

现在前端技术非常成熟,体系也很庞大,为何抛弃React、Angular、Vue这些前端框架不选,偏偏选择Blazor WebAssembly呢?我想大概几个方面吧:

  • 我想尝试一下微软原生WebAssembly的开发框架,看看开发者体验如何
  • 我打算仍然选择微软技术来展示案例(之前有读者对我使用Java系的Keycloak有质疑,其实Keycloak在整个案例架构中只是一个IdP,它跟PostgreSQL、Redis这些组件等价,这么说能理解吧?)
  • 我对C#技术栈更为熟悉,功能开发和问题调查都会更加方便快捷,而且不容易出错。在微服务的开发模式中,技术选择其实跟团队成员的偏好也有一定的关系,在能够满足各种功能性和非功能性需求的前提下,团队当然希望采用更为熟知的技术来完成研发。聊到我的前端技术,我个人对Angular比较熟悉,因为之前做过Angular的前端项目,React和Vue一直都没有机会实践(或许我也不应该再“卷”下去了)

除了微软的Microsoft Learn和在线教育平台Edink之外,还是有
不少站点是基于Blazor技术构建
的,微软官方也给了
几个客户案例
,它们大多数都是US的公司,国内很少使用。

从上面三点可以看到,我在这个案例中选择Blazor WebAssembly,主观因素更多一些,在实际项目中,大概率大家也不会选择Blazor WebAssembly来构建自己的前端应用,原因也会是多方面的。由于本系列文章所介绍的案例比较简单,前端部分暂时也不会有特别高的要求,所以我就基于自己的主观需求,选择了Blazor WebAssembly。读者完全可以基于本案例的服务端代码,使用自己熟悉的前端技术来重制“贴纸墙”的前端部分。

构建Stickers.Web应用

首先就是创建一个Blazor WebAssembly的应用,并启用认证机制,因为后面需要集成认证和授权流程。此外,我还在项目中使用了
Blazor Bootstrap
组件库,这个组件库对主要的Bootstrap组件进行了封装,并让其在Blazor应用中完美运行。使用Blazor Bootstrap需要有一些配置工作,这里不多介绍了,官方文档有
Get Started
操作流程。

Blazor WebAssembly的开发过程这里也不多做介绍了,请直接参考本文的源代码。这里主要介绍三个话题:自定义组件、使用HttpClient访问后端服务,以及认证与授权。

自定义组件

通常我们会把一些能够重复使用的前端代码封装成一个组件,并通过参数来接受数据并定制业务逻辑,执行过程中又通过事件与其它组件交互。比如,一个分页功能就可以封装成一个组件,它可以通过参数来设置分页按钮的样式以及一次展现多少个分页按钮,当用户点击某个页码时,它又以事件的方式通知相关的其它组件(比如父页面)被点击的页码数,以便触发页面更新等后续操作。

下面的代码是案例中的“编辑贴纸”的组件,这个组件有一个参数:
StickerEditModel
,用来指定用户操作行为类型(新建/编辑)以及将要新建/被编辑贴纸的数据模型,此外还包含两个事件:
OnCloseClickCallback

OnSaveClickCallback
,当组件界面上的“关闭”和“保存”按钮被点击时,会触发这两个事件。
StickerEditModel
的定义如下:

public enum EditMode
{
    Create,
    Edit
}

public class StickerEditModel
{
    public string? Content { get; set; }
    public int Id { get; set; }
    public string? Title { get; set; }
    public EditMode EditMode { get; set; }
}

StickerEditModel
看起来跟
Sticker
业务对象很像,但它只关注界面上所需的数据,所以,在
StickerEditModel
中,并没有
CreatedOn

ModifiedOn
这些属性,因为这些属性都是在创建或者修改贴纸时由系统自动生成的,新建/编辑贴纸的界面上并不需要这些信息。以下是“编辑贴纸”的组件
EditStickerComponent
的代码:

@using Stickers.Web.ViewModels

@if (Model is not null)
{
    <div class="mb-3">
        <input @ref="_txtTitleRef" type="text" class="form-control" placeholder="请输入贴纸标题" @bind-value="Model.Title">
    </div>
    <div class="mb-3">
        <InputTextArea class="form-control" placeholder="请输入贴纸内容" @bind-Value="Model.Content"/>
    </div>

    <div class="d-grid gap-2 d-md-flex justify-content-md-end mt-2">
        <Button Color="ButtonColor.Secondary" @onclick="OnCloseClickCallback"> 取消 </Button>
        <Button Color="ButtonColor.Primary" @onclick="OnSaveClick"> 保存 </Button>
    </div>
}

@code {
    [Parameter]
    public StickerEditModel? Model { get; set; }

    [Parameter]
    public EventCallback<MouseEventArgs> OnCloseClickCallback { get; set; }

    [Parameter]
    public EventCallback<StickerEditModel> OnSaveClickCallback { get; set; }

    private ElementReference? _txtTitleRef;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (_txtTitleRef.HasValue)
        {
            await _txtTitleRef.Value.FocusAsync(true);
        }
    }

    private async Task OnSaveClick()
    {
        await InvokeAsync(() => OnSaveClickCallback.InvokeAsync(Model));
    }
}

它提供了两个文本框,分别让用户输入贴纸的标题和内容,还有两个按钮,让用户保存所做的修改或者取消所做的操作。调用组件会生成一个StickerEditModel的实例,通过Model参数传入这个组件,然后以对话框的形式显示该组件以接收用户输入,当用户完成操作并点击保存或者取消按钮时,通过事件将用户的输入返回给调用方。

使用HttpClient访问后端服务

在Blazor WebAssembly中访问后端服务是非常方便的,只需在
Program.cs
中加入HttpClient的支持,比如:

builder.Services.AddHttpClient(
    "myHttpClient", 
    client => client.BaseAddress = new Uri("http://localhost:5000")
);

然后,在Razor页面或者组件中,通过注入
HttpClientFactory
,就可以使用注册的HttpClient了:

@inject IHttpClientFactory HttpClientFactory

@code {
    private override async Task OnInitializedAsync()
    {
        // ...
        using var httpClient = HttpClientFactory.GetClient("myHttpClient");
        var responseMessage = await httpClient.GetAsync("api/any-api");
        // ...
    }
}

HttpClient在Blazor中的使用,跟ASP.NET Core中非常类似,可以直接阅读
官方文档
来了解详细内容,这里就不多做介绍了。

认证与授权


Stickers.Web
项目中需要调用后端的
Stickers.WebApi
RESTful API来实现其功能,而后端API是需要认证和授权的,所以,前端界面在HttpClient发送API调用请求时,就需要把access token带上,否则API调用是不会成功的。在Blazor WebAssembly中,要实现这个逻辑,就需要自定义
DelegatingHandler
,然后在HttpClient中使用这个自定义的Handler。

Blazor WebAssembly支持一种称之为
AuthorizationMessageHandler

DelegatingHandler
,它可以直接拿来使用,以便将access token附加到发出的HTTP请求上。只需要在添加HttpClient的时候,指定HttpMessageHandler即可:

builder.Services.AddHttpClient(
    "myHttpClient", 
    client => client.BaseAddress = new Uri("http://localhost:5000")
).AddHttpMessageHandler<AuthorizationMessageHandler>();

认证用户可以登录站点,并不表示该用户可以访问所有的页面并进行所有的操作,比如前文中所创建的nobody用户,它只能被认证,却没有任何授权,所以,对于该用户而言,它是无法使用“贴纸”功能的。在这个用户登录之后,即便登录没有问题,使用该用户的access token去访问后端API服务仍然会得到
403 Forbidden
的错误,比如,在这个用户点击“我的贴纸墙”页面时,下面的代码就会抛出未授权异常:

@code {
    protected override async Task OnInitializedAsync()
    {
        CurrentPage = await ReadStickersAsync();
        // 此处由于异常未被处理,造成页面出错
        await base.OnInitializedAsync();
    }

    private async Task<StickersPage?> ReadStickersAsync(
        int pageNumber = 1,
        int pageSize = DefaultPageSize)
    {
        using var httpClient = HttpClientFactory.CreateClient("stickersHttpClient");
        var httpResponseMessage = await httpClient
            .GetAsync($"api/stickers?page={pageNumber}&size={pageSize}");
        httpResponseMessage.EnsureSuccessStatusCode(); // 此处抛出异常
        var responseJson = await httpResponseMessage.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<StickersPage>(responseJson);
    }
}

解决这个问题的思路有两种:

  1. 由于WebAssembly是可以得到用户的access token的,所以也可以像之前Stickers API里设计的那样,获得用户的授权信息,然后根据用户的授权信息来设计前端的授权机制(Blazor WebAssembly默认基于角色授权,也可以自己开发自定义的Policy来实现更为灵活的授权方案),再根据这套机制和用户本身的授权信息以判定某个组件是否应该显示、是否可以被该用户使用
  2. 简单粗暴,在调用API时,如果异常,则捕获异常并直接跳转到登录界面或者错误界面,提示用户没有权限

第一种方案其实更为合理,一方面如果用户本来就没有权限,那就可以直接把不可以访问的组件隐藏掉或者禁用,没必要等到用户点击的时候才报错;另一方面,设计一个前端授权机制也会使得组件和页面的访问控制变得更为灵活,如果设计合理,还可以跟Blazor WebAssembly的授权机制无缝整合,大大减少出错的几率。而第二种方案则相对简单一些,适用于像本文这样的demo场景(Blazor应用的授权设计不是本案例的重点)。

首先可以自定义一个
AuthorizationMessageHandler
,然后通过
AddHttpMessageHandler
方法,将这个Handler注册到HttpClient上:

public class StickersMessageHandler : AuthorizationMessageHandler
{
    private readonly NavigationManager _navigationManager;
    
    public StickersMessageHandler(IAccessTokenProvider provider, NavigationManager navigation) : base(provider,
        navigation)
    {
        _navigationManager = navigation;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken)
    {
        try
        {
            var responseMessage = await base.SendAsync(request, cancellationToken);
            if (responseMessage.StatusCode == HttpStatusCode.Forbidden)
            {
                _navigationManager.NavigateTo("/forbidden");
            }
            return responseMessage;
        }
        catch (AccessTokenNotAvailableException ex)
        {
            ex.Redirect();
            return new HttpResponseMessage();
        }
    }
}

这个类首先注入一个
NavigationManager
实例,然后在重载的
SendAsync
方法中,判断返回的状态码是否为
403 Forbidden
,如果是的话,就直接跳转到/forbidden页面就可以了。这里的代码虽然对状态码进行了判断,但是在调用端的
EnsureSuccessStatusCode
方法仍然会因为状态码不是2XX而抛出异常。这里只要稍微处理一下就可以了:

protected override async Task OnInitializedAsync()
{
    try
    {
        CurrentPage = await ReadStickersAsync();
    }
    catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Forbidden)
    {
        return;
    }
    
    await base.OnInitializedAsync();
}

总结

本文简单介绍了基于Blazor WebAssembly实现前端的几个主要方面,前端代码很多,一篇文章也无法全部介绍完整,有兴趣的读者请直接下载源码阅读。在运行本案例的过程中,你会发现,登录用户之前还能互相看到对方所创建的贴纸,这是一个bug,在下一讲中,我将通过引入多租户的初步设计,将这个bug修复掉。

源代码

本章源代码在chapter_5这个分支中:
https://gitee.com/daxnet/stickers/tree/chapter_5/

下载源代码前,请先删除已有的
stickers-pgsql:dev

stickers-keycloak:dev
两个容器镜像,并删除
docker_stickers_postgres_data
数据卷。

下载源代码后,进入docker目录,然后编译并启动容器:

$ docker compose -f docker-compose.dev.yaml build
$ docker compose -f docker-compose.dev.yaml up

现在就可以直接用Visual Studio 2022或者JetBrains Rider打开stickers.sln解决方案文件,然后同时启动Stickers.WebApi和Stickers.Web两个项目进行调试运行了。