一、业务背景

营销自动化平台支持多种不同类型运营活动策略(比如:短信推送策略、微信图文推送策略、App Push推送策略),每种活动类型都有各自不同的执行流程和活动状态。比如短信活动的活动执行流程如下:

(图1-1:短信活动状态转移)

整个短信活动经历了 未开始 → 数据准备中 → 数据已就绪 → 活动推送中→ 活动结束 多个状态变更流程。不仅如此, 我们发现在活动业务逻辑处理过程中,都有以下类似的特点:

  • 每增加一种新的活动业务类型,就要新增相应的活动状态以及处理状态变更的逻辑;

  • 当一个类型的活动业务流程有修改时,可能需要对原先的状态转移过程进行变更;

  • 当每个业务都各自编写自己的状态转移的业务代码时,核心业务逻辑和控制逻辑耦合性会非常强,扩展性差,成本也很高。

针对系统状态的流转管理,计算机领域有一套标准的理论方案模型——有限状态机。

二、理解状态机

2.1 状态机定义

有限状态机(Finite-State Machine , 缩写:FSM),简称状态机。是表示有限个状态以及这些状态之间的转移和触发动作的模型。

  • 状态是描述系统对象在某个时刻所处的状况。

  • 转移指示状态变更,一般是通过外部事件为条件触发状态的转移。

  • 动作是对给定状态下要进行的操作。

简而言之,状态机是由事件、状态、动作三大部分组成。三者的关系是:事件触发状态的转移,状态的转移触发后续动作的执行。其中动作不是必须的,也可以只进行状态转移,不进行任何操作。

(图2-1:状态机组成)

所以将上述【图1-1:短信活动状态转移 】使用状态机模型来描述就是:

(图2-2:短信活动状态机)

状态机本质上是对系统的一种数学建模,将问题解决方案系统化表达出来。下面我们来看下在实际开发中有哪些实现状态机的方式 。

2.2 状态机的实现方式

2.2.1 基于条件判断的实现

这是最直接的一种实现方式,所谓条件判断就是通过使用 if-else 或 switch-case 分支判断进行硬编码实现。对于前面短信活动,基于条件判断方式的代码实例如下:

/**
* 短信活动状态枚举
*/
public enum ActivityState {
NOT_START(0), //活动未开始
DATA_PREPARING(1), //数据准备中
DATA_PREPARED(2), //数据已就绪
DATA_PUSHING(3), //活动推送中
FINISHED(4); //活动结束
} /**
* 短信活动状态机
*/
public class ActivityStateMachine {
//活动状态
private ActivityState currentState;
public ActivityStateMachine() {
this.currentState = ActivityState.NOT_START;
}
/**
* 活动时间开始
*/
public void begin() {
if (currentState.equals(ActivityState.NOT_START)) {
this.currentState = ActivityState.DATA_PREPARING;
//发送通知给运营人员
notice();
}
// do nothing or throw exception ...
} /**
* 数据计算完成
*/
public void finishCalData() {
if (currentState.equals(ActivityState.DATA_PREPARING)) {
this.currentState = ActivityState.DATA_PREPARED;
//发送通知给运营人员
notice();
}
// do nothing or throw exception ...
} /**
* 活动推送开始
*/
public void beginPushData() {
//省略
}
/**
* 数据推送完成
*/
public void finishPushData() {
//省略
}
}

通过条件分支判断来控制状态的转移和动作的触发,上述的 if 判断条件也可以换成 switch 语句,以当前状态为分支来控制该状态下可以执行的操作。

适用场景

适用于业务状态个数少或者状态间跳转逻辑比较简单的场景。

缺陷

当触发事件和业务状态之间对应关系不是简单的一对一时,就需要嵌套多个条件分支判断,分支逻辑会变得异常复杂;当状态流程有变更时,也需要改动分支逻辑,不符合开闭原则,代码可读性和扩展性非常差。

2.2.2 基于状态模式的实现

