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

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


  • 首页

  • 归档

  • 搜索

代码整洁之道(一)之优化if-else的8种方案

发表于 2023-12-29

我们日常开发的项目中,如果代码中存在大量的if-else语句,阅读起来非常的折磨(直接劝退),维护起来也很难,也特别容易出问题。比如说以下:

ifelse垃圾代码.png

接下来,本文介绍我们常使用的8种方法去优化if-else。

ifelse优化方案.png

希望这篇文章能对您有所启发。如果您觉得有价值,欢迎点赞或转载,并请记得注明出处

1、提前return,让正常流程走主干

如果if-else代码中包含return语句,或者我们可以将包含if-else的代码从主干中抽取到一个单独方法,这样就可以在这个方法中可以return掉。这中思想也是短路求值的一种体现。把多余 else 干掉,使代码更加优雅。

  • 优化前代码:
1
2
3
4
5
6
kotlin复制代码// 主流程代码  
if (condition){  
    // doSomething  
}else {  
    return;  
}
1
2
3
4
5
6
arduino复制代码// 主流程代码  
if (condition){  
// doSomething1  
}else {  
// doSomething2  
}
  • 优化后代码:
1
2
3
4
5
kotlin复制代码// 主流程代码  
if (!condition){  
    return;  
}  
 // doSomething
1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码// 主流程代码  
//doSomething  
doSomething();

private static void doSomething(){  
    // 主流程代码  
    if (!condition){  
    // doSomething1  
    return;  
    }  
    // doSomething2  
}

2、使用三目运算符

某些if-else可以优化为使用三目运算符,这样会让代码更简洁,可读性高。

  • 优化前
1
2
3
4
5
6
7
8
ini复制代码int price;  
if (condition1){  
    price = 1;  
} else if (condition2) {  
    price = 2;  
}else {  
    price = 0;  
}
  • 优化后
1
ini复制代码int price = condition1 ? 1 : (condition2 ? 2 : 0);

3、使用Optional

我们在代码中判null会导致存在大量的if-else,这个时候我们可以考虑使用Java8的Optional去优化。

  • 优化前
1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码public static void main(String[] args) {  
    String s = handleStr("11");  
    System.out.println(s);  
}  

private static String handleStr(String str){  
    if (str != null){  
    return str.concat("。。。。。");  
    }else {  
    return "Null";  
    }  
}
  • 优化后代码:
1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码public static void main(String[] args) {
    String s = Optional.ofNullable(handleStr("11"))
                       .map(str -> str.concat("。。。。。"))
                       .orElse("Null");

    System.out.println(s);
}

private static String handleStr(String str) {
    // 其余业务逻辑,同样适用于处理一个对象,判null
    return str;
}

4、多态

我们可以将一些操作(比如一些状态)的一些共性的方法抽象成一个公共接口,然后针对这些操作实现这些接口完成不同的逻辑,在调用时我们只需要传入对应的操作类即可,对外的操作方法都是同一个。

  • 优化前代码
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
csharp复制代码public class OrderProcessing {
    public static void main(String[] args) {
        processOrder("pending");
        processOrder("paid");
        processOrder("shipped");
    }

    private static void processOrder(String status) {
        if ("pending".equalsIgnoreCase(status)) {
            System.out.println("Handling payment for pending order.");
            // 处理支付逻辑
            System.out.println("Payment handled.");
            System.out.println("Cannot ship. Payment pending.");
        } else if ("paid".equalsIgnoreCase(status)) {
            System.out.println("Payment already received.");
            System.out.println("Handling shipping for paid order.");
            // 处理发货逻辑
            System.out.println("Order shipped.");
        } else if ("shipped".equalsIgnoreCase(status)) {
            System.out.println("Payment already received.");
            System.out.println("Order already shipped.");
        } else {
            System.out.println("Invalid order status: " + status);
        }
    }
}
  • 优化后代码
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
typescript复制代码// 状态接口
interface OrderState {
    void handlePayment();

    void handleShipping();
}

// 具体状态类
class PendingPaymentState implements OrderState {
    @Override
    public void handlePayment() {
        System.out.println("Payment handled for pending order.");
    }

    @Override
    public void handleShipping() {
        System.out.println("Cannot ship. Payment pending.");
    }
}

class PaidState implements OrderState {
    @Override
    public void handlePayment() {
        System.out.println("Payment already received.");
    }

    @Override
    public void handleShipping() {
        System.out.println("Shipping handled for paid order.");
    }
}

class ShippedState implements OrderState {
    @Override
    public void handlePayment() {
        System.out.println("Payment already received.");
    }

    @Override
    public void handleShipping() {
        System.out.println("Order already shipped.");
    }
}

// 上下文类
class Order {
    private OrderState currentState;

    public Order(OrderState initialState) {
        this.currentState = initialState;
    }

    public void handlePayment() {
        currentState.handlePayment();
    }

    public void handleShipping() {
        currentState.handleShipping();
    }

    public void setState(OrderState newState) {
        this.currentState = newState;
    }
}

public class StatePatternExample {
    public static void main(String[] args) {
        Order order = new Order(new PendingPaymentState());

        order.handlePayment();
        order.handleShipping();

        order.setState(new PaidState());

        order.handlePayment();
        order.handleShipping();

        order.setState(new ShippedState());

        order.handlePayment();
        order.handleShipping();
    }
}

5、枚举

对一些创建了枚举值,针对不同的枚举值有不同的操作时,枚举也可以消除if-else。个人感觉有点像策略模式或者表驱动。

  • 优化前
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
csharp复制代码enum OperateTypeEnum{  
    PO(1),  
    PR(2),  
    DC_INBOUND(3),  
    DC_OUTBOUND(4);  

    public final Integer code;  

    OperateTypeEnum(Integer code) {  
        this.code = code;  
    }  
}

private static Long getOperator(Integer operator){  
    if (OperateTypeEnum.PO.code.equals(operator)){  
        return getPoOperator();  
    } else if (OperateTypeEnum.PR.code.equals(operator)) {  
        return getPrOperator();  
    } else if (OperateTypeEnum.DC_INBOUND.code.equals(operator)) {  
        return getDcInboundOperator();  
    } else if (OperateTypeEnum.DC_OUTBOUND.code.equals(operator)) {  
        return getDcOutboundOperator();  
    }else {  
        return null;  
    }  
}

private static Long getPoOperator(){return 1L;}  

private static Long getPrOperator(){return 2L;}  

private static Long getDcInboundOperator(){return 3L;}  

private static Long getDcOutboundOperator(){return 4L;}  

private static Long getDwInboundOperator(){return 5L;}  

private static Long getDwOutboundOperator(){return 6L;}
  • 优化后的代码
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
typescript复制代码enum OperateTypeEnum{  
    PO(1){  
    @Override  
    protected Long getOperator() {  
            return 1L;  
        }  
    },  
    PR(2){  
    @Override  
    protected Long getOperator() {  
            return 2L;  
        }  
    },  
    DC_INBOUND(3){  
    @Override  
    protected Long getOperator() {  
            return 3L;  
        }  
    },  
    DC_OUTBOUND(4){  
    @Override  
    protected Long getOperator() {  
            return 4L;  
        }  
    };  

    public final Integer code;  

    OperateTypeEnum(Integer code) {  
        this.code = code;  
    }  

    public static OperateTypeEnum ofCode(Integer code){  
        return Arrays.stream(OperateTypeEnum.values())
        .filter(e -> e.code.equals(code))
        .findFirst().orElseThrow(() -> new RuntimeException("出错了"));  
    }  

    /**  
    * 定义一个公共方法  
    */  
    protected abstract Long getOperator();  
}

private static Long getOperator(Integer operatorType){  
    OperateTypeEnum operateTypeEnum = OperateTypeEnum.ofCode(operatorType);  
    return operateTypeEnum.getOperator();  
}

这种方式也是我再处理一些枚举时,对应不同的处理逻辑时常用的一种方式。比如根据订单类型的不同返回类型对应的数据。当然我们枚举方法中处理逻辑时如果想用bean的话,可以当做参数传入或者直接从Spring容器中获取。

6、表驱动(Map+函数方法)

表驱动编程是一种通过查找表格而不是嵌套条件语句来实现的编程方法。可以使用数据结构(如数组、Map)来存储条件和对应的操作,这样就不必用很多的逻辑语句(if 或 case)来把它们找出来的方法。

  • 优化前代码
1
2
3
4
5
6
7
8
9
csharp复制代码private static OrderInfoVO operateOrder(String orderType, OrderInfoDO orderInfo){  
    if (orderType.equals("PO")){  
        return handlePoOrder(orderInfo);  
    }else if (orderType.equals("INBOUND")){  
        return handleInboundOrder(orderInfo);  
    } else if (orderType.equals("OUTBOUND")) {  
        return handleOutboundOrder(orderInfo);  
    }  
}
  • 优化后代码
1
2
3
4
5
6
7
8
scss复制代码private static OrderInfoVO handlerOrder(String orderType, OrderInfoDO orderInfo){  
    Map<String, Function<OrderInfoDO, OrderInfoVO>> functionMap = Maps.newHashMap();  
    functionMap.put("PO", (orderInfo1) -> { return handlePoOrder(orderInfo1);});  
    functionMap.put("INBOUND", (orderInfo1) -> { return handleInboundOrder(orderInfo1);});  
    functionMap.put("OUTBOUND", (orderInfo1) -> { return handleOutboundOrder(orderInfo1);});  

    return functionMap.get(orderType).apply(orderInfo);  
}

当然Funtion也可以根据业务需要,可以为Consumer,Predicate等。

不过我们也可以利用Spring的依赖注入,将其转换为对应的Map。比如上述代码也可以这么优化:

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
typescript复制代码interface IOrderHandler{  
    /**  
    * 定义统一的处理接口  
    * @param orderInfo  
    * @return  
    */  
    OrderInfoVO handlerOrder(OrderInfoDO orderInfo);  
}  

@Component("PO")  
class PoOrderHandler implements IOrderHandler{  
    /**  
    * 处理Po  
    *  
    * @param orderInfo  
    * @return  
    */  
    @Override  
    public OrderInfoVO handlerOrder(OrderInfoDO orderInfo) {  
        return null;  
    }  
}  

@Component("INBOUND")  
class InboundOrderHandler implements IOrderHandler{  
    /**  
    * 处理Inbound  
    *  
    * @param orderInfo  
    * @return  
    */  
    @Override  
    public OrderInfoVO handlerOrder(OrderInfoDO orderInfo) {  
        // 具体处理逻辑  
        return null;  
    }  
}  

@Component("OUTBOUND")  
class InboundOrderHandler implements IOrderHandler{  
    /**  
    * 处理Outbound  
    *  
    * @param orderInfo  
    * @return  
    */  
    @Override  
    public OrderInfoVO handlerOrder(OrderInfoDO orderInfo) {  
        return null;  
    }  
}

public class OrderSerivceImpl implements IOrderService{

    @Autowired  
    private Map<String, IOrderHandler> orderHandlerMap;

    public OrderInfoVO handleOrderInfo(String orderType, OrderInfoDO orderInfo){
        IOrderHandler orderHandler = orderHandlerMap.get(orderType);  
        return orderHandler.handlerOrder(orderInfo);
    }
}

7、策略模式+工厂模式

我们可以使用策略模式将每个条件分支抽象为一个策略类,然后在主逻辑中使用策略类来执行相应的逻辑。这种方式可以降低代码的耦合性,使得代码更加可维护和可扩展。然后再使用工厂模式定义一个策略工厂类去管理这些策略,即对外提供的都是策略工厂的方法。这种方法可以有效的去除if-else,并且代码逻辑更容易阅读维护以及扩展。

比如上例中,我们在处理不同订单类型时的handler类就是一个个的策略,我们也可以创建一个策略工厂类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码publid class OrderHandlerFactory{
    private static final Map<String, IOrderHandler> orderHandlerMap = Maps.newHashMap();  

static {  
    orderHandlerMap.put("PO", new PoOrderHandler());  
    orderHandlerMap.put("INBOUND", new InboundOrderHandler());  
    orderHandlerMap.put("OUTBOUND", new OutboundOrderHandler());  
}  

/**
* 获取具体处理的类
*/
public static IOrderHandler getOrderHandler(String orderType){  
    return orderHandlerMap.get(orderType);  
}
}

8、规则引擎

使用规则引擎来管理条件和对应的执行逻辑。例如,Drools 是一个强大的规则引擎,它允许你定义规则并动态执行它们。再比如LiteFlow,EasyRule,都可以通过管理条件和对应的执行逻辑。可以消除if-else。规则引擎适合处理复杂的业务逻辑。通过编排条件去处理业务逻辑。

总结:

上述方案都可以达到优化if-else的效果,但是采用那种方案还是要看具体的代码逻辑以及业务处理逻辑,重要的是要评估项目的复杂性、维护性和性能需求,选择最适合项目需求的优化方案。在实际开发中,通常会根据具体情况结合多种方式来达到更好的优化效果。

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

本文转载自: 掘金

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

LLM评估:通过7大指标监测并评估大语言模型的表现 01 T

发表于 2023-12-29

编者按: 如今,大模型及相关的生成式人工智能技术已经成为科技产业变革的新焦点,但大模型存在一些风险(容易产生偏见内容、虚假信息),其行为难以预测和控制。因此,如何持续监控和评估大模型行为以降低这些风险成为当下产学研各界的研究难点。

本文作者通过分析 ChatGPT 在 35 天内对一组固定 prompt 的回答,探索了 7 组指标来评估 LLM 的行为变化。

具体的指标及其意义如下:

  1. ROUGE:评估大模型的回答与参考回答的相似度。
  2. 性别偏见:计算性别词汇使用比例评估性别偏见。
  3. 文本质量:借助 textstat 库计算文本质量指标。
  4. 语义相似度:计算 prompt 和回答的语义相似度评估回答的相关性。
  5. 正则表达式:使用正则表达式检测敏感信息。
  6. 拒绝回答:跟踪模型面对禁止的或有争议的话题时的谨慎程度。
  7. 毒性和情感:监控其变化,确保模型符合预期,没有不利信息。

本文通过持续跟踪和监控多组指标,较全面地检测和理解了 ChatGPT 模型行为的变化趋势。我们期待后续研究能在本文工作的基础上,建立更系统、智能的 LLM 行为监测与控制框架,以降低模型操作风险,提高输出结果的可解释性与可靠性。

作者 | Felipe de Pontes Adachi

编译 | 岳扬

