Dubbo泛化调用返回结果对象参数丢失的问题分析

news2024/12/28 23:11:52

最近在使用jvm-sandbox-repeater做引流回放,针对dubbo的流量做相应的回放,结果发现一个问题dubbo接口录制的返回值与回放的返回值不一样。

问题排查

我们先看看返回的差异在什么地方

在这里插入图片描述
左侧是回放的流量返回结果, 右侧是录制的返回结果。 结果发现流量回访多了一个pages, 少了一个optimizeJoinOfCountSql, 我们这里先不考虑class 。

这里首先怀疑的对象是我们回放的时候mock的逻辑出了异常,可是经过调试以后这里主要是通过arthas watch了对应的dubbo方法的返回值,发现确实就是我们mock的结果值, 所以可以排除不是我们mock那块的问题,那为什么dubbo返回结果就有差异呢?

这里就要说到一个问题,我们dubbo服务的回放逻辑其实用的是dubbo的泛化调用的原理,它与正常的服务间的dubbo调用还是有一定的差异的。所以有问题 也就是这块了。那怎么确定确实是因为这个原因的,突然想起来,我们部分还专门做一个通过http转dubbo调用的服务,也是通过dubbo泛化来调用的,我只要验证下,这块是否有问题就知道是不是一样的了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3vXzIMJ4-1681951293088)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fd8db0cd-2f6c-4b87-9d26-1bd65ac05b0e/Untitled.png)]

结果确实是的,那问题就很明显了,确实是在dubbo泛化这块出了问题,那具体是什么问题呢,我们问问gpt看看。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WGzLVb08-1681951293089)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/692b9408-5dbd-4d00-a26a-5730e1c3086c/Untitled.png)]

这么看来,问题出在序列化的嫌疑是最大的了。

不够 这个内容讲的不太详细, 找到了一篇文章 **Dubbo源码分析 - 泛化调用

我们需要看这块的代码逻辑