了解设计模式的童鞋,很容易就可以把状态机和状态模式这两个概念联系起来,状态模式其实可以作为状态机的一种实现方式。主要实现思路是通过状态模式将不同状态的行为进行分离,根据状态变量的变化,来调用不同状态下对应的不同方法。代码示例如下:

/**
* 活动状态接口
*/
interface IActivityState {
ActivityState getName();
//触发事件
void begin();
void finishCalData();
void beginPushData();
void finishPushData();
} /**
* 具体状态类—活动未开始状态
*/
public class ActivityNotStartState implements IActivityState {
private ActivityStateMachine stateMachine;
public ActivityNotStartState(ActivityStateMachine stateMachine) {
this.stateMachine = stateMachine;
} @Override
public ActivityState getName() {
return ActivityState.NOT_START;
} @Override
public void begin() {
stateMachine.setCurrentState(new ActivityDataPreparingState(stateMachine));
//发送通知
notice();
} @Override
public void finishCalData() {
// do nothing or throw exception ...
}
@Override
public void beginPushData() {
// do nothing or throw exception ...
}
@Override
public void finishPushData() {
// do nothing or throw exception ...
}
} /**
* 具体状态类—数据准备中状态
*/
public class ActivityDataPreparingState implements IActivityState {
private ActivityStateMachine stateMachine;
public ActivityNotStartState(ActivityStateMachine stateMachine) {
this.stateMachine = stateMachine;
} @Override
public ActivityState getName() {
return ActivityState.DATA_PREPARING;
}
@Override
public void begin() {
// do nothing or throw exception ...
}
public void finishCalData() {
stateMachine.setCurrentState(new ActivityDataPreparedState(stateMachine));
//TODO:发送通知
}
@Override
public void beginPushData() {
// do nothing or throw exception ...
}
@Override
public void finishPushData() {
// do nothing or throw exception ...
}
}
...(篇幅原因,省略其他具体活动类) /**
* 状态机
*/
public class ActivityStateMachine {
private IActivityState currentState;
public ActivityStateMachine(IActivityState currentState) {
this.currentState = new ActivityNotStartState(this);
}
public void setCurrentState(IActivityState currentState) {
this.currentState = currentState;
}
public void begin() {
currentState.begin();
}
public void finishCalData() {
currentState.finishCalData();
}
public void beginPushData() {
currentState.beginPushData();
}
public void finishPushData() {
currentState.finishCalData();
}
}

状态模式定义了状态-行为的对应关系, 并将各自状态的行为封装在对应的状态类中。我们只需要扩展或者修改具体状态类就可以实现对应流程状态的需求。

适用场景

适用于业务状态不多且状态转移简单的场景,相比于前面的if/switch条件分支法,当业务状态流程新增或修改时,影响粒度更小,范围可控,扩展性更强。

缺陷

同样难以应对业务流程状态转移复杂的场景,此场景下使用状态模式会引入非常多的状态类和方法,当状态逻辑有变更时,代码也会变得难以维护。

可以看到,虽然以上两种方式都可以实现状态机的触发、转移、动作流程,但是复用性都很低。如果想要构建一个可以满足绝大部分业务场景的抽象状态机组件,是无法满足的。

2.2.3 基于DSL的实现

2.2.3.1 DSL 介绍

DSL 全称是 Domain-Specific Languages,指的是针对某一特定领域,具有受限表达性的一种计算机程序设计语言。不同于通用的编程语言,DSL只用在某些特定的领域,聚焦于解决该领域系统的某块问题。DSL通常分为 内部 DSL ( Internal DSLs ),外部 DSL ( external DSLs ) 。

  • 内部DSL :基于系统的宿主语言,由宿主语言进行编写和处理的 DSL,比如:基于 Java 的 内部 DSL 、基于 C++ 的内部 DSL 、基于 Javascript 的 内部 DSL 。

  • 外部DSL :不同于系统宿主语言,由自定义语言或者其他编程语言编写并处理的 DSL,有独立的解析器。比如:正则表达式、XML、SQL、HTML 等。

(有关DSL的更多内容可以了解:Martin Fowler《Domain Specific Languages》)。

