Dubbo配置详解

概述

Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

Oinone平台默认使用dubbo-v2.7.22版本,本文以该版本为例进行描述。

基本概念

Dubbo在注册provider/consumer时使用Netty作为RPC调用的核心服务,其具备客户端/服务端(C/S)的基本特性。即:provider作为服务端consumer作为客户端

客户端通过服务中心发现有服务可被调用时,将通过服务中心提供的服务端调用信息,连接服务端并发起请求,从而实现远程调用。

服务注册(绑定Host/Port)

JAVA程序启动时,需要将provider的信息注册到服务中心,并在当前环境为Netty服务开启Host/Port监听,以实现服务注册功能。

在下文中,我们通过绑定Host/Port表示Netty服务的访问地址,通过注册Host/Port表示客户端的访问地址。

使用yaml配置绑定Host/Port

PS:该配置可在多种环境中通用,改变部署方式无需修改此配置。

dubbo:
  protocol:
    name: dubbo
    # host: 0.0.0.0
    port: -1

假设当前环境的可用IP为192.168.1.100

以上配置将使得Netty服务默认绑定在0.0.0.0:20880地址,服务注册地址为192.168.1.100:20880

客户端将通过192.168.1.100:20880调用服务端服务

若发生20880端口占用,则自动向后查找可用端口。如20881、20882等等

若当前可用端口为20881,则以上配置将使得Netty服务默认绑定在0.0.0.0:20881地址,服务注册地址为192.168.1.100:20881

使用环境变量配置注册Host/Port

当服务端被放置在容器环境中时,由于容器环境的特殊性,其内部的网络配置相对于宿主机而言是独立的。因此为保证客户端可以正常调用服务端,还需在容器中配置环境变量,以确保客户端可以通过指定的注册Host/Port进行访问。

以下示例为体现无法使用20880端口的情况,将宿主机可访问端口从20880改为20881。

DUBBO_IP_TO_REGISTRY=192.168.1.100
DUBBO_PORT_TO_REGISTRY=20881

假设当前宿主机环境的可用IP为192.168.1.100

以上配置将使得Netty服务默认绑定在0.0.0.0:20881地址,服务注册地址为192.168.1.100:20881

客户端将通过192.168.1.100:20881调用服务端服务

使用docker/docker-compose启动

需添加端口映射,将20881端口映射至宿主机20881端口。(此处容器内的端口发生变化,若需要了解具体原因,可参考题外话章节)

docker-run

IP=192.168.1.100

docker run -d --name designer-allinone-full \
-e DUBBO_IP_TO_REGISTRY=$IP \
-e DUBBO_PORT_TO_REGISTRY=20881 \
-p 20881:20881 \

docker-compose

services:
  backend:
    container_name: designer-backend
    image: harbor.oinone.top/oinone/designer-backend-v5.0
    restart: always
    environment:
      DUBBO_IP_TO_REGISTRY: 192.168.1.100
      DUBBO_PORT_TO_REGISTRY: 20881
    ports:
     - 20881:20881 # dubbo端口

使用kubernetes启动

工作负载(Deployment)

kind: Deployment
apiVersion: apps/v1
spec:
  replicas: 1
  template:
    spec:
      containers:
        - name: designer-backend
          image: harbor.oinone.top/oinone/designer-backend-v5.0
          ports:
            - name: dubbo
              containerPort: 20881
              protocol: TCP
          env:
            - name: DUBBO_IP_TO_REGISTRY
              value: "192.168.1.100"
            - name: DUBBO_PORT_TO_REGISTRY
              value: "20881"

服务(Services)

kind: Service
apiVersion: v1
spec:
  type: NodePort
  ports:
    - name: dubbo
      protocol: TCP
      port: 20881
      targetPort: dubbo
      nodePort: 20881

PS:此处的targetPort为对应Deployment#spec. template.spec.containers.ports.name配置的端口名称。若未配置,可使用20881直接指定对应容器的端口号。

使用kubernetes其他暴露服务方式

在Kubernetes中部署服务,有多种配置方式均可用暴露服务。上述配置仅用于通过Service/NodePort20881端口暴露至宿主机,其他服务可用通过任意Kubernetes节点IP进行调用。

