2024年6月

一、写在开头

在这篇文章中记录一下之前自己面试时学到的东西,是关于transient关键字的,当时面试官问我IO的相关问题,基本上全答出来了,关于如何不序列化对象中某个字段时,我果断的选择了static和transient,但面试官紧接着问了我:“transient关键字修饰的变量当真不可序列化吗?”,这个问题直接给我整不确定了,因为以当时的知识储备,这个问题确实不知道,最终虚心的向这位面试官请教,他告诉了我答案。

虽然那场面试我还是通过了,但是我没去,哈哈!不过还是挺感谢那个耐心的面试官的,随口的一个问题,其实大部分面试官是不会负责给你解答的。

二、案例测试

今天,我们就花一点时间,来把这个问题梳理一遍。我们先写一个测试类,去看一下static和transient关键字修饰的字段,在序列化过程中的表现:

public class Test {
    public static void main(String[] args) throws IOException {
        //初始化对象信息
        Person person = new Person();
        person.setName("JavaBuild");
        person.setAge(30);
        System.out.println(person.getName()+" "+person.getAge());

        //序列化过程
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:\\person.txt"));) {
            objectOutputStream.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }
        person.par1 = "序列化后静态字段";
        //反序列化过程
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:\\person.txt"));) {
            Person p = (Person) objectInputStream.readObject();
            System.out.println(p);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}
class Person implements Serializable{

    private static final long serialVersionUID = 8711922740433840551L;
    private String name;
    private int age;

    public static String par1 = "静态字段";
    transient String par2 = "临时字段";
    transient int high = 175;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", par1=" + par1 +
                ", high=" + high +
                ", par2='" + par2 + '\'' +
                '}';
    }
}

输出:

JavaBuild 30
Person{name='JavaBuild', age=30, par1=序列化后静态字段, high=0, par2='null'}

通过打印结果我们可以看到,static修饰的字段,并没有参与序列化,读取到了后面修改的值;transient关键字修饰的字段也没参与,而且在反序列化过程中,会被重置为默认值,例如基本数据类型为 0,引用类型为 null。至于原因我们在这里不展开了,上一篇文章里已经提到,大家可以去看看。

三、直入主题

我们再回过头来看看起初的问题:
transient 修饰的字段真的不能被序列化?
至少通过Serializable接口标示的序列化方式里,transient字段时不可被序列化的,因为在序列化过程中调用的ObjectStreamClass对象,里面有个方法为getDefaultSerialFields(),已经明确的标记出了transient修饰符不可被序列化!

image

那我们怎么办呢?

Externalizable接口:

其实呀,除了 Serializable 之外,Java 还提供了一个序列化接口 Externalizable,它是Serializable的子接口,使用 Externalizable 进行反序列化的时候,会调用被序列化类的无参构造方法去创建一个新的对象,然后再将被保存对象的字段值复制过去;实现Externalizable接口时,必须重写其中的writeExternal() 和 readExternal()方法,我们通过这两个方法进行序列化的设计与读取。

image

适应场景:
因为Externalizable接口拥有着更高的序列化控制能力,所以在序列化过程中,我们需要对一些敏感信息进行加密处理时,它的作用就会体现啦。

我们使用这个接口进行序列化尝试,并且使用transient关键字修饰字段,看一下结果:

public class Test implements Externalizable {

    private transient String text = "我可以被序列化!!!";

    public static void main(String[] args) throws Exception {
        Test test = new Test();
        //序列化
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("E:\\transient.txt"));
        out.writeObject(test);
        //反序列化
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("E:\\transient.txt"));
        test = (Test)in.readObject();
        System.out.println(test.text);
        //关闭流
        out.close();
        in.close();
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(text);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        text = (String) in.readObject();
    }
}

输出:

我可以被序列化!!!

数据成功被序列化到txt文件中,并成功的反序列化读取到程序中了!即便text被transient修饰着!

四、总结

通过上面的学习,我们知道了在Java的序列化中有 Serializable、Externalizable这两个接口,前者没有任何方法,只是一个标识,而后者作为子类,提供了必须重写的方法,用以自定义序列化设计。此外,transient 关键字只能修饰字段,而不能修饰方法和类,需要注意。

前言