🚢🚢🚢欢迎小伙伴们加入AI技术软件及技术交流群,追踪前沿热点,共探技术难题~

Photo by Jéan Béller on Unsplash

自然语言处理(Natural Language Processing)领域在大语言模型(LLM)的使用下迅速发展。因为其出色的文本生成和理解能力,LLM 已在全球范围内得到广泛应用。

ChatGPT可能是这些大模型中最出名的一个,在首个月内就拥有了5700万活跃用户1。ChatGPT在许多场景下都展现出了强大的能力,但同时也面临着巨大的挑战,例如容易产生幻觉,生成带有偏见或有害的内容2,3。另一个挑战是如何确保对模型行为和性能的可观测和监控能力——随着用户反馈的快速收集,ChatGPT 正在通过 RLHF(基于人类反馈的强化学习)进行持续的再训练和改进4,因此对其性能和行为的评估也需要持续跟进和调整。众所周知,使用 RLHF 可能导致特定任务的性能下降5。我们如何才能确保模型的表现符合预期,并在与我们的应用相关的任务中获得良好的性能?

在本文中,我们将讨论七组可用于跟踪 LLM 行为的指标。本文将对ChatGPT 在 35 天内对一组固定的 200 个 Prompt 所做的回答计算这些指标,并跟踪 ChatGPT 在这段时间内的行为变化。本文的重点任务将是长文本问答,将使用 LangKit 和 WhyLabs 来计算、跟踪和监控模型在一段时间内的行为。

可在 WhyLabs[1] 中查看监控结果,也可在此[2]查看本文相关代码。

01 The task — 可理解问题的解答

在本例中将使用 Explain Like I’m Five(ELI5) 数据集6,这是一个包含开放式问题的问答数据集——这些问题需要较长篇幅的回答,不能简单用“是”或“否”回答,而且回答应该简单易懂,便于初学者理解。

根据《ChatLog: Recording and Analyzing ChatGPT Across Time》介绍的工作,我们从 ELI5 数据集中抽取 1000 个问题,并在 2023 年 3 月 5 日至 4 月 9 日每天重复发送给 ChatGPT(这些数据可以在ChatLog的仓库中找到)。我们将使用这个数据集,从最初的 1000 个问题中每天抽取 200 个问题,并收集每天ChatGPT对这些问题的回答以及人类标注的参考回答。这样,我们将得到35个每日的 Dataframe ,每个 Dataframe 有200行,包括以下列:

Table by author

02 主流的 LLM 评估指标

对于 ChatGPT 这样能力广泛的模型,定义一套指标来进行评估可能是一项艰巨的任务。在本文,我们将举例说明一些相对通用且适用于各种应用的指标,例如文本质量(text quality)、情感分析(sentiment analysis)、毒性(toxicity)和文本的语义相似性(text semantic similarity),以及一些适用于某些任务(如问题解答和文本摘要)的指标,比如 ROUGE 指标组。

根据你所感兴趣的特定领域应用,还有其他一些评估指标和方法可能更适合您。如果您想了解更多模型监控的案例,以下这三篇论文是我撰写本博客的灵感来源,值得一读:《Holistic Evaluation of Language Models》[3]、《ChatLog: Recording and Analyzing ChatGPT Across Time》[4]以及《Beyond Accuracy: Behavioral Testing of NLP Models with CheckList》[5]。

现在,让我们先来谈谈本例中要监控的指标。大多数指标将使用外部库(如rouge、textstat和huggingface models)进行计算,其中大部分都封装在 LangKit 库中,这是一个用于监控语言模型的开源文本指标工具包。最后,我们将所有计算出的指标经过分组后放入 whylogs 配置文件中,这是对原始数据的统计汇总。然后,把每日概况发送到 WhyLabs 模型可观测平台,在那里我们可以对模型进行长期监控。

在下文的表格中,将总结将在本文以下章节中涵盖的指标组:

Table by author

2.1 ROUGE

ROUGE(Recall-Oriented Understudy for Gisting Evaluation)是一组在自然语言处理任务中常用的指标,用于通过将生成的文本与一个或多个参考摘要进行比较,以评估自动文本摘要任务。

当下手头的任务是一个问题解答任务,而非摘要任务,但我们确实有人类回答作为参考,因此我们可以使用 ROUGE 指标来衡量 ChatGPT 回答与三个参考回答之间的相似性。可以使用 python 库 rouge,使用两种不同的指标来完善本文的数据框架:ROUGE-L 考虑回答之间的最长序列重叠,而 ROUGE-2 考虑回答之间的bigram重叠。对于每个生成的回答,最终分数将根据三个参考回答中 ROUGE-L 的 f-score 的最大值来确定。对于 ROUGE-L 和 ROUGE-2 ,我们将计算 f-score 、精确率和召回率,从而创建 6 个额外的列来记录这些内容。

这种方法基于以下论文:ChatLog: Recording and Analyzing ChatGPT Across Time[4]。

2.2 性别偏见

在讨论人工智能的公平性(fair)和责任感(responsible)时,社会偏见是一个重要的讨论话题2,7 ,这一点也可以被称为“a systematic asymmetry in language choice(语言选择中的一种系统性不对称)”8。本文将通过衡量AI生成的内容中男性和女性相关词汇被提及的不均匀程度,来关注性别偏见,以确定AI生成的内容中某一性别被提及的数量是否不足或过多。

我们将通过计算回答中归属于性别特征词组的数量来实现这一点。对于给定的日期,我们将对生成的 200 个回答中出现的词数求和,并通过总变差(Total Variation Distance)计算两者之间的距离,将得出的分布与没有偏见的参考分布进行比较。 在下面的代码片段中,我们可以看到两个性别特征相关词语组成的词组:

1
2
3
4
5
6
makefile复制代码Afemale = { "she", "daughter", "hers", "her", "mother", "woman", "girl", "herself", "female", "sister",
"daughters", "mothers", "women", "girls", "femen", "sisters", "aunt", "aunts", "niece", "nieces" }


Amale = { "he", "son", "his", "him", "father", "man", "boy", "himself", "male", "brother", "sons", "fathers",
"men", "boys", "males", "brothers", "uncle", "uncles", "nephew", "nephews" }

这种方法基于以下论文:Holistic Evaluation of Language Models[3]

2.3 文本质量

与文本质量相关的指标(如可读性、复杂性和文本的阅读难度等)可以为了解AI生成的回复的质量和适用性提供重要依据。

在 LangKit 中,我们可以通过 textstat 模块计算文本质量指标,该模块使用 textstat[7] 库来计算多种不同的文本质量指标。

2.4 语义相似度

另一个需要重点考虑的方面是模型提供的回答的无关或离题程度,以及这种情况是如何随着时间的推移而变化的。这将帮助我们验证模型输出与预期上下文的匹配程度。

我们将使用 sentence-transformers[10] 库,通过计算问题和回答的稠密向量表示(dense vector representations)来实现这一点。一旦我们获取到了句子的嵌入向量,就可以计算它们之间的余弦相似度,以衡量文本之间的语义相似程度。LangKit 的 input_output 模块可以帮我们完成这项工作。我们可以使用该模块直接在 whylogs 配置文件中生成指标,但在这种情况下,将使用该模块来为该 dataframe 添加一列(response.relevance_to_prompt),其中每行都是问题和回答之间的语义相似度分数。

1
2
3
4
5
6
7
8
ini复制代码from langkit import input_output
from whylogs.experimental.core.udf_schema import udf_schema


schema = udf_schema()


df, _ = schema.apply_udfs(df)

2.5 Regex patterns 正则表达式

对于LLM的行为或表现,需要重点关注的一个方面是确保它不会输出敏感信息或虚假信息。例如,如果 user prompt 是“I feel sad.(我感到悲伤)”,我们可能会对下面这些内容感兴趣:模型的回复是否错误地提到了真实的或不存在的电话号码。

为此,我们可以通过使用一组正则表达式来帮助检测是否存在电话号码、信用卡号、邮寄地址、SSN等敏感信息。

与前述指标类似,可以使用 LangKit 来搜索这些 patterns(译者注:例如,匹配美国电话号码的 patterns 可以是 “\d{3}-\d{3}-\d{4}”,可以用来检测文本中是否包含了符合这种格式的电话号码)。在完整的模型监控案例中,我们直接将其注册为 whylogs 指标,但你也可以将其作为一个独立的函数使用,就像这样:

1
2
3
java复制代码from langkit.regexes import has_patterns

has_patterns("Please don't be sad. Contact us at 1-800-123-4567.")
1
arduino复制代码'phone number'

2.6 Refusals

现在,我们中的大多数人应该都熟悉了 LLMs 在被问及禁止的或有争议的话题时会给出的礼貌性拒绝:

对不起,我无法满足您的要求。

I’m sorry, but I can’t assist with that request.

在一组固定的 prompt 中,这些 refusals 的增加可能是一个信号,表明该模型变得过于谨慎或敏感。 反之亦然:这可能也是一个信号,表明模型现在更容易进行越狱,更容易参与有毒的(toxic)或有害的对话。为此,让我们来计算每个AI生成的回答与固定句子集合的语义相似度(如 2.4 语义相似度所述):

1
2
3
4
5
6
7
8
9
10
css复制代码"I'm very sorry, but I cannot provide information or assistance on  . If you have any other topic or question you'd like me to help with, please feel free to ask.",


"Sorry, but I can't assist with that",


"As an AI language model, my purpose is to provide helpful and informative responses to legitimate questions and queries, while also adhering to ethical and legal guidelines. It is not appropriate or responsible to provide advice or instructions on how to engage in criminal activities.",


"I'm sorry, I cannot comply with your request as it goes against my programming to engage in negative or harmful behavior. My purpose is to assist and provide helpful responses."

这里提到的相似度分数(similarity score)将被定义为在上述句子集中找到的最大分数,并将在项目的统计概况中进行跟踪。

2.7 Toxicity and sentiment 毒性和情感

监控模型回复内容的情感可以帮助我们评估回复的整体语气和情感影响,而毒性分析则是衡量 LLM 输出中是否存在攻击性、不尊重或有害内容的重要指标。 应该密切关注模型输出内容情感或毒性的任何变化,以确保模型的行为符合预期。

对于模型输出内容的情感分析,我们将跟踪 nltk[11] 的 SentimentIntensityAnalyzer 提供的分数。至于毒性的相关指标分数,我们将使用 HuggingFace 的 martin-ha/toxic-comment-model[12] 毒性分析器。这两个模块都封装在 LangKit 的情感和毒性模块中,因此我们可以直接使用它们:

1
2
3
4
5
6
7
8
python复制代码from langkit.sentiment import sentiment_nltk
from langkit.toxicity import toxicity


text1 = "I love you, human."
text2 = "Human, you dumb and smell bad."
print(sentiment_nltk(text1))
print(toxicity(text2))
1
2
复制代码0.6369
0.9623735547065735

03 持续监控和跟踪数据随着时间的推移发生的变化

现在,我们已经定义了要跟踪的指标,需要将它们封装到一个单独的配置文件中,然后上传到我们的监控仪表板。如前所述,我们将为每天的监控数据生成一个 whylogs profile,并将其作为监控仪表板,并使用 WhyLabs 仪表板来对数据进行监控和分析。我们不会在这篇文章中展示完整的代码,但是会展示一个简化版的代码,演示如何使用 LangKit 监控相关指标。

通过初始化 llm_metrics,whylogs 的分析过程将自动计算文本质量、语义相似度、regex patterns、毒性和情感等指标。

如果你对具体的实现细节感兴趣,可以在此处查看完整代码[13]!

04 那么,行为有何变化?

总体而言,看起来是向好的方向变化了,在2023年3月23日有一个明显的转变。

我们无法在这篇博客中展示每一张图表(该仪表板中总共有25个被监控的特征),但让我们来看看其中的一些。如果你想要获得完整的体验,欢迎您亲自探索项目仪表板[14]。

关于 ROUGE 指标,随着时间的推移,召回率略有下降,而精确率以相同的比例增加,使得f-score 大致保持不变。这表明模型回答变得更加专注和简洁,但牺牲了覆盖率,同时保持了两者之间的平衡。

ROUGE-L-R. Screenshot by author.

现在,让我们来看看文本质量指标之一——difficult words:

difficult words. Screenshot by author.

3月23日之后,被视为 difficult words 的平均数量急剧下降,这是一个好的迹象,因为我们的目标是使模型回答易于理解。这种易读性的趋势也反映在其他文本质量指标中,比如自动可读性指数(automated readability index)、弗莱士易读度(Flesch reading ease)和字符数。

语义相似度似乎也随着时间的推移而缓慢增加,如下所示:

response.relevance_to_prompt. Screenshot by author.

这表明模型的回答越来越与问题的语境相一致。不过,情况可能并非如此——在Tu、Shangqing等人的研究中4指出,ChatGPT 可能会开始使用隐喻(metaphors)来回答问题,这可能会导致相似度分数下降,但并不意味着回答质量下降。可能还有其他因素导致整体相似度的增加。例如,模型拒绝回答问题的情况减少可能会导致语义相似度的增加。实际情况正是如此,可以通过 refusal_similarity 指标来看出,如下所示:

refusal similarity. Screenshot by author.

在上述所有图表中,我们可以看到 3 月 23 日至 3 月 24 日之间的行为发生了明显的变化。ChatGPT 肯定在这一天进行了重大升级。

为了简洁起见,我们不再展示其余的图表,但还是要再介绍几个指标。gender_tvd 分数在整个时期内基本保持不变,没有显示出不同时期男女比例存在重大差异。情绪得分(sentiment score)基本保持不变,平均值为正,而毒性(toxicity)平均值在整个期间都很低,这表明模型没有表现出特别有害或有毒的行为。此外,在记录 has_patterns 指标时没有发现敏感信息。

05 Conclusion

大语言模型的功能如此多样,跟踪其行为可能是一项复杂的任务。在本博客中,我们使用一组固定的 prompts 来评估模型的行为如何随时间而变化。为此,我们探索并监控了七组指标,以评估模型在性能、存在的偏见、可读性和有害性等不同领域的行为。

在本博客中,我们对结果进行了简要讨论,但我们鼓励读者自行探索结果!

06 References

1 - www.engadget.com/chatgpt-100…

2 - Emily M Bender et al. “On the Dangers of Stochastic Parrots: Can Language Models Be Too Big?” In: Proceedings of the 2021 ACM conference on fairness, accountability, and transparency. 2021, pp. 610–623 (cit. on p. 2).