若其他服务也在Kubernetes中进行部署,则可以通过Service/Service方式进行调用。将DUBBO_IP_TO_REGISTRY配置为${serviceName}.${namespace}即可。

若其他服务无法直接访问Kubernetes的master服务,则可以通过Ingress/Service方式进行调用。将DUBBO_IP_TO_REGISTRY配置为Ingress可解析域名即可。

Dubbo调用链路图解

PS: Consumer绑定Host/Port是其作为Provider使用的,下面所有图解仅演示单向的调用链路。

名词解释

  • Provider: 服务提供者(JVM)
  • Physical Machine Provider: 服务提供者所在物理机
  • Provider Container: 服务提供者所在容器
  • Kubernetes Service: Kubernetes Service资源类型
  • Consumer: 服务消费者(JVM)
  • Registration Center: 注册中心;可以是zookeepernacos等。
  • bind: 服务绑定Host/Port到指定ip:port
  • registry: 服务注册;注册Host/Port到注册中心的信息。
  • discovery: 服务发现;注册Host/Port到消费者的信息。
  • invoke: 服务调用;消费者通过注册中心提供的提供者信息向提供者发起服务调用。
  • forward: 网络转发;通常在容器环境需要进行必要的网络转发,以使得服务调用可以到达服务提供者。

物理机/物理机调用链路

image.png

``` mermaid
sequenceDiagram

participant p as Provider<br>(bind 0.0.0.0:20880)
participant m as Physical Machine Provider<br>(bind 192.168.1.100:20881)
participant rc as Registration Center<br>(zookeeper/nacos)
participant c as Consumer<br>(bind 0.0.0.0:20881)

p-->>+m: bind 192.168.1.100:20880
m->>+rc: registry 192.168.1.100:20880
rc->>+c: discovery 192.168.1.100:20880
c->>+m: invoke 192.168.1.100:20880
m-->>+p: forward 0.0.0.0:20881
```

PS: 此处虚线部分表示提供者部署在物理机上,并不存在真实的网络处理。

容器/物理机调用链路

image.png

``` mermaid
sequenceDiagram

participant p as Provider<br>(bind 0.0.0.0:20881)
participant pc as Provider Container<br>(bind 172.17.1.100:20881)
participant m as Physical Machine Provider<br>(bind 192.168.1.100:20881)
participant rc as Registration Center<br>(zookeeper/nacos)
participant c as Consumer<br>(bind 0.0.0.0:20882)

p-->>+pc: bind 172.26.1.100:20881
pc->>+m: mapping 192.168.1.100:20881
pc->>+rc: registry 192.168.1.100:20881
rc->>+c: discovery 192.168.1.100:20881
c->>+m: invoke 192.168.1.100:20881
m->>+pc: forward 172.17.1.100:20881
pc-->>+p: forward 0.0.0.0:20881
```

PS: 此处虚线部分表示提供者部署在容器中,并不存在真实的网络处理。

Kubernetes/物理机(Service/NodePort模式)调用链路

image.png

``` mermaid
sequenceDiagram

participant p as Provider<br>(bind 0.0.0.0:20881)
participant pc as Provider Container<br>(bind 172.17.1.100:20881)
participant ks as Kubernetes Service<br>targetPort 172.17.1.100:20881<br>nodePort 192.168.1.100:20881
participant m as Physical Machine Provider<br>(bind 192.168.1.100:20881)
participant rc as Registration Center<br>(zookeeper/nacos)
participant c as Consumer<br>(bind 0.0.0.0:20882)

p-->>+pc: bind 172.26.1.100:20881
pc->>+ks: mapping 192.168.1.100:20881
ks->>+m: mapping 192.168.1.100:20881
pc->>+rc: registry 192.168.1.100:20881
rc->>+c: discovery 192.168.1.100:20881
c->>+m: invoke 192.168.1.100:20881
m->>+ks: forward 192.168.1.100:20881
ks->>+pc: forward 172.17.1.100:20881
pc-->>+p: forward 0.0.0.0:20881
```

PS: 此处虚线部分表示提供者部署在容器中,并不存在真实的网络处理。