2.2.3.2 DSL 的选型和状态机实现

使用DSL作为开发工具,可以用更加清晰和更具表达性的形式来描述系统的行为。DSL 也是目前实现状态机比较推荐的方式,可以根据自身的需要选用内部 DSL 或者外部DSL 来实现。

  • 内部 DSL :业务系统如果只希望通过代码直接进行状态机的配置,那么可以选择使用内部 DSL,特点是简单直接,不需要依赖额外的解析器和组件。

Java 内部 DSL 一般是利用 Builder Pattern 和 Fluent Interface 方式(Builder 模式和流式接口),实现示例:

StateMachineBuilder builder = new StateMachineBuilder();
builder.sourceState(States.STATE1)
.targetState(States.STATE2)
.event(Events.EVENT1)
.action(action1());
  • 外部 DSL :可以利用外部存储和通用脚本语言的解析能力,实现运行时动态配置、支持可视化配置和跨语言应用场景。

外部 DSL 本质上就是将状态转移过程用其他外部语言进行描述,比如使用 XML 的方式:

<state id= "STATE1">
<transition event="EVENT1" target="STATE2">
<action method="action1()"/>
</transition>
</state> <state id= "STATE2">
</state>

外部 DSL 一般放在配置文件或者数据库等外部存储中,经过对应的文本解析器,就可以将外部 DSL 的配置解析成类似内部 DSL 的模型,进行流程处理;同时由于外部存储的独立性和持久性,可以很方便地支持运行时动态变更和可视化配置。

Java开源的状态机框架基本上都是基于DSL的实现方式。

三、开源状态机框架

我们分别使用三种开源状态机框架来完成短信活动状态流转过程。

3.1 Spring Statemachine