继上一篇
Android 调用 Termux 执行命令
,执行命令的问题基本解决,但是
bash

awk

clangd
这类命令可以从标准输入读取信息并维持运行,Termux 第三方调用缺乏有效支持。而
RunCommandService
可以允许命令后台运行,然后我们以某种方式取得该后台程序的标准输入/输出,便可以实现前后端的持续通信。


一、Android 进程间通信(IPC)

进程间通信(In-Process Communication, IPC)主要实现多进程间的数据通信问题。比如一个 UI 程序后台调用一个 CLI 程序执行某功能,每个程序会启动一个进程,UI 进程给 CLI 进程发送输入数据,CLI 进程接收后返回处理结果给 UI 进程。

Android 实现跨进程通信有多种方式,其各有优缺点:

文件
:直接在设备上创建文件实现数据通信。原理简单,操作方便,但是效率低。

Binder
:结构上 Binder 是一个虚拟的设备驱动(/dev/binder),连接 Service 进程、Client 进程和 Service Manager 进程。其数据只在内核空间与用户空间复制一次,效率较高,但是限制 1M 数据。另外,基于 Binder 的方法有 AIDL、ContentProvider、Messenger 等。

SharedMemory
:共享内存在 Android SDK 27 引入,允许开辟一块共享内存空间用于进程间的数据交互。SharedMemory 配合 AIDL/Binder 使用,可以破除 1M 的限制,传输大文件。但是注意直接对内存进行操作,使用完毕需要手动销毁。

Unix Domain Socket
:又叫 Local Domain Socket,本地套字节是 Linux 内核提供的功能,数据经过内核,实现本地进程间通信。其效率高,但是 Android 9+ 限制用户 App 间使用 UDS 通信。

Socket
:套字节本质上是网络通信,采用 TCP 或 UDP 协议,主要用于网络通信。其中 TCP 协议较复杂,用于建立稳定的通信;UDP 则速度快而不安全。

受安卓不同应用之间的权限限制,支持进程间字节数据 IO 通信的方案较少,本次采用
Socket
实现。

二、Netcat 网络瑞士军刀

Netcat 是一个小巧强大的网络工具,用于网络监听测试等。Netcat 可建立网络通信,支持 TCP/UDP/Unix 协议。在 Termux 端使用 Netcat 建立套字节通信,并将 stdin/stdout 重定向到一个子进程,如此实现通信。

通过以下方式建立 TCP 通信。服务器端:

nc -l -s 127.0.0.1 -p 1234

客户端:

nc 127.0.0.1 1234

此时在客户端输入的内容可传至服务器端,而服务器端输入的内容可传回客户端,二者间实现通信。

另外,使用 Netcat 可以方便反弹 shell 程序。下面是服务器端命令:

nc -l -s 127.0.0.1 -p 1234 bash

用客户端登录到 127.0.0.1:1234,建立连接后,服务端会启动
bash
程序,并接收来自客户端的标准输入,将标准输出发送给客户端。

三、第三方 App 与 Termux 建立 TCP/Socket 通信

通过
RunCommandService
调用 Termux 执行
nc
命令反弹某个程序,然后通过
java.net.Socket
建立 Socket 连接,取得 Socket 的 IO 流,即可实现进程间通信。

调用 Termux。注意,Termux 可使用两个版本的 Netcat:安卓自带的
/system/bin/nc
和 Termux 仓库的
netcat-openbsd
。前者随 ToyBox 在 Android Marshmallow 被引入,支持反弹 shell,而后者不支持;后者支持抽象命名空间 UDS。所以我们使用
/system/bin/nc

intent.setClassName("com.termux", "com.termux.app.RunCommandService");
intent.setAction("com.termux.RUN_COMMAND");
intent.putExtra("com.termux.RUN_COMMAND_PATH", "/system/bin/nc");
intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"-l", "-s", "127.0.0.1", "-p", "1234", "bash"});
intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", true);
intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", "0");
startService(intent);

建立 Socket 连接:

