4.1.21 框架之分布式消息

消息中间件是在分布式开发中常见的一种技术手段,用于模块间的解耦、异步处理、数据最终一致等场景。

一、介绍

oinone对开源的RocketMQ进行了封装,是平台提供的一种较为简单的使用方式,并非是对RocketMQ进行的功能扩展。同时也伴随着两个非常至关重要的目的:

  1. 适配不同企业对RocketMQ的不同版本选择,不至于改上层业务代码。目前已经适配RocketMQ的开源版本和阿里云版本。 下个版本会对API进行升级支持不同类型MQ,以适配不同企业对MQ的不同要求,应对一些企业客户已经对MQ进行技术选择

  2. 对协议头进行扩展:如多租户的封装,saas模式中为了共用MQ基础资源,需要在消息头中加入必要租户信息。

二、使用准备

demo工程默认已经依赖消息,这里只是做介绍无需大家额外操作,大家可以用maven依赖树命令查看引用关系。

依赖包

增加对pamirs-connectors-event的依赖


<dependency>
    <groupId>pro.shushi.pamirs.framework</groupId>
    <artifactId>pamirs-connectors-event</artifactId>
</dependency>

图4-1-21-1 分布式消息的依赖包

相关功能引入

增加模型、触发器都依赖MQ

<!-- 增强模型 -->
<!-- 增强模型 -->
<dependency>
    <groupId>pro.shushi.pamirs.core</groupId>
    <artifactId>pamirs-channel</artifactId>
</dependency>
<!-- 触发器 api -->
<dependency>
    <groupId>pro.shushi.pamirs.core</groupId>
    <artifactId>pamirs-trigger-api</artifactId>
</dependency>
<!-- 触发器 core -->
<dependency>
    <groupId>pro.shushi.pamirs.core</groupId>
    <artifactId>pamirs-trigger-core</artifactId>
</dependency>

图4-1-21-2 增加模型、触发器都依赖MQ

yml配置文件参考

详见4.1.1【模块之yml文件结构详解】的“pamirs.event”部分。

三、使用说明

发送消息(NotifyProducer)

概述

NotifyProducer是Pamirs Event中所有生产者的基本API,它仅仅定义了消息发送的基本行为,例如生产者自身的属性,启动和停止,当前状态,以及消息发送方法。它本身并不决定消息如何发送,而是根据具体的实现确定其功能。

目前仅实现了RocketMQProducer,你可以使用下面介绍的方法轻松使用这些功能。

使用方法

Notify注解方式

使用示例

@Component
public class DemoProducer {

    @Notify(topic = "test", tags = "model")
    public DemoModel sendModel() {
        return new DemoModel();
    }

    @Notify(topic = "test", tags = "dto")
    public DemoDTO sendDTO() {
        return new DemoDTO();
    }
}

图4-1-21-3 Notify注解方式使用示例

解释说明

  1. 使用Component注解方式注册Spring Bean。

  2. Notify注解指定topic和tags。

  3. topic和tags对应NotifyEvent中的topic和tags。

RocketMQProducer方法调用

使用示例

@Component
public class SendRocketMQMessage {

    @Autowired
    private RocketMQProducer rocketMQProducer;

    /**
     * 发送普通消息
     */
    public void sendNormalMessage() {
        rocketMQProducer.send(new NotifyEvent("test", "model", new DemoModel()));
        rocketMQProducer.send(new NotifyEvent("test", "dto", new DemoDTO()));
    }

    /**
     * 发送有序消息
     */
    public void sendOrderlyMessage() {
        DemoModel data = new DemoModel();
        data.setAge(10);
        rocketMQProducer.send(new NotifyEvent("test", "model", data)
                .setQueueSelector((queueSize, event) -> {
                    DemoModel body = (DemoModel) event.getBody();
                    return body.getAge() % queueSize;
                }));
    }

    /**
     * 发送事务消息
     */
    public void sendTransactionMessage() {
        rocketMQProducer.send(new NotifyEvent("test", "model", new DemoModel())
                .setIsTransaction(true)
                .setGroup("demoTransactionListener"));
    }
}

图4-1-21-4 RocketMQProducer方法调用

解释说明

  1. 使用Component注解方式注册Spring Bean。
  2. 使用Autowired注解方式装配RocketMQProducer实例。
  3. 使用send方法发送指定消息。
  4. 在【发送普通消息】方法中,该实现效果与Notify注解方式所示完全一致。
  5. 在【发送有序消息】方法中,队列选择器是必须配置的,queueSize属性为MQ队列的总数量,在broker中配置。有序消息必须配合有序消费者才能达到有序消费的目的,否则还是无序的常规消息,消费者需要配置@NotifyListener(consumerType=ConsumerType.ORDERLY )注解。
  6. 在【发送事务消息】方法中指定的group为ProducerGroup,事务消息是通过不同的Producer发出的,事务消息监听请参考TransactionListener注解的相关使用方法。(示例中的group与下文介绍中的一致)

使用TransactionListener开启事务消息监听

使用示例

@Component
@TransactionListener
public class DemoTransactionListener implements NotifyTransactionListener {

    @Override
    public NotifyTransactionState checkLocalTransaction(NotifyEvent event) {
        return NotifyTransactionState.COMMIT;
    }

}

图4-1-21-5 使用TransactionListener开启事务消息监听

解释说明

  1. 实现NotifyTransactionListener接口。

  2. 使用Component注解方式注册Spring Bean。

  3. 添加TransactionListener注解注册事务监听,生成对应的生产者。

  4. 当前ProducerGroup将使用这个类的BeanName,即"demoTransactionListener"。如果你想自定义ProducerGroup,可以使用TransactionListener的value属性进行设置。

消费消息(NotifyConsumer)

概述

NotifyConsumer是Pamirs Evnet中所有消费者的基本API,与NotifyProducer类似,它仅仅定义了消息消费的基本行为,例如消费者自身的属性,启动和停止,当前状态,以及消息的监听和订阅方法。它本身并不决定消息如何消费以及如何被订阅,而是根据具体的实现确定其功能。

目前仅实现了RocketMQEventPushConsumer,你可以使用下面介绍的方法轻松使用这些功能。

使用方法

在类上使用NotifyListener注解

使用示例

@Component
@NotifyListener(topic = "test", tags = "model")
public class DemoConsumerClass implements NotifyEventListener {

    @Override
    public void consumer(NotifyEvent event) {
        DemoModel data = (DemoModel) event.getBody();
        // do some things.
    }
}

图4-1-21-6 在类上使用NotifyListener注解

解释说明

  1. 实现NotifyEventListener接口。

  2. 使用Component注解方式注册Spring Bean。

  3. 当前ConsumerGroup将使用这个类的BeanName,即"demoConsumerClass"。如果你想自定义ConsumerGroup,可以使用Component的value属性进行设置。

  4. 从body中将可以获取生产者发送的数据对象,并且已经做好了类型处理,可以直接使用。

  5. 使用原生的RocketMQ发送的消息,类型可能是无法识别的,你可以使用NotifyListener中提供的bodyClass来指定类型。

  6. topic和tags对应NotifyEvent中的topic和tags。

在方法上使用NotifyListener注解

使用示例

@Component
public class DemoConsumerMethod {