题外话

dubbo-v2.7.22源码中,作者发现Host/Port的获取方式并不对等,这里目前不太清楚是dubbo设计如此还是作者对dubbo设计理解不足。

  • 现象:

    • DUBBO_IP_TO_REGISTRY配置与dubbo.protocol.host无关。
    • DUBBO_PORT_TO_REGISTRY配置优先级高于dubbo.protocol.port配置。
  • 作者理解:

    • 客户端向服务端发起请求时,应使用注册Host/Port进行调用,只要该访问地址可以与服务端连通,则远程调用就可以正常运行。
    • 注册Host/Port绑定Host/Port应支持完全独立配置。当注册Host/Port绑定Host/Port均被配置时,注册Host绑定Host是独立生效的,但绑定Port却强制使用了注册Port。(这一点也是经常在容器环境中无法正常调用的主要原因)

常用配置

yaml配置

dubbo:
  application:
    name: pamirs-test
    version: 1.0.0
  registry:
    address: zookeeper://127.0.0.1:2181
    # group: demo
    # timeout: 5000
  protocol:
    name: dubbo
    # host: 0.0.0.0
    port: -1
    serialization: pamirs
    payload: 104857600
  scan:
    base-packages: pro.shushi
  cloud:
    subscribed-services:
  • dubbo.registry.address: 注册中心地址
  • dubbo.registry.group: 全局group配置
  • dubbo.registry.timeout: 全局超时时间配置
  • dubbo.protocol.name: 协议名称
  • dubbo.protocol.host: 绑定主机IP配置;默认:0.0.0.0
  • dubbo.protocol.port: 绑定主机端口配置;-1表示自动获取可用端口;默认:20880
  • dubbo.protocol.serialization: 序列化配置;Oinone平台必须使用pamirs作为序列化方式。
  • dubbo.protocol.payload: RPC调用数据大小限制;单位:字节(byte)
  • dubbo.scan.base-packages: provider/consumer扫描包路径
  • dubbo.cloud.subscribed-services: 多提供者配置;示例中该参数配置为空是为了避免启动时的警告日志,一般无需配置。

环境变量配置

DUBBO_IP_TO_REGISTRY=127.0.0.1
DUBBO_PORT_TO_REGISTRY=20880
  • DUBBO_IP_TO_REGISTRY:注册Host配置
  • DUBBO_PORT_TO_REGISTRY:注册Port配置

源码参考

  • org.apache.dubbo.config.ServiceConfig#findConfigedHosts
private String findConfigedHosts(ProtocolConfig protocolConfig,
                                 List<URL> registryURLs,
                                 Map<String, String> map) {
    boolean anyhost = false;

    String hostToBind = getValueFromConfig(protocolConfig, DUBBO_IP_TO_BIND);
    if (hostToBind != null && hostToBind.length() > 0 && isInvalidLocalHost(hostToBind)) {
        throw new IllegalArgumentException("Specified invalid bind ip from property:" + DUBBO_IP_TO_BIND + ", value:" + hostToBind);
    }

    // if bind ip is not found in environment, keep looking up
    if (StringUtils.isEmpty(hostToBind)) {
        hostToBind = protocolConfig.getHost();
        if (provider != null && StringUtils.isEmpty(hostToBind)) {
            hostToBind = provider.getHost();
        }
        if (isInvalidLocalHost(hostToBind)) {
            anyhost = true;
            logger.info("No valid ip found from environment, try to get local host.");
            hostToBind = getLocalHost();
        }
    }

    map.put(BIND_IP_KEY, hostToBind);

    // registry ip is not used for bind ip by default
    String hostToRegistry = getValueFromConfig(protocolConfig, DUBBO_IP_TO_REGISTRY);
    if (hostToRegistry != null && hostToRegistry.length() > 0 && isInvalidLocalHost(hostToRegistry)) {
        throw new IllegalArgumentException(
                "Specified invalid registry ip from property:" + DUBBO_IP_TO_REGISTRY + ", value:" + hostToRegistry);
    } else if (StringUtils.isEmpty(hostToRegistry)) {
        // bind ip is used as registry ip by default
        hostToRegistry = hostToBind;
    }

    map.put(ANYHOST_KEY, String.valueOf(anyhost));

    return hostToRegistry;
}
  • org.apache.dubbo.config.ServiceConfig#findConfigedPorts