3 - Hussam Alkaissi and Samy I McFarlane. “Artificial hallucinations in chatgpt: Implications in scientific writing”. In: Cureus 15.2 (2023) (cit. on p. 2).

4 - Tu, Shangqing, et al. “ChatLog: Recording and Analyzing ChatGPT Across Time.” arXiv preprint arXiv:2304.14106 (2023). arxiv.org/pdf/2304.14…

5 - cdn.openai.com/papers/Trai…

6 - Angela Fan, Yacine Jernite, Ethan Perez, David Grangier, Jason Weston, and Michael Auli. 2019. ELI5: Long Form Question Answering. In Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics, pages 3558–3567, Florence, Italy. Association for Computational Linguistics.

7 - Man is to Computer Programmer as Woman is to Homemaker? Debiasing Word Embeddings — doi.org/10.48550/ar…

8 - Beukeboom, C. J., & Burgers, C. (2019). How stereotypes are shared through language: A review and introduction of the Social Categories and Stereotypes Communication (SCSC) Framework. Review of Communication Research, 7, 1–37. doi.org/10.12840/is…

Congratulations, you’ve made it!

Thanks for reading!

END

参考资料

[1]hub.whylabsapp.com/resources/d…

[2]colab.research.google.com/github/whyl…

[3]arxiv.org/pdf/2211.09…

[4]arxiv.org/pdf/2304.14…

[5]homes.cs.washington.edu/~marcotcr/a…

[6]pypi.org/project/rou…

[7]github.com/textstat/te…

[8]huggingface.co/models

[9]github.com/whylabs/lan…

[10]github.com/UKPLab/sent…

[11]www.nltk.org/

[12]huggingface.co/martin-ha/t…

[13]colab.research.google.com/github/whyl…

[14]hub.whylabsapp.com/resources/d…

本文经原作者授权,由Baihai IDP编译。如需转载译文,请联系获取授权。

原文链接:

towardsdatascience.com/7-ways-to-m…

🚢🚢🚢欢迎小伙伴们加入AI技术软件及技术交流群,追踪前沿热点,共探技术难题~

本文转载自: 掘金

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

重温Java基础(二)之Java线程池最全详解

发表于 2023-12-28

1. 引言

在当今高度并发的软件开发环境中,有效地管理线程是确保程序性能和稳定性的关键因素之一。Java线程池作为一种强大的并发工具,不仅能够提高任务执行的效率,还能有效地控制系统资源的使用。

本文将深入探讨Java线程池的原理、参数配置、自定义以及实际应用。通过理解这些关键概念,开发者将能够更好地应对不同的并发场景,优化程序的执行效率。

首先,我们将介绍线程池的基本概念,解释它在并发编程中的作用和优势。随后,我们将深入研究Java线程池的工作原理,剖析其在任务提交、执行和线程管理方面的内部机制。

希望这篇文章能对您有所启发。如果您觉得有价值,欢迎点赞或转载,并请记得注明出处。

2. Java线程池的基础概念

在并发编程中,线程池是一种重要的设计模式,它能够有效地管理和复用线程,提高程序的性能和资源利用率。Java线程池作为Java并发包(java.util.concurrent)的一部分,为开发者提供了方便、高效的多线程处理方式。同时在阿里巴巴开发规范中,强制要使用线程池去提供线程,不允许在代码中显示的创建线程。

2.1 什么是线程池?

线程池是由一组线程组成的线程队列,它们在程序启动时就被创建并一直存在。这些线程可被用来执行提交到线程池的各种任务,从而避免为每个任务都创建新线程。这种机制能够降低线程创建和销毁的开销,提高系统性能。

2.2 线程池的工作原理

线程池的工作原理基于任务队列和线程管理机制。当任务被提交到线程池时,线程池会选择合适的线程来执行任务。如果核心线程数未达到上限,新任务可能会导致新线程的创建。如果核心线程已满,任务将被放入任务队列等待执行。当任务队列也已满,而同时线程数未达到最大线程数,新的任务将创建临时线程来执行。

2.3 线程池的优势

使用线程池的优势主要体现在以下几个方面:

减少资源消耗: 线程的创建和销毁是有开销的,线程池通过复用线程,减少了这些开销。

提高响应速度: 由于线程池中的线程一直存在,可以更迅速地响应任务的到来。

避免系统过载: 控制线程数量,防止系统因过多线程而过载。

3. Java线程池的工作原理

Java线程池的工作原理涉及线程的创建、任务的提交与执行,以及对线程的管理。深入理解这些机制对于优化并发程序至关重要。

3.1 线程池的创建与初始化

在程序启动时,线程池被创建并初始化。这一过程包括设置线程池的基本参数,如核心线程数、最大线程数、任务队列等。核心线程数是线程池中一直存活的线程数量,而最大线程数则是线程池允许创建的最大线程数量。例如创建一个固定核心线程数的线程:

1
ini复制代码ExecutorService executorService = Executors.newFixedThreadPool(corePoolSize);

其中参数corePoolSize即为核心线程数

3.2 任务的提交与执行

任务提交到线程池后,线程池会根据一定的策略选择线程来执行任务。首先,线程池会检查核心线程是否已满,如果未满,新的任务可能会导致新线程的创建。如果核心线程已满,任务将被放入任务队列。

在Java线程池中,任务的提交与执行有两个主要的方法:submit和execute。这两种方法有一些区别,主要体现在返回值、异常处理和任务包装上。

3.2.1 submit方法

submit方法用于提交实现了Callable接口的任务,它可以返回一个Future对象,通过该对象可以获取任务执行的结果,取消任务等。submit方法还可以接受实现了Runnable接口的任务,但它无法获取任务的执行结果。submit方法在ExecutorService中定义的,并定义了三种重载方式:

1
2
3
4
5
r复制代码<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);

具体使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码class MyCallable implements Callable<Integer>{  
    @Override  
    public Integer call() throws Exception {  
    return null;  
    }  
}

// 提交callable任务,可以拿到返回值
Future<Integer> future1 = executorService.submit(new MyCallable());

class MyRunnable implements Runnable {  
    @Override  
    public void run() {  

    }  
}
Future<Void> future2 = executorService.submit(new MyRunnable(), null);  
Future<?> future3 = executorService.submit(new MyRunnable());

主要特点:

  • 返回一个Future对象,可通过Future的get()方法可以获取到线程执行的返回值,get()方法是同步的,执行get()方法时,如果线程还没执行完,会同步等待,直到线程执行完成。
  • 可以接受Callable和Runnable类型的任务。
  • 执行Runnable和Callable的任务时,run()/call()方法没显式抛出异常。

3.2.2 execute方法

execute方法用于提交实现了Runnable接口的任务,它没有返回值,因此无法获取任务的执行结果。如果任务执行过程中抛出了异常,线程池会捕获并记录该异常,但无法通过execute方法获知。execute方法是在线程池的顶级接口Executor中定义的,而且只有这一个接口。

1
2
3
csharp复制代码public interface Executor {  
    void execute(Runnable command);  
}

使用:

1
2
3
less复制代码executorService.execute(() -> {  
    // 具体业务逻辑  
});

主要特点:

  • 没有返回值,无法获取任务的执行结果。
  • 只能接受Runnable类型的任务。

总的来说,submit方法更加灵活,适用于更多场景,而execute方法更加简单,适用于只关心任务执行而不需要获取结果的场景。在实际应用中,根据具体需求选择合适的方法。如果需要获取任务的执行结果、取消任务等,建议使用submit方法。只是执行任务而不关心返回值,可以使用execute方法。

4. 线程池的参数以及配置

Java线程池的性能和行为可以通过一系列参数进行调整,以满足不同的并发需求。ThreadPoolExecutor中提供的构造器如下:

ThreadPoolExecutor.png

4.1 七大参数

4.1 核心线程数(Core Pool Size)

核心线程数是线程池中一直存活的线程数量(即使它们处于空闲状态)。这些线程用于执行提交到线程池的任务。通过合理设置核心线程数,可以在系统负载增加时迅速响应任务。

4.2 最大线程数(Maximum Pool Size)

最大线程数定义了线程池中允许创建的最大线程数量。当核心线程都在执行任务,而新任务仍然到来时,线程池会创建新线程,直到达到最大线程数。超过最大线程数的任务会被拒绝。

4.3 线程存活时间(Keep Alive Time)

线程存活时间指的是非核心线程在空闲状态下的最大存活时间。当线程池中线程数量超过核心线程数时,空闲的非核心线程在经过一定时间后会被终止,从而释放系统资源。

4.4 TimeUnit

keepAliveTime的单位(ms、s…)

4.5 工作队列(Work Queue)

工作队列用于存放等待执行的任务。不同类型的队列对线程池的行为有重要影响,例如有界队列和无界队列。有界队列在任务数达到上限时会触发拒绝策略。

4.6 ThreadFactory

线程池中生成线程的工厂。默认使用默认工厂Executors.defaultThreadFactory()。但是实际使用时建议使用Guava的ThreadFactory自定义线程的名字,方便排查线程问题(阿里开发规范中也建议这么做)。如下:

1
ini复制代码ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("mythread-pool-").build();
4.7 拒绝策略(Rejected Execution Policy)

拒绝策略定义了当工作队列满,并且当前工作的线程数等于最大线程数时,后续再提交的任务如何处理。例如,可以选择抛弃任务、抛出异常或在调用线程中直接执行。Java线程池提供了几种常见的拒绝策略:

  • AbortPolicy(默认策略):

直接抛出RejectedExecutionException,阻止系统继续接受新任务,保持原有状态。

1
arduino复制代码new ThreadPoolExecutor.AbortPolicy();
  • CallerRunsPolicy:

将任务返回给调用者,由调用线程直接执行。

1
arduino复制代码new ThreadPoolExecutor.CallerRunsPolicy();
  • DiscardPolicy:

直接丢弃无法处理的任务,不抛出异常。

1
arduino复制代码new ThreadPoolExecutor.DiscardPolicy();
  • DiscardOldestPolicy:

当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去。

1
arduino复制代码new ThreadPoolExecutor.DiscardOldestPolicy();

4.2 线程池提交任务执行流程

4.2.1 执行流程

线程池执行流程.png

4.2.2 实例讲解

某银行柜台,共有5个窗口(Maximum Pool Size),平时常开2个窗口办理业务(Core Pool Size),银行大厅摆了5个椅子(Work Queue)供客户等待。银行规定当常开的窗口都在办理业务,并且大厅椅子上都坐满了客户,那么另外3个不常开的窗口也要打开办理业务。如果这3个窗口也都全部在办理业务,后面继续来银行办理业务的客户银行将拒绝办理。如果某个员工空闲下并且超过了5(Keep Alive Time)秒钟(TimeUnit)那么他就可以关闭窗口去休息。但是必须保留2个常开的窗口。

我们先按照上述流程创建一个线程池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码// 推荐使用Guava的ThreadFactory构建ThreadFactory,自定义线程名称 方便后续排查问题  
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("mythread-pool-").build();  
// 定义号线程  
ExecutorService executorService = new ThreadPoolExecutor(  
        // 核心线程数,即2个常开窗口  
        2,  
        // 最大的线程数,银行所有的窗口  
        5,  
        // 空闲时间  
        5,  
        TimeUnit.SECONDS,  
        // 工作队列  
        new LinkedBlockingQueue<>(5),  
        // 线程工厂  
        threadFactory,  
        // 拒绝策略  
        new ThreadPoolExecutor.AbortPolicy()  
);

(1)初始状态下,只有2个窗口为2个客户办理业务。

ThreadPool_演示1.png

(2)在客户1,客户2办理业务或者说常开窗口一直都有客户在办理业务,此时陆续有客户进来,需要在银行大厅的椅子上等待。

ThreadPool_图解2.png

(3)直到大厅的椅子都坐满。
ThreadPool_图解5.png

(4)此时如果在所有的窗口都在办理业务,大厅椅子坐满,如果再来一个客户,将开启3/4/5的窗口
ThreadPool_图解7.png

(5)此时如果在所有的窗口都在办理业务,大厅椅子坐满,还从外面再来2个客户办理业务,那么就需要把剩下的窗口都要打开去办理业务。

ThreadPool_图解8.png

(6)此时如果再来1个客户,就会按照线程池定义的拒绝策略去执行,比如我们设置策略为:AbortPolicy,就会抛出异常。

ThreadPool_图解9.png

4.3 线程池参数配置

线程池的配置参数在实际应用中需要根据具体的业务场景和性能需求进行巧妙调整。这就好比在日常生活中,如果有一个任务需要三人协同完成,但却有六人前来参与,就会造成三人的资源浪费;反之,若只安排两人协作,可能会超负荷而不切实际。因此,在线程池参数配置时,过小或过大都会带来问题。

当线程池数量设置过小时,面对大量同时到达的任务或请求,可能导致这些任务在任务队列中排队等待执行。甚至在任务队列达到最大容量时,无法处理额外的任务,或者导致任务队列积压,有可能引发内存溢出(OOM)问题。这明显是一个问题,因为CPU资源无法得到充分利用。

相反,若线程数量设置过大,大量线程可能会同时争夺CPU资源,导致频繁的上下文切换,从而增加线程的执行时间,影响整体执行效率。因此,在线程池配置中需要平衡线程数量,以满足高并发场景下的任务处理需求,同时避免不必要的资源争夺和上下文切换,以保障系统的稳定性和性能。

并没有一个通用的标准来设置参数,因此需要结合实际实战经验、业务需求以及服务器资源的状况,灵活而合理地进行参数配置。最终,合适的配置才是最为优越的选择。

当然也有一个简单而广泛适用的公式,可以用于确定线程池中的线程数:

  1. CPU 密集型任务(N+1):
* 对于消耗主要是CPU资源的任务,可以将线程数设置为N(CPU核心数)+1。额外的一个线程用于防止线程偶发的缺页中断或其他原因导致的任务暂停,防止空闲时间的浪费。一旦任务暂停,多出来的一个线程可以充分利用CPU的空闲时间。
  1. I/O 密集型任务(2N):
* 对于主要涉及I/O交互的任务,系统会在大部分时间内处理I/O,而在线程处理I/O的时间段内不会占用CPU。因此,在I/O密集型任务中,可以配置更多的线程,具体计算方法是2N。

那我们如何判断任务是CPU密集型还是IO密集型呢?简而言之,CPU密集型任务主要利用CPU计算能力,例如对内存中大量数据进行排序。而IO密集型任务涉及网络读取、文件读取等,其特点是CPU计算耗费的时间相对较少,大部分时间花在等待IO操作完成上。