enum ActivityState {
NOT_START(0),
DATA_PREPARING(1),
DATA_PREPARED(2),
DATA_PUSHING(3),
FINISHED(4); private int state;
private ActivityState(int state) {
this.state = state;
}
} enum ActEvent {
ACT_BEGIN, FINISH_DATA_CAL,FINISH_DATA_PREPARE,FINISH_DATA_PUSHING
} @Configuration
@EnableStateMachine
public class StatemachineConfigurer extends EnumStateMachineConfigurerAdapter<ActivityState, ActEvent> {
@Override
public void configure(StateMachineStateConfigurer<ActivityState, ActEvent> states)
throws Exception {
states
.withStates()
.initial(ActivityState.NOT_START)
.states(EnumSet.allOf(ActivityState.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<ActivityState, ActEvent> transitions)
throws Exception {
transitions
.withExternal()
.source(ActivityState.NOT_START).target(ActivityState.DATA_PREPARING)
.event(ActEvent.ACT_BEGIN).action(notice())
.and()
.withExternal()
.source(ActivityState.DATA_PREPARING).target(ActivityState.DATA_PREPARED)
.event(ActEvent.FINISH_DATA_CAL).action(notice())
.and()
.withExternal()
.source(ActivityState.DATA_PREPARED).target(ActivityState.DATA_PUSHING)
.event(ActEvent.FINISH_DATA_PREPARE).action(notice())
.and()
.withExternal()
.source(ActivityState.DATA_PUSHING).target(ActivityState.FINISHED)
.event(ActEvent.FINISH_DATA_PUSHING).action(notice())
.and() ;
}
@Override
public void configure(StateMachineConfigurationConfigurer<ActivityState, ActEvent> config)
throws Exception {
config.withConfiguration()
.machineId("ActivityStateMachine");
}
public Action<ActivityState, ActEvent> notice() {
return context -> System.out.println("【变更前状态】:"+context.getSource().getId()+";【变更后状态】:"+context.getTarget().getId());
} //测试类
class DemoApplicationTests {
@Autowired
private StateMachine<ActivityState, ActEvent> stateMachine; @Test
void contextLoads() {
stateMachine.start();
stateMachine.sendEvent(ActEvent.ACT_BEGIN);
stateMachine.sendEvent(ActEvent.FINISH_DATA_CAL);
stateMachine.sendEvent(ActEvent.FINISH_DATA_PREPARE);
stateMachine.sendEvent(ActEvent.FINISH_DATA_PUSHING);
stateMachine.stop();
}
}

通过重写配置模板类的三个configure方法,通过流式Api形式完成状态初始化、状态转移的流程以及状态机的声明,实现Java内部DSL的状态机。外部使用状态机通过sendEvent事件触发,推动状态机的自动流转。

优势

  • Spring Statemachine 是 Spring 官方的产品,具有强大生态社区。

  • 功能十分完备,除了支持基本的状态机配置外,还具备可嵌套的子状态机、基于zk的分布式状态机和外部存储持久化等丰富的功能特性。

缺陷

  • Spring Statemachine 在每个 statemachine 实例内部保存了当前状态机上下文相关的属性,也就是说是有状态的(这一点从触发状态机流转只需事件作为参数也可以看出来),所以使用单例模式的状态机实例不是线程安全的。要保证线程安全性只能每次通过工厂模式创建一个新的状态机实例,这种方式在高并发场景下,会影响系统整体性能。

  • 代码层次结构稍显复杂,二次开发改造成本大,一般场景下也并不需要使用如此多的功能,使用时观感上显得比较沉重。

3.2 Squirrel Foundation

public class SmsStatemachineSample {
// 1. 状态定义
enum ActivityState {
NOT_START(0),
DATA_PREPARING(1),
DATA_PREPARED(2),
DATA_PUSHING(3),
FINISHED(4); private int state;
private ActivityState(int state) {
this.state = state;
}
} // 2. 事件定义
enum ActEvent {
ACT_BEGIN, FINISH_DATA_CAL,FINISH_DATA_PREPARE,FINISH_DATA_PUSHING
} // 3. 状态机上下文
class StatemachineContext {
} @StateMachineParameters(stateType=ActivityState.class, eventType=ActEvent.class, contextType=StatemachineContext.class)
static class SmsStatemachine extends AbstractUntypedStateMachine {
protected void notice(ActivityState from, ActivityState to, ActEvent event, StatemachineContext context) {
System.out.println("【变更前状态】:"+from+";【变更后状态】:"+to);
}
} public static void main(String[] args) {
// 4. 构建状态转移
UntypedStateMachineBuilder builder = StateMachineBuilderFactory.create(SmsStatemachine.class);
builder.externalTransition().from(ActivityState.NOT_START).to(ActivityState.DATA_PREPARING).on(ActEvent.ACT_BEGIN).callMethod("notice");
builder.externalTransition().from(ActivityState.DATA_PREPARING).to(ActivityState.DATA_PREPARED).on(ActEvent.FINISH_DATA_CAL).callMethod("notice");
builder.externalTransition().from(ActivityState.DATA_PREPARED).to(ActivityState.DATA_PUSHING).on(ActEvent.FINISH_DATA_PREPARE).callMethod("notice");
builder.externalTransition().from(ActivityState.DATA_PUSHING).to(ActivityState.FINISHED).on(ActEvent.FINISH_DATA_PUSHING).callMethod("notice"); // 5. 触发状态机流转
UntypedStateMachine fsm = builder.newStateMachine(ActivityState.NOT_START);
fsm.fire(ActEvent.ACT_BEGIN, null);
fsm.fire(ActEvent.FINISH_DATA_CAL, null);
fsm.fire(ActEvent.FINISH_DATA_PREPARE, null);
fsm.fire(ActEvent.FINISH_DATA_PUSHING, null);
}
}

squirrel-foundation 是一款轻量级的状态机库,设计目标是为企业使用提供轻量级、高度灵活、可扩展、易于使用、类型安全和可编程的状态机实现。

优势

  • 和目标理念一致,与 Spring Statemachine 相比,不依赖于spring框架,设计实现方面更加轻量,虽然也是有状态的设计,但是创建状态机实例开销较小,功能上也更加简洁,相对比较适合二次开发。

  • 对应的文档和测试用例也比较丰富,开发者上手容易。

缺陷

  • 过于强调“约定优于配置”的理念,不少默认性的处理,比如状态转移后动作是通过方法名来调用,不利于操作管理。

  • 社区活跃度不高。

3.3 Cola Statemachine

/**
* 状态机工厂类
*/
public class StatusMachineEngine {
private StatusMachineEngine() {
}
private static final Map<OrderTypeEnum, String> STATUS_MACHINE_MAP = new HashMap(); static {
//短信推送状态
STATUS_MACHINE_MAP.put(ChannelTypeEnum.SMS, "smsStateMachine");
//PUSH推送状态
STATUS_MACHINE_MAP.put(ChannelTypeEnum.PUSH, "pushStateMachine");
//......
} public static String getMachineEngine(ChannelTypeEnum channelTypeEnum) {
return STATUS_MACHINE_MAP.get(channelTypeEnum);
} /**
* 触发状态转移
* @param channelTypeEnum
* @param status 当前状态
* @param eventType 触发事件
* @param context 上下文参数
*/
public static void fire(ChannelTypeEnum channelTypeEnum, String status, EventType eventType, Context context) {
StateMachine orderStateMachine = StateMachineFactory.get(STATUS_MACHINE_MAP.get(channelTypeEnum));
//推动状态机进行流转,具体介绍本期先省略
orderStateMachine.fireEvent(status, eventType, context);
} /**
* 短信推送活动状态机初始化
*/
@Component
public class SmsStateMachine implements ApplicationListener<ContextRefreshedEvent> { @Autowired
private StatusAction smsStatusAction;
@Autowired
private StatusCondition smsStatusCondition; //基于DSL构建状态配置,触发事件转移和后续的动作
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
StateMachineBuilder<String, EventType, Context> builder = StateMachineBuilderFactory.create();
builder.externalTransition()
.from(INIT)
.to(NOT_START)
.on(EventType.TIME_BEGIN)
.when(smsStatusAction.checkNotifyCondition())
.perform(smsStatusAction.doNotifyAction());
builder.externalTransition()
.from(NOT_START)
.to(DATA_PREPARING)
.on(EventType.CAL_DATA)
.when(smsStatusCondition.doNotifyAction())
.perform(smsStatusAction.doNotifyAction());
builder.externalTransition()
.from(DATA_PREPARING)
.to(DATA_PREPARED)
.on(EventType.PREPARED_DATA)
.when(smsStatusCondition.doNotifyAction())
.perform(smsStatusAction.doNotifyAction());
...(省略其他状态)
builder.build(StatusMachineEngine.getMachineEngine(ChannelTypeEnum.SMS));
} //调用端
public class Client {
public static void main(String[] args){
//构建活动上下文
Context context = new Context(...);
// 触发状态流转
StatusMachineEngine.fire(ChannelTypeEnum.SMS, INIT, EventType.SUBMIT, context);
}
}
}

Cola Statemachine 是阿里COLA开源框架里面的一款状态机框架,和前面两者最大的不同就是:无状态的设计——触发状态机流转时需要把当前状态作为入参,状态机实例中不需要保留当前状态上下文消息,只有一个状态机实例,也就直接保证了线程安全性和高性能。

优势

  • 轻量级无状态,安全,性能高。

  • 设计简洁,方便扩展。

  • 社区活跃度较高。

缺陷

  • 不支持嵌套、并行等高级功能。

3.4 小结

三种开源状态机框架对比如下:

希望直接利用开源状态机能力的系统,可以根据自身业务的需求和流程复杂度,进行合适的选型。

四、营销自动化业务案例实践

4.1 设计选型

vivo营销自动化的业务特点是:

  • 运营活动类型多,业务流量大,流程相对简单,性能要求高。

  • 流程变更频繁,经常需要新增业务状态,需要支持快速新增配置和变更。

  • 在状态触发后会有多种不同的业务操作,比如状态变更后的消息提醒,状态完结后的业务处理等,需要支持异步操作和方便扩展。

针对以上业务特点,在实际项目开发中,我们是基于开源状态的实现方案——基于内部DSL的方式进行开发。同时汲取了以上开源框架的特点,选用了无状态高性能、功能简洁、支持动作异步执行的轻量设计。

  • 无状态高性能:保证高性能,采用无状态的状态机设计,只需要一个状态机实例就可以进行运转。

  • 功能简洁:最小设计原则,只保留核心的设计,比如事件触发,状态的基本流转,后续的操作和上下文参数处理。

  • 动作异步执行:针对异步业务流程,采用线程池或者消息队列的方式进行异步解耦。

4.2 核心流程

  • 沿用开源状态机的内部DSL流式接口设计,在应用启动时扫描状态机定义;

  • 创建异步处理线程池支持业务的后置动作;

  • 解析状态机的DSL配置,初始化状态机实例;

  • 构建执行上下文,存放各个状态机的实例和其他执行过程信息;

  • 状态机触发时,根据触发条件和当前状态,自动匹配转移过程,推动状态机流转;

  • 执行后置同步/异步处理操作。

(图4-1:核心流程设计)

4.3 实践思考

1)状态机配置可视化,结合外部DSL的方式(比如JSON的方式,存储到数据库中),支持更快速的配置。

2)目前只支持状态的简单流转,在流转过程加入流转接口扩展点,应对未来可能出现的复杂场景。

五、总结

状态机是由事件、状态、动作三大部分组成。三者的关系是:事件触发状态的转移,状态的转移触发后续动作的执行。利用状态机进行系统状态管理,可以提升业务扩展性和内聚性。状态机可以使用条件分支判断、状态模式和基于DSL来实现,其中更具表达性的DSL也是很多开源状态机的实现方式。可以基于开源状态机的特点和自身项目需求进行合适的选型,也可以基于前面的方案自定义状态机组件。

本篇是《营销自动化技术解密》系列专题文章的第三篇,系列文章回顾:

《营销自动化技术解密|开篇》

《设计模式如何提升 vivo 营销自动化业务拓展性|引擎篇01》

后面我们将继续带来系列专题文章的其他内容,每一篇文章都会对里面的技术实践进行详尽解析,敬请期待。

作者:vivo互联网服务器团队-Chen Wangrong

状态机引擎在vivo营销自动化中的深度实践 | 引擎篇02的更多相关文章