Socket mSocket;
InputStream mInput;
OutputStream mOutput;
public void connect() {
  mSocket = new Socket();
  new Thread(){
    public run() {
      try {
        sk.connect(new InetSocketAddress("127.0.0.1", 1234));
        mInput = sk.getInputStream();
        // 写入命令/发出数据
        mOutput = sk.getOutputStream();
        mOutput.write("ls\n");
        mOutput.flush();
        // 读取结果
        Thread.sleep(200L);
        int l = mInput.avaliable();
        byte[] bs = new byte[l];
        mInput.read(bs);
        System.out.println(new String(bs));
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }.start();
}

四、应用:调用 LSP 语言服务器

语言服务器协议(Language Server Protocol, LSP)是微软推出的一个基于 JSONRPC 的数据协议,用于

Termux 的软件仓库里正好有
clangd

ccls
两个 C/C++ 的 LSP 服务器端。笔者测试
ccls
时遇到 BUG,故选用
clangd
测试。

安装
clangd

apt install clangd


nc
反弹
clangd

/system/bin/nc -l -s 127.0.0.1 -p 48455 clangd

Android 客户端建立 Socket IO 通信:

Socket sk = new Socket(new InetAddress("127.0.0.1", 48455));

注意,安卓中 Socket 的 IO 流不允许在 UI 主线程进行操作,需要另起线程,以免阻塞主线程引起卡顿。

读取线程:

new Thread() {
  public void run() {
    InputStream mIn = sk.getInputStream();
    final int L = 1024;
    byte[] buf = new byte[L];
    while (mIn.read(buf, 0, 16)!=-1) {
      if (new String(buf, 0, 16).equals("Content-Length: ")) {
        // read int c
        // skip \r\n\r\n
        // read c bytes
      }
    }
  }
}.start();

写入线程:

Thread td = new Thread() {
  public void run() {
    try {
      OutputStream mOut = sk.getOutputStream();
      byte[] s="{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"initialize\",\"params\":{}}".getBytes(StandardCharsets.UTF_8);
      mOut.write(("Content-Length: "+s.length+"\r\n\r\n").getBytes());
      mOut.write(s);
      mOut.flush();
    } catch (IOException ioe) {
      ioe.printStackTrace();
    }
  }
};
td.start();
td.join(); // 阻塞写入线程,避免同时写入

笔者的开源项目:
TermuC - github.com/RainbowC0


参见

  1. android共享内存(ShareMemory)的实现 - 简书
  2. 彻底弄懂netcat命令的使用 - CSDN
  3. What is toybox? - Landley
  4. 语言服务器协议概述 - Microsoft Learn


title: Nuxt3 的生命周期和钩子函数(三)
date: 2024/6/27
updated: 2024/6/27
author:
cmdragon

excerpt:
摘要:概述了Nuxt3的关键生命周期钩子用途,如page:finish用于页面加载后处理,page:transition:finish处理过渡效果完成,kit:compatibility扩展兼容性检查,ready标示应用启动就绪,close执行应用关闭清理,及restart控制应用重启流程,附带示例代码

categories:

  • 前端开发

tags:

  • Nuxt3
  • 生命周期
  • 钩子函数
  • 前端开发
  • 页面加载
  • 过渡动画
  • 兼容性检查


image

image

扫码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长

page:finish

参数:
pageComponent

环境:
客户端

描述:
page:finish
钩子是在Nuxt.js中客户端环境下,当页面组件完全加载并且所有的异步依赖(如API调用)都解析完成后触发的。这个钩子特别适用于在
Suspense
组件解析完成后执行一些操作,比如状态更新或日志记录。通过使用
export default defineNuxtPlugin((nuxtApp) => { ... })
的方式,可以在Nuxt插件中注册此钩子。

详细解释、用法和案例demo:

在Nuxt.js中,
page:finish
钩子允许你访问当前页面的组件实例(
pageComponent
),这样你就可以在页面完全加载后执行一些额外的逻辑。

用法:

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('page:finish', (pageComponent) => {
    // 在这里编写你的逻辑
  });
});

案例demo:

以下是一个使用
page:finish
钩子的示例,该示例在页面加载完成后打印页面组件的名称:

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('page:finish', (pageComponent) => {
    // 打印页面组件的名称
    console.log('Page component name:', pageComponent.$options.name);
    
    // 你可以在这里执行其他操作,比如:
    // - 更新全局状态
    // - 执行日志记录
    // - 初始化第三方库
  });
});