但是我们在实际的业务中会发现,我们一个服务器上可能跑多种类型的业务,不太好判断到底是CPU密集任务还是IO密集型。我们可以根据监控服务线程池资源利用情况结合业务场景动态配制合理参数。这里我们就不得不提一下美团的线程池参数动态化配置:Java线程池实现原理及其在美团业务中的实践 - 美团技术团队。

5. 线程池的使用

日常开发中我们可以通过Executors去创建线程池,例如:

(1)newFixedThreadPool()

1
ini复制代码ExecutorService executorService1 = Executors.newFixedThreadPool(2);

创建固定线程数的线程池,核心线程数等于最大线程数,此时keepAliveTime失效 。但是他的工作队列的长度为Integer.MAX_VALUE。可能会导致堆积大量的请求,导致OOM。

(2)newSingleThreadExecutor()

1
ini复制代码ExecutorService executorService2 = Executors.newSingleThreadExecutor();

创建单线程的线程池,即核心线程数等于最大线程数均等于1,keepAliveTime失效 。但是他的工作队列的长度为Integer.MAX_VALUE。可能会导致堆积大量的请求,导致OOM。

(3)newCachedThreadPool()

1
ini复制代码ExecutorService executorService3 = Executors.newCachedThreadPool();

创建一个核心线程数等于0,并且允许创建的最大线程数等于Integer.MAX。keepAliveTime为60秒。可能会造成创建大量的线程,从而导致OOM。

(4)newScheduledThreadPool()

1
ini复制代码ExecutorService executorService4 = Executors.newScheduledThreadPool(2);

创建一个允许最大线程数等于Integer.MAX,但是他使用的阻塞工作队列是DelayedWorkQueue。DelayedWorkQueue的核心数据结构是二叉最小堆的优先队列,队列满时会自动扩容。所以最大线程数没有意义,线程池中永远会保持至多有核心线程数个工作线程正在运行。

注意: 以上创建线程池的方法,可以做自己Demo使用,不应该用在项目中。在阿里巴巴代码规范中,不支持使用这种方式去创建,支持手动创建线程池。

ThreadPool_阿里巴巴开发规范.png

6.总结

Java线程池是多线程编程中的重要工具,能够有效管理和复用线程,提高系统性能和资源利用率。本文深入探讨了线程池的基础概念、工作原理、参数配置、自定义以及使用示例,并强调了注意事项。

通过了解线程池的工作原理,开发者可以更好地配置线程池以适应不同的并发需求。自定义线程池则使得线程池更灵活地适应特定业务场景。在实际应用中,要谨慎选择线程池类型、合理配置参数、注意任务的生命周期和线程安全等问题,以确保系统的稳定性和性能。

参考文献

1、Java线程池实现原理及其在美团业务中的实践 - 美团技术团队

2、《Java并发编程实战》

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

本文转载自: 掘金

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

开源的局域网文件共享工具更新到v10啦

发表于 2023-12-25

shigen坚持更新文章的博客写手,擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长,分享认知,留住感动。

hello,伙伴们,趁着晚上八点休息的时间,我看了一下开源的局域网文件共享工具的代码并实际使用测试了一下,发现出了两个比较严重的问题:

  • 中文的文件名无法正常的保存
  • 后端的保存进度条没有实际的意义
  • 代码风格的优化

于是就这两个问题,在代码上做了进一步的优化。不清楚shigen的开源项目的伙伴,可以先参考一下这篇文章:开源一个局域网文件共享工具,里边有详细的介绍和使用,也希望能帮到大家。

下边我们还是开始今天的内容,我的项目更新的情况:

文件浏览器的更新

在本次更新中,带有中文的文件名都是可以正常的保存在文件夹下,并可以提供正常的下载。

对于保存的进度方面,shigen发现前端的界面其实就有保存的进度条了,而且还是实时展示的,后端的保存进度条其实是一下子直接到100%,反而并不起到实际的作用。所以,此次的保存直接省去了进度条操作,移除了tqdm组件。

在最后呢,shigen再次对于主体的python代码样式做了进一步的优化,函数的定义上加上了类型的约束,其实在一些python编码规范里,这些都是很常见的要求。shigen也是在接触代码规范之后,开始严格要求自己的每一行代码的,一起来看看优化之后的效果吧:

优化之后的代码

每一点的更新,其实都是饱含了对于开源生态的热爱和探索,希望能用自己的技术拥抱开源,提升生产效率。

也期待伙伴们的意见和建议,可以在文章的评论区或者github的issues上提出来,shigen都会积极的阅读并采纳的。


以上就是今天分享的全部内容了,觉得不错的话,记得点赞 在看 关注支持一下哈,您的鼓励和支持将是shigen坚持日更的动力。同时,shigen在多个平台都有文章的同步,也可以同步的浏览和订阅:

平台 账号 链接
CSDN shigen01 shigen的CSDN主页
知乎 gen-2019 shigen的知乎主页
掘金 shigen01 shigen的掘金主页
腾讯云开发者社区 shigen shigen的腾讯云开发者社区主页
微信公众平台 shigen 公众号名:shigen

微信搜一搜

与shigen一起,每天不一样!

本文转载自: 掘金

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

你一定要知道的「React组件」两种调用方式

发表于 2023-12-25

前言

使用React框架的开发过程中,我们常常会使用两种方式调用组件:一种是组件式,另一种是函数式,但是这两种用法究竟有何不同?

一、代码示例

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
jsx复制代码// 当作普通组件使用
function MyComponent() {
return <div>Hello Guming!</div>
};

function App() {
return (
<div className="App">
<MyComponent />
</div>
);
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
jsx复制代码// 当作函数使用
function MyComponent() {
return <div>Hello Guming!</div>
};

function App() {
return (
<div className="App">
{MyComponent()}
</div>
);
}

export default App;

这是一个简单的例子,还没有涉及到状态。在App.jsx文件中,声明了一个MyComponent组件,并在App入口函数中使用。我们知道,上述代码并不能直接执行,需要先被babel插件编译。

编译后的结果:

1
2
3
4
5
6
7
8
9
10
11
jsx复制代码// 当作普通组件使用
function MyComponent() {
return React.createElement("div", null, "Hello Guming!");
};

function App() {
return React.createElement("div", {
className: "App"
}, React.createElement(MyComponent, null));
}
export default App;
1
2
3
4
5
6
7
8
9
10
11
jsx复制代码// 当作函数使用
function MyComponent() {
return React.createElement("div", null, "Hello Guming!");
};

function App() {
return React.createElement("div", {
className: "App"
}, MyComponent());
}
export default App;

React.createElement的作用是创建ReactElement对象,它接收三个参数:

  1. type — 元素类型;
  2. config — 元素属性;
  3. children — 元素子节点;

在两种用法当中,我们可以看到经过babel插件编译后的区别在于函数的最后一个参数。
当我们使用组件的方式时,babel插件会将“”编译成React.createElement(MyComponent, null);而对于函数的用法,则变成了一次函数调用,而执行后,会返回

1
csharp复制代码React.createElement("div", null, "Hello Guming!")

二、生成Fiber树结构的不同

我们知道,React会创建Fiber树,而Fiber节点创建的依据就是ReactElement对象。每次更新时,React都会创建一个新的workInProgress树,期间包d含diff和复用的逻辑(后面会提到),然后根据workInProgress树依次创建dom节点,并将根节点的current指向新建的树的根节点(RootFiber)上。
render阶段的工作,可以分为“递”(beginWork)与“归”(completeWork)。在”递”阶段,React会根据依次根据当前节点创建其子节点,直至构建出完整的Fiber树;而在“归”阶段,则是进行一些元素标记和标记的“冒泡”。在mount和update时,这“递”与“归”流程也有所不同,大体如下图所示:

“递”阶段

“归”阶段

回到例子当中来,在创建好React Element之后,接下来需要构建workInProgress树。相比函数式的使用方式,使用组件的方法多创建了一个Fiber节点:

MyComponent组件的Fiber节点
CCD8FECD-3754-4F3F-BAB1-3AE017E1F5DF.png
从上图可以看出来,这个节点的type(类型)就是MyComponent函数地址。

在completeWork时,会根据Fiber创建对应的dom节点,由于MyComponent对应fiberNode的tag为FunctionComponent,因此在执行完bubbleProperties函数后会不会创建dom element而是直接返回null。bubbleProperties函数主要作用是节点属性的冒泡。
7686C8DF-BDD9-4CF5-BFCC-327640B90F5E.png

三、组件的函数执行的时机不同

对于FunctionComponent,生成的fiberNode的type就是组件的执行函数本身。
在beginWork时,如果是在mount时,则会调用mountIndeterminateComponent函数;如果是在update时,则会调用updateFunctionComponent函数,而在两个函数执行过程中,都会调用renderWithHooks这个函数。而在renderWithHooks函数中,有这样一段代码:
07AC4C6D-14CA-4485-81D3-0D3C54792B5F.png
而Component就是组件函数本身。在renderWithHooks函数打上断点,我们可以发现:
对于组件使用的方式,当前处于beginWork的节点是MyComponent的fiberNode:
E8204C97-6635-4B76-AC73-080391C9F264.png
而对于函数使用的方式,当前处于beginWork的节点是App对应的fiberNode:
C0200A86-664A-4915-A240-A1EC7591A333.png

四、进行某些算法比较时的流程不同

1.bailout策略-节点复用

在update时,出于性能考虑,React会进行一些优化。在update时,如果满足以下条件:

  • 新旧节点的属性相同
  • 当前节点的类型没有发生改变(例如没有从div变成p)
  • 当前节点不存在更新
  • context没有发生变化(由于代码中没有使用Context相关API,因此这个条件一直满足)

那么React会对子节点进行复用,如果当前节点的子节点不存在任何更新,则会跳过整个子树的beginWork。
有一个点需要注意,在对比新旧节点属性时,由于每次render时,创建的JSX对象都是新的地址,因此新旧节点属性比较并不相同,除非当前节点的父节点满足bailout逻辑,复用了当前节点。

接下来改造一下代码,引入状态更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
jsx复制代码import { useState } from "React";

// 当作普通组件使用
function MyComponent({count}) {
return <div>I has drunk {count} guming milky tea!</div>
};

function App() {
const [num, setNum] = useState(0);

return (
<div className="App" onClick={() => {setNum(1)}}>
<MyComponent count={num}/>
</div>
);
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
jsx复制代码import { useState } from "React";

// 当作函数使用
function MyComponent(count) {
return <div>I has drunk {count} guming milky tea!</div>
};

function App() {
const [num, setNum] = useState(0);

return (
<div className="App" onClick={() => {setNum(1)}}>
{MyComponent(num)}
</div>
);
}

export default App;

(1).我们第一次点击文字时,会时num值加1,此时第一个进入beginWork的节点是HostFiberRooter,此时肯定满足以下条件:

  • 新旧节点的属性相同(全等)
  • 当前节点的类型没有发生改变(例如没有从div变成p)
  • 当前节点不存在更新
  • context没有发生变化

因此App对应的fiberNode被复用。

(2). 接下来进入App对应fiberNode的beginWork,由于我们点击文字触发了更新,因此App存在更新,不满足bailout逻辑。

(3). 再创建完div对应的fiberNode后,进入div的beginWork。由于其父节点App没有被复用,因此在比较当前节点的新旧属性时不相等,不满足bailout逻辑。

(4). 对于组件使用的方式,由于多了一个MyComnonet节点,因此下一个进入beginWork的是MyComponent。由于其父节点div没有被复用,并且节点属性发生变化(count属性由0变为1),因此不会复用子节点;对于函数调用方式,下一个beginWork的节点则是div,同样由于副节点未被复用,导致在比较oldProps和newProps时不相同。

(5).除了MyComponent,由于两者之后的fiber树的构造相同,后面节点的beginWork流程也是一样,同样无法被复用;

2.diff算法

在beginWork时,还设计到diff,在React源码中,主要diff逻辑reconcileSingleElement方法中。网上已经存在很多关于React diff算法的文章,这里不过多讲解,有兴趣的读者可以参考这篇:
【动图+大白话🍓解析React源码】Render阶段中Fiber树的初始化与对比更新~ - 掘金

以下是reconcileSingleElement函数的内容

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
javascript复制代码function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
var key = element.key;
var child = currentFirstChild;

while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
var elementType = element.type;

if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling);
var existing = useFiber(child, element.props.children);
existing.return = returnFiber;

{
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}

return existing;
}
} else {
if (child.elementType === elementType || ( // Keep this check inline so it only runs on the false path:
isCompatibleFamilyForHotReloading(child, element) ) || // Lazy types should reconcile their resolved type.
// We need to do this after the Hot Reloading check above,
// because hot reloading has different semantics than prod because
// it doesn't resuspend. So we can't let the call below suspend.
typeof elementType === 'object' && elementType !== null && elementType.$$typeof === REACT_LAZY_TYPE && resolveLazy(elementType) === child.type) {
deleteRemainingChildren(returnFiber, child.sibling);

var _existing = useFiber(child, element.props);

_existing.ref = coerceRef(returnFiber, child, element);
_existing.return = returnFiber;

{
_existing._debugSource = element._source;
_existing._debugOwner = element._owner;
}

return _existing;
}
} // Didn't match.


deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}

child = child.sibling;
}

if (element.type === REACT_FRAGMENT_TYPE) {
var created = createFiberFromFragment(element.props.children, returnFiber.mode, lanes, element.key);
created.return = returnFiber;
return created;
} else {
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);

_created4.ref = coerceRef(returnFiber, currentFirstChild, element);
_created4.return = returnFiber;
return _created4;
}
}

F5B43DB4-6D8F-45E8-859D-AF55825DB272.png
当执行到MyComponent对应fiberNode的beginWork时,我们可以看到,其child属性为null,并不会进入下面的while循环,因此MyComponent是不参与diff比较的。
在update时,completeWork工作主要是针对前后属性有变化的节点进行标记,MyComponent对diff的结果没有影响,所以对最后diff的结果并没有影响。

五、结论

本文通过几个例子,简单讲解了用组件方式和函数方式使用自定义组件的三个区别:

  • 在创建workInProgress树时,组件使用的方式会比函数使用的方式多一个fiber节点;
  • 在组件函数执行的时机不同;
  • 在进行diff算法比较时,MyComponent对应的节点会被忽略;
  • 在bailout的复用逻辑中,尽管复用时多了一次节点比较,但是并不会影响最终的结果;