private Integer findConfigedPorts(ProtocolConfig protocolConfig,
                                  String name,
                                  Map<String, String> map, int protocolConfigNum) {
    Integer portToBind = null;

    // parse bind port from environment
    String port = getValueFromConfig(protocolConfig, DUBBO_PORT_TO_BIND);
    portToBind = parsePort(port);

    // if there's no bind port found from environment, keep looking up.
    if (portToBind == null) {
        portToBind = protocolConfig.getPort();
        if (provider != null && (portToBind == null || portToBind == 0)) {
            portToBind = provider.getPort();
        }
        final int defaultPort = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(name).getDefaultPort();
        if (portToBind == null || portToBind == 0) {
            portToBind = defaultPort;
        }
        if (portToBind <= 0) {
            portToBind = getRandomPort(name);
            if (portToBind == null || portToBind < 0) {
                portToBind = getAvailablePort(defaultPort);
                putRandomPort(name, portToBind);
            }
        }
    }

    // registry port, not used as bind port by default
    String key = DUBBO_PORT_TO_REGISTRY;
    if (protocolConfigNum > 1) {
        key = getProtocolConfigId(protocolConfig).toUpperCase() + "_" + key;
    }
    String portToRegistryStr = getValueFromConfig(protocolConfig, key);
    Integer portToRegistry = parsePort(portToRegistryStr);
    if (portToRegistry != null) {
        portToBind = portToRegistry;
    }

    // save bind port, used as url's key later
    map.put(BIND_PORT_KEY, String.valueOf(portToBind));

    return portToBind;
}
  • org.apache.dubbo.config.ServiceConfig#getValueFromConfig
private String getValueFromConfig(ProtocolConfig protocolConfig, String key) {
    String protocolPrefix = protocolConfig.getName().toUpperCase() + "_";
    String value = ConfigUtils.getSystemProperty(protocolPrefix + key);
    if (StringUtils.isEmpty(value)) {
        value = ConfigUtils.getSystemProperty(key);
    }
    return value;
}
  • org.apache.dubbo.common.utils.ConfigUtils#getSystemProperty
public static String getSystemProperty(String key) {
    String value = System.getenv(key);
    if (StringUtils.isEmpty(value)) {
        value = System.getProperty(key);
    }
    return value;
}

Oinone社区 作者:张博昊原创文章,如若转载,请注明出处:https://doc.oinone.top/backend/16028.html

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

(1)
张博昊的头像张博昊数式管理员
上一篇 2024年8月9日 pm6:02
下一篇 2024年8月12日 pm5:37