    @Bean
    @NotifyListener(topic = "test", tags = "model")
    public NotifyEventListener modelConsumer() {
        return event -> {
            DemoModel data = (DemoModel) event.getBody();
            // do some things.
        };
    }

    @Bean
    @NotifyListener(topic = "test", tags = "dto")
    public NotifyEventListener dtoConsumer() {
        return event -> {
            DemoDTO data = (DemoDTO) event.getBody();
            // do some things.
        };
    }
}

图4-1-21-7 在方法上使用NotifyListener注解

解释说明

  1. 使用Bean注解方式注册Spring Bean。

  2. 方法返回值为NotifyEventListener类型。

  3. 当前ConsumerGroup将使用对应方法生成的BeanName,即"modelConsumer"和"dtoConsumer"。如果你想自定义ConsumerGroup,可以使用Bean的value属性进行设置。

实战

约定:每个模块下的Topic和Tags必须定义常量池进行统一管理,主要是为了方便维护与管理,技术没有限制

常规消息(举例)

Step1 新建PetNotifyEnum

用PetNotifyEnum来管理模块的所有Topic和Tags的常量定义

package pro.shushi.pamirs.demo.api.mq;

public class PetNotifyEnum {

    public static class PetItemInventroy{

        public interface Topic{
            String PET_ITEM_INVENTROY_CHANGE ="petItemInventroyChange";
        }
        public interface Tag{
            String CREATE ="create";
            String UPDATE ="update";
            String DELETE ="delete";
        }

    }

}

图4-1-21-8 新建PetNotifyEnum

Step2 新建PetItemInventoryMqProducer

新建PetItemInventroy模型对应消息生产者,用于发送PetItemInventroy模型变动的相关消息

package pro.shushi.pamirs.demo.core.mq;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.demo.api.model.PetItemInventroy;
import pro.shushi.pamirs.demo.api.mq.PetNotifyEnum;
import pro.shushi.pamirs.framework.connectors.event.engine.NotifyEvent;
import pro.shushi.pamirs.framework.connectors.event.rocketmq.RocketMQProducer;

@Component
public class PetItemInventoryMqProducer {

    @Autowired
    private RocketMQProducer rocketMQProducer;

    /**
     * 发送普通消息
     */
    public void sendNormalMessage(PetItemInventroy data,String tag) {
        rocketMQProducer.send(new NotifyEvent(PetNotifyEnum.PetItemInventroy.Topic.PET_ITEM_INVENTROY_CHANGE, tag, data));
    }

}

图4-1-21-9 新建PetItemInventoryMqProducer

Step3 新建PetItemInventoryMqConsumer

新建PetItemInventoryMqConsumer,订阅PetItemInventroy模型变动的相关消息,并进行相关处理

package pro.shushi.pamirs.demo.core.mq;

import org.springframework.stereotype.Component;
import pro.shushi.pamirs.demo.api.model.PetItemInventroy;
import pro.shushi.pamirs.demo.api.mq.PetNotifyEnum;
import pro.shushi.pamirs.framework.connectors.event.annotation.NotifyListener;
import pro.shushi.pamirs.framework.connectors.event.api.NotifyEventListener;
import pro.shushi.pamirs.framework.connectors.event.engine.NotifyEvent;
import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j;

@Component
@NotifyListener(topic = PetNotifyEnum.PetItemInventroy.Topic.PET_ITEM_INVENTROY_CHANGE, tags = "*")
@Slf4j
public class PetItemInventoryMqConsumer implements NotifyEventListener {
    @Override
    public void consumer(NotifyEvent event) {
        PetItemInventroy petItemInventroy = (PetItemInventroy)event.getBody();
        log.info("petItemInventroy的消息, 库存数量为:" + petItemInventroy.getQuantity());
    }
}

图4-1-21-10 PetItemInventoryMqConsumer

Step4 修改PetItemInventroyAction

在修改PetItemInventroy完成之后,发送topic为【PetNotifyEnum.PetItemInventroyMq.Topic.PET_ITEM_INVENTROY_CHANGE】,tag为【PetNotifyEnum.PetItemInventroyMq.Tag.UPDATE】的消息出去

package pro.shushi.pamirs.demo.core.action;
……引入依赖类
@Model.model(PetItemInventroy.MODEL_MODEL)
@Component
public class PetItemInventroyAction {

    @Autowired
    private PetItemInventoryMqProducer petItemInventoryMqProducer;

    @Function.Advanced(type= FunctionTypeEnum.UPDATE)
    @Function.fun(FunctionConstants.update)
    @Function(openLevel = {FunctionOpenEnum.API})
    public PetItemInventroy update(PetItemInventroy data){
        ……其他代码
        petItemInventoryMqProducer.sendNormalMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE);
        return data;
    }
}

图4-1-21-11 修改PetItemInventroyAction

Step5 重新看效果

  1. 编辑商品库存记录

image.png

图4-1-21-12 编辑商品库存记录

  1. 查看后端日志是否打印

image.png

图4-1-21-13 查看后端日志是否打印

顺序消息(举例)

Step1 修改PetItemInventoryMqProducer

增加一个顺序消息发送的方法,发送时根据商品库存的Id就分队列,相同队列顺序消费

package pro.shushi.pamirs.demo.core.mq;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.demo.api.model.PetItemInventroy;
import pro.shushi.pamirs.demo.api.mq.PetNotifyEnum;
import pro.shushi.pamirs.framework.connectors.event.engine.NotifyEvent;
import pro.shushi.pamirs.framework.connectors.event.rocketmq.RocketMQProducer;

@Component
public class PetItemInventoryMqProducer {

    @Autowired
    private RocketMQProducer rocketMQProducer;

    /**
     * 发送普通消息
     */
    public void sendNormalMessage(PetItemInventroy data,String tag) {
        rocketMQProducer.send(new NotifyEvent(PetNotifyEnum.PetItemInventroy.Topic.PET_ITEM_INVENTROY_CHANGE, tag, data));
    }

    /**
     * 发送有序消息
     */
    public void sendOrderlyMessage(PetItemInventroy data,String tag) {

        rocketMQProducer.send(new NotifyEvent(PetNotifyEnum.PetItemInventroy.Topic.PET_ITEM_INVENTROY_CHANGE, tag, data)
                .setQueueSelector((queueSize, event) -> {
                    PetItemInventroy body = (PetItemInventroy) event.getBody();
                    Long queue = body.getId().longValue() % Long.valueOf(queueSize);
                    return queue.intValue();
                }));

    }
}

图4-1-21-14 修改PetItemInventoryMqProducer

Step2 修改PetItemInventoryMqConsumer

增加@NotifyListener(consumerType= ConsumerType.ORDERLY)的注解

package pro.shushi.pamirs.demo.core.mq;

import org.springframework.stereotype.Component;
import pro.shushi.pamirs.demo.api.model.PetItemInventroy;
import pro.shushi.pamirs.demo.api.mq.PetNotifyEnum;
import pro.shushi.pamirs.framework.connectors.event.annotation.NotifyListener;
import pro.shushi.pamirs.framework.connectors.event.api.NotifyEventListener;
import pro.shushi.pamirs.framework.connectors.event.engine.NotifyEvent;
import pro.shushi.pamirs.framework.connectors.event.enumeration.ConsumerType;
import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j;