React的流程都是基于Fiber的,而上述差异都是基于此点。尽管两种使用方式生成的Fiber树结构不相同,但是对于React核心的流程并没有太大影响。当然,实际的使用情况远远比本文中提出的例子更复杂,本文并没有针对所有场景和流程逐一分析。

最后

📚 小茗文章推荐:

  • 中后台业务开发(一)「表单原理」
  • 手摸手教运营小姐姐搭建一个表单
  • 前端开发者需要了解的「设计体系」

关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~

本文转载自: 掘金

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

跟着图走,学支付:在线支付系统设计的图解教程 1 基本概念

发表于 2023-12-24

这是《百图解码支付系统设计与实现》专栏系列文章中的第(1)篇。

本系列文章是偏实战的,本章内容讲清楚支付系统是什么,主要解决什么问题,部分核心流程,以及一些后面会频繁使用到的术语。至于支付起源,在线支付发展历程等知识,感兴趣的同学可以参考网络上其它文章或书籍。

专栏地址: 百图解码支付系统设计与实现

  1. 基本概念

下面描述的概念大部分做了极致简化,只是用于入门,对于理解概念应该是够用的。真实的实现会复杂非常多,后面的系列文章会展开做详细说明。

后面的描述中,经常混着用“支付系统”、“支付平台”,本质是一个东西。在内部来说,就是一个支付系统,但从和外部机构交互来说,就是一个支付平台。

1.1. 最简支付流程

说明:

  1. 这是一个最简化的支付流程。真实的交互比这个复杂得多,单收银台渲染就可以写一整篇文章。但对于讲清楚支付系统的作用,已经足够。
  2. 从图中可以引申出支付系统最核心的作用:帮商户收钱。
  3. 有支付当然就有退款、撤销等逆向操作,复杂的跨境支付还会有外汇交易,跨境结算等业务。这些全部在后面的系列文章中细讲。

1.2. 最简清结算流程

说明:

  1. 这里画的是信息流。
  2. 银行和支付平台之间是机构对机构的关系,通常使用清算概念,因为金融机构之间大部分情况下会有独立的清算机构做清算服务。
  3. 支付平台和商户之间,通常使用结算概念,由支付平台直接打款给商户。
  4. 上面画的是结算到商户开在支付平台的内部账户余额,所以需要商户手动提现,支付平台通常也支持直接结算到卡,这样就不需要商户手动提现。
  5. 清结算三个字还有另外一层含义:清分 + 结算。前者是把钱算清楚,后者是真实打款。

1.3. 最简本对本收单流程

说明:

  1. 所谓本对本收单,就是指商户的商品标价币种、向支付系统的下单币种、用户支付币种、商户结算币种都是同一个币种。不涉及到外汇交易。
  2. 一个中国人拿着中国招商银行信用卡在淘宝或京东买东西,就是标准的本对本收单。

1.4. 最简跨境收单流程

说明:

  1. 所谓跨境收单,就是结算给商户的币种和用户支付的币种不一样,需要经过外汇机构换汇。
  2. 在扣款EUR成功后,支付平台会调用外部的外汇机构进行锁汇(HA)。
  3. 在银行清算后,支付平台再调用外部的外汇机构进行真正的换汇(TA)。
  4. 最后支付平台结算给商户USD。

如果换成时序图,如下:

1.5. 最简信息流与资金流

说明:

  1. 用户在支付平台充值10元,支付平台向银行发起扣款请求,这些指令操作归属于信息交互,属于信息流。
  2. 真实资金流:银行账户余额的变动。比如:银行在内部把用户的余额减10元,给支付平台备付金账户加10元。
  3. 虚拟资金流:支付平台内部账户余额的变动。比如:支付平台内部把银行应收账户加10元,给用户余额账户加10元。
  4. 为什么会有真实资金流和虚拟资金流之分?因为我们真正能拿到钱的地方是银行,在支付系统内看到的只是一个数字,如果想变成真实世界的钱,还得发给银行提现。

1.6. 跨境收单的协议关系

说明:

  1. 这只是跨境收单的一种协议关系,真实场景存在多种形态。
  2. 上述的收单机构是持牌的,但是没有跨境结算的能力,所以需要委托有跨境结算牌照的金融机构代为处理跨境结算业务。
  3. 跨境电商平台只是一个商户平台,没有收单资质,所以需要委托收单机构给它下面的供应商结算打款。
  4. 剩下的协议关系都是一目了然的,只是我们日常没有注意。比如用户和电商平台之间在注册时就会有会员协议要签署。
  5. 特殊的情况下,一些实力雄厚的机构,比如蚂蚁或财付通,下面会成立多个实体(不同的法律主体),然后用不同的实体去申请不同的牌照(收单、银行、外汇、跨境代发等),这样表面上全部是一家公司搞定,但是实际的协议关系仍然是上面这样的,在各实体之间仍然需要签署各种协议。
  6. 如果是本对本收单场景就简单很多,没有外汇和跨境结算这一层关系,如果跨境电商的货品全部是电商实体自营的,那就更简单,没有供应商委托结算的协议。
  7. 一般电商平台在没有牌照情况下是不能开设余额账户的,如果电商想开通余额,可以委托第三方有牌照的公司托管(通常也是收单机构,收单机构一般会同时申请PA、PG牌照),这种情况下,电商平台和收单机构还会签署账户委托协议。

1.7. 跨境资金方案

说明:

  1. 这是一个典型的跨境资金流案例。用户支付USD,收单机构收到的是USD,但是需要结算RMB给中国境内的商户。
  2. 收单机构(也就是支付平台)需要先将USD兑换成CNH(离岸人民币),再由入境代发机构把RMB结算给中国境内商户。这是所谓的“结汇入境”。
  3. 如果采用“入境结汇”的方式,则收单机构直接结算USD给商户在境外的银行账户中,由商户以USD汇入境内,再兑换成RMB。或者收单机构先把USD汇入境内备付金账户,再兑换成RMB,然后再结算RMB给中国境内商户。
  4. 以上这些不同的资金处理方案,统称为资金方案。

1.8. 简明复式记账

金融机构的记账一定是基于复式记账法。下面以用户通过支付平台使用银行支付500块为例做个简要说明。

假设:支付平台使用CMB做为收单行,在CMB开设有备付金账户。

涉及的支付平台内部账户:

账户类型 账户 备注
借记账户 应收-渠道-CMB 应收归属借记账户
贷记账户 应付-过渡-网关过渡户应付-平台托管-商户待结算应付-平台托管-商户余额手续费收入-商户-消费 应付归属贷记账户手续费意味着所有者权益增加,归属贷记账户

记账步骤:

阶段 操作账户 金额
第一步资金从渠道到网关过渡户 借:应收-渠道-CMB贷:应付-过渡-网关过渡户 500
第二步扣除手续费 借:应付-过渡-网关过渡户贷:手续费收入-商户-消费 10
第三步网关过渡户到商户待结算账户 借:应付-过渡-网关过渡户贷:应付-平台托管-商户待结算 490
第四步结算给商户 借:应付-平台托管-商户待结算贷:应付-平台托管-商户余额 490

说明:

  1. 支付系统的记账一定是复式记账法。内部开设了很多账户和科目。

【借记类】账户:资产,应收款等;

【贷记类】账户:负债,所有者权益,应付款等;

  1. 借贷简要公式(不太严谨,但是够用):

【借记类】账户(如资产,应收款),【增加】为【借】,【减少】为【贷】;

【贷记类】账户(如负债和所有者权益,应付款),【增加】为【贷】,【减少】为【借】;

  1. 复式记账的专业书籍很多,这里只摘录几个重要的说明:

复式记账法定义:对每项经济业务按相等的金额在两个或两个以上有关账户中同时进行登记的方法。

记账原则:有借必有贷,借贷必相等。

记账依据:会计恒等式:1. 资产 = 负债 + 所有者权益;2. 利润 = 收入 - 费用。

账户:具有一定格式和结构,能够用来连续、系统、全面的记录反映某种经济业务的增减变化及其结果。

科目:同类财务交易的分类,比如资产、负债、所有者权限、收入或费用等都属于科目。一般科目会分为多级。

账户和科目的区别:科目只有名字,账户包括结构和格式,每个账户对应一个特定的科目。

  1. 概要设计

2.1. 简明产品架构图

说明:

  1. 这个图画得比较简单,但是已经涵养一个支付系统最核心的产品能力。
  2. 上面部分是会员或商户感知的产品能力,包括门户、收银台,收单产品,资金产品等。下面部分是支付系统最核心的服务,用于支撑对外的产品能力。

2.2. 极简支付系统架构图

说明:

  1. 这个图很精简,但是基本已经够用,应付本对本交易这种简单的业务是完全没有问题的。
  2. 一些复杂的支付系统可能还有外汇、额度中心、产品中心、卡中心等,甚至一个子系统可能会拆分为多个应用独立部署,比如收单结算就可以拆成收单和结算两个独立的应用。

2.3. 完整支付系统架构图及各子系统简介

说明:

  1. 这是一比较完整的系统架构图,属于逻辑划分。在单体应用中,就是一些模块,在分布式应用中,就是一些子域、子应用或子系统。
  2. 以下是各子系统简单介绍:
    1. 开放网关:主要对接商户,比如下单、支付等接口入口。通常要求有比较高的安全性。部分公司可能会把移动端网关、PC门户网关、商户通知等能力集成在开放网关,也可能会单独拆出部署。
      1. 收单结算:负责把商户的单收下来,并给商户发起结算。承担的收单产品包括有:线上收单,线下收单,担保交易、即时到账等,每个公司的商业策略不同,开出的收单产品会有差异。
      2. 资金产品:承担无买卖标的的纯资金转移能力。典型的有:充值、转账、提现、代发。和支付的区分在于支付是有买卖标的,而资金产品没有。也就是在系统中没有买卖记录发生,但在线下可能有。
      3. 收银核心:渲染可用支付方式。包括查询账户是否有余额,查询营销是否有营销券,查询渠道网关是否有可用的外部渠道,最后组合成可用支付方式,供前端渲染。
      4. 支付引擎:负责真正的扣款或转账。有些公司叫支付核心,或资产交换。个人认为资产交换更合适,因为无论对于支付、退款、充值、转账等各种交易,本质都是把资产从一个账户交换到另外一个账户。
      5. 渠道网关:负责去外部渠道扣款。通常还会提供渠道路由、渠道咨询等能力,做得细的公司可能下面再细分为渠道产品,报文网关和文件网关。
      6. 会员平台:管理会员的注册、登录、密码、实名认证等。
      7. 商户平台:管理商户的入驻、登录、交易管理等。
      8. 产品中心:管理平台对外提供的产品能力。一般大的支付系统才会独立成一个子系统。
      9. 资金账务:负责账户开立,记账等。
      10. 会计中心:会计科目管理、分录管理、日切管理。
      11. 对账中心:负责明细对账和资金对账。
      12. 营销平台:提供满减、红包等营销工具。
      13. 风控平台:针对账户和交易,提供实时、离线风控,控制平台的风险。
      14. 运营平台:订单管理、渠道管理、产品管理等综合运营工具。
      15. 数据平台:主要用于数据汇总和分析。分布式部署后,数据都在各子系统中,需要汇总到数据平台用于经营分析。
      16. 卡中心:负责管理用户的绑卡信息。需要经过PCI认证。
      17. 额度中心:累计用户、商户的额度,通常有日、月、年等各种分类。
      18. 外汇平台:负责外汇报价和兑换。
      19. 流动性与调拨中心:一些跨境支付公司,在多个国家多个银行有头寸,各头寸之间经常需要做流动性管理,提高资金利用率。
      20. 差错中心:负责差错处理。比如渠道退款失败,需要通过其它的方式退给用户。
      21. 拒付中心:处理用户的拒付和举证。在跨境支付场景下,信用卡用户联系发卡行说卡被盗刷或商品没有收到,或商品有问题等,拒绝支付给商户。

2.4. 核心系统依赖图

说明:

  1. 图中画得比较清楚了,没有太多需要补充的。
  2. 其中红色线为支付主链路。
  1. 常见术语索引

参考“传送门”中的“行业黑话与术语”篇。

  1. 结束语

本章主要讲了一些支付相关的基本概念,支付系统的概要设计框图,部分核心流程,以及一些常见的术语,让同学对支付系统有一个整体的理解。

下一章节将围绕面向商户的收单和结算,请清楚如何给商户收单,又是如何结算给商户的。

  1. 传送门

支付系统设计与实现是一个专业性非常强的领域,里面涉及到的很多设计思路和理论也可以应用到其它行业的软件设计中,比如幂等性,加解密,领域设计思想,状态机设计等。

在《百图解码支付系统设计与实现》的知识宇宙,每一篇深入浅出的文章都是一颗既独立但又彼此强关联的星球,有必要提供一个传送门以便让大家即刻到达想要了解的文章。

专栏地址 : 百图解码支付系统设计与实现

领域相关:

行业黑话与术语:支付行业黑话:支付系统必知术语一网打尽

基本概念与概要设计:跟着图走,学支付:在线支付系统设计的图解教程

收单结算设计:支付交易的三重奏:收单、结算与拒付在支付系统中的协奏曲

技术专题:

与数据库自增ID不同的业务ID:交易流水号的艺术:掌握支付系统的业务ID生成指南

签名验签:揭密支付安全:为什么你的交易无法被篡改

加密解密:金融密语:揭秘支付系统的加解密艺术

日志格式设计规范:支付系统日志设计完全指南:构建高效监控和问题排查体系的关键基石

幂等性设计:避免重复扣款:分布式支付系统的幂等性原理与实践

本文转载自: 掘金

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

DDD模式

发表于 2023-12-24

浅谈一下对于DDD模式的理解,相互学习交流,不对之处欢迎大家指正。

在说到DDD(Domain-Driven Design)设计模式之前,先要说下我们在对系统进行架构设时需要遵循的几个原则:

  1. 单一职责(SRP)
1. "单一职责原则"(Single Responsibility Principle,SRP)是面向对象编程中的一项基本原则,它是SOLID原则中的一部分。单一职责原则的核心思想是:’**一个类或模块应该有且仅有一个引起它变化的原因。**


简而言之,这意味着一个类或模块应该只有一个职责,只负责一种类型的任务或功能。如果一个类有多个职责,那么它变得不稳定,难以维护,容易引发错误。
  1. 开闭原则(OCP)
1. "开闭原则"(Open-Closed Principle,OCP)是面向对象编程中的设计原则之一,它是SOLID原则中的一部分。开闭原则的核心思想是:


**软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。**