在这个示例中,当页面加载完成后,
page:finish
钩子被触发,并且我们通过
pageComponent
参数获取了当前页面的组件实例。然后,我们打印了组件的名称。这个信息可以用于调试或者执行一些基于组件名称的条件逻辑。

page:transition:finish

参数:
pageComponent?

环境:
客户端

描述:
page:transition:finish
钩子是在Nuxt.js中客户端环境下,当页面过渡动画结束(即
onAfterLeave
事件触发后)调用的。这个钩子可以用来执行那些依赖于页面过渡完成的操作,例如,你可能希望在页面完全过渡后再加载某些资源或者更新页面状态。

详细解释、用法和案例demo:

在Nuxt.js应用中,页面之间的过渡是通过Vue的
<transition>
元素实现的。
page:transition:finish
钩子允许你在页面过渡动画完成后执行自定义逻辑。

用法:

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('page:transition:finish', (pageComponent) => {
    // 在这里编写你的逻辑
  });
});

案例demo:

以下是一个使用
page:transition:finish
钩子的示例,该示例在页面过渡完成后执行一个函数:

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('page:transition:finish', (pageComponent) => {
    if (pageComponent) {
      console.log('Transition finished for component:', pageComponent.$options.name);
    } else {
      console.log('Transition finished for a non-component page.');
    }
    
    // 在这里执行页面过渡完成后的操作
    // 例如,初始化某个库或执行状态更新
    initializeSomethingAfterTransition();
  });
});

function initializeSomethingAfterTransition() {
  // 初始化代码...
  console.log('Initialized something after transition.');
}

在这个示例中,当页面过渡完成后,
page:transition:finish
钩子被触发。我们首先检查
pageComponent
是否存在,如果存在,则打印出过渡完成的组件名称。然后,我们调用了一个名为
initializeSomethingAfterTransition
的函数来执行一些页面过渡完成后的初始化工作。这个函数可以根据你的具体需求来实现。

kit:compatibility

参数:
compatibility, issues

环境:
通用(服务器端和客户端)

描述:
kit:compatibility
钩子允许在Nuxt
3应用中扩展兼容性检查。这个钩子可以在Nuxt的兼容性检查阶段被调用,用于添加自定义的兼容性问题检查或处理兼容性问题。它接收两个参数:
compatibility
对象,其中包含了当前检查的兼容性设置;
issues
数组,用于收集兼容性问题。

详细解释、用法和案例demo:

kit:compatibility
钩子是在Nuxt 3的构建过程中调用的,它允许开发者对兼容性进行检查,并报告潜在的问题。通过这个钩子,开发者可以自定义兼容性规则,以便更好地控制应用在不同环境下的行为。

用法:

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('kit:compatibility', (compatibility, issues) => {
    // 在这里编写你的兼容性检查逻辑
  });
});

案例demo:

以下是一个使用
kit:compatibility
钩子的示例,该示例添加了一个自定义的兼容性问题检查:

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('kit:compatibility', (compatibility, issues) => {
    // 假设我们想要检查某个特定的兼容性设置
    if (compatibility.someSetting === 'someValue') {
      // 如果设置不符合预期,添加一个兼容性问题
      issues.push({
        message: '不兼容的设置:someSetting 应该不等于 someValue',
        path: 'nuxt.config.js', // 指出问题可能存在的文件路径
        help: '请检查 someSetting 的值并更改为推荐的值' // 提供解决问题的建议
      });
    }
    
    // 这里可以添加更多的自定义兼容性检查
  });
});

在这个示例中,我们假设有一个名为
someSetting
的兼容性设置需要检查。如果这个设置的值不是我们期望的
someValue
,我们就将一个描述问题的对象添加到
issues
数组中。这个对象包含了错误消息、可能存在问题的文件路径以及解决问题的建议。这样,在构建过程中,开发者就可以得到关于兼容性问题的反馈,并采取相应的措施来解决这些问题。

ready

参数:
nuxtApp

环境:
服务器端和客户端

描述:
ready
钩子在 Nuxt 实例初始化完成后被调用,这意味着 Nuxt 已准备好工作。在这个钩子中,你可以访问已经完成初始化的
Nuxt 应用实例,并在服务器端和客户端上执行任何需要的操作。