@Activate(group = Constants.PROVIDER, order = -20000)
public class GenericFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
        // 1 是否是泛化调用: 方法名:$invoke & 参数个数:3 & 调用接口非 GenericService
        if (inv.getMethodName().equals(Constants.$INVOKE)
                && inv.getArguments() != null && inv.getArguments().length == 3
                && !invoker.getInterface().equals(GenericService.class)) {
            // 调用的服务方法名
            String name = ((String) inv.getArguments()[0]).trim();
            // 调用的服务方法参数类型
            String[] types = (String[]) inv.getArguments()[1];
            // 嗲用的服务方法参数列表
            Object[] args = (Object[]) inv.getArguments()[2];

            try {
                // 2. 反射获得提供方的方法对象,注意这里的 invoker 是服务端的,因此 interface 是服务接口,而非GenericService
                Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
                // 2.1 获取服务提供方的目标方法的参数类型
                Class<?>[] params = method.getParameterTypes();
                if (args == null) {
                    args = new Object[params.length];
                }

                // 3 获得 generic 配置项
                String generic = inv.getAttachment(Constants.GENERIC_KEY);
                if (StringUtils.isBlank(generic)) {
                    generic = RpcContext.getContext().getAttachment(Constants.GENERIC_KEY);
                }

                // 4 根据 generic 的配置项反序列化参数值
                // 4.1 如果没有设置 generic 或者 generic = true,反序列化参数,Map->Pojo (在 java 中,pojo通常用map来表示)
                if (StringUtils.isEmpty(generic) || ProtocolUtils.isDefaultGenericSerialization(generic)) {
                    args = PojoUtils.realize(args, params, method.getGenericParameterTypes());

                    // 4.2 generic = nativejava, 反序列化参数, byte[]-> Pojo
                } else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
                    for (int i = 0; i < args.length; i++) {
                        if (byte[].class == args[i].getClass()) {
                            try {
                                UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[]) args[i]);
                                args[i] = ExtensionLoader.getExtensionLoader(Serialization.class)
                                        .getExtension(Constants.GENERIC_SERIALIZATION_NATIVE_JAVA)
                                        .deserialize(null, is).readObject();
                            } catch (Exception e) {
                                throw new RpcException("Deserialize argument [" + (i + 1) + "] failed.", e);
                            }
                        } else {
                            throw new RpcException(
                                    "Generic serialization [" +
                                            Constants.GENERIC_SERIALIZATION_NATIVE_JAVA +
                                            "] only support message type " +
                                            byte[].class +
                                            " and your message type is " +
                                            args[i].getClass());
                        }
                    }

                    // 4.3 generic = bean ,反序列化参数,JavaBeanDescriptor -> Pojo
                } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
                    for (int i = 0; i < args.length; i++) {
                        if (args[i] instanceof JavaBeanDescriptor) {
                            args[i] = JavaBeanSerializeUtil.deserialize((JavaBeanDescriptor) args[i]);
                        } else {
                            throw new RpcException(
                                    "Generic serialization [" +
                                            Constants.GENERIC_SERIALIZATION_BEAN +
                                            "] only support message type " +
                                            JavaBeanDescriptor.class.getName() +
                                            " and your message type is " +
                                            args[i].getClass().getName());
                        }
                    }
                }
                
                // 5 方法参数转换完毕,进行方法调用。
                // 注意此时创建了一个新的 RpcInvocation 对象。$invoke 泛化调用被转为具体的普通调用
                Result result = invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));

                // 如果调用结果有异常,并且非GenericException异常,则使用 GenericException 包装
                if (result.hasException() && !(result.getException() instanceof GenericException)) {
                    return new RpcResult(new GenericException(result.getException()));
                }

                // generic=nativejava的情况下,序列化结果, 结果 -> btyp[]
                if (ProtocolUtils.isJavaGenericSerialization(generic)) {
                    try {
                        UnsafeByteArrayOutputStream os = new UnsafeByteArrayOutputStream(512);
                        ExtensionLoader.getExtensionLoader(Serialization.class)
                                .getExtension(Constants.GENERIC_SERIALIZATION_NATIVE_JAVA)
                                .serialize(null, os).writeObject(result.getValue());
                        return new RpcResult(os.toByteArray());
                    } catch (IOException e) {
                        throw new RpcException("Serialize result failed.", e);
                    }

                    // generic=bean 的情况下,序列化结果, 结果 -> JavaBeanDescriptor
                } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
                    return new RpcResult(JavaBeanSerializeUtil.serialize(result.getValue(), JavaBeanAccessor.METHOD));

                    // generic=true 的情况下,序列化结果,Pojo -> Map
                } else {
                    return new RpcResult(PojoUtils.generalize(result.getValue()));
                }
            } catch (NoSuchMethodException e) {
                throw new RpcException(e.getMessage(), e);
            } catch (ClassNotFoundException e) {
                throw new RpcException(e.getMessage(), e);
            }
        }

        // 普通调用(包括调用泛化实现)
        return invoker.invoke(inv);
    }
}

这段代码的意思如下: 以下来自GPT:

这是一段 Java 代码,实现了一个过滤器,用于处理 Dubbo RPC 框架中的泛化调用。泛化调用是指通过传递方法名、参数类型和参数值来调用服务方法,而不需要使用服务接口的实现类。

具体来说,该过滤器先判断是否是泛化调用,如果是,就进行反序列化参数值的处理,然后将泛化调用转换为具体的普通调用,即调用服务提供方的具体方法,最后将返回结果进行序列化处理。

该过滤器的实现具体步骤如下:

  1. 判断是否是泛化调用,如果是,则获取调用的服务方法名、参数类型和参数值。

  2. 通过反射获得服务提供方的方法对象,并获取该方法的参数类型。

  3. 获取泛化配置项。

  4. 根据泛化配置项进行反序列化参数值的处理,具体分为以下几种情况:

    a. 如果没有设置泛化配置项或者泛化配置项为 true,则将参数值反序列化为 POJO(在 Java 中,POJO 通常用 Map 来表示)。

    b. 如果泛化配置项为 nativejava,则将参数值反序列化为 byte[],再将其转换为 POJO。

    c. 如果泛化配置项为 bean,则将参数值反序列化为 JavaBeanDescriptor 对象,再将其转换为 POJO。

  5. 进行方法调用,注意此时创建了一个新的 RpcInvocation 对象,将泛化调用转换为具体的普通调用。

  6. 如果调用结果有异常,并且非 GenericException 异常,则使用 GenericException 包装。

  7. 根据泛化配置项进行序列化处理,具体分为以下几种情况:

    a. 如果泛化配置项为 true,则将返回结果序列化为 Map。

    b. 如果泛化配置项为 nativejava,则将返回结果序列化为 byte[]。

    c. 如果泛化配置项为 bean,则将返回结果序列化为 JavaBeanDescriptor 对象。