@Component
//@NotifyListener(topic = PetNotifyEnum.PetItemInventroy.Topic.PET_ITEM_INVENTROY_CHANGE, tags = "*")
@NotifyListener(topic = PetNotifyEnum.PetItemInventroy.Topic.PET_ITEM_INVENTROY_CHANGE, tags = "*",consumerType= ConsumerType.ORDERLY)
@Slf4j
public class PetItemInventoryMqConsumer implements NotifyEventListener {
    @Override
    public void consumer(NotifyEvent event) {
        PetItemInventroy petItemInventroy = (PetItemInventroy)event.getBody();
        log.info("petItemInventroy的消息, 库存数量为:" + petItemInventroy.getQuantity());
    }
}

图4-1-21-15 修改PetItemInventoryMqProducer

Step3 修改PetItemInventroyAction

调用petItemInventoryMqProducer的顺序消息发送接口

package pro.shushi.pamirs.demo.core.action;
……引入依赖类
@Model.model(PetItemInventroy.MODEL_MODEL)
@Component
public class PetItemInventroyAction {

    @Autowired
    private PetItemInventoryMqProducer petItemInventoryMqProducer;

    @Function.Advanced(type= FunctionTypeEnum.UPDATE)
    @Function.fun(FunctionConstants.update)
    @Function(openLevel = {FunctionOpenEnum.API})
    public PetItemInventroy update(PetItemInventroy data){
        ……其他代码
//        petItemInventoryMqProducer.sendNormalMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE);
        petItemInventoryMqProducer.sendOrderlyMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE);
        return data;
    }
}

图4-1-21-16 修改PetItemInventroyAction

Step4 重启看效果

同常规消息

事务消息(举例)

注:这种写法默认第一次是UNKNOWN,然后通过MQ回调二次确认,在消息时效性要求高的场景下是不符合要求的。对实效性要求高的,请见下面事务消息-优化(举例)章节。

Step1 新建NotifyTransactionListener

package pro.shushi.pamirs.demo.core.mq;

import org.springframework.stereotype.Component;
import pro.shushi.pamirs.framework.connectors.event.annotation.TransactionListener;
import pro.shushi.pamirs.framework.connectors.event.api.NotifyTransactionListener;
import pro.shushi.pamirs.framework.connectors.event.engine.NotifyEvent;
import pro.shushi.pamirs.framework.connectors.event.enumeration.NotifyTransactionState;
import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j;

@Component
@Slf4j
@TransactionListener
public class PetItemInventoryMqTransactionListener implements NotifyTransactionListener {

    @Override
    public NotifyTransactionState checkLocalTransaction(NotifyEvent event) {
        log.info("消息回滚");
        return NotifyTransactionState.ROLLBACK;
    }
}

图4-1-21-17 新建NotifyTransactionListener

Step2 修改PetItemInventoryMqProducer

增加发送事务性消息的方法,通过.setIsTransaction(true)来显示设置该消息是事务消息,通过setGroup("petItemInventoryMqTransactionListener")来匹配事务监听处理器

/**
 * 发送事务消息
 */
public void sendTransactionMessage(PetItemInventroy data,String tag) {

    rocketMQProducer.send(new NotifyEvent(PetNotifyEnum.PetItemInventroy.Topic.PET_ITEM_INVENTROY_CHANGE, tag, data)
            .setQueueSelector((queueSize, event) -> {
                PetItemInventroy body = (PetItemInventroy) event.getBody();
                Long queue = body.getId().longValue() % Long.valueOf(queueSize);
                return queue.intValue();
            })
            .setIsTransaction(true)
            .setGroup("petItemInventoryMqTransactionListener")
    );
}

图4-1-21-18 修改PetItemInventoryMqProducer

Step3 修改PetItemInventroyAction

调用petItemInventoryMqProducer的事务性消息发送接口

package pro.shushi.pamirs.demo.core.action;
……引入依赖类
@Model.model(PetItemInventroy.MODEL_MODEL)
@Component
public class PetItemInventroyAction {

    @Autowired
    private PetItemInventoryMqProducer petItemInventoryMqProducer;

    @Function.Advanced(type= FunctionTypeEnum.UPDATE)
    @Function.fun(FunctionConstants.update)
    @Function(openLevel = {FunctionOpenEnum.API})
    public PetItemInventroy update(PetItemInventroy data){
        ……其他代码
        //petItemInventoryMqProducer.sendNormalMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE);
        //petItemInventoryMqProducer.sendOrderlyMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE);
        petItemInventoryMqProducer.sendTransactionMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE);
        return data;
    }
}

图4-1-21-19 修改PetItemInventroyAction

Step4 重启看效果

  1. 编辑商品库存记录

image.png

图4-1-21-20 编辑商品库存记录

  1. 查看后端日志是否打印

消息回滚,没有消息消费的日志,从日志打印时间上看MQ回调会有延迟

image.png

图4-1-21-21 MQ日志打印一

image.png

图4-1-21-22 MQ日志打印二

Step5 修改NotifyTransactionListener

package pro.shushi.pamirs.demo.core.mq;

import org.springframework.stereotype.Component;
import pro.shushi.pamirs.framework.connectors.event.annotation.TransactionListener;
import pro.shushi.pamirs.framework.connectors.event.api.NotifyTransactionListener;
import pro.shushi.pamirs.framework.connectors.event.engine.NotifyEvent;
import pro.shushi.pamirs.framework.connectors.event.enumeration.NotifyTransactionState;
import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j;

@Component
@Slf4j
@TransactionListener
public class PetItemInventoryMqTransactionListener implements NotifyTransactionListener {

    @Override
    public NotifyTransactionState checkLocalTransaction(NotifyEvent event) {
        log.info("消息提交");
        //正常业务情况下,这里要增加自己的逻辑判断,来确定返回状态值
        return NotifyTransactionState.COMMIT;
    }
}

图4-1-21-23 修改NotifyTransactionListener

Step6 重启看效果

消息提交,并看到消息消费的日志,从日志打印时间上看MQ回调会有延迟

image.png

图4-1-21-24 MQ日志打印三

image.png

图4-1-21-25 MQ日志打印三

事务消息-优化(举例)

Step1 修改PetItemInventoryMqProducer

修改PetItemInventoryMqProducer事务性消息发送方法sendTransactionMessage,增加NotifyExecuteLocalTransactionCallback入参。

/**
 * 发送事务消息
 */
public void sendTransactionMessage(PetItemInventroy data, String tag, NotifyExecuteLocalTransactionCallback callback) {

    rocketMQProducer.send(new NotifyEvent(PetNotifyEnum.PetItemInventroy.Topic.PET_ITEM_INVENTROY_CHANGE, tag, data)
            .setQueueSelector((queueSize, event) -> {
                PetItemInventroy body = (PetItemInventroy) event.getBody();
                Long queue = body.getId().longValue() % Long.valueOf(queueSize);
                return queue.intValue();
            })
            .setIsTransaction(true)
            .setGroup("petItemInventoryMqTransactionListener")
            .setExecuteLocalTransactionCallback(callback)
    );
}

图4-1-21-26 修改PetItemInventoryMqProducer

Step2 修改PetItemInventroyAction