详细解释、用法和案例demo:

ready
钩子是在 Nuxt 应用的整个生命周期中仅调用一次的,它在服务器端和客户端上都可用。这个钩子提供了一个完整的 Nuxt
应用实例,可以用于执行任何需要在应用启动时完成的操作。

close

restart

余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长
,阅读完整的文章:
Nuxt3 的生命周期和钩子函数(三) | cmdragon's Blog

往期文章归档:

记得很久之前,听朋友说过一次出差“奇”旅:他当时在北京出差,需要从地铁站中转一下再去机场。

在转站的过程中,就跑呀跑,一边跑一边想:北京的地铁,怎么台阶这么高、这么长。最重要的是,完全没有扶梯!


他后来转念一想,这么大的地铁站,不装扶梯完全不合理,于是开始给12345打电话,反映这个情况。

惊喜的是,在1个小时内,12345那边就找到了北京地铁的相关方给出了回复。

地铁方的回复内容大意是:地铁内其实是有直梯的,可能路标引导稍弱,导致大家无法第一时间找到直梯,接下来,他们会在地铁站内的各个位置摆放一些比较显眼的引导,减少这类问题的发生。

晚些时候,12345也对他进行了一次回访,主要目的是咨询这单反馈的处理流程,反馈人是否满意等等。

不管是朋友还是听了这个故事的我,都有一个很清晰的共识:地铁方可能不会因为某个人的几句反馈或抱怨就立马进行大幅改造,毕竟地铁会有自己的既定规划。但他们能及时给到反馈,且态度认真诚恳,这一点其实是最打动人的。

对朋友来说,虽然他不常在北京,但这种处理方式让他对北京这座城市又增添了一些好感度。

说到这了,就不得不提前段时间他身上发生的一件糟心事:趁着母亲节活动,朋友网购了一件东西,不幸的是,收到货后发现这件商品已经坏了。于是便开启了漫长而艰难的投诉之旅——快递方说是发货的问题,商家说是运输的问题。双方互相推诿,只苦了消费者这个皮球在中间被踢来踢去。最终的处理办法是,让平台客服介入,而的朋友态度则是不关注过程,只要一个处理结果。

两种情况、两种问题,得到的却是两种截然不同的处理态度。很明显,前者的问题解决机制更为有效,也更让人舒服。

事实上,不论是产品使用,还是一次出行,或是一次温馨的聚餐,当过程中出现各种无法预料的意外或失误时,高效的问题解决机制尤为重要。

在这篇文章中,我们要深入了解的是ITR(Issue to Resolution)流程,一个由华为提出的客户服务体系构建方法和管理流程。

一、问题发生到解决

ITR(Issue to Resolution)流程,也叫问题到解决流程,是华为三大业务流程(IPD-产品集成开发、LTC-线索到回款、ITR-问题到解决)体系之一。这一流程旨在以客户为中心,打通从问题发现、反馈到问题解决的完整服务过程,以端到端的方式打造服务闭环。

它涉及反馈的提交、评审、分发、跟踪和验证,直至最终关闭反馈等各个阶段。流程的目的是快速响应并解决客户或运维人员提出的问题,从而提高客户满意度和运维效率。

二、ITR流程的提出

在发展初期,华为面临着很多已经非常成熟强大的对手,如诺基亚、爱立信、摩托罗拉等。这些西方的巨头,他们的产品、技术等各方面都很完善。对初建的华为来说,要怎样才能从他们手里争下一份蛋糕呢?

事情的转机出现了。有一个客户以前和爱立信合作,现在开始寻求新的合作商。由于爱立信是一家瑞典公司,周六周天不上班,因此当产品出现一些问题时,客户这边无法得到及时的服务响应。在这个背景下,华为凭借24小时的问题响应服务,成功地让该客户由爱立信转向华为。

对客户来说,一个高效、及时的问题响应与解决流程,也许就是影响决策的关键因素。

三、ITR流程的挑战与解决