相关推荐

  • 后端代码规范

    前言 虽然oinone框架减少了很多的代码,但是低代码部分的代码质量也需要高度关注,不管是写的代码bug多,或者说被吐槽代码不行,还是说写的代码经常被重构,核心点还是没有代码规范的意识和技巧,下面摘录了一些常见的规范要求,去提高后端的代码质量,代码质量提高后,自然效率也会提升。 常见代码规范 **1、规范命名** 命名是写代码中最频繁的操作,比如类、属性、方法、参数等。好的名字应当能遵循以下几点: **见名知意** 比如需要定义一个变量需要来计数 int i = 0; 名称 i 没有任何的实际意义,没有体现出数量的意思,所以我们应当指明数量的名称 int count = 0; **能够读的出来** 如下代码: private String sfzh; private String dhhm; 这些变量的名称,根本读不出来,更别说实际意义了。 所以我们可以使用正确的可以读出来的英文来命名 private String idCardNo; private String phone; **2、规范代码格式** 好的代码格式能够让人感觉看起来代码更加舒适。 好的代码格式应当遵守以下几点: 合适的空格 代码对齐,比如大括号要对齐 及时换行,一行不要写太多代码 好在现在开发工具支持一键格式化,可以帮助美化代码格式,大家统一使用idea的规范即可。 **3、写好代码注释** 在《代码整洁之道》这本书中作者提到了一个观点,注释的恰当用法是用来弥补我们在用代码表达意图时的失败。换句话说,当无法通过读代码来了解代码所表达的意思的时候,就需要用注释来说明。 书的作者之所以这么说,是因为作者觉得随着时间的推移,代码可能会变动,如果不及时更新注释,那么注释就容易产生误导,偏离代码的实际意义。而不及时更新注释的原因是,程序员不喜欢写注释。😒 但是这不意味着可以不写注释,当通过代码如果无法表达意思的时候,就需要注释,比如如下代码: for (Integer id : ids) { if (id == 0) { continue; } //做其他事 } 为什么 id == 0 需要跳过,代码是无法看出来了,就需要注释了。 好的注释应当满足一下几点: 解释代码的意图,说明为什么这么写,用来做什么 对参数和返回值注释,入参代表什么,出参代表什么 有警示作用,比如说入参不能为空,或者代码是不是有坑 当代码还未完成时可以使用 todo 注释来标记 代码review发现漏洞时 可以使用 fixme 注释来标记 **4、try catch 内部代码抽成一个方法** try catch代码有时会干扰我们阅读核心的代码逻辑,这时就可以把try catch内部主逻辑抽离成一个单独的方法 如下图是Eureka服务端源码中服务下线的实现中的一段代码 整个方法非常长,try中代码是真正的服务下线的代码实现,finally可以保证读锁最终一定可以释放。 所以这段代码其实就可以对核心的逻辑进行抽取。 protected boolean internalCancel(String appName, String id, boolean isReplication) { try { read.lock(); doInternalCancel(appName, id, isReplication); } finally { read.unlock(); } // 剩余代码 } private boolean doInternalCancel(String appName, String id, boolean isReplication) { //真正处理下线的逻辑 } **5、方法别太长** 方法别太长就是字面的意思。一旦代码太长,给人的第一眼感觉就很复杂,让人不想读下去; 同时方法太长的代码可能读起来容易让人摸不着头脑,不知道哪一些代码是同一个业务的功能。 比如代码中有那种2000+行大类,各种if else判断,光理清代码思路就需要用很久时间。🤷🏻‍♀️ 所以一旦方法过长,可以尝试将相同业务功能的代码单独抽取一个方法,最后在主方法中调用即可。 **6、抽取重复代码** 当一份代码重复出现在程序的多处地方,就会造成程序又臭又长,当这份代码的结构要修改时,每一处出现这份代码的地方都得修改,导致程序的扩展性很差。 所以一般遇到这种情况,可以抽取成一个工具类,还可以抽成一个公共的父类。 **7、多用return** 在有时我们平时写代码的情况可能会出现if条件套if的情况,当if条件过多的时候可能会出现如下情况: if (条件1) { if (条件2) { if (条件3) { if (条件4) { if (条件5) { System.out.println("11111"); } } } } } 面对这种情况,可以换种思路,使用return来优化 if (!条件1) { return; } if (!条件2) { return; } if (!条件3) { return; } if (!条件4) { return; } if (!条件5) { return; } System.out.println("11111"); 这样优化就感觉看起来更加直观 **8、if条件表达式不要太复杂**…

    2024年12月11日
    2.6K00
  • IWrapper、QueryWrapper和LambdaQueryWrapper使用

    条件更新updateByWrapper 通常我们在更新的时候new一个对象出来在去更新,减少更新的字段 Integer update = new DemoUser().updateByWrapper(new DemoUser().setFirstLogin(Boolean.FALSE), Pops.<DemoUser>lambdaUpdate().from(DemoUser.MODEL_MODEL).eq(IdModel::getId, userId) 使用基础模型的updateById方法更新指定字段的方法: new 一下update对象出来,更新这个对象。 WorkflowUserTask userTaskUp = new WorkflowUserTask(); userTaskUp.setId(userTask.getId()); userTaskUp.setNodeContext(json); userTaskUp.updateById(); 条件删除updateByWrapper public List<T> delete(List<T> data) { List<Long> petTypeIdList = new ArrayList(); for(T item:data){ petTypeIdList.add(item.getId()); } Models.data().deleteByWrapper(Pops.<PetType>lambdaQuery().from(PetType.MODEL_MODEL).in(PetType::getId,petTypeIdList)); return data; } 构造条件查询数据 示例1: LambdaQueryWrapper拼接查询条件 private void queryPetShops() { LambdaQueryWrapper<PetShop> query = Pops.<PetShop>lambdaQuery(); query.from(PetShop.MODEL_MODEL); query.setSortable(Boolean.FALSE); query.orderBy(true, true, PetShop::getId); List<PetShop> petShops2 = new PetShop().queryList(query); System.out.printf(petShops2.size() + ""); } 示例2: IWrapper拼接查询条件 private void queryPetShops() { IWrapper<PetShop> wrapper = Pops.<PetShop>lambdaQuery() .from(PetShop.MODEL_MODEL).eq(PetShop::getId,1L); List<PetShop> petShops4 = new PetShop().queryList(wrapper); System.out.printf(petShops4.size() + ""); } 示例3: QueryWrapper拼接查询条件 private void queryPetShops() { //使用Lambda获取字段名,防止后面改字段名漏改 String nameField = LambdaUtil.fetchFieldName(PetTalent::getName); //使用Lambda获取Clumon名,防止后面改字段名漏改 String nameColumn = PStringUtils.fieldName2Column(nameField); QueryWrapper<PetShop> wrapper2 = new QueryWrapper<PetShop>().from(PetShop.MODEL_MODEL) .eq(nameColumn, "test"); List<PetShop> petShops5 = new PetShop().queryList(wrapper2); System.out.printf(petShops5.size() + ""); } IWrapper转为LambdaQueryWrapper @Function.Advanced(type= FunctionTypeEnum.QUERY) @Function.fun(FunctionConstants.queryPage) @Function(openLevel = {FunctionOpenEnum.API}) public Pagination<PetShopProxy> queryPage(Pagination<PetShopProxy> page, IWrapper<PetShopProxy> queryWrapper) { LambdaQueryWrapper<PetShopProxy> wrapper = ((QueryWrapper<PetShopProxy>) queryWrapper).lambda(); // 非存储字段从QueryData中获取 Map<String, Object> queryData = queryWrapper.getQueryData(); if (null != queryData && !queryData.isEmpty()) { String codes = (String) queryData.get("codes"); if (org.apache.commons.lang3.StringUtils.isNotBlank(codes)) { wrapper.in(PetShopProxy::getCode, codes.split(",")); } } return new PetShopProxy().queryPage(page, wrapper); }

    2024年5月25日
    1.8K00
  • 如何扩展自有的文件存储系统

    介绍 数式Oinone默认提供了阿里云、腾讯云、华为云、又拍云、Minio和本地文件存储这几种文件存储系统,如果我们有其他的文件存储系统需要对接,或者是扩展现有的文件系统,可以通过SPI继承AbstractFileClient注册新的文件存储系统。 代码示例 这里以扩展自有的本地文件系统为例 继承了内置的本地文件存储LocalFileClient,将其中上传文件的方法重写 package pro.shushi.pamirs.demo.core.file; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest; import pro.shushi.pamirs.framework.connectors.cdn.client.LocalFileClient; import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j; import pro.shushi.pamirs.meta.common.spi.SPI; import javax.servlet.http.HttpServletRequest; @Slf4j @Component // 注册新的文件存储系统类型 @SPI.Service(DemoLocalFileClient.TYPE) @RestController @RequestMapping("/demo_file") public class DemoLocalFileClient extends LocalFileClient { public static final String TYPE = "DEMO_LOCAL"; @Override public CdnFileForm getFormData(String fileName) { CdnConfig cdnConfig = getCdnConfig(); CdnFileForm fileForm = new CdnFileForm(); String uniqueFileName = Spider.getDefaultExtension(CdnFileNameApi.class).getNewFilename(fileName); String fileKey = getFileKey(cdnConfig.getMainDir(), uniqueFileName); //前端获取uploadUrl,上传文件到该地址 fileForm.setUploadUrl(cdnConfig.getUploadUrl() + "/demo_file/upload"); //上传后,前端将downloadUrl返回给后端 fileForm.setDownloadUrl(getDownloadUrl(fileKey)); fileForm.setFileName(uniqueFileName); Map<String, Object> formDataJson = new HashMap<>(); formDataJson.put("uniqueFileName", uniqueFileName); formDataJson.put("key", fileKey); fileForm.setFormDataJson(JSON.toJSONString(formDataJson)); return fileForm; } @ResponseBody @RequestMapping(value = "/upload", produces = "multipart/form-data;charset=UTF-8",method = RequestMethod.POST) public String uploadFileToLocal(HttpServletRequest request) { MultipartFile file = ((StandardMultipartHttpServletRequest) request).getFile("file"); // 例如可以根据file文件类型判断哪些文件是否可以上传 return super.uploadFileToLocal(request); } } 在application.yml内配置 cdn: oss: name: 本地文件系统 # 这里的type与代码中定义的文件存储系统类型对应 type: DEMO_LOCAL bucket: pamirs uploadUrl: http://127.0.0.1:8190 downloadUrl: http://127.0.0.1:6800 validTime: 3600000 timeout: 600000 active: true referer: localFolderUrl: /Users/demo/workspace/static

    2024年10月24日
    74600
  • 【Excel导入/导出】多Sheet导入导出示例(4.7.x及以上版本)

    代码示例 示例仅供参考 点击下载代码示例 业务场景 准备工作:两个模型,物料(Material)和物料类别(MaterialCategory)。目标:在一个Excel模板中同时导入和导出两个模型的数据。 Material模型 @Model.model(Material.MODEL_MODEL) @Model.Advanced(unique = {"code"}) @Model(displayName = "物料", labelFields = {"name"}) public class Material extends IdModel { private static final long serialVersionUID = -2594216864389636135L; public static final String MODEL_MODEL = "maas.Material"; @Field.String @Field(displayName = "物料编码", required = true) private String code; @Field.String @Field(displayName = "物料名称", required = true) private String name; } MaterialCategory模型 @Model.model(MaterialCategory.MODEL_MODEL) @Model.Advanced(unique = {"code"}) @Model(displayName = "物料类别", labelFields = {"name"}) public class MaterialCategory extends IdModel { private static final long serialVersionUID = 6300896634558908349L; public static final String MODEL_MODEL = "maas.MaterialCategory"; @Field.String @Field(displayName = "类别编码", required = true) private String code; @Field.String @Field(displayName = "类别名称", required = true) private String name; } 模板定义 MaterialTemplate @Component public class MaterialTemplate implements ExcelTemplateInit { public static final String TEMPLATE_NAME = "materialTemplate"; @Override public List<ExcelWorkbookDefinition> generator() { WorkbookDefinitionBuilder builder = WorkbookDefinitionBuilder.newInstance(Material.MODEL_MODEL, TEMPLATE_NAME) .setDisplayName("物料和物料类别") .setEachImport(Boolean.FALSE);//设置importData的入参为 (ExcelImportContext importContext, List<MaterialCategory> data),如入参是单个对象,请删除setEachImport(Boolean.FALSE) createMaterialSheet(builder); createMaterialCategorySheet(builder); return Collections.singletonList(builder.build()); } private static void createMaterialSheet(WorkbookDefinitionBuilder builder) { builder.createSheet().setName("物料") .createBlock(Material.MODEL_MODEL, ExcelAnalysisTypeEnum.FIXED_HEADER, ExcelDirectionEnum.HORIZONTAL, "A1:B2") .createHeader().setStyleBuilder(ExcelHelper.createDefaultStyle()).setIsConfig(Boolean.TRUE) .createCell().setField("code").setAutoSizeColumn(Boolean.TRUE).and() .createCell().setField("name").setAutoSizeColumn(Boolean.TRUE).and() .and() .createHeader().setStyleBuilder(ExcelHelper.createDefaultStyle(v -> v.setBold(Boolean.TRUE)).setHorizontalAlignment(ExcelHorizontalAlignmentEnum.CENTER)) .createCell().setValue("物料编码").and() .createCell().setValue("物料名称"); } private static void createMaterialCategorySheet(WorkbookDefinitionBuilder builder) { builder.createSheet().setName("物料类别") .createBlock(MaterialCategory.MODEL_MODEL, ExcelAnalysisTypeEnum.FIXED_HEADER, ExcelDirectionEnum.HORIZONTAL, "A1:B2") .createHeader().setStyleBuilder(ExcelHelper.createDefaultStyle()).setIsConfig(Boolean.TRUE) .createCell().setField("code").setAutoSizeColumn(Boolean.TRUE).and()…

    2024年4月23日
    1.2K00
  • 【后端】项目开发后端知识要点地图

    目录 工程结构篇 协议篇 GraphQL请求:后端接口实现逻辑解析 基本功能及配置篇 Dubbo Dubbo配置详解 Nacos Oinone项目引入Nacos作为注册中心 Oinone项目引入Nacos作为配置中心 Nacos做为注册中心调用其他系统的SpringCloud服务 OSS OSS(CDN)配置和文件系统的一些操作 MINIO无公网访问地址下OSS的配置 Trigger/Async/Schedule 函数之触发与定时配置和示例 函数之异步执行 Excel导入/导出(file) Excel批量导入 【Excel导入/导出】多Sheet导入导出示例 如何自定义Excel导入功能 如何自定义Excel导出功能 Excel导入导出模板翻译 Expression(表达式) 扩展内置函数表达式 ShardingJDBC(分库分表) 分库分表与自定义分表规则 Elasticsearch(ES) Oinone引入搜索引擎(增强模型) 引入搜索(增强模型Channel)常见问题解决办法 数据库方言配置(Dialect) 【DM】后端部署使用Dameng数据库(达梦) 【PostgreSQL】后端部署使用PostgreSQL数据库(PGSQL) 【OpenGauss】后端部署使用OpenGauss数据库(高斯) 【MSSQL】后端部署使用MSSQL数据库(SQLServer) 【KDB】后端部署使用Kingbase数据库(人大金仓/电科金仓) 【Oracle】后端部署使用Oracle数据库 【OceanBase】后端部署使用 OceanBase 数据库(海扬/OB) 其他功能使用文档 框架之MessageHub(信息提示) DsHint(指定数据源)和BatchSizeHint(指定批次数量) IWrapper、QueryWrapper和LambdaQueryWrapper使用 查询时自定义排序字段和排序规则 如何在代码中使用自增ID和获取序列 非存储字段搜索,适应灵活的搜索场景 如何使用位运算的数据字典 全局首页及应用首页配置方法(homepage) 如何增加用户中心的菜单 自定义RSQL占位符(placeholder)及在权限中使用 Function、Action函数使用规范 特定场景解决方案 Oinone连接外部数据源方案 如何自定义SQL(Mapper)语句 工程部署 后端部署 Oinone平台部署及依赖说明 v4.7 v5.0 v5.1 v5.3 v6.2 Oinone License 许可证使用常见问题 后端无代码设计器Jar包启动方法 Oinone环境保护(v5.2.3以上) 设计器部署 Oinone设计器部署参数说明 Oinone离线部署设计器镜像 Oinone离线部署设计器JAR包 Docker部署常见问题 其他环境部署 东方通Web和Tomcat部署Oinone项目 可视化调试工具 Oinone平台可视化调试工具 协同开发 Oinone协同开发使用手册 工作流 项目中工作流引入和流程触发 【工作流】流程扩展自定义函数示例代码汇总 工作流-流程代办等页面自定义 工作流审核撤回/回退/拒绝钩子使用 如何添加工作流运行时依赖(前后端) 数据可视化运行时 如何添加数据可视化运行时依赖 界面设计器 如何实现页面间的跳转 界面设计器的导入导出 流程设计器 流程设计器的导入导出 数据可视化 数据可视化-项目中如何引用图表、报表、大屏 数据可视化中图表的低无一体 数据可视化-如何自定义查询数据方法 数据可视化的导入导出 其他 EIP开放接口使用MD5验签发起请求(v5.x) 缓存连接由Jedis切换为Lettuce Oinone登录扩展:对接SSO QA 导入设计数据时dubbo超时导入失败

    2024年10月23日
    3.1K00

Leave a Reply

登录后才能评论