  1. 工作流引擎在vivo营销自动化中的应用实践 | 引擎篇03

    作者:vivo 互联网服务器团队- Cheng Wangrong 本文是<vivo营销自动化技术解密>的第4篇文章,分析了在营销自动化业务引入工作流技术的背景和工作流引擎的介绍,同时介绍了 ...

  2. 实时营销引擎在vivo营销自动化中的实践 | 引擎篇04

    作者:vivo 互联网服务器团队 本文是<vivo营销自动化技术解密>的第5篇文章,重点分析介绍在营销自动化业务中实时营销场景的背景价值.实时营销引擎架构以及项目开发过程中如何利用动态队列 ...

  3. 设计模式如何提升 vivo 营销自动化业务扩展性 | 引擎篇01

    在<vivo 营销自动化技术解密 |开篇>中,我们从整体上介绍了vivo营销自动化平台的业务架构.核心业务模块功能.系统架构和几大核心技术设计. 本次带来的是系列文章的第2篇,本文详细解析 ...

  4. vivo营销自动化技术解密|开篇

    一.营销自动化概览 1.1. 什么是营销自动化 营销自动化是指专门为营销部门或组织设计的软件平台和技术,可以更有效地在线进行多渠道营销并使重复性任务自动化.营销部门和销售人员通过制定任务和流程的操作标 ...