搭建公司自己的ITR流程时,一般会有以下几个步骤:

  1. 用户、运维、市场等人员提出问题后,及时创建反馈;
  2. 专人进行评审反馈是否合理、有效;
  3. 评审通过的反馈进行分发,需要有指定人员进行解决、处理;
  4. 及时跟进反馈状态与阶段;
  5. 验证反馈是否得到解决、解决方案是否满意;
  6. 关闭反馈。

虽然看似简单,但在实际应用中,ITR流程往往也面临着很多挑战,比如:反馈跟踪困难、处理流程长、等待时间长和反馈分发等问题。

接下来,想跟大家分享一下禅道团队的ITR流程是怎样的。

禅道团队ITR流程

首先,无论是客户还是运维人员发现问题后,客户侧同事会判断问题是否之前出现过。若是已知问题,会直接回复客户答案或解决方案。若问题是新发生的,则会在禅道中记录,由产品经理评审反馈的合理性与描述的清晰度。合理且清晰的反馈,会进入处理流程,产品经理接下来会考虑该反馈的考虑解决方案,并可能将反馈转换为工单、Bug、用户需求或研发任务。

禅道团队问题反馈转化

具体处理方式取决于反馈的类型和紧急程度:

  • 若反馈为紧急需求或紧急Bug,但影响的是某一个或某几个特定客户,通常会转换为工单,以便快速解决影响客户流程的问题;若反馈的是较为通用的紧急需求或Bug,一般转换为任务或Bug。
  • 在考虑需求紧急程度的维度中,一般紧急的需求会转为工单,普通需求转化为研发需求或用户需求。

同样,需求的清晰度也会影响处理方式,因为客户提出的可能仅是初步想法,需产品经理细化、拆分后才能进入研发阶段。

在做完反馈的流转后,接下来,又将由哪些人员负责跟进处理呢?在禅道团队中,具体划分为以下四个小组:

禅道研发小组分工
  • 第一个是通用产品研发小组,主要负责通用产品的需求实现以及Bug解决。通用产品研发小组采用敏捷开发的方式,且周期也较为固定,一般按照两周一个迭代的节奏交付,最终交付物是通用产品的版本。
  • 除通用产品需求外,还会有一些比较小众的研发需求。这里会有一个小众需求开发小组,同样采用敏捷开发的方式。由于小众需求量较少,处理周期也较短,迭代节奏是一周一迭代,最终交付物为插件或功能包。
  • 对不同的客户来说,通用版本可能无法满足各种特定需求。因此,禅道成立了定制开发小组,主要负责定制需求的实现和Bug解决。定制开发小组的开发模式一般是融合瀑布模型,最终交付物是定制版本。
  • 最后,禅道也成立了应急响应小组,主要负责处理非常紧急,需要立刻响应客户、给出解决方案的问题。应急响应小组的处理方案一般为工单,采用看板的开发方式。在应急响应小组中,没有固定的周期开发,通常是以小时为单位做估算,最终交付物为补丁或插件等。

这便是禅道的反馈跟踪矩阵。

禅道的反馈跟踪矩阵

四、具体的ITR流程在禅道中应如何落地?

在禅道中,有一个反馈模块,客户可以通过反馈模块来条目化地管理问题和反馈,还可以通过工作流功能自定义公司实际的反馈流程。

禅道反馈模块

在跟踪、监控反馈的过程中,也可以通过禅道的BI模块,了解现阶段的反馈响应速度等情况。

禅道BI模块

五、应急响应小组的工程实践

应急响应小组的工程实践

在之前的反馈处理流程中,获取到一个反馈后,产品经理会依据反馈的紧急程度和适用群体,转为工单,并指派给应急响应小组。此时,应急响应小组会根据工单的基本信息,进行相应的环境搭建,进行问题修复,再以补丁或插件的形式交付给客户。


整个修复过程会耗时1~2小时,这种时长对紧急程度高的工单而言,效率较低。因此,应急响应小组编写了两个脚本来自动化流程:一是环境配置,只需输入版本号,脚本便能自动完成版本信息的提供、下载、替换加密文件、获取授权文件以及环境搭建,整个过程仅需约15秒;二是自动打包,研发人员在编码完成后只需提供版本信息和反馈ID,脚本便能自动完成打包、编写描述文件、压缩和加密,整个过程约需40秒。