修改PetItemInventroyAction的update方法

package pro.shushi.pamirs.demo.core.action;
……引入依赖类
@Model.model(PetItemInventroy.MODEL_MODEL)
@Component
public class PetItemInventroyAction {

    @Autowired
    private PetItemInventoryMqProducer petItemInventoryMqProducer;

    @Function.Advanced(type= FunctionTypeEnum.UPDATE)
    @Function.fun(FunctionConstants.update)
    @Function(openLevel = {FunctionOpenEnum.API})
    public PetItemInventroy update(PetItemInventroy data){
        //petItemInventoryMqProducer.sendNormalMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE);
        //petItemInventoryMqProducer.sendOrderlyMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE);
//        petItemInventoryMqProducer.sendTransactionMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE);
        petItemInventoryMqProducer.sendTransactionMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE,new NotifyExecuteLocalTransactionCallback(){
            @Override
            public NotifyTransactionState callback(NotifyTransactionState executeState, NotifyEvent event) {

                List<PetItemInventroy> inventroys = new ArrayList<>();
                inventroys.add(data);
                PamirsSession.directive().disableOptimisticLocker();
                try{
                    int i = data.updateBatch(inventroys);
                } finally {
                    PamirsSession.directive().enableOptimisticLocker();
                }
                //业务代码执行完毕,提交消息
                return NotifyTransactionState.COMMIT;
            }
        });
        return data;
    }
}

图4-1-21-27 修改PetItemInventroyAction

Step3 重启看效果

编辑商品库存记录,发现消息的处理几乎没有延迟

image.png

图4-1-21-28 示例效果几乎没有延迟

Step4 模拟业务成功,消息发送失败

修改PetItemInventroyAction的update方法

@Function.Advanced(type= FunctionTypeEnum.UPDATE)
@Function.fun(FunctionConstants.update)
@Function(openLevel = {FunctionOpenEnum.API})
public PetItemInventroy update(PetItemInventroy data){
    //petItemInventoryMqProducer.sendNormalMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE);
    //petItemInventoryMqProducer.sendOrderlyMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE);
//        petItemInventoryMqProducer.sendTransactionMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE);
    petItemInventoryMqProducer.sendTransactionMessage(data, PetNotifyEnum.PetItemInventroy.Tag.UPDATE,new NotifyExecuteLocalTransactionCallback(){

        @Override
        public NotifyTransactionState callback(NotifyTransactionState executeState, NotifyEvent event) {

            List<PetItemInventroy> inventroys = new ArrayList<>();
            inventroys.add(data);
            PamirsSession.directive().disableOptimisticLocker();
            try{
                int i = data.updateBatch(inventroys);
            } finally {
                PamirsSession.directive().enableOptimisticLocker();
            }
            //模拟业务成功,消息发送失败
            throw new RuntimeException();
            //业务代码执行完毕,提交消息
            //return NotifyTransactionState.COMMIT;
        }
    });
    return data;
}

图4-1-21-29 修改PetItemInventroyAction的update方法

Step5 重启看效果

这个效果跟第一种事务消息一样,消息提交延迟严重。

image.png

图4-1-21-30 MQ日志打印

image.png

图4-1-21-31 MQ日志打印

Step6 总结

优化后的事务消息,在正常情况下时效性非常高,异常情况下也能通过NotifyTransactionListener做保障

Oinone社区 作者:史, 昂原创文章,如若转载,请注明出处:https://doc.oinone.top/oio4/9296.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(0)
史, 昂的头像史, 昂数式管理员
上一篇 2024年5月23日
下一篇 2024年5月23日