  5. vivo浏览器的快速开发平台实践-总览篇

    一.什么是快速开发平台 快速开发平台,顾名思义就是可以使得开发更为快速的开发平台,是提高团队开发效率的生产力工具.近一两年,国内很多公司越来越注重研发效能的度量和提升,基于软件开发的特点,覆盖管理和优 ...

  6. Sitecore营销自动化

    增加与战略性自动化营销系统的互动 Sitecore营销自动化基于DMS中的Sitecore个性化功能.营销自动化系统使用诸如位置,设备和先前访问或购买之类的客户数据来影响用户沿着购买路径的旅程.这些系 ...

  7. U-Mail邮件群发触发器功能助力营销自动化

    小编在朋友圈看到的人工智能讨论越来越多,越来越多的上班族惶恐不安,担心自己的饭碗不保将被人工智能所取代,这说明智能化.自动化正成为各行业的趋势,营销也概莫能外.营销的自动化意味着将大大节省从业人员的精 ...

  8. PCB 围绕CAM自动化,打造PCB规则引擎

    AutoCAM自动化平台,前端管理订单,而后端执行任务,前端UIl界面有板厚,铜厚,板材,表面处理,层数等信息,而这些信息并不是后端最终所需要的信息后.拿钻孔补偿来说,后端需要的是钻孔补偿值,但前端并 ...