这些脚本的应用显著提高了应急响应小组的工单处理速度,原本需耗时一小时的流程现在不到一分钟就能完成。处理完毕后,程序还会自动将打包文件推送到通讯工具中,供相关人员下载。这一自动化工程实践不仅提高了问题解决的效率,还减少了重复性工作,极大地缩短了问题响应解决时间。

由此出发,客户满意度的提高也不是什么稀罕事了。

对每一个公司来说,在如今的经济下行期,也许ITR(从问题到解决)流程会变得至关重要。华为打造的ITR流程,或许就是复制华为成功经验的绝佳路径。

package com.aswatson.cdc.test;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import java.time.LocalDateTime;
import java.util.concurrent.*;

/**
 *  boolean success = lock.tryLock(0, 0, TimeUnit.SECONDS); // 表示尝试获取锁,等待0秒,持有锁0秒钟
 *  注意问题,存在的隐患: 虽然 tryLock(0, 0, TimeUnit.SECONDS)
 *
 *  首先1. 但实际锁的释放仍然会受到 Redisson 看门狗机制的影响。如果持有锁的线程未能在续约周期内续约锁的持有时间,那么锁可能会在超时后被自动释放。
 *  (默认是每隔 30 秒进行一次续约)来维持锁的有效性,避免因为持有锁的线程未能释放而造成锁的永久占用。或者自己unLock。
 *
 *  其次2. 确保你使用的 Redisson 版本与 Redis 版本兼容,并且不会因为版本问题导致锁的行为异常。目前测试用的是redis(2.7.17)、redisson(3.24.3)
 *
 *  其次3. 默认情况下,Redisson 的看门狗会定期发送续约请求给 Redis 服务器,以延长当前持有的锁的有效期。但是也有不会续约的可能性:
 *         Redis 连接中断、Redisson 配置问题、持有锁的线程崩溃、锁的最大持有时间到期。
 *
 *  其次4.  即使在业务逻辑中调用了阻塞操作(如 sleep),Redisson 也会在后台继续进行续约操作,以防止锁被意外释放。
 *
 */
public class TestRedissonLeaveTimeLock {

    public static void main(String[] args) throws Exception {

        Config config = new Config();
        config.useSingleServer().setAddress("redis://10.95.35.93:37495");
        RedissonClient redissonClient = Redisson.create(config);
        RLock lock = redissonClient.getLock("lockName");
        System.out.println("创建好了RedissonClient" + getName());

        int numThreads = 10;
        ExecutorService executor = Executors.newFixedThreadPool(numThreads);
        CountDownLatch startLatch = new CountDownLatch(1);
        CountDownLatch doneLatch = new CountDownLatch(numThreads);

        // 尽管 for 循环看起来是按顺序逐个,但实际上每个任务会并发地在后台执行。
        // 这是因为每次调用 submit时,任务被提交给线程池,而线程池会根据可用的线程资源并发执行这些任务。
        for (int i = 0; i < numThreads; i++) {
            executor.submit(() -> {
                try {
                    startLatch.await(); // 等待主线程的启动信号

                    System.out.println("获取锁前的时间:"+ getName());
                    boolean success = lock.tryLock(0, 0, TimeUnit.SECONDS); // 尝试获取锁,等待0秒,持有锁0秒钟
                    System.out.println("获取锁后的时间:"+ getName());
                    if (success) {
                        System.out.println("拿到锁"+ getName());
                        // 模拟业务处理耗时 大于锁过期,可能导致非自己持有的锁被释放。
                        TimeUnit.SECONDS.sleep(20);
                    } else {
                        System.out.println("未能获取到锁,已放弃尝试" + getName());
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    doneLatch.countDown();// 每次减去1

                    // 判断当前线程是否持有锁
                    if (lock.isHeldByCurrentThread()) {
                        System.out.println("释放锁"+ getName());
                        lock.unlock();
                    }
                }
            });
        }

        System.out.println("主线程即将释放所有等待的线程...");
        startLatch.countDown(); // 释放定义的1条线程,开始并发执行
        doneLatch.await(); // 等待所有线程10条完成
        executor.shutdown();
        System.out.println("所有线程执行完成" + getName());
    }

    public static String getName() {
        return Thread.currentThread().getName() + "---" + LocalDateTime.now();
    }

}