相关推荐

  • 页面设计

    1. 功能说明 页面设计时界面设计器中「页面」模块的设计入口,在这个界面,进行页面的设置、搭建、设计、排版。 主要分为顶部操作栏、左侧工具栏、中部设计画布区、右侧属性面板。 2. 操作栏 进入页面设计,顶部显示了页面的标题,以及返回、发布等操作。 2.1 发布 页面设计完成后,点击「发布」运行页面生效。若不点击发布,页面也有自动保存的功能,但在发布前,自动保存也等同于草稿,不会正式生效。 发布时如果有属性不符合校验规则(必填的属性未填、输入的内容校验不通过),会发布失败,相应的字段会特殊标记,需要查看并修改属性。 2.2 显示/隐藏母版 进入页面设计时默认不展示母版,可以手动操作显示母版 3. 工具栏 左侧的工具栏中包括组件库、页面设置等模块。 3.1 组件库 组件库中包含组件和模型,组件是当前设计器支持的所有组件,模型是页面所在模型下的所有字段和动作。 3.1.1 组件 组件中展示了系统支持的所有组件。包含 1)布局类组件,如分组、选项卡等,使用布局组件可以将页面进字段分类、分页; 2)字段类组件,如单行文本、整数、日期等等,使用字段类组件时都会在模型下对应创建一个字段; 3)动作类组件,如跳转动作、提交动作、链接动作等。 3.1.2 模型 组件库顶部,由组件可切换为模型:模型选项下,会展示当前模型的所有字段,以及系统默认动作。可以直接拖拽字段至设计画布中,会应用形成某个组件,可对组件进行多样的属性设置,优化交互。 3.1.2 组件和模型有什么区别 1展示内容维度不同 组件中展示的内容是组件信息,如分组、选项卡、单行文本、文件上传等;模型中展示的是模型下已有的所有字段。 2使用功能不同 组件中的组件使用前需要在模型中创建一个字段,当然,创建好的字段也会存在于模型中;模型中的字段可直接使用,并且使用时会在设计画布中对应生成个默认组件。 3使用场景不同 如果模型中已经存在目标字段,应直接选择从模型中拖拽字段;如果模型中没有需要的字段,可以在页面中增加一个组件,实际上也是在新增一个字段。 3.2 页面设置 页面设置中可以修改当前页面的标题、分组、页面描述,同时也是给页面上传缩略图的唯一入口。 4. 设计画布 将组件或字段拖拽至设计画布区,会生成样式。点击组件,右侧可对其进行设置,大部分属性可实时在画布中展示效果。 5. 属性面板 右侧属性面板抽屉中可以设置属性或查看字段信息,通过不同的属性配置化实现组件的多样化。 5.1 属性 属性中包括基本信息(如标题、占位提示、描述说明等)、校验信息(如是否必填、长度校验等)、交互信息(如排序方式、是否展示计数器等) 5.2 字段 首次从组件中拖拽,会先展示字段信息,并且是需要先创建一个字段,字段创建成功后,再次切换字段选项,只读展示当前组件所对应的字段基本信息。同理,如果是直接从字段中拖拽,字段选项中,只读展示当前字段基本信息。

    2024年6月20日
    3.2K00
  • 1.4 Oinone对软件特性的思考

    我在个人的微信公众号上《浅谈企业IT架构的十年困局》一文中写了“企业或者软件公司在工程领域都关注哪些特征,而这些特征又应与具体研发人员的个体能力无关”的相关内容。收到很多业内人士的留言,也引起了很多同行的共鸣,所以今天在这里也打算针对这个话题,跟大家再做个深入的探讨。 一、首先为什么强调要跟研发个体能力无关 我们先来看一个故事: 轮扁是春秋时期齐国的木工,齐桓公召其入宫打造物件。有一天,齐桓公在堂上看书,轮扁在堂下用椎、凿等工具做车轮。 齐桓公看书看到得意处,不由得读出声来。轮扁听到读书声,想了想,放下手里的工具,走上堂来,在齐桓公面前几步远的地方停下,恭恭敬敬地说:“请恕臣斗胆问一下,君王读的是什么书?”齐桓公没想到这个老木匠会走上堂来,倒有点意外。不过看在他年纪大的份上,倒也不去斥责他,就回答说:“寡人读的是圣人写的书。”轮扁问:“圣人还在吗?”齐桓公说:“已经死了。”轮扁说:“这样看起来,君王所读的,不过是古人的糟粕而已!”齐桓公勃然大怒,说:“寡人读书,你一个做车轮的怎么敢议论?你说,这书上怎么会是古人的糟粕?说出道理便罢,说不出道理便难逃一死!” 轮扁不慌不忙地说:“臣是根据臣所从事的活计而明白这个道理的。砍削轮子,榫头做得宽了则松滑而不牢固,做得太紧就必然涩滞而安不进去,臣制作的榫头松紧适宜,是因为心里怎样想的手便怎样去做。然而尽管所需要的分寸度数心里都明白,要把它用言辞表达出来却实在不可能,全靠自己手与心的配合。所以,臣无法将其中的奥秘传授给儿子,臣的儿子也无法从臣这里学到其中的奥秘。因此,臣如今七十多岁了,还只好亲手去干制作轮子的活。这样看来,古人之道的精华都已随着古人死去而无法传世,那么君王所读的,不就是古人的糟粕了吗?” 这就是著名的成语故事——轮扁斫轮,出自《庄子·天道》。庄子通过轮扁的言论,深刻地揭示了高妙之技的难以言传。 而当我们转换视角,在企业数字化转型领域,无论是软件公司还是甲方IT团队,核心上是应用级开发需求,更多的精力应该放在业务场景理解、需求把控以及业务系统实现上。但往往在一个项目进入研发之前,会花很大力气在技术架构设计、技术栈选型、通用能力对接、扩展点设计这些跟业务场景无关的技术事项上,且需要高级别的架构师来主导。大部分情况下,架构师会选开源框架来实现,慢慢沉淀为企业的研发标准体系,所以底层架构的能力往往依赖架构师个人能力。不禁发现他们与轮扁有着异曲同工之处。架构师所积累的个人经验和技术能力,往往难以通过简单的手把手教学、技术评审会完全传递给团队中的其他成员。即使有所传授,其效率也可能仅达到50%,并且随着团队成员数量的增加,这种效率还可能持续递减。因此,我们需要更多地依赖于技术手段,将架构师的经验和能力固化下来,形成一套可复制、可推广的标准技术产品。这样,每个团队成员都能够通过学习和运用这些技术,达到至少70%的传递效率,从而确保团队整体技术水平的稳步提升。这也正是开篇所强调的,企业或软件公司在工程领域所关注的特征,应当与具体研发人员的个体能力相剥离,而更多地依赖于标准化、系统化的技术手段,来确保团队整体的高效运作。 二、软件公司在工程化领域都关注哪些特征 接下来,我将从技术角度深入剖析设计初衷和技术实现原理,以展现技术公司应当“被标准化的特征”究竟长什么样。 先做个名称解释,下文中涉及“标品”、“升级”、“扩展逻辑”,这是站在软件公司角度出发描述的,如果是企业内部可以把标品理解为特定业务应用平台,升级则是业务应用平台的正常规划迭代,扩展逻辑理解为脱离平台发展的临时性需求。 1. 可逆计算 可逆计算,在应用上的特征图 场景:调查发现企业研发至少有40%的精力在跟各条业务线的团队在评审项目需求,判断需求是否合理。而且业务线对需求完善时间要求紧,每天盯着研发进度,经常问“这个需求什么时候支持,我们等着用”。导致产研部门的研发抱怨产品节奏乱,无法按照自身节奏进行迭代,被项目推着走,没有时间思考,人手不足,加班多,工作压力大…… 价值:该特性很好的规避了研发因为时间紧迫,写的一些临时代码腐蚀核心业务系统。它需要做到不论从数据模型、业务逻辑、交互展示都能有扩展能力,并且这些扩展能力与个体研发无关才行。它同时所描述的也是一个具备差量计算能力的软件架构模式,它允许用户通过添加或移除扩展包来定制标准应用,同时保持应用的可逆性和独立性。这种架构模式的核心优势在于其灵活性和可维护性,使得应用的定制和恢复变得简单而高效。 技术原理:它所描述的是一个基于元数据驱动和差量计算的软件架构模式,它允许用户通过添加或移除扩展包来定制标准应用,同时保持应用的可逆性和独立性。这种架构模式的核心优势在于其灵活性和可维护性,通过元数据来驱动应用的构建和变更,使得应用的定制和恢复变得简单而高效 在这种架构中,元数据起到了至关重要的作用。元数据是关于数据的数据,它描述了数据的结构、属性、关系等信息。在软件应用中,元数据可以用来描述应用的组件、功能、配置等信息。通过元数据驱动应用可以根据元数据的描述来动态地构建和配置自身的功能和结构 差量计算则是实现应用可逆性的关键。当添加或移除扩展包时,系统会根据扩展包中的元数据与标准应用的元数据进行差量计算,确定需要添加或移除的功能和组件。这种差量计算可以确保在添加扩展包后,应用能够保持原有的功能和稳定性,同时新增扩展包带来的新功能,而在去除扩展包时,应用能够恢复到原始的标准状态,不会留下任何冗余或冲突的代码和配置。 为了实现这种架构模式,元数据注册表和分布式部署能力是非常重要的。元数据注册表需要能够存储和管理大量的元数据信息,并且提供高效的查询和更新机制。分布式部署能力则能够确保应用在不同的环境中都能够稳定运行,并且能够快速地响应扩展包的添加和移除操作,即差量(扩展包》可独立存在又相互作用。 总的来说,这种基于元数据驱动和差量计算的软件架构模式为应用的定制和恢复提供了强大的支持,使得应用能够根据不同的需求进行灵活的定制和扩展。同时,它也提高了应用的可维护性和可靠性,降低了开发和维护的成本 2. 协同演进 协同演进,在应用上的特征图 场景:它所描述的场景是一个复杂的软件升级过程,其中涉及了标准应用的升级以及用户个性化扩展的保留。通过面向对象的方式扩展标准应用的功能,可以在升级过程中保持用户自定义逻辑的完整性,并同时集成新版本中的新特性。 价值:很多号称产品型的软件公司,在交付客户项目的时候,都是从标品复制一个分支,然后客户个性化直接在这个分支上改。这种模式会带来两个问题: 是当客户数量变大,每个客户的版本都不一致,维护成本很高; 是当标品升级带来的新特性无法复制给客户,导致客户满意度下降甚至流失。协同演进就是要解决这个问题。 技术原理:它需要在第一个差量计算的特性基础上才能得以完成,同时在这种升级能力中,元数据驱动和模型驱动是关键所在。元数据驱动确保了应用能够理解和处理不同版本之间的变化,包括功能的增删改以及结构的调整。模型驱动则提供了描述和管理应用结构、组件和行为的能力,它不仅能够描述模型间的关系,还能够支持面向对象的特性,如继承、重写和重载等。 具体来说,当标准应用从V1升级到V2时,元数据驱动机制会首先识别和分析两个版本之间的差异。对于用户应用1中已经扩展的A功能,由于采用了面向对象的方式进行扩展,因此在升级过程中,A+逻辑作为A功能的重写或重载版本会被保留下来。同时,V2版本中新增的B功能也会被集成到用户应用1中,因为它是作为标准应用的新特性而存在的。 这种升级能力的实现依赖于一个强大的元数据注册表和模型管理能力。元数据注册表需要能够存储和管理不同版本应用的元数据信息,包括功能、组件、结构等。模型管理能力则需要能够解析和应用这些元数据,以生成正确的应用结构和行为。同时,还需要一套高效的升级机制来确保升级过程的平滑和可靠。 总的来说,通过元数据驱动和模型驱动的结合,可以实现标准应用的平滑升级,同时保留用户个性化扩展的完整性。这种能力对于提高软件的可维护性、可扩展性和用户满意度具有重要意义 3. 公民研发和专业研发共同参与 专业研发与公民研发共同参与,在应用上的特征图 场景:它所描述是在应用开发的整个生命周期中,专业研发专注在标品的长期规划与迭代,当出现临时性的需求或者应急性的辅助场景则由非专业人士进行即公民研发方式进行。这种模式下,专业研发可以按照规划有节奏的迭代产品,做更高级的事情,不至于忙于应对临时性的事务没有深度思考,更加避免了因为临时代码堆积导致产品从内部腐化。同时利用独立的扩展逻辑包和无代码方式解决了业务的紧迫感,毕竟业务需求的合理性是很难争论出高低的。它在前两个特性基础上让研发效能进一步得到释放。 价值:它的本质是,在专业研发在以低代码的方式下实现应用,并通过无代码的方式,快速扩展逻辑功能和创建辅助性应用。整个过程无缝衔接,我们给他取个名字专业名称叫:“低无一体”。它大大降低了技术门槛,使得专业和非专业的研发人员都能参与到应用扩展和定制中来。此外,它还提高了业务响应能力,使得企业能够更快速地适应市场变化和客户需求。 技术原理:它的核心要求就是元数据在线,元数据在线能力是指能够实时地、在线地管理和操作元数据,这种能力为企业或组织带来了诸多优势。通过无 代码的方式,用户可以更加灵活地进行应用的个性化扩展,以应对各种应急性需求,从而显著提升业务的响应能力。此外,元数据在线管理还确保核心应用、核心应用扩展以及辅助应用都是基于一套统一的技术体系构建的,这为不同角色的用户(包括专业和非专业的研发人员)提供了多样化的参与方式。同时,元数据在线管理需要符合开闭原则,这确保了系统的稳定性和可扩展性,使得新的功能或需求可以通过添加新的元数据或配置来实现,而非修改现有系统。 这种低代码开发与无代码一体化的优势在于,它大大降低了技术门槛,使得专业和非专业的研发人员都能参与到应用扩展和定制中来。此外,它还提高了业务响应能力,使得企业能够更快速地适应市场变化和客户需求。 总之,从用户应用到业务实施的过程通过元数据在线得到了优化和升级。低代码开发与无代码一体化的优势使得整个过程更加高效、灵活和易于维护,为企业带来了显著的价值和竞争优势。 4. 基于平台级别的AOP能力出现反向集成 反向集成,在应用上的特征图 场景:平台级别的AOP(面向切面编程)能力允许开发者在应用程序的特定点“切入”额外的逻辑,而无需修改原有的业务代码。这种能力特别适用于横向追加平台逻辑,即在多个不同服务或功能点插入通用的处理逻辑,如日志记录、权限检查、审计、多租户、多语言等。过往在微服务架构中,这些能力都需要业务系统各自主动去对接,有了平台级别的AOP能力,则这些通用能力可以反向为所有业务系统增加特性能力,无需业务系统研发感知。这种现象我们称之为“反向集成”,能让业务研发更加专注在业务研发本身,不需要关心与业务无关的通用功能上。 价值:AOP的核心思想是将这些横切关注点(cross-cutting concerns)从业务逻辑中分离出来,使得业务代码更加清晰和专注于其核心功能。在平台级别的AOP中,标准化协议是实现这一能力的关键。平台具备统一的入口和扩展能力是非常重要的,因为它允许开发者在不修改现有代码的情况下添加新功能或修改现有功能的行为。这种能力对于快速响应业务需求变化、减少维护成本和提高代码质量都是非常有益的。 技术原理:标准化协议确保了不同组件之间的通信与语义是统一的,从而使得AOP能够更容易地实施。例如: a前后端通信要标准协议(与端无关): 这意味着无论前端是使用Web、移动应用还是其他类型的客户端,后端服务都应该能够以一种标准的方式与之通信。 bORM层要有标准协议(与数据库无关): 对象关系映射 (ORM)层应该提供一个标准的接口来与数据库进行交互,这样无论底层使用哪种数据库(如MySQL、PostgreSQL、Oracle等),上层的业务逻辑都不需要改变。 cRPC需要标准协议(与Dubbo和Spring Cloud无关): 远程过程调用 (RPC)应该遵循一种标准协议,以便不同的服务可以无缝地进行通信,而不受特定框架 (如Dubbo、Spring Cloud等)的限制。 d所有逻辑调用统一fun调用: 这意味着平台上的所有功能调用都应该通过一个统一的入口点(如一个函数或方法)进行,这样AOP就可以在这个入口点切入额外的逻辑。 总的来说,平台级别的AOP能力通过标准化协议和统一的调用入口,为开发者提供了一种强大而灵活的方式来管理和扩展平台的逻辑功能。 5. 应用研发与部署无关 应用研发与部署无关,在应用上的特征图 场景:现在研发在选择部署方式的时候往往会选择分布式部署,或者你的客户招标需求里就写着“微服务”,构建一个微服务系统并不是一件容易的事,构建的复杂度远远超过单体系统,开发人员需要付出一定的学习成本去掌握更多的架构知识和框架知识。服务与服务之间通过HTTP协议或者消息传递机制通信,开发者需要选出最佳的通信机制,并解决网络服务较差时带来的风险。另外服务与服务之间相互依赖,如果修改某一个服务,会对另一个服务产生影响,如果掌控不好。会产生不必要的麻烦。由于服务的依赖性,测试也会变得很复杂,比如修改一个比较基础的服务,可能需要重启所有的服务才能完成测试。前段时间有篇很火的文章,《从微服务转为单体架构、成本降低 90%!》,无论是选择何种部署方式,我认为这都应该跟应用研发无关。 价值:应用研发与部署无关的理念确实为现代软件架构带来了显著的优势,它使得研发团队能够专注于业务逻辑和功能实现,而无需担心具体的部署细节。这种分离带来了灵活性、效率以及成本效益的多重提升。应该采用一种同时支持分布式和单体部署、且可以自由切换的架构,我们称之为可分可合。 首先,可分可合的能力使得系统能够灵活应对业务量的变化。在业务量小的时候,可以采用单体部署的方式,简化部署流程,降低初期成本。随着业务量的增长,系统可以平滑地过渡到分布式部署,通过拆分微服务来提高系统的处理能力和扩展性。这种灵活性确保了系统既能满足未来发展的需要,又能兼顾当下的成本效益。 其次,应用级别扩容的能力使得系统性能不再受限。通过增加微服务实例或调整资源配置,系统可以按需进行扩容,从而确保在业务高峰期或突发流量下仍能保持稳定的性能。这种按需扩容的方式不仅提高了系统的可靠性,还降低了运维成本。 技术原理:核心在于逻辑调用的统一执行和智能判断。通过如funEngine这一统一调用引擎,系统能够智能地选择最适合当前业务场景和性能需求的fun调用方式。无论是同步调用、异步调用还是基于消息队列的调用方式,funEngine都能进行智能决策,确保调用的高效性和可靠性。这种统一调用的方式简化了开发过程,降低了开发难度,同时也提高了系统的可维护性和可扩展性。 此外如果作为低代码或者其他研发平台来说。被集成特性也是实现该特性的关键所在。它提供了一套标准化的接口和协议,使得其他系统或应用能够轻松地与其进行集成。这种平台框架化的特性能够作为一个统一的、可扩展的框架来支撑整个系统的运行。 综上所述,具备可分可合的能力、应用级别扩容以及逻辑调用的统一执行和被集成特性,共同构成了应用研发与部署无关这一核心特性。该特性使得软件系统能够灵活地应对业务变化,实现高效、可扩展和可维护的运行,从而满足客户的长期发展需求并兼顾当下的成本效益。

    2024年5月23日
    1.5K10
  • 3.5.3 Action的类型

    各类动作我们都碰到过,但都没有展开讲过。这篇文章我们来系统介绍下oinone涉及到的所有Action类型。 一、动作类型 服务器动作ServerAction 类似于Spring MVC的控制器Controller,通过模型编码和动作名称路由,定义存储模型或代理模型将为该模型自动生成动作名称为consturct,queryOne,queryPage,create,update,delete,deleteWithFieldBatch的服务器动作。定义传输模型将为该模型自动生成动作名称为consturct的服务器动作 窗口动作ViewAction 站内跳转,通过模型编码和动作名称路由。系统将为存储模型和代理模型自动生成动作名称为redirectDetailPage的跳转详情页窗口动作,动作名称为redirectListPage的跳转列表页窗口动作,动作名称为redirectCreatePage的跳转新增页窗口动作,动作名称为redirectUpdatePage的跳转更新页窗口动作。 跳转动作UrlAction 外链跳转 客户端动作ClientAction 调用客户端函数 二、默认动作 如果在UI层级,有开放新增语义函数,则会默认生成新增的窗口动作ViewAction,跳转到新增页面 如果在UI层级,有开放更新语义函数,则会默认生成修改的窗口动作ViewAction,跳转到更新页面 如果在UI层级,有开放删除语义函数,则会默认生成删除的客户端动作ClientAction,弹出删除确认对话框 三、第一个服务器动作ServerAction 回顾第一个ServerAction 第一个ServerAction是在3.3.2【模型的类型】一文中的“代理模型”部分出现的,再来看下当时的定义代码 package pro.shushi.pamirs.demo.core.action; ……引用类 @Model.model(PetShopProxy.MODEL_MODEL) @Component public class PetShopProxyAction extends DataStatusBehavior<PetShopProxy> { @Override protected PetShopProxy fetchData(PetShopProxy data) { return data.queryById(); } @Action(displayName = "启用") public PetShopProxy dataStatusEnable(PetShopProxy data){ data = super.dataStatusEnable(data); data.updateById(); return data; } ……其他代码 } 图3-5-3-1 回顾第一个ServerAction @Action注解将创建服务器动作,并@Model.model绑定 自定义ServerAction请勿使用get、set、unset开头命名方法或toString命名方法。 ServerAction之校验(举例) Step1 为动作配置校验表达式 使用@Validation注解为PetShopProxyAction的dataStatusEnable服务端动作进行校验表达式配置 package pro.shushi.pamirs.demo.core.action; ……引用类 @Model.model(PetShopProxy.MODEL_MODEL) @Component public class PetShopProxyAction extends DataStatusBehavior<PetShopProxy> { @Override protected PetShopProxy fetchData(PetShopProxy data) { return data.queryById(); } @Validation(ruleWithTips = { @Validation.Rule(value = "!IS_BLANK(data.code)", error = "编码为必填项"), @Validation.Rule(value = "LEN(data.shopName) < 128", error = "名称过长,不能超过128位"), }) @Action(displayName = "启用") public PetShopProxy dataStatusEnable(PetShopProxy data){ data = super.dataStatusEnable(data); data.updateById(); return data; } ……其他代码 } 图3-5-3-2 为动作配置校验表达式 注: ruleWithTips可以声明多个校验规则及错误提示; IS_BLANK和LEN为内置文本函数,更多内置函数详见4.1.12【函数之内置函数与表达式】一文; 当内置函数不满足时参考4.1.13【Action之校验】一文。 Step2 重启看效果 在商店管理页面点击【启用】得到了预期返回错误信息,显示"编码为必填项" 图3-5-3-3 在商店管理页面点击启用得到了预期返回错误信息 ServerAction之前端展示规则(举例) 既然后端对ServerAction发起提交做了校验,那能不能在前端就不展示呢?当然可以,我们现在就来试下。 Step1 配置PetShopProxyAction的dataStatusEnable的前端出现规则 用注解@Action.Advanced(invisible="!(activeRecord.code !== undefined && !IS_BLANK(activeRecord.code))")来表示,注意这里配对invisible是给前端识别的,所以写法上跟后端的校验有些不一样,但如内置函数IS_BLANK这些是前后端一致实现的,activeRecord在前端用于表示当前记录。 package pro.shushi.pamirs.demo.core.action; ……引用类 @Model.model(PetShopProxy.MODEL_MODEL) @Component public class PetShopProxyAction extends DataStatusBehavior<PetShopProxy> { @Override protected PetShopProxy fetchData(PetShopProxy data) { return data.queryById(); } @Validation(ruleWithTips = { @Validation.Rule(value = "!IS_BLANK(data.code)", error = "编码为必填项"), @Validation.Rule(value = "LEN(data.name) < 128", error = "名称过长,不能超过128位"), }) @Action(displayName = "启用") @Action.Advanced(invisible="!(activeRecord.code !== undefined…

    2024年5月23日
    1.3K00
  • 1.5 Oinone与行业对比

    随着企业数字化转型的推进,软件公司获得了许多机会。尽管竞争日趋激烈,但由于需求旺盛,各种模式仍在不断涌现。因此,当前市场上存在各种各样的数字化转型解决方案,围绕企业的各个方面展开。每种解决方案都有其优点和缺点。本文将从定位、技术和产品等方面简单比较,帮助您从不同的视角了解Oinone的差异。 1.4.1 整体视角对比 一、与对标公司Odoo的对比 Odoo Oinone 定位 一站式全业务链管理平台:赋能企业信息化升级 一站式低代码商业支撑平台:赋能企业数字化升级 需求变化 关注单一企业的管理、流程、效率的提升 关注企业价值链的网络竞争,围绕外部协同、运营、数据、商业展开 技术更替 关注稳定、安全、功能丰富度 除了稳定、安全、功能丰富度以外,更强调需求响应速度、用户体验、系统承载极限与弹性扩展、智能化 表1-1 Oinone与对标公司Odoo的对比 二、与国内低代码或无代码公司对比 低代码或无代码公司 Oinone 定位 低代码开发工具:提供各类系统模版,基于模版快速搭建和个性化配置。但系统模版无法再升级 平台型SaaS:提供各类系统产品,产品安装后客户可以根据需求进行个性化调整,同时产品永远在线可升级 场景差异 只能支持企业内部人员使用,以完成部门级边缘系统为主,一般多为没有专业软件厂商支撑和强临时性特性 从内外部协同的商业场景出发,关注企业核心业务场景,适应【企业业务在线化后,所有的业务变化与创新都需要通过系统来触达上下游】的时代背景,以敏捷响应业务的变化与创新为目标 技术代差 单表支撑100万数据已是业内天花板 支撑单模型数据过亿,无单点瓶颈。封装互联网架构并且做到单体与分布式的灵活部署,为不同大小公司提供不同技术支撑 表1-2 Oinone与国内低代码/无代码公司对比 1.4.2 从技术角度对比 我们不会与其他无代码平台进行比较,因为它们不能解决业务复杂性的问题。相反,我们将重点介绍三种不同的低代码平台模式(如下图1-8所示)。 第一种模式是最基础的低代码平台,也被称为代码生成器。它通过预定义应用程序模板和必要的配置生成代码,简化了工程搭建并提供了一些基础逻辑。虽然在信息化时代内部流程标准化方面较为适合,但在数字化时代外部协同业务在线的情况下就不那么合适了。因为这种模式不能减少研发难度和提高效率,也无法体现敏捷迭代快速创新的优势。 第二种模式是经典的低代码平台,以元数据为基础,以模型为驱动。当无法满足需要时,通过特定方式将代码以插件的形式注入平台,作为低代码平台的内置逻辑,供设计器使用。它的优点在于降低了研发门槛,当无法满足需求时才需要编写代码。它可以实现企业内部的复杂流程和复杂逻辑,但其性能和工程管理存在局限性。性能问题使其不适合处理互联网化的在线业务,而工程管理问题则使其不适合处理快速变化的业务。这也是许多研发人员反对低代码的核心原因之一,因为研发人员变成了辅助角色,而软件工程是一门需要技术能力的学科,让没有技术能力的人主导是违反常理的。对于软件产品公司来说,产品需要迭代规划,需要多人协作,需要工程化管理。 第三种模式是oinone提出的基于互联网架构的低代码平台,它采用低无一体的设计。首先,oinone屏蔽了互联网架构带来的复杂性。其次,同样以元数据为基础,以模型为驱动,但是元数据的生成方式有两种:一种是使用无代码设计器(与经典低代码相同),另一种是通过代码来描述元数据。通过使用代码来描述元数据,可以无缝地与代码衔接,并在不改变研发习惯的情况下降低门槛、提高效率,并进行工程化管理。 最后总结来说:低无一体不仅仅是指两种模式的结合,还包括两种模式的融合应用方式。具体来说,这种融合应用方式可以分为两种情况: 当开发核心产品时,主要采用低代码开发,无代码设计器作为辅助。这种方式可以提高开发效率和代码质量,同时保证产品的快速迭代和升级。 当需要满足个性化或非产品支持的需求时,主要采用无代码设计器,低代码作为辅助。这种方式可以快速地满足客户需求,并且避免对产品的核心代码产生影响。 简单来说,低代码模式适用于产品的迭代升级,而无代码设计器则适用于满足个性化和非产品支撑的额外需求。低代码和无代码模式在整个软件生命周期中都有各自的价值,在不同场景下可以相互融合,发挥最大的优势。 图1-8 代码生成器、低代码平台与Oinone的优缺点对比 1.4.3 从产品角度对比 产品上的对比,从客户、场景满足度、再次销售三个方面来做简易的对比 一、Oinone vs 数字化软件服务商 客户 满足度 销售 Oinone 一站式商业智能软件,更高性价比、用户体验客户范围:5000万~5亿、5亿~100亿、标杆:100亿~1000亿、1000亿以上 满足企业核心业务需求,并联合伙伴一起满足企业所有需求,无需集成提供统一工作台、数据接口、底层协议,无论基于Oinone的开源框架还是增加其他应用都有很好的扩展性 支持OP+SaaS两种模式,收费方式不同:OP按买断方式进行,SaaS按效果付费跟账号数无关新的模块进行二次销售 数字化软件服务商 针对成熟的大型企业需投入巨大资源和成本客户范围:100亿~1000亿、1000亿以上 满足企业部分需求,无法输出技术标准,无法解决多供应商一起开发的问题,只能通过集成实现对接 OP模式进行销售,通过设置权限来进行来实现二次销售或无法进行二次销售 表1-3 Oinone vs 数字化软件服务商 二、Oinone vs 低代码或无代码行业 客户 满足度 销售 Oinone 一站式商业智能软件客户范围:5000万~5亿、5亿~100亿、标杆:100亿~1000亿、1000亿以上 从外部商业场景出发,强业务场景驱动,符合企业从信息化管理到业务创新的数字化转变的趋势。提供统一工作台、数据接口、底层协议,无论基于oinone的开源框架还是增加其他应用都有很好的扩展性 支持OP+SaaS两种模式,收费方式不同:OP按买断方式进行,SaaS按效果付费跟账号数无关新的模块进行二次销售 低代码或无代码公司 针对小微企业内部信息化管理诉求,以表单流程为主客户范围:5亿以下 满足企业部门级信息化的适应性需求,无法满足企业核心业务管理与业务创新诉求 按应用模块进行收费,新的模块进行二次销售 表1-4 Oinone vs 低代码或无代码行业 三、Oinone vs 国外对标公司Odoo 客户 满足度 销售 Oinone 一站式商业智能软件客户范围:5000万~5亿、5亿~100亿、标杆:100亿~1000亿、1000亿以上 从外部商业场景出发,强业务场景驱动,符合企业从信息化管理到业务创新的数字化转变的趋势。基线产品覆盖:采购、营销、服务、销售、交易等企业商业领域。主要涉及行业:零售品牌。其他领域或行业靠合作伙伴共建方式进行 支持OP+SaaS两种模式,收费方式不同:OP按买断方式进行,SaaS按效果付费跟账号数无关新的模块进行二次销售 Odoo 一站式企业管理软件客户范围:5000万~5亿、5亿~100亿、标杆:100亿~1000亿、1000亿以上 从企业内部管理需求出发,逐渐拥有互联网相关应用组件,但还是属于强内部管理、弱外部场景。基线产品覆盖:业务财务一体化、人财务、进销存。主要涉及行业:建造业。其他领域或行业靠合作伙伴共建方式进行 支持OP+SaaS两种模式,收费方式相同:按用户数+应用模块进行收费新的模块进行二次销售 表1-5 Oinone vs 国外对标公司Odoo

    2024年5月23日
    1.7K20

Leave a Reply

登录后才能评论