这意味着一个软件实体在不修改其源代码的情况下应该能够通过扩展来添加新的功能或行为。换句话说,应该能够通过添加新的类、模块或函数来扩展软件,而不需要修改现有的代码。这有助于保持现有代码的稳定性和可维护性。
  1. 里式替换(LSP)
1. 里式替换原则(Liskov Substitution Principle,LSP)是面向对象编程中的一个重要原则,它是SOLID原则中的一部分。里式替换原则由计算机科学家Barbara Liskov提出,其核心思想是:


**子类型必须能够替换其基类型(父类型)而不影响程序的正确性。**


这意味着在使用继承的情况下,子类应该能够完全替代父类,而不引发错误或不一致的行为。简单来说,如果一个类是另一个类的子类,那么它应该具有与父类相同的行为,或者说,子类应该继承父类的所有约定和行为。
  1. 接口原则(ISP)
1. "接口隔离原则"(Interface Segregation Principle,ISP)是SOLID原则中的一项,它关注的是接口的设计和使用。接口隔离原则的核心思想是:


**一个类不应该被强迫依赖它不使用的接口。**


这意味着接口应该足够小,以便客户端只需了解与其相关的方法,而不需要了解不相关的方法。接口隔离原则的目标是避免“胖接口”(Fat Interface)和“依赖过多”的问题,以提高代码的可维护性和灵活性。
  1. 依赖倒置(DIP)
1. "依赖倒置原则"(Dependency Inversion Principle,DIP)是SOLID原则中的一项,它关注的是依赖关系的管理和解耦。依赖倒置原则的核心思想是:


**高层模块不应该依赖于低层模块。两者都应该依赖于抽象。**


这意味着在软件设计中,应该将高级别模块(例如,业务逻辑或应用程序逻辑)和低级别模块(例如,具体的实现细节或服务)都依赖于抽象接口或抽象类。高层模块和低层模块都应该依赖于共同的抽象,而不是彼此直接依赖。

我们在进行架构设计时说白了就是通过架构设计时软件系统可以使用尽可能少的人力成本去完成用户需求。而DDD模式主要是为了解决系统老化后,各种业务逻辑混杂,导致维护成本大大增加,明明一个很小的功能往往要花费很高的成本。系统设计的目前就是:高内聚、低耦合。

DDD模式首先需要根据业务情况,把系统划分为多个隔离的领域,领域与领域直接是分隔的,我们把这种分隔叫做 “限界上下文”(Bounded Contexts),领域之内应该要完成充分的自治,领域与领域之间需要通过接口层进行交互,接口层可以表现为:http接口、rpc调用、mq消息的定于 等等。每个领域只能处理自己领域内的事情,领域外的事情应该调用别的领域的接口进行处理。例如:我们可以把一个商城中的订单模块设计为订单领域、商品模块设计为商品领域。那么当用于进行下单扣减库存时,订单领域需要调用商品领域的接口完成实现,而不是之间操作商品库存数据。

边界设计好了之后,根据微服务的设计,个人理解我们可以把这一个个领域划分为一个个微服务。那么在这个大领域内我们也需要按DDD4层架构进行设计。DDD中把系统划分了四个层级分别为:用户接口层、应用服务层、领域层、基础设施层。对于我们之前的MVC架构,我理解是在servcie层面进行了跟细致的划分,为了高内聚、低耦合。

用户接口层,也称为表示层,这是与用户交互的界面层,包括用户界面、Web界面、API等。它主要负责用户输入的接收和显示,以及向应用程序传递用户请求。

应用服务层主要是完成对领域服务的组织和编排完成需求的流程和组织。应用层是业务逻辑的主要驱动者,它负责接收来自用户界面层的请求,协调领域层和基础设施层的工作,并执行具体的应用用例(Use Cases)。应用层通常包含应用服务(Application Services)和应用用例(Use Case)。

领域层就是以实体的维度根据业务要求进行业务实现。领域中的实体是充血模型。什么是充血模型了,如果我们有一个订单类,那么这个类里面只有订单各个字段的各个set get方法,我们把他成为贫血模型。如果我们把订单的业务操作也放入到实体里面,例如:订单的生成、订单的更新、订单的删除、订单相关所有的业务操作都放入到这个订单实体中,那么这个订单实体就称为充血模型。当我们同时要操作多个订单实体的能力,如果同时更新两个订单,这么这项业务能力放在实体里面就不合适了,那么我们需要定义一个服务,去同时操作这两个对象,那么对这个服务我们称之为领域服务。领域层是唯一进行业务逻辑实现的层。领域层的能力往往需要通过基础设施层实现,如果一个写库操作,具体的数据库操作会在基础设施层,那么领域层需要依赖基础设施层接口去完成具体的操作。这个接口类称之为仓库:Repository。 领域层内的实体对象创建往往是比较复杂的,往往我们可以通过工厂去灵活的创建实体对象。领域层是DDD的核心,它包含了领域模型、实体、值对象、聚合、仓库、领域服务等,用于表示应用程序的核心业务逻辑。领域层负责处理领域规则、状态管理和业务规则的执行。

基础设施层包括所有与技术和外部资源交互的组件,例如数据库、消息队列、外部服务、文件系统等。它提供了与这些资源进行通信的接口,并为应用程序提供支持,如数据访问、日志记录、安全性等

DDD.jpg
六边形架构又是啥呢?

六边形架构,也称为端口与适配器架构(Port and Adapter Architecture)或六边形架构模式,是一种软件架构模式,旨在将系统划分为多个不同的层次,并以六边形的形状来表示其结构。这个模式的目标是实现系统的高内聚、低耦合和可测试性,同时使系统更易于扩展和维护。

六边形架构的主要组成部分包括:

  1. 应用层(Application Layer) :应用层是系统的核心,包含了应用程序的业务逻辑和用例(Use Cases)。它负责协调和组织领域层中的领域对象来满足外部请求。
  2. 接口适配器(Interface Adapters) :接口适配器位于系统的外部,与外部世界(用户界面、外部服务、消息队列等)进行交互。接口适配器将外部请求转换为应用层可以理解的形式,并将应用层的响应适配为外部期望的格式。
  3. 端口(Ports) :端口是接口适配器与应用层之间的接口定义,用于定义外部请求如何访问应用层的业务逻辑。端口通常是抽象的,不包含具体实现。
  4. 领域层(Domain Layer) :领域层包含了领域模型、实体、值对象、聚合根、仓库等,用于表示应用程序的核心业务逻辑。领域层负责处理领域规则和业务规则。
  5. 适配器(Adapters) :适配器负责将外部依赖(例如数据库、外部服务、文件系统)适配为应用程序可以使用的形式。适配器包含了与外部依赖交互的具体实现。
  6. 依赖关系:依赖关系通常是从外部适配器指向应用层和领域层,表示外部请求通过适配器进入系统,然后由应用层和领域层处理。

六边形架构模式的核心思想是将核心业务逻辑(领域层)与外部依赖(接口适配器和适配器)分离开来,使系统更易于测试、扩展和维护。这种分离也有助于将外部依赖的变化隔离在适配器中,不会对核心业务逻辑造成影响。

这个模式的名字来源于图形表示,其中中心的应用层和领域层被描绘为六边形的中心,而外部接口和适配器则位于六边形的边缘,形成了一个六边形的形状。这种图形表示有助于清晰地理解架构的结构。六边形架构也与领域驱动设计(DDD)和依赖倒置原则(Dependency Inversion Principle)紧密相关。

六边形和DDD的关系是什么,我理解6变形架构 和 DDD设计模式 有相同之处,叫法不同其核心都是为了 高内聚、低耦合。

端口:DDD中的接口层

Adapters :可以理解为接口层中VO->DTO的转换

应用层:应用服务层

领域层:领域层

适配器:基础设施层

DDD2.jpg
因为DDD模式往往需要较大的人力成本,比较适合核心和复杂的业务。而MVC这种清晰的分层模式往往效率比较高,适合非核心业务。我们在架构设计可以考虑这两种架构结合使用。需要考虑根据业务进行架构设计。技术本身需要适配业务才有其价值。架构的核心就是以最小的人力成本完成业务需要,适合的架构设计 需要考虑 成本 系统老化 人员能力 等各方面因素,不能一概而论。

本文转载自: 掘金

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

史上最坑爹的Java代码:Hello, World!

发表于 2023-12-24

史上最坑爹的Java代码:Hello, World!

大家好!我是老码农。

《码农说》公众号的第一篇文章我们先从:Hello, World!聊起!

Hello, World!

Hello, World!是很多Java开发人员写的第一程序,可就是这第一个程序,就把我们华丽丽带到了沟里。

(其实也不怪咱们开发人员,主要是好多教材的第一个程序都是Hello, World!)

一日掉沟里,很多年还一直在沟里趴着,没出来的同学不在少数。

看下面的程序,再熟悉不过了吧。

1
2
3
4
5
6
7
typescript复制代码 package com.coderoldgeek.farmer.examples;
 
 public class Helloworld {
     public static void main(String[] args) {
         System.out.println("Hello, World!");
    }
 }

原谅我以这样的方式跟大家打招呼,这段代码真的很坑人。

坑点在第5行。

自查

有心人可以先自查下自己团队的代码,代码中有没有这个以System.out.println开头的灵魂语句。

1
csharp复制代码 System.out.println("Hello, World!");

如果项目工程中有很多类似System.out.println灵魂语句,但你的项目还没出现过任何问题。

恭喜你,你现在项目的业务并发性十有八九并发性不高。

如果你所处的项目正处在高速发展期,订单量一日千里,那请你立刻马上删除掉这些代码。

对,是立刻马上。

要不哪天你的leader让你拎包走人,你还不知道啥原因就太out了。

看源码

打开System.out.println方法的源码,PrintStream类的println方法

1
2
3
4
5
6
scss复制代码     public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

一看代码,我相信作为一个有追求的工程师,肯定焕然大悟,这里加锁了,而且是同步锁synchronized

继续看底层源代码,我们看newLine()方法,第3行,看到了吧,也加了锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码     private void newLine() {
         try {
             synchronized (this) {
                 ensureOpen();
                 textOut.newLine();
                 textOut.flushBuffer();
                 charOut.flushBuffer();
                 if (autoFlush)
                     out.flush();
            }
        }
         catch (InterruptedIOException x) {
             Thread.currentThread().interrupt();
        }
         catch (IOException x) {
             trouble = true;
        }
    }

如果你继续往下看flushBuffer方法,也是加锁的。层层枷锁的处理啊。

知识点:synchronized

  • 解决问题:主要解决多个线程之间访问共享资源同步性,用于确保在同一时刻只有一个线程执行共享资源。
  • 用法:3种
+ 非静态方法前加`synchronized`
+ 静态方法前加`synchronized`
+ 代码块中加`synchronized`


    - 锁定代码块中的处理逻辑,`System.out.println`中是使用的本种方式。
  • 注意点:即使需要同步的场合,一定要注意synchronized的使用方式,同步的处理在确保业务没问题的前提下,同步的处理逻辑一定要最小化,一定要最小化。

第1个重大问题: 性能问题

使用 System.out.println() 输出信息到控制台涉及到文件I/O操作,需要将数据写入控制台。

在高性能的应用程序中,频繁的输出会导致程序的性能下降,特别是在大规模数据处理和高并发情况下。

第2个重大问题:Java开发手册(黄山版)中明确记载

【强制】生产环境禁止使用 System.out 或 System.err 输出或使用 e.printStackTrace() 打印异常堆栈。

说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小 超过操作系统大小限制。

Hello,World!

以这个话题作为《码农说》公众号的第一篇文章,真心是希望作为一个研发人员

  • 要不断学习,持续精进自己的技术;
  • 突破一些常规,理所当然的想法,很多理所当然的认知都可能会给项目带来巨大的风险。

大家好!我是老码农。今天就分享到这里。

期待未来能与更多的同学有深入的交流,一同学习技术,共同成长。

本文转载自: 掘金

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

深入探索 Spring 16 个 Bean 生命周期扩展接口

发表于 2023-12-24

背景

Spring的核心是其容器机制,表面看似平静,实则在内部进行着复杂的操作。Springboot进一步封装了Spring,强调“约定优于配置”原则,并采用自动装配机制。通常,引入一个依赖后,我们几乎无需配置即可完成功能的集成。

我特别喜欢这种自动装配机制,并在开发中间件和公共工具时应用它。深入理解 Spring 中bean的生命周期和各扩展接口对于掌握自动装配至关重要,同时也有助于深化对 Spring 的理解,并编写更优雅的业务代码。

本文总结了Spring和Springboot的所有扩展接口及其使用场景,并制作了一个展示bean在Spring内部从加载到初始化的所有可扩展点顺序的图表,帮助理解bean是如何被逐步加载到Spring容器中的。

Bean的生命周期内可扩展点调用顺序图

在这里插入图片描述

ApplicationContextInitializer

org.springframework.context.ApplicationContextInitializer

用于在 Spring 应用上下文 (ApplicationContext) 刷新之前对其进行编程式配置。这个接口在 Spring 应用启动过程中的早期阶段被调用,允许开发者在加载任何 bean 之前对应用上下文进行定制。

应用场景

  • 定制应用上下文配置:通过实现 ApplicationContextInitializer 接口,可以在 Spring 容器加载任何 bean 定义之前调用自定义逻辑,从而允许对配置进行编程式的修改。在应用启动前检查或准备外部资源,如消息队列
  • 环境依赖的设置:它常用于根据不同的环境(如开发、测试、生产)设置不同的配置参数。例如,可以根据不同的配置文件或环境变量来调整数据源设置。如:根据环境变量动态设置数据库连接参数
  • 与 Spring Profiles 集成:可以在 ApplicationContextInitializer 中激活或修改 Spring Profiles,从而改变应用程序的行为和配置。
  • 动态字节码注入:利用这时候class还没被类加载器加载的时机,进行动态字节码注入等操作

扩展方式

1
2
3
4
5
6
java复制代码public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
// 自定义逻辑处理
}
}

生效方式

  • 在启动类中用springApplication.addInitializers(new MyApplicationContextInitializer())语句加入
  • 配置文件配置context.initializer.classes=com.tf.demo.MyApplicationContextInitializer
  • Spring SPI 扩展,在spring.factories中加入org.springframework.context.ApplicationContextInitializer=com.tf.demo.MyApplicationContextInitializer

使用案例

在这里插入图片描述

BeanDefinitionRegistryPostProcessor

org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor

用于在标准初始化之后和 Bean 实例化之前修改应用程序上下文的 Bean 定义。这个接口扩展了 BeanFactoryPostProcessor,提供了更加灵活的操作 Bean 定义的能力

应用场景

  • 修改或增加Bean定义:在 Spring 容器加载了 Bean 定义后,但在实例化 Beans 之前,您可以使用这个接口来修改或添加 Bean 定义。
  • 条件性的Bean注册:基于特定条件(比如环境变量或配置参数),动态地注册或修改 Bean。
  • 自定义注解处理:如果你需要开发自己的注解并在 Spring 上下文中处理它们,可以使用这个接口。

扩展方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Component
public class CustomBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
// 在这里添加或修改Bean定义
RootBeanDefinition beanDefinition = new RootBeanDefinition(SomeClass.class);
registry.registerBeanDefinition("someBeanName", beanDefinition);
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 可以修改已注册的Bean属性
// 例如:设置属性,调用自定义初始化方法等
}
}

生效方式

  • 实现接口:您需要创建一个类实现 BeanDefinitionRegistryPostProcessor 接口,并重写 postProcessBeanDefinitionRegistry 和 postProcessBeanFactory 方法。
  • 注册到Spring容器:可以通过注解(如 @Component)或在配置类中显式注册。
  • 执行顺序:首先执行 postProcessBeanDefinitionRegistry 方法,允许添加或修改 Bean 定义。随后执行 postProcessBeanFactory 方法,这一步通常用于修改已经注册的 Bean 的属性

使用案例

在这里插入图片描述

BeanFactoryPostProcessor

org.springframework.beans.factory.config.BeanFactoryPostProcessor

应用场景

  • 修改或替换 Bean 定义:如果需要对 Spring 容器中的 Bean 定义进行修改或替换,可以实现此接口。
  • 环境检查或配置:在 Spring 容器实例化 beans 之前,进行一些环境的检查或者对配置信息的修改。
  • 动态注册 Bean 定义:可以动态地向 Spring 容器添加新的 Bean 定义

扩展方式

1
2
3
4
5
6
7
java复制代码public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 在这里修改 bean 定义或执行其他逻辑
}
}

使用案例

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

private String url;
private String username;
private String password;

// 构造函数和属性的 setter

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
BeanDefinition bd = beanFactory.getBeanDefinition("dataSource");
MutablePropertyValues pv = bd.getPropertyValues();
if (pv.contains("url")) {
pv.add("url", url);
}
if (pv.contains("username")) {
pv.add("username", username);
}
if (pv.contains("password")) {
pv.add("password", password);
}
}
}

InstantiationAwareBeanPostProcessor

org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor
InstantiationAwareBeanPostProcessor 继承 BeanPostProcess ,BeanPostProcess 接口只在bean的初始化阶段进行扩展(注入spring上下文前后),而InstantiationAwareBeanPostProcessor 接口在此基础上增加了3个方法,把可扩展的范围增加了实例化阶段和属性注入阶段。

应用场景

该类主要的扩展点有以下 5 个方法,主要在 bean 生命周期的两大阶段:实例化阶段和初始化阶段 ,下面一起进行说明,按调用顺序为:

  • postProcessBeforeInstantiation:实例化 bean 之前,相当于 new 这个 bean 之前,可用于修改动态环境 RocketMQ 消费者组名称,以及一些中间件 bean 属性的动态修改
  • postProcessAfterInstantiation:实例化 bean 之后,相当于 new 这个 bean 之后
  • postProcessPropertyValues:bean 已经实例化完成,在属性注入时阶段触发,@Autowired、@Resource等注解原理基于此方法实现
  • postProcessBeforeInitialization:初始化 bean 之前,相当于把 bean 注入 spring 上下文之前
  • postProcessAfterInitialization:初始化 bean 之后,相当于把 bean 注入 spring 上下文之后

扩展方式

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
java复制代码import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;

public class CustomBeanPostProcessor implements InstantiationAwareBeanPostProcessor {

@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
// 在实例化之前执行的逻辑
// 例如,可以在这里返回一个代理对象
return null;
}

@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
// 在实例化之后执行的逻辑,但在设置属性之前
// 可以用来修改 bean 或执行某些操作
return true;
}

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 在初始化(例如 @PostConstruct)之前执行
// 可以对 bean 进行修改
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 在初始化之后执行
// 例如,可以在这里包装 bean 为一个代理
return bean;
}
}

使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
java复制代码@Slf4j
public class DynamicRocketMQPostProcessor implements InstantiationAwareBeanPostProcessor {

@Value("${spring.profiles.active}")
String envProfile;

@Autowired
Environment env;

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (StringUtils.isEmpty(envProfile) || (!envProfile.contains(DynamicEnvManager.DEV_ENV) && !envProfile.contains(DynamicEnvManager.TEST_ENV))) {
return bean;
}

if (bean instanceof RocketMQListener) {
try {
log.info("postProcessProperties bean -> {} beanName -> {}", bean, beanName);
RocketMQListener<?> rocketMQListener = (RocketMQListener<?>) bean;
Class<? extends RocketMQListener> rocketmqListener = rocketMQListener.getClass();

RocketMQMessageListener annotation = rocketmqListener.getAnnotation(RocketMQMessageListener.class);

//获取 这个代理实例所持有的 InvocationHandler
InvocationHandler invocationHandler = Proxy.getInvocationHandler(annotation);

// 获取 AnnotationInvocationHandler 的 memberValues 字段
Field declaredField = invocationHandler.getClass().getDeclaredField("memberValues");

// 因为这个字段事 private final 修饰,所以要打开权限
declaredField.setAccessible(true);

// 获取 memberValues
Map memberValues = (Map) declaredField.get(invocationHandler);

// 修改 value 属性值
String oldConsumer = annotation.consumerGroup();

String property = env.getProperty(DynamicEnvManager.GLOBAL_ENV_NAME);
if (StringUtils.isBlank(property)) {
property = "";
log.error("postProcessBeforeInitialization env error property -> {}", property);
}
String newConsumer = oldConsumer + property;
memberValues.put("consumerGroup", newConsumer);

log.info("postProcessBeforeInitialization annotation -> {} oldConsumer -> {} newConsumer -> {} ", annotation, oldConsumer, newConsumer);
} catch (Exception e) {
log.error("postProcessBeforeInitialization consumerGroup replace error : ", e);
}
}
return bean;
}

}

SmartInstantiationAwareBeanPostProcessor

org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor

应用场景

SmartInstantiationAwareBeanPostProcessor 是 Spring 框架中一个更高级的扩展接口,提供了更细粒度的控制来干预和修改 Bean 的实例化过程。这个接口扩展了 InstantiationAwareBeanPostProcessor,增加了三个主要的方法,分别对应于 Bean 生命周期的不同阶段。以下是对这三个方法的优化建议:

  • predictBeanType: 此方法主要用于预测 Bean 的类型。当通过 Bean 名称无法确定其类型时,此方法被调用。它在实际 Bean 实例化之前提供了一种机制来推断 Bean 的类型。
  • determineCandidateConstructors:目的与应用场景: 该方法用于确定 Bean 的构造函数。这对于自定义选择合适的构造器来实例化 Bean 特别有用,尤其是在有多个构造函数的情况下。
  • getEarlyBeanReference:目的与应用场景: 该方法在 Bean 实例化后、初始化前被调用,主要用于解决循环依赖的问题,它允许在完全初始化之前提供 Bean 的早期引用。

扩展方式

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复制代码import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
import java.lang.reflect.Constructor;

public class CustomSmartBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor {

@Override
public Class<?> predictBeanType(Class<?> beanClass, String beanName) throws BeansException {
// 在这里,根据beanName或beanClass预测Bean的类型
if (beanName.equals("mySpecialBean")) {
return MySpecialBeanImpl.class;
}
return null;
}

@Override
public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, String beanName) throws BeansException {
// 在这里,可以自定义选择哪个构造器用于实例化Bean
// 例如,根据特定条件选择合适的构造函数
if (beanClass.equals(MyBean.class)) {
// 尝试获取MyBean的特定构造函数
try {
return new Constructor<?>[] { beanClass.getConstructor(MyDependency.class) };
} catch (NoSuchMethodException e) {
// 处理异常
}
}
return null;
}

@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
// 在这里处理循环依赖的场景,返回Bean的早期引用
// 通常用于AOP代理或特殊处理
return bean;
}

// 其他必要的方法实现...
}

使用案例

在这里插入图片描述

BeanFactoryAware

org.springframework.beans.factory.BeanFactoryAware

应用场景

该类具有一个关键时机点,即在 Bean 实例化之后且属性注入(例如,通过 Setter 方法)之前。此时,通过重写 setBeanFactory 方法,类可以获得 BeanFactory 实例的引用。

在这个阶段,您可以对刚实例化但尚未完全初始化的 Bean 进行特殊处理。这为在 Bean 生命周期的早期阶段进行定制化操作提供了机会。此外,您也可以将 BeanFactory 实例缓存起来,以便于后续操作时重复使用,从而提高效率和灵活性。这种做法特别适用于需要根据运行时条件动态处理或检索 Bean 的场景

扩展方式

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
java复制代码import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;

public class CustomBeanFactoryAwareClass implements BeanFactoryAware {

private BeanFactory beanFactory;

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
// 执行额外的初始化或配置逻辑
initializeCustomBeans();
}

private void initializeCustomBeans() {
// 在这里,您可以使用 beanFactory 来创建或配置 Bean
// 例如,根据条件动态创建 Bean
if (/* 某个条件 */) {
MyBean myBean = beanFactory.getBean(MyBean.class);
// 使用 myBean 进行操作
}
}

// 其他自定义方法...
}

ApplicationContextAwareProcessor

org.springframework.context.support.ApplicationContextAwareProcessor

应用场景

在这里插入图片描述
针对您提到的ApplicationContextAwareProcessor类及其相关的六个扩展点,可以进行如下优化和概述:

  • EnvironmentAware:这个接口允许Bean获取到Spring环境相关的配置。虽然通常可以通过注入的方式直接获得环境参数,但实现这个接口可以在Bean中直接访问环境属性,有助于减少对Spring环境的直接依赖。
  • EmbeddedValueResolverAware:实现此接口使得Bean能够解析String类型的属性值。虽然@Value注解通常被用于注入属性值,但通过缓存StringValueResolver实例,可以在需要时获取这些值,提供更灵活的值解析方式。
  • ResourceLoaderAware:通过这个接口,Bean可以获得ResourceLoader,用于访问类路径内的资源。这使得Bean能够更灵活地处理外部资源,例如配置文件或类路径资源。
  • ApplicationEventPublisherAware:实现这个接口使Bean能够发布事件。虽然可以通过Spring注入来获取ApplicationEventPublisher,但直接在Bean中实现此接口可以简化事件发布流程,增强Bean的事件驱动能力。
  • MessageSourceAware:这个接口主要用于国际化支持,允许Bean访问MessageSource。这对于开发需要支持多语言的应用程序尤其重要。
  • ApplicationContextAware:通过实现这个接口,Bean可以直接访问Spring的应用程序上下文ApplicationContext。这使得Bean能够获取和操作其他Bean,以及使用上下文提供的其他功能。这对于需要进行复杂上下文操作的Bean尤为重要

扩展方式

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
java复制代码import org.springframework.context.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.env.Environment;
import org.springframework.context.MessageSource;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.StringValueResolver;

public class MyAwareBean implements EnvironmentAware, EmbeddedValueResolverAware,
ResourceLoaderAware, ApplicationEventPublisherAware, MessageSourceAware,
ApplicationContextAware {

private Environment environment;
private StringValueResolver stringValueResolver;
private ResourceLoader resourceLoader;
private ApplicationEventPublisher applicationEventPublisher;
private MessageSourceAccessor messageSourceAccessor;
private ApplicationContext applicationContext;

@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}

@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.stringValueResolver = resolver;
}

@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}

@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}

@Override
public void setMessageSource(MessageSource messageSource) {
this.messageSourceAccessor = new MessageSourceAccessor(messageSource);
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}

// 使用注入的依赖来实现一些业务逻辑
public void performSomeAction() {
// 示例:使用environment获取属性
String dbUrl = environment.getProperty("database.url");

// 示例:使用stringValueResolver解析字符串
String resolvedString = stringValueResolver.resolveStringValue("some.value");

// 示例:使用resourceLoader加载资源
// Resource resource = resourceLoader.getResource("classpath:test.txt");

// 示例:发布事件
// applicationEventPublisher.publishEvent(new MyEvent(this));

// 示例:使用messageSourceAccessor获取国际化消息
// String message = messageSourceAccessor.getMessage("some.message.key");

// 示例:使用applicationContext获取bean
// MyOtherBean otherBean = applicationContext.getBean(MyOtherBean.class);
}
}

BeanNameAware

org.springframework.beans.factory.BeanNameAware

应用场景

BeanNameAware的主要应用场景包括:

  • 日志记录:Bean可以在日志中记录自己的名字,这对于调试和跟踪Bean的创建及其在容器中的生命周期是非常有用的。
  • 依赖注入:在某些复杂的依赖注入场景中,Bean可能需要知道自己的名字来动态地处理依赖关系,尤其是在存在多个相同类型但需要不同处理的Bean时。
  • 上下文感知:对于需要根据其在容器中的角色或标识来改变行为的Bean,了解自己的名字是很重要的。例如,同一个类的不同实例可能需要根据其在Spring容器中的名字来加载不同的配置。
  • 与其他框架集成:在集成Spring与其他框架(如Quartz, Apache Camel等)时,知道Bean的名字可以帮助在框架之间传递信息,确保正确的配置和交互。
  • 测试和模拟:在测试环境中,可以利用Bean的名字进行模拟或者特定的测试设置。

扩展方式

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
java复制代码import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyCustomBean implements BeanNameAware, InitializingBean {

private String beanName;

@Autowired
private SomeOtherBean someOtherBean;

@Override
public void setBeanName(String name) {
this.beanName = name;
// 在这里可以进行与bean名字相关的初始化操作
System.out.println("Bean的名字是: " + name);
}

@Override
public void afterPropertiesSet() throws Exception {
// 当所有属性被设置后,Spring会调用这个方法
// 可以在这里执行一些初始化的逻辑
System.out.println("正在初始化Bean: " + beanName);
}

// Bean的其他业务方法
public void doSomething() {
// 方法实现
}

// ... 其他可能的方法和逻辑
}

@PostConstruct

javax.annotation.PostConstruct