所以这里我们重点需要看的时候将结果序列化为map的逻辑是如何的就好了,也就是7.1

所以我们要关注的是 PojoUtilsgeneralize 方法

Map<String, Object> map = new HashMap<String, Object>();
history.put(pojo, map);
map.put("class", pojo.getClass().getName());
for (Method method : pojo.getClass().getMethods()) {
    if (ReflectUtils.isBeanPropertyReadMethod(method)) {
        try {
            map.put(ReflectUtils.getPropertyNameFromBeanReadMethod(method),
                    generalize(method.invoke(pojo), history));
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }
}
// public field
for(Field field : pojo.getClass().getFields()) {
    if (ReflectUtils.isPublicInstanceField(field)) {
        try {
            Object fieldValue = field.get(pojo);
            // public filed同时也有get/set方法,如果get/set存取的不是前面那个 public field 该如何处理
            if (history.containsKey(pojo)) {
                Object pojoGenerilizedValue = history.get(pojo);
                if (pojoGenerilizedValue instanceof Map
                    && ((Map)pojoGenerilizedValue).containsKey(field.getName())) {
                    continue;
                }
            }
            if (fieldValue != null) {
                map.put(field.getName(), generalize(fieldValue, history));
            }
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }
}
return map;

这块的代码就解释的很清楚了。

map其实就是最后返回给我的对象数据, 而它里面的内容是通过反射类中的getter以及is的方法拿到对象的,当然还有包括后面的public的成员变量了。

所以归根到底我们还是需要去看看,这个dubbo方法返回的类型是什么样的才行了。我们可以看下

public class Page<T> implements IPage<T> {
    private static final long serialVersionUID = 8545996863226528798L;
    protected List<T> records;
    protected long total;
    protected long size;
    protected long current;
    protected List<OrderItem> orders;
    protected boolean optimizeCountSql;
    protected boolean searchCount;
    protected boolean optimizeJoinOfCountSql;
    protected String countId;
    protected Long maxLimit;

    public Page() {
        this.records = Collections.emptyList();
        this.total = 0L;
        this.size = 10L;
        this.current = 1L;
        this.orders = new ArrayList();
        this.optimizeCountSql = true;
        this.searchCount = true;
        this.optimizeJoinOfCountSql = true;
    }

    public Page(long current, long size) {
        this(current, size, 0L);
    }

    public Page(long current, long size, long total) {
        this(current, size, total, true);
    }

    public Page(long current, long size, boolean searchCount) {
        this(current, size, 0L, searchCount);
    }

    public Page(long current, long size, long total, boolean searchCount) {
        this.records = Collections.emptyList();
        this.total = 0L;
        this.size = 10L;
        this.current = 1L;
        this.orders = new ArrayList();
        this.optimizeCountSql = true;
        this.searchCount = true;
        this.optimizeJoinOfCountSql = true;
        if (current > 1L) {
            this.current = current;
        }

        this.size = size;
        this.total = total;
        this.searchCount = searchCount;
    }

    public boolean hasPrevious() {
        return this.current > 1L;
    }

    public boolean hasNext() {
        return this.current < this.getPages();
    }

    public List<T> getRecords() {
        return this.records;
    }

    public Page<T> setRecords(List<T> records) {
        this.records = records;
        return this;
    }

    public long getTotal() {
        return this.total;
    }

    public Page<T> setTotal(long total) {
        this.total = total;
        return this;
    }

    public long getSize() {
        return this.size;
    }

    public Page<T> setSize(long size) {
        this.size = size;
        return this;
    }

    public long getCurrent() {
        return this.current;
    }

    public Page<T> setCurrent(long current) {
        this.current = current;
        return this;
    }

    public String countId() {
        return this.countId;
    }

    public Long maxLimit() {
        return this.maxLimit;
    }

    private String[] mapOrderToArray(Predicate<OrderItem> filter) {
        List<String> columns = new ArrayList(this.orders.size());
        this.orders.forEach((i) -> {
            if (filter.test(i)) {
                columns.add(i.getColumn());
            }

        });
        return (String[])columns.toArray(new String[0]);
    }

    private void removeOrder(Predicate<OrderItem> filter) {
        for(int i = this.orders.size() - 1; i >= 0; --i) {
            if (filter.test(this.orders.get(i))) {
                this.orders.remove(i);
            }
        }

    }

    public Page<T> addOrder(OrderItem... items) {
        this.orders.addAll(Arrays.asList(items));
        return this;
    }

    public Page<T> addOrder(List<OrderItem> items) {
        this.orders.addAll(items);
        return this;
    }

    public List<OrderItem> orders() {
        return this.orders;
    }

    public boolean optimizeCountSql() {
        return this.optimizeCountSql;
    }

    public static <T> Page<T> of(long current, long size, long total, boolean searchCount) {
        return new Page(current, size, total, searchCount);
    }

    public boolean optimizeJoinOfCountSql() {
        return this.optimizeJoinOfCountSql;
    }

    public Page<T> setSearchCount(boolean searchCount) {
        this.searchCount = searchCount;
        return this;
    }

    public Page<T> setOptimizeCountSql(boolean optimizeCountSql) {
        this.optimizeCountSql = optimizeCountSql;
        return this;
    }

    public long getPages() {
        return super.getPages();
    }

    public static <T> Page<T> of(long current, long size) {
        return of(current, size, 0L);
    }

    public static <T> Page<T> of(long current, long size, long total) {
        return of(current, size, total, true);
    }

    public static <T> Page<T> of(long current, long size, boolean searchCount) {
        return of(current, size, 0L, searchCount);
    }

    public boolean searchCount() {
        return this.total < 0L ? false : this.searchCount;
    }

    /** @deprecated */
    @Deprecated
    public String getCountId() {
        return this.countId;
    }

    /** @deprecated */
    @Deprecated
    public Long getMaxLimit() {
        return this.maxLimit;
    }

    /** @deprecated */
    @Deprecated
    public List<OrderItem> getOrders() {
        return this.orders;
    }

    /** @deprecated */
    @Deprecated
    public boolean isOptimizeCountSql() {
        return this.optimizeCountSql;
    }

    /** @deprecated */
    @Deprecated
    public boolean isSearchCount() {
        return this.searchCount;
    }

    public void setOrders(final List<OrderItem> orders) {
        this.orders = orders;
    }

    public void setOptimizeJoinOfCountSql(final boolean optimizeJoinOfCountSql) {
        this.optimizeJoinOfCountSql = optimizeJoinOfCountSql;
    }

    public void setCountId(final String countId) {
        this.countId = countId;
    }

    public void setMaxLimit(final Long maxLimit) {
        this.maxLimit = maxLimit;
    }
}

关键是我们要关注一个点 少了的 optimizeJoinOfCountSql 有没有get或者is的方法,嗯 确实是没有的,所以这个就是为什么少了这个参数的原因了,那为什么会多出来一个pages呢,我们可以看下 Page这个类 实际上还实现了IPage这个接口 我们看下

public interface IPage<T> extends Serializable {
    List<OrderItem> orders();

    default boolean optimizeCountSql() {
        return true;
    }

    default boolean optimizeJoinOfCountSql() {
        return true;
    }

    default boolean searchCount() {
        return true;
    }

    default long offset() {
        long current = this.getCurrent();
        return current <= 1L ? 0L : Math.max((current - 1L) * this.getSize(), 0L);
    }

    default Long maxLimit() {
        return null;
    }

    default long getPages() {
        if (this.getSize() == 0L) {
            return 0L;
        } else {
            long pages = this.getTotal() / this.getSize();
            if (this.getTotal() % this.getSize() != 0L) {
                ++pages;
            }

            return pages;
        }
    }

    default IPage<T> setPages(long pages) {
        return this;
    }

    List<T> getRecords();

    IPage<T> setRecords(List<T> records);

    long getTotal();

    IPage<T> setTotal(long total);

    long getSize();

    IPage<T> setSize(long size);

    long getCurrent();

    IPage<T> setCurrent(long current);

    default <R> IPage<R> convert(Function<? super T, ? extends R> mapper) {
        List<R> collect = (List)this.getRecords().stream().map(mapper).collect(Collectors.toList());
        return this.setRecords(collect);
    }

    default String countId() {
        return null;
    }
}

这里我们就可以发现了,存在一个 getPages 这就是为什么我们会多出pages这个属性的原因了。

总结

综上,dubbo的回放结果根据序列化的情况就可能出现不一样的情况,我们可能需要根据情况进行接口层级的字段忽略对比才行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/437387.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

7款神仙级非常漂亮的 Linux 操作系统UI,你都用过吗

Linux 的发行版有很多&#xff0c;这里罗列7个漂亮的 Linux 发行版&#xff0c;可以说是Linux操作系统界的颜值担当了。 1、elementary OS 网站&#xff1a;https://elementaryos.cn elementary OS操作系统是最漂亮的Linux发行版之一。它基于macOS外观&#xff0c;同时为Linu…

裸机和RTOS系统区别与联系

1. 裸机和rtos的多任务处理 试想一种场景&#xff0c;我们正在打游戏&#xff0c;但女朋友在你打游戏的过程中给你发送消息&#xff0c;你需要回复消息 1.1 裸机处理方式 while(1) { 打游戏();回复信息(); }玩过51或者stm32的裸机编程的人都知道&#xff0c;我们往往会在应…

【MATLAB图像处理实用案例详解(11)】——基于Hough变换的人眼虹膜定位方法

目录 一、Hough变换基本原理二、算法流程2.1 分离瞳孔并估算虹膜内半径2.2 Hough变换定位内外虹膜 三、效果演示 一、Hough变换基本原理 Hough 变换作为一种参数空间变换算法&#xff0c;直线和其他参数化形状检测的重要工具。Hough 变换具有较强的稳定性和鲁棒性&#xff0c;…

【AI绘画】外网一直进不去?别担心,还有AI绘画小程序和文心一格

这是加入新星计划的最后一周的最后一篇文章啦&#xff01;文章的最后是我想总结一下从三月底到四月中旬这几十天的感想&#xff5e; 系列文章&#xff1a; 【AutoGPT】你自己运行&#xff0c;我先睡了—— ChatGPT过时了吗&#xff1f;_山楂山楂丸的博客-CSDN博客 目录 前言 …

pytest.mark

目录 一、Pytest简介 二、安装 三、pytest.mark 1.标记 2.参数化 3. skip跳过 4. xfail 该用例置为失败 一、Pytest简介 Pytest是python一个第三方测试框架&#xff0c;有非富的第三方插件可以扩展 特点&#xff1a; 简单灵活&#xff0c;容易上⼿&#xff1b;⽀持参数…

ASEMI代理ADM202EARNZ-REEL原装ADI车规级ADM202EARNZ-REEL

编辑&#xff1a;ll ASEMI代理ADM202EARNZ-REEL原装ADI车规级ADM202EARNZ-REEL 型号&#xff1a;ADM202EARNZ-REEL 品牌&#xff1a;ADI/亚德诺 封装&#xff1a;SOIC-16 批号&#xff1a;2023 引脚数量&#xff1a;16 安装类型&#xff1a;表面贴装型 ADM202EARNZ-REE…

【软件测试】Postman简单使用教程

【软件测试】Postman使用教程 创建集合 新建一个collection 新建一个请求 默认get 举例测试get请求 后端controller代码&#xff0c;该功能是查询所有信息 // 查询所有员工信息RequestMapping(value "/employee",method RequestMethod.GET)public String g…

ELK日志分析初

ELK是一个开源的日志分析系统 ELK是三个开源软件的缩写&#xff0c;分别表示&#xff1a;Elasticsearch , Logstash, Kibana , 它们都是开源软件。新增了一个FileBeat&#xff0c;它是一个轻量级的日志收集处理工具(Agent)&#xff0c;Filebeat占用资源少&#xff0c;适合于在…

代码随想录算法训练营第三十一天| 理论基础 、455.分发饼干 、376. 摆动序列、53. 最大子序和

文章目录 491.理论基础455.分发饼干376. 摆动序列:star:53. 最大子序和:star: 491.理论基础 链接:代码随想录 解题思路&#xff1a; 通过局部最优&#xff0c;推出整体最优 如何验证贪心算法的正确性 最好用的策略就是举反例&#xff0c;如果想不到反例&#xff0c;那么就试一…

前端项目打包并部署

一、vue项目打包 1.1 方式一&#xff1a;vue项目命令行打包 在当前项目路径下&#xff0c;执行命令 npm run build 在当前项目路径下&#xff0c;生成 一个dist文件夹。 将来部署项目&#xff0c;是部署的dist这个文件。 1.2 方式二&#xff1a;使用vue ui打包项目 在终端中…

前端学习之路 来自前端方向学生的总结

恭喜您&#xff01;您发现了宝藏&#xff01; 我发现很多小伙伴&#xff0c;对于前端感兴趣&#xff0c;也很想去学好&#xff0c;但是却无从下手&#xff0c;不知道如何去学习。作为一名现处于大三即将大四的学生&#xff0c;借此机会来分享分享我的前端学习之路&#xff01;…

扬帆优配|TMT板块密集发布减持计划 火爆行情潜藏估值难以匹配隐忧

4月以来&#xff0c;多家上市公司发表股东减持公告&#xff0c;其中一季度大热的TMT&#xff08;科技、媒体和电信&#xff09;板块的股东减持最为引人注目。 32只TMT股拟减持上限占比超1% 到4月18日&#xff0c;4月以来已有61家TMT板块上市公司发布减持方案。从拟变动数量上限…

神策数据荣登胡润百富 2023 全球独角兽榜

4 月 18 日&#xff0c;胡润研究院于广州 2023 全球独角兽 CEO 大会发布《2023 全球独角兽榜》&#xff08;Global Unicorn Index 2023&#xff09;&#xff0c;列出了全球成立于 2000 年之后&#xff0c;价值 10 亿美元以上的非上市公司&#xff08;估值计算日期截止 2022 年 …

自阿里P8爆出内部1031道java面试题后,在Boss直聘狂拿千份Offer

开始之前我问大家几个问题&#xff0c;看大家是如何思考的&#xff1a; 1.程序员一定要去一线城市漂泊吗&#xff1f;在自己家乡如何拿到一份满意的薪水&#xff1f; 2.程序员被裁员、找不到工作&#xff0c;代表什么&#xff1f; 3.程序员一定要进一线大厂吗&#xff1f;你…

手把手教你通过 Docker 部署前后端分离项目(亲测可用)

安装Docker 安装Nginx 安装Mysql 部署SpringBoot项目 部署Vue项目 一、安装Docker 1、安装&#xff1a; yum install docker 2、启动/停止/重启docker服务 service docker start service docker stop service docker restart 3、查看docker版本信息 docker version…

UDS 14229-1 诊断服务,两万字长文Trace版详细解读

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

Tomcat—javaEE

文章目录 1.定义及启动2.熟悉重点目录作用2.1bin2.2conf/server.xml2.3日志文件2.4webapps 3.部署和访问 1.定义及启动 &#xff08;1&#xff09;Tomcat属于web服务器的一种&#xff0c;也是servlet的一种 &#xff08;2&#xff09;Windows下&#xff0c;双击Tomcat下/bin/s…

ArcGIS|一文彻底搞懂GIS图斑编号

实际工作中&#xff0c;经常会有对各类图斑进行编号的需求。数据中图斑数比较少时&#xff0c;我们可以手动进行编号&#xff0c;但数据量较大时就必须得想办法自动实现图斑编号。今天&#xff0c;将分享几种常见的图斑自动编号方式&#xff0c;主要包括&#xff1a;图斑顺序编…

Mysql 触发器(复习)

今天考虑一个删除记录回收站的时候&#xff0c;突然想到了触发器这个东西&#xff0c;基本上之前也很少使用。废话不不多说&#xff0c;先看它的解释&#xff1a; 在MySQL中&#xff0c;触发器&#xff08;Trigger&#xff09;是一种特殊的存储过程&#xff0c;它会在指定的事…

[java/初学者]java常用API(2)——字符串

前言 所谓的字符串其实就是一串连续的字符&#xff0c;它是由许多单个字符连接而成的。如多个英文字母所组成的一个英文单词。字符串中可以包含任意字符&#xff0c;这些字符必须包含在一对双引号之内&#xff0c;例如“Dufeng”。 而与字符串相关的类都放在java.lang包中&…