  9. 转!!MySQL中的存储引擎讲解(InnoDB,MyISAM,Memory等各存储引擎对比)

    MySQL中的存储引擎: 1.存储引擎的概念 2.查看MySQL所支持的存储引擎 3.MySQL中几种常用存储引擎的特点 4.存储引擎之间的相互转化 一.存储引擎: 1.存储引擎其实就是如何实现存储数 ...

随机推荐

  1. linux查看和替换python软连接

    linux查看和替换python软连接 查看使用的python版本的路径 # which python 这里是在/usr/bin/python 然后查看链接指向, # ls -l /usr/bin/p ...

  2. JavaWeb 11_jsp九大内置对象

    1. out: 输出对象,向客户端输出内容2. request: 请求对象;存储"客户端向服务端发送的请求信息" request对象的常见方法: String getParamet ...

  3. [实验吧](web)因缺思厅的绕过 源码审计绕过

    0x00 直接看源码吧 早上写了个注入fuzz的脚本,无聊回到实验吧的题目进行测试,发现了这道题 地址:http://ctf5.shiyanbar.com/web/pcat/index.php 分析如 ...

  4. Basler acA1300-200uc相机使用教程

    https://www.baslerweb.com/cn/products/cameras/area-scan-cameras/ace/aca1300-200uc/ 开发文档 https://zh.d ...

  5. CF1504X Codeforces Round #712

    CF1504D Flip the Cards(找规律+贪心) 题目大意:给你n张牌,正反面都有数字,保证所有牌上的数字在$[1,2n]$内且互不相同.你可以翻转任意张牌,接下来需要把牌按正面的数字从小 ...

  6. Rust-Sqlx极简教程

    简介 sqlx 是 rust 中的一个数据库访问工具.具有以下特点: 异步:原生就支持异步,在并发性高的场合能够得到更好的支持 编译时检查查询:sqlx可以在 cargo build 的时候检查执行s ...

  7. 半吊子菜鸟学Web开发 -- PHP学习2-正则,cookie和session

    1正则表达式 1.1基本的匹配字符串 $p = '/apple/'; $str = "apple banna"; if (preg_match($p, $str)) { echo ...

  8. 超声波模块HC-SR04简介以及编程

    HC-SR04 一.主要参数1:使用电压:DC-5V2:静态电流:小于2mA3:电平输出:高5V4:电平输出:底0V5:感应角度:不大于15度6:探测距离:2cm-450cm7:高精度 可达0.2cm ...

  9. Altium_Designer PCB文件的绘制(上:PCB基础和布局)

    PCB设计基础知识 PCB面板 在PCB设计中,最重要的一个面板就是"PCB面板".该面板的功能主要是对电路板中的各个对象进行精确定位,并以特定的效果显示出来.该面板还可以对各种对 ...

  10. HTML5 Canvas绘制效率如何?

    js运行效率在提升 编程语言的效率是前提,js自然比不上native的C语言效率,所以Canvas效率无疑比不上原生的2D图形绘制,但是js效率的提升是有目共睹的,以js与as为例,基本操作(运算操作 ...