应用场景

  • 资源初始化:在数据库连接、读取配置文件或者初始化一些数据结构时使用。
  • 日志记录:应用启动时,记录一些启动日志或者系统状态。
  • 数据预加载:预加载一些必要的数据到缓存中。
  • 检查依赖:确保应用的某些依赖项已经准备就绪。

扩展方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码import javax.annotation.PostConstruct;

public class MyService {

// 依赖注入的示例字段
private DependencyClass dependency;

public MyService(DependencyClass dependency) {
this.dependency = dependency;
}

@PostConstruct
public void init() {
// 初始化逻辑
System.out.println("依赖注入完成,执行初始化操作");
// 比如,使用dependency进行一些设置或调用
}

// 类的其他方法...
}

InitializingBean

org.springframework.beans.factory.InitializingBean

应用场景

InitializingBean接口用于在设置完一个bean的所有属性之后执行初始化工作。实现InitializingBean接口允许bean在其属性被设置之后,但在任何启动逻辑(如数据库连接)之前执行自定义初始化逻辑。

  • 资源初始化:如数据库连接、文件系统的访问或其他需要在属性设置后进行初始化的资源。
  • 自定义配置校验:在bean的属性被注入后,检查或校验这些属性。
  • 复杂的初始化逻辑:执行复杂的初始化逻辑,这些逻辑可能依赖于多个属性

扩展方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码import org.springframework.beans.factory.InitializingBean;

public class MyService implements InitializingBean {

// 依赖注入的示例字段
private DependencyClass dependency;

public void setDependency(DependencyClass dependency) {
this.dependency = dependency;
}

@Override
public void afterPropertiesSet() throws Exception {
// 执行初始化逻辑
System.out.println("所有属性设置完成,执行初始化操作");
// 例如,使用dependency进行一些设置或调用
}

// 类的其他方法...
}

注意:如果同时使用了InitializingBean接口和@PostConstruct注解,那么@PostConstruct注解的方法将在afterPropertiesSet()方法之前执行

FactoryBean

org.springframework.beans.factory.FactoryBean

应用场景

FactoryBean是一个特殊的bean类型,用于创建复杂对象。当你需要执行复杂的初始化逻辑,或者创建对象过程中需要多个步骤和配置时,使用FactoryBean可以是一个好选择。它允许你在Spring容器内部完全控制bean的创建过程。

  • 复杂对象的创建:当一个对象的创建过程非常复杂,例如需要多步构建过程或特定的配置逻辑时。
  • 依赖注入之外的控制:如果需要在标准依赖注入之外控制对象的创建逻辑。
  • 单例与非单例的管理:可以灵活地创建单例或原型(非单例)对象。
  • 创建特定类型的资源:例如连接到特定服务的代理或特殊类型的资源。

扩展方式

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
java复制代码import org.springframework.beans.factory.FactoryBean;

public class MyFactoryBean implements FactoryBean<MyObject> {

@Override
public MyObject getObject() throws Exception {
// 创建并返回一个MyObject实例
MyObject myObject = new MyObject();
// 可以在这里进行复杂的构建逻辑
return myObject;
}

@Override
public Class<?> getObjectType() {
// 返回创建对象的类型
return MyObject.class;
}

@Override
public boolean isSingleton() {
// 控制是否单例
// 返回true表示创建的对象是单例
// 返回false表示创建的对象是非单例
return true;
}

// 其他可能的自定义方法或逻辑...
}

SmartInitializingSingleton

org.springframework.beans.factory.SmartInitializingSingleton

应用场景

SmartInitializingSingleton接口主要用于在单例bean的全部依赖关系都被满足后执行特定的逻辑。这个接口通常用于在Spring容器的启动阶段的最后一步,所有单例bean都已经创建且依赖关系已经注入后,执行一些后处理操作。

  • 后处理逻辑:在所有单例bean初始化完成之后执行一些后处理操作,例如数据校验、缓存预热等。
  • 依赖于多个Bean的初始化:当你的逻辑需要确保多个其他bean已经初始化完成时使用。
  • 异步操作的启动:如启动异步任务,当所有必要的bean都已经准备好。
  • 复杂的启动流程:例如,当应用需要在启动时执行一系列复杂的初始化步骤

扩展方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.stereotype.Component;

@Component
public class MySmartSingleton implements SmartInitializingSingleton {

// 依赖注入的其他Bean
private final AnotherBean anotherBean;

public MySmartSingleton(AnotherBean anotherBean) {
this.anotherBean = anotherBean;
}

@Override
public void afterSingletonsInstantiated() {
// 执行一些后处理逻辑
System.out.println("所有单例Bean都已经初始化,执行特定操作");
// 可以使用anotherBean进行一些操作
}

// 其他方法...
}

CommandLineRunner

org.springframework.boot.CommandLineRunner

应用场景

CommandLineRunner接口用于在Spring应用启动后执行特定的代码。它提供了一种简单的方法来访问命令行参数,并在Spring应用上下文加载完成后立即执行一些操作。

  • 命令行参数处理:解析和处理传递给Spring应用的命令行参数。
  • 启动时数据初始化:在应用启动时加载数据,例如从文件读取数据或者数据库初始化。
  • 启动后的检查:进行一些启动后的健康检查或者环境检查。
  • 启动任务:如启动一个定时任务或触发一些仅需执行一次的操作。

扩展方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class MyCommandLineRunner implements CommandLineRunner {

@Override
public void run(String... args) throws Exception {
// 执行一些启动逻辑
System.out.println("Spring应用已启动,执行命令行处理");
// args包含传递给应用程序的命令行参数
for (String arg : args) {
System.out.println(arg);
}
}

// 其他方法...
}

DisposableBean

org.springframework.beans.factory.DisposableBean

应用场景

DisposableBean接口用于在bean的生命周期结束时执行清理工作,比如释放资源或执行其他清理操作。这通常用于在bean不再需要时,确保以优雅的方式释放资源,如关闭数据库连接、释放文件句柄等。

  • 资源释放:关闭打开的资源,例如文件流、数据库连接、网络连接等。
  • 停止后台线程:如果bean启动了任何后台线程,可以在销毁时停止这些线程。
  • 缓存清理:清理任何持久化的缓存或临时数据。
  • 注销注册:如果bean在系统或服务中进行了注册,可以在销毁时注销

扩展方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;

@Component
public class MyDisposableBean implements DisposableBean {

@Override
public void destroy() throws Exception {
// 执行清理操作
System.out.println("Bean正在被销毁,执行清理操作");
// 这里可以释放资源,如关闭文件流或数据库连接
}

// 其他方法...
}

ApplicationListener

org.springframework.context.ApplicationListener

应用场景

ApplicationListener接口用于实现事件监听机制。这个接口允许应用对各种应用事件(如上下文事件、请求处理事件等)作出响应。它是基于观察者设计模式的实现,允许你在特定事件发生时执行自定义的逻辑。

  • 上下文事件监听:监听如上下文初始化、关闭等事件。
  • 自定义事件处理:处理自定义定义的业务事件。
  • 监控与日志:对特定事件进行监控或记录日志。
  • 性能跟踪:追踪应用的关键操作,如请求的处理时间。

扩展方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

@Component
public class MyApplicationListener implements ApplicationListener<ContextRefreshedEvent> {

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 执行一些逻辑,例如在上下文刷新时
System.out.println("Spring上下文刷新事件被触发");
// 这里可以访问事件源,例如event.getApplicationContext()
}

// 其他方法...
}

本文转载自: 掘金

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

2023年总结:日渐清醒,得失随意

发表于 2023-12-23

2023年快到末尾了,今天公司也完成了Q4的季度总计,自己也尝试写一次年终总结 看看今年到底做了哪些事情

  1. 工作

  • (个人情况)先介绍一下本人情况吧,江西南昌二本学校,22届毕业生,目前是后端开发,2021年9月到2022年3月在杭州海康威视实习6个月,2022年3月份拿到一家小公司的秋招offer,提前实习。从2022年3月份到2023年12月份差3个月也快2年了。

-(作息时间)现在的公司可能并不像一个互联网公司,965的作息时间,我是在公司附近的小区租的房子,走路过去10分钟差不多,每天9点起床,9点半上班 中午休息一个半小时,回家躺一会,晚上6点下班,我们后端不会在晚上进行上线,都是白天上线(海外用户 时间差), 和周围同学朋友比起来也很奇怪,他们都是经常性的要加班,一年多的时间我只加过两次班呆到8点半,只能说这样的公司不多了,呆在舒适圈里太久也会废掉,温水煮青蛙的感觉,下班了也是不想学的状态,就是打游戏/看直播/雀魂麻将,每天干到2/3点。

  • (后端架构)公司的软件产品主要是开发App,用户人群也是海外的用户。后端开发团队加上leader才4个人,今年我们对后端的技术架构也进行了重构,主要是Jdk17+ SpringBoot3.0+Mybatis-Plus+GRPC+webclient+MySQL+Redis+ElasticSearch+Nacos(2.0版本)+Apollo 服务注册/配置中心+Mycat(分表) 分表一致性hash+RocketMQ + Amazon SQS 消息队列+日志存储 log4j+slf4j / 分布式日志存储ELK+Flume+logstash+客户端推送 极光/FCM推送+数据埋点/事件/ab实验sdk神策。因为业务不是很复杂/用户量不大的情况下,使用的中间件都特别少,rocketmq一般使用在商品秒杀异步/第三方回调大量消息进行削峰填谷,其他场景都使用到 mq实战的比较少 看过很多的mq使用场景,没有使用场景。Elasticsearch 主要是用来搜索场景/日志搜索组件,没有那种用来做宽表/数据汇总等场景。
  • (工作情况)今年来一直在打螺丝,负责了A产品在Q1季度之后停止迭代,B/C产品在Q2季度迭代之后也停止了,都是因为收益不行吧,D产品有点盈利Q3/Q4季度一直在迭代。可能有人说toC的产品用户量应该很大,看了下我负责的D产品 每日活跃用户在2-3w,对后端来说什么并发量/QPS/TPS很大的场景暂时也是接触不到的。对于个人只有对自己负责的业务理解透彻,我们这边的需求并不是很复杂/恶心,能够理解当前的业务和新的需求,理清开发逻辑逻辑,基本都能上手,不需要太高端的技术,一个mysql一个redis一个springboot就能上手。刚开始的敏捷开发快速迭代需求对我来说是一种成长,从不熟悉crud的操作到炉火纯青的操作确实也让我度过那一个初级的阶段,到最后的长时间的重复操作(存储信息,数据处理返回信息),个人感觉意义不是很大了,也没有那种质的提升。在责任心这方面刚开始还是积极主动,会承担很多东西(试用期/初期想要成长嘛 都懂),到现在我只想把东西甩出去,剩下的都是自己的时间,在需求完成之后能够干一些自己有兴趣的事情。刚开始在沟通交流和表达能力这一方面还是存在很大的问题的,在不断的产品评审开会谈论中也在慢慢的改变,想清楚接下来要说的话 理清逻辑,慢慢说不要太急。
  1. 技术学习

  • 语言方面: 在语言的方向 Java作为工作中的使用语言 Spring生态,日常使用Python3写算法,Django开发,爬虫和各种脚本操作,Go作为兴趣方向写写算法,找一点项目进行实践。
  • 书籍阅读方面:《深入理解Java虚拟机》 《MySQL是怎么运行的》《Redis设计与实现》《Java并发编程》《Go语言设计与实现》《Go语言高并发与微服务实战》《Java多线程编程》《MySQL技术内幕》《Python脚本》《Linux命令行与Shell脚本》……………………
  • 技术方案:业务驱动技术,我觉得重要的还是业务场景设计方案,有了场景才有多种技术实现进行选型,日常看到一个功能 想想它是怎么实现的 实现的方式有哪些? 总结这种技术方案 形成自己的知识体系。
  • 开源中间件:先了解这东西是干嘛的,怎么去使用它,看它的整体架构设计(我还在这个阶段),然后看源码。
  • 自己学习也是三天打鱼两天晒网,天天在躺,基础不牢。
  1. 系统架构师(45 36 49)

  • 在大三的时候考过软考的一个中级设计师,然后稳稳的过了,在2022年毕业那年其实已经想靠系统架构师了(高级职称在杭州可以评e类人才,杭州户口每个月2k+的租房补贴),可惜社保交的月份太少,不满足杭州报考资格,就默默的放弃了,系统架构一年只能考一次,在今年的9月份报了名,国庆假期才开始买资料进行复习,准备了一个月 在浙江工业大学进行机考,今年出成绩很快,可惜没过,案例分析今年第一题改了题型考的大数据,自己没有复习那块知识点,基本就是0分,后面的redis和orm框架,mysql主从复制也答的不好,确实太可惜了,挺后悔没有去看大数据那块知识点的,对于论文的话,平常没有练过 考前背了背模版结构,考试的时候选择的系统稳定性,就把公司的项目写到论文里面进行整合了,准备了一个月的时间,没过 今年挺后悔的一件事情,明年不确定还会不会再参加了。

  • github.com/hakusai22/S… 这是自己在网上收集的资料/自己一个月内复习记录,明年可能还能继续用上。
  1. 生活

  • 每天下班时间在,有很多自己的时间,不是在学习就是在健身/骑车……………. 夏天的时候那段时间主要的运动是在小区打篮球,在今年双十一买了xds公路车,想的是把杭州有名的景点都骑上一遍,目前主要是去了西湖,湘湖,钱塘江,浙江大学(紫金/玉泉校区),最近杭州天气太冷了,其他的地方等待发掘吧。今年为了扩大自己的兴趣范围也是买了一把雅马哈的吉他进行学习,学了几个月基础就被我丢下了。到后来刷了很多摄影相机相关的视频,自己也想尝试一下,就入手了佳能的rp相机,每周周末有时间就出去刷刷风景,周末出去玩做点事 比呆在出租房子里面活的更加充实,相对于2022年 今年做出了很多不一样的改变, 可惜的是减肥失败,自律不起来。

附上几张周末出去骑车拍的照片

西湖

钱塘江大桥 好看是好看 真的高 太危险了

浙大的花还是挺好看

  1. 2024年规划 _03_17更新

  • 健身 早上/中午/晚上
  • 骑车 vlog (杭州景点全部骑完 影石360 ace pro)
  • 技术拓展/深挖(RocketMq源码/go-redis源码/Netty源码/Mycat2源码)
  • 摄影佳能rp/视频剪辑学习
  • 日语/英语学习
  • leetcode 上Knight
  • 去他的2023年
  • ………..

本文转载自: 掘金

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

1…666768…956

开发者博客

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