2023年10月

使用支付宝沙箱的时候经常踩坑 TAT 自己排查问题还要花好长时间。

今天总结了一下支付宝沙箱的使用教程,希望帮助到大家,避免重复踩坑~ψ(*`ー´)ψ

我们先看下官方对沙箱的说明,如下:

我们可以看到官方文档上说明的指出了沙箱的三个特性

  • 使用账号不需要任何的资质
  • 账号不需要开通任何权限
  • 沙箱环境下的调用不会对生产环境数据造成影响

这不就说明了这是可以在官方给的接口范围内
随便乱搞
认真调研了嘛。

沙箱账号体系说明

为了实现上面这三个功能,支付宝给沙箱单独做了一套
账号体系

应用体系
以及
独立的支付宝沙箱 APP
(暂时仅支持Android,iOS 使用详见:
iOS 如何下载使用沙箱钱包
)。

  • 账号体系:第一次登录支付宝开放平台的小伙伴记得先按照指引入驻一下,传送门:
    沙箱账号获取入口

  • 应用体系:

  • 支付宝沙箱 APP:暂时仅支持Android。

划重点:支付宝沙箱钱包也只能使用沙箱账号登录

划重点:支付宝沙箱钱包也只能使用沙箱账号登录

划重点:支付宝沙箱钱包也只能使用沙箱账号登录

重要的事情说三遍,大家在登录的时候记得
不要直接用自己的账号登录
,选择下面的【更多选项】-【用支付宝/邮箱登录】去输入沙箱账号。

相信我,你们一定不会想体会重复被提示“账号不存在”,反复检查账号的感觉。

我把之前遇到关于支付宝沙箱 APP 的问题列在这边,大家共勉~/(ㄒoㄒ)/~~

沙箱接口开发流程说明

支付宝沙箱目前不仅后端接口开发,还支持小程序前端开发。

目前沙箱产品支持的范围参考:
支付宝沙箱支持的产品范围

这个范围我觉得 ok,至少市面上主流的能力都能测试,可以覆盖产品的绝大部分核心链路和对接逻辑。

接下来将分别从
前端

后端
讲解如何具体使用沙箱。

前端如何使用沙箱

首先介绍下小程序如何使用沙箱进行调试。

正式和沙箱环境的区别


知道之前有没有用过支付宝沙箱的小伙伴,以前支付宝沙箱如果要调试小程序的话,还需要在 IDE 上下载沙箱插件,重启之后才能生效,非常麻烦。

不知道是不是听到了广大群众的呼声,这次支付宝沙箱在小程序开发上做了一次升级,抛弃了沙箱插件,直接把正式环境的小程序同步到了沙箱里。
针对 2021 开头的小程序,小程序会自动同步到沙箱环境。

简单来说在同一套代码、不切换环境的情况下,你推到沙箱 APP 上了就是沙箱环境测试;推到正式的 APP 上就是正式环境测试了。

调试步骤

  • 第一步:使用
    线上APP
    扫码登录支付宝小程序开发工具
  • 第二步:选择需要开发的小程序
    • 这里需要
      选择 2021 开头的小程序 APPID,否则会报错

      码已失效,请刷新二维码后重试。详见:[
      沙箱支持的小程序范围
      ]
  • 第三步:正常调试小程序,如果遇到线上环境没有权限的功能,可以直接使用沙箱的 APP 去扫调试码,推送到沙箱 APP上进行真机调试,记住沙箱 APP 登录的是
    商家账号
    ,不要使用买家账号去登录。
    • ⚠️注意:最好把
      小程序自动推送给支付宝
      选项的勾选给去掉,不然就会推送到正式的支付宝 APP上了

可以使用
my.env
的 clientEnv 动态获取支付宝 APP 的环境信息,来决定网络请求接口的目标地址。

后端如何使用沙箱

接下来我们就来介绍下,服务端如何使用沙箱环境。

正式和沙箱环境的区别

因为账号体系不一样,应用、密钥和支付宝网关都是不一样的,最主要的区别还是支付宝网关地址。

  • 正式环境支付宝网关地址为:
    https://openapi.alipay.com/gateway.do
  • 沙箱环境支付宝网关地址为:
    https://openapi-sandbox.dl.alipaydev.com/gateway.do

最好配置两套密钥,方便正式和沙箱环境来回搞。

调试步骤

就以「当面付」产品为例,看一下沙箱环境如何调用。

第一步:获取配置信息

服务端接口所需要的平台上的信息就是
APPID

接口加签方式中的密钥信息

支付宝网关地址

  • APPID、接口加签方式和支付宝网关地址获取位置截图如下:

  • 点击【查看】获取密钥

第二步:将配置信息入参到代码中

为了方便说明,这边就直接引用官方提供的代码示例了,详情参考:[
当面付示例代码
]。

在 Config 类中,参数与平台中对应的关系如下:

  • gatewayUrl:支付宝应用网关
  • app_id:APPID
  • merchant_private_key:应用私钥
  • alipay_public_key:支付宝公钥

配置完成,调用成功之后就会返回

{"alipay_trade_precreate_response":{"code":"10000","msg":"Success","out_trade_no":"2023888888888888","qr_code":"https://qr.alipay.com/bax04390c1pudiutvskn009b"},"sign":"lcwdCgQgkUGcjoqWDlfmQojtFSof+XigNflDorz8Lmep1yGfSMB75S8S0ZbPfzA5p8fRWnnyDyVatlRcnA+AmBeh/VlDxxQ9KmE9BR+0UHZtd6+ucdBDGU/hAhZd3+NbCmZ61eAEeArpE7HPOQWwac/nOynrPjnp3YSTOCR7U+O8o3gbz1Hc6QQo0aVz63t0DKGPanvrk4hkRJhLsZj1oMoguevj86mPoixRX5mxGiheIPbmtB4CjJAS5kx6EGEuBd3uk2UL8BQjQ/XqyCsDHSNJ8zglGwW0PyBuRPhF1S2LSIazlOz32cQDFh2NZLpXtJl/fFSQzQhy99i57+z/dg=="}

第三步:使用沙箱钱包支付

上述 qr_code 参数里面的链接就可以转成二维码用
沙箱钱包扫码支付
了。

沙箱钱包账单:

其他常见问题

在使用支付宝沙箱过程中,还会遇到一些其他常见问题,这里全都列出来供大家参考查阅:

以上就是关于支付宝沙箱支付的所有内容了,希望对大家有所帮助(ノ ̄▽ ̄)~❀欢迎留言交流

在UI交互中,拖拽操作是一种非常简单友好的交互。尤其是在ListBox,TabControl,ListView这类列表控件中更为常见。通常要实现拖拽排序功能的做法是自定义控件。本文将分享一种在原生控件上设置附加属性的方式实现拖拽排序功能。

该方法的使用非常简单,仅需增加一个附加属性就行。

<TabControl
    assist:SelectorDragDropAttach.IsItemsDragDropEnabled="True"
    AlternationCount="{Binding ClassInfos.Count}"
    ContentTemplate="{StaticResource contentTemplate}"
    ItemContainerStyle="{StaticResource TabItemStyle}"
    ItemsSource="{Binding ClassInfos}"
    SelectedIndex="0" />

实现效果如下:
image

主要思路

WPF中核心基类UIElement包含了
DragEnter

DragLeave

DragEnter

Drop
等拖拽相关的事件,因此只需对这几个事件进行监听并做相应的处理就可以实现WPF中的UI元素拖拽操作。

另外,WPF的一大特点是支持数据驱动,即由数据模型来推动UI的呈现。因此,可以通过通过拖拽事件处理拖拽的源位置以及目标位置,并获取到对应位置渲染的数据,然后操作数据集中数据的位置,从而实现数据和UI界面上的顺序更新。

首先定义一个附加属性类
SelectorDragDropAttach
,通过附加属性
IsItemsDragDropEnabled
控制是否允许拖拽排序。

public static class SelectorDragDropAttach
{
    public static bool GetIsItemsDragDropEnabled(Selector scrollViewer)
    {
        return (bool)scrollViewer.GetValue(IsItemsDragDropEnabledProperty);
    }

    public static void SetIsItemsDragDropEnabled(Selector scrollViewer, bool value)
    {
        scrollViewer.SetValue(IsItemsDragDropEnabledProperty, value);
    }

    public static readonly DependencyProperty IsItemsDragDropEnabledProperty =
        DependencyProperty.RegisterAttached("IsItemsDragDropEnabled", typeof(bool), typeof(SelectorDragDropAttach), new PropertyMetadata(false, OnIsItemsDragDropEnabledChanged));

    private static readonly DependencyProperty SelectorDragDropProperty =
        DependencyProperty.RegisterAttached("SelectorDragDrop", typeof(SelectorDragDrop), typeof(SelectorDragDropAttach), new PropertyMetadata(null));

    private static void OnIsItemsDragDropEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        bool b = (bool)e.NewValue;
        Selector selector = d as Selector;
        var selectorDragDrop = selector?.GetValue(SelectorDragDropProperty) as SelectorDragDrop;
        if (selectorDragDrop != null)
            selectorDragDrop.Selector = null;
        if (b == false)
        {
            selector?.SetValue(SelectorDragDropProperty, null);
            return;
        }
        selector?.SetValue(SelectorDragDropProperty, new SelectorDragDrop(selector));

    }

}

其中
SelectorDragDrop
就是处理拖拽排序的对象,接下来看下几个主要事件的处理逻辑。
通过
PreviewMouseLeftButtonDown
确定选中的需要拖拽操作的元素的索引

void selector_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (this.IsMouseOverScrollbar)
    {
        //Set the flag to false when cursor is over scrollbar.
        this.canInitiateDrag = false;
        return;
    }

    int index = this.IndexUnderDragCursor;
    this.canInitiateDrag = index > -1;

    if (this.canInitiateDrag)
    {
        // Remember the location and index of the SelectorItem the user clicked on for later.
        this.ptMouseDown = GetMousePosition(this.selector);
        this.indexToSelect = index;
    }
    else
    {
        this.ptMouseDown = new Point(-10000, -10000);
        this.indexToSelect = -1;
    }
}


PreviewMouseMove
事件中根据需要拖拽操作的元素创建一个
AdornerLayer
,实现鼠标拖着元素移动的效果。其实拖拽移动的只是这个
AdornerLayer
,真实的元素并未移动。

void selector_PreviewMouseMove(object sender, MouseEventArgs e)
{
    if (!this.CanStartDragOperation)
        return;

    // Select the item the user clicked on.
    if (this.selector.SelectedIndex != this.indexToSelect)
        this.selector.SelectedIndex = this.indexToSelect;

    // If the item at the selected index is null, there's nothing
    // we can do, so just return;
    if (this.selector.SelectedItem == null)
        return;

    UIElement itemToDrag = this.GetSelectorItem(this.selector.SelectedIndex);
    if (itemToDrag == null)
        return;

    AdornerLayer adornerLayer = this.ShowDragAdornerResolved ? this.InitializeAdornerLayer(itemToDrag) : null;

    this.InitializeDragOperation(itemToDrag);
    this.PerformDragOperation();
    this.FinishDragOperation(itemToDrag, adornerLayer);
}

DragEnter

DragLeave

DragEnter
事件中处理
AdornerLayer
的位置以及是否显示。

Drop
事件中确定了拖拽操作目标位置以及渲染的数据元素,然后移动元数据,通过数据顺序的变化更新界面的排序。从代码中可以看到列表控件的
ItemsSource
不能为空,否则拖拽无效。这也是后边将提到的一个缺点。

void selector_Drop(object sender, DragEventArgs e)
{
    if (this.ItemUnderDragCursor != null)
        this.ItemUnderDragCursor = null;

    e.Effects = DragDropEffects.None;

    var itemsSource = this.selector.ItemsSource;
    if (itemsSource == null) return;

    int itemsCount = 0;
    Type type = null;
    foreach (object obj in itemsSource)
    {
        type = obj.GetType();
        itemsCount++;
    }

    if (itemsCount < 1) return;
    if (!e.Data.GetDataPresent(type))
        return;

    object data = e.Data.GetData(type);
    if (data == null)
        return;

    int oldIndex = -1;
    int index = 0;
    foreach (object obj in itemsSource)
    {
        if (obj == data)
        {
            oldIndex = index;
            break;
        }
        index++;
    }
    int newIndex = this.IndexUnderDragCursor;

    if (newIndex < 0)
    {
        if (itemsCount == 0)
            newIndex = 0;
        else if (oldIndex < 0)
            newIndex = itemsCount;
        else
            return;
    }
    if (oldIndex == newIndex)
        return;

    if (this.ProcessDrop != null)
    {
        // Let the client code process the drop.
        ProcessDropEventArgs args = new ProcessDropEventArgs(itemsSource, data, oldIndex, newIndex, e.AllowedEffects);
        this.ProcessDrop(this, args);
        e.Effects = args.Effects;
    }
    else
    {
        dynamic dItemsSource = itemsSource;
        if (oldIndex > -1)
            dItemsSource.Move(oldIndex, newIndex);
        else
            dItemsSource.Insert(newIndex, data);
        e.Effects = DragDropEffects.Move;
    }
}

优点与缺点

优点:

  • 用法简单,封装好拖拽操作的附加属性后,只需一行代码实现拖拽功能。
  • 对现有项目友好,对于已有项目需要扩展拖拽操作排序功能,无需替换控件。
  • 支持多种列表控件扩展。派生自
    Selector

    ListBox

    TabControl

    ListView
    ,
    ComboBox
    都可使用该方法。

缺点:

  • 仅支持通过数据绑定动态渲染的列表控件,XAML硬编码或者后台代码循环添加列表元素创建的列表控件不适用该方法。
  • 仅支持列表控件内的元素拖拽,不支持穿梭框拖拽效果。
  • 不支持同时拖拽多个元素。

小结

本文介绍列表拖拽操作的解决方案不算完美,功能简单但轻量,并且很好的体现了WPF的数据驱动的思想。个人非常喜欢这种方式,它能让我们轻松的实现列表数据的增删以及排序操作,而不是耗费时间和精力去自定义可增删数据的控件。

参考

https://www.codeproject.com/Articles/17266/Drag-and-Drop-Items-in-a-WPF-ListView#xx1911611xx

代码示例

SelectorDragDropSamples

一、简介
从今天开始一个长系列,Net 高级调试的相关文章,我自从学习了之后,以前很多模糊的地方现在很清楚了,原来自己的功力还是不够,所以有很多不明白,通过学习 Net 高级调试,眼前豁然开朗,茅塞顿开。其实,刚开始要学习《Net 高级调试》,还是很是很困难的,很多工具不会用,又不知道如何调试,痛苦的又很多次想放弃,但是,最终还是坚持下来,收获也不小。
既然坚持下来了,我就把学习的过程记录下来,也许以后自己的能用的到,可以方便查询。或许,有其他人也有同样的困扰,或许可以在我这里得到一些帮助,有帮助我当然很开心。当然,Net 高级调试的路还很远,我也是刚起步,不足之处太多,也希望大家原谅,有不对之处,欢迎指正。我入门《Net 高级调试》,还要感谢【 一线码农】的视频,帮了我很多的忙。
调试环境
操作系统:Windows Professional 10
调试工具:Windbg Preview(可以去Microsoft Store 去下载)
开发工具:Visual Studio 2022
Net 版本:Net Framework 4.8
CoreCLR源码:
源码下载

二、
调试工具介绍

俗话说得好,工欲善其事,必先利其器,我们要想调试程序,必须有很好的工具,如果连调试工具都没有,那真就成了巧妇难为无米之炊。所以,接下来,我先介绍一些调试工具,每种调试工具都有各自的用途。

2.0、测试代码

我们想要演示 Windbg 的使用过程,使用方法,调试程序的各种问题,必须有程序作为载体,由于这是【Net 高级调试】的第一节课,所以只是简单的演示一下,例子代码没有实际的作用,作为演示还是够了的。
本节有两分代码,分别是:Example_1_1_1和 Example_1_1_2
Example_1_1_1的代码如下:

1 namespaceExample_1_1_12 {3     internal classProgram4 {5         static void Main(string[] args)6 {7             Console.WriteLine("Hello World");8 Console.ReadLine();9 }10 }11 }



Example_1_1_2的代码如下:

1 namespaceExample_1_1_22 {3     internal classProgram4 {5         private static IList<byte[]> list=new List<byte[]>();6 
7         static void Main(string[] args)8 {9             Task.Run(() =>
10 {11                 for (int i = 0; i < int.MaxValue; i++)12 {13                     list.Add(new byte[10000]);14                     if (i % 10 == 0)15 {16                         list[i] = null;17 }18                     Console.WriteLine($"当前索引 Index={i}");19 }20 });21 Console.ReadLine();22 }23 }24 }


2.1、SOS

SOS 调试扩展】允许我们查看有关在 CLR 内运行的代码的信息。 例如,可以使用 【SOS 调试扩展】显示有关【托管堆】的信息、查找堆损坏情况、显示【运行时】所使用的内部数据类型以及查看有关在【运行时】内运行的所有托管代码的信息。它就是一个 dll,包含一组
访问 CLR 内部数据的接口函数,可以使我们使用 Windbg 调试器调试 Net 程序,解决程序问题的时候更简单。

Windbg-----------------SOS------------CLR,这是一个有关SOS的示意图,SOS的作用就像一个中介者一样,Windbg可以通过 SOS 来调试 CLR。


2.1.1、文件位置

这个程序集是随.NET Framework一起安装的,一般不需要单独安装。SOS 调试扩展是有2个版本的,分别是32位和64位,安装的位置如下
32位安装位置:C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll


64位安装位置:C:\Windows\Microsoft.NET\Framework64\v4.0.30319\SOS.dll




2.1.2、如何加载

Windbg Preview
是不用单独执行加载的工作的,它会自动加载它所需要的版本,如果是老版本的 Windbg,比如:windbg10 ,可以通过 .load 命令加载 SOS.dll。一般情况,使用windbg自带的命令【.load sos】即可自动加载,使用【.chain】查看加载是否成功。
如果没有加载 SOS.dll,我们可以手动加载,执行如下命令:

0:000> .load C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll

我们可以通过【.chain】命令检查是否成功加载 SOS.dll。红色字体显示已经加载了 SOS 调试扩展。

1 0:000>.chain2 Extension DLL search Path:3     C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86\WINXP;C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86\winext;C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86\winext\arcade;C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86\pri;C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86;C:\Users\Administrator\AppData\Local\Dbg\EngineExtensions32;C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86;C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\amd64;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\Program Files\dotnet\;D:\Program Files (x86)\Microsoft SQL Server\100\Tools\Binn\;D:\Program Files\Microsoft SQL Server\100\Tools\Binn\;D:\Program Files\Microsoft SQL Server\100\DTS\Binn\;D:\Program Files (x86)\Microsoft SQL Server\100\Tools\Binn\VSShell\Common7\IDE\;C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies\;D:\Program Files (x86)\Microsoft SQL Server\100\DTS\Binn\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;D:\XIMEA\API;C:\XIMEA\API;D:\Program Files\Git\cmd;C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps;C:\Users\Administrator\.dotnet\tools4 Extension DLL chain:5     C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll: image 4.8.4300.0, API 1.0.0, built Thu Oct  8 08:41:14 2020
6         [path: C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll]7     JsProvider: image 10.0.25877.1004, API 0.0.0,8         [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86\winext\JsProvider.dll]9     DbgModelApiXtn: image 10.0.25877.1004, API 0.0.0,10         [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86\winext\DbgModelApiXtn.dll]11     F:\Software\DebugTools\SOS\SOSEX\sosex_32\sosex.dll: image 4.5.0.0, API 1.0.0, built Fri Mar  7 23:17:26 2014
12 [path: F:\Software\DebugTools\SOS\SOSEX\sosex_32\sosex.dll]13     CLRComposition: image 10.0.25877.1004, API 0.0.0,14         [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86\winext\CLRComposition.dll]15     wow64exts: image 10.0.25877.1004, API 1.0.0,16         [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86\WINXP\wow64exts.dll]17     dbghelp: image 10.0.25877.1004, API 10.0.6,18         [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86\dbghelp.dll]19     exts: image 10.0.25877.1004, API 1.0.0,20         [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86\WINXP\exts.dll]21     uext: image 10.0.25877.1004, API 1.0.0,22         [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86\winext\uext.dll]23     ntsdexts: image 10.0.25877.1004, API 1.0.0,24         [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2306.14001.0_x64__8wekyb3d8bbwe\x86\WINXP\ntsdexts.dll]


2.1.3、如何使用


说到是第一节讲 Windbg 使用的文章,所以具体的使用步骤还是要说明的详细一点。
代码案例:Example_1_1_1
1)、加载程序集
A、编译程序源码,生成 Dll 或者是 Exe 程序集,可以在拷贝地址,当然这是我的习惯,你可以选择 Windbg 查找文件也是可以的。

B、打开 Windbg 调试器。通过菜单选择【文件】-->【launch executable】,弹出窗口,找到指定的程序集文件,选择打开,就进入了 Windbg 调试器页面,是暂停的状态,此时,就可以根据自己的需要,选择下一步的操作。


2)我们执行一些命令,来一个直观感觉。
A、.cls 刚进来,内容太多,可以清楚一下屏幕。
.cls


【Debuggee is running】其实这里是停在了 Console.ReadLine();这行代码这里,点击【Break】按钮,我们就行调试了。




D、~os 切换到主线程。

1 0:001> ~0s2 eax=00000000 ebx=000000a4 ecx=00000000 edx=00000000 esi=004ff10c edi=00000000
3 eip=773410fc esp=004feff4 ebp=004ff054 iopl=0nv up ei pl nz na po nc4 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
5 ntdll!NtReadFile+0xc:6 773410fc c22400          ret     24h

E、!sos.help 我们可以查看 SOS 的所有命令。

1 0:000> !sos.help2 -------------------------------------------------------------------------------
3 SOS is a debugger extension DLL designed to aid inthe debugging of managed4 programs. Functions are listed by category, then roughly inorder of5 importance. Shortcut names for popular functions are listed inparenthesis.6 Type "!help <functionname>" fordetailed info on that function.7 
8 Object Inspection                  Examining code and stacks9 -----------------------------      -----------------------------
10 DumpObj (do)                       Threads11 DumpArray (da)                     ThreadState12 DumpStackObjects (dso)             IP2MD13 DumpHeap                           U14 DumpVC                             DumpStack15 GCRoot                             EEStack16 ObjSize                            CLRStack17 FinalizeQueue                      GCInfo18 PrintException (pe)                EHInfo19 TraverseHeap                       BPMD20 COMState21 
22 Examining CLR data structures      Diagnostic Utilities23 -----------------------------      -----------------------------
24 DumpDomain                         VerifyHeap25 EEHeap                             VerifyObj26 Name2EE                            FindRoots27 SyncBlk                            HeapStat28 DumpMT                             GCWhere29 DumpClass                          ListNearObj (lno)30 DumpMD                             GCHandles31 Token2EE                           GCHandleLeaks32 EEVersion                          FinalizeQueue (fq)33 DumpModule                         FindAppDomain34 ThreadPool                         SaveModule35 DumpAssembly                       ProcInfo36 DumpSigElem                        StopOnException (soe)37 DumpRuntimeTypes                   DumpLog38 DumpSig                            VMMap39 RCWCleanupList                     VMStat40 DumpIL                             MinidumpMode41 DumpRCW                            AnalyzeOOM (ao)42 DumpCCW43 
44 Examining the GC history           Other45 -----------------------------      -----------------------------
46 HistInit                           FAQ47 HistRoot48 HistObj49 HistObjFind50 HistClear

F、!dumpheap -stat 我们可以查看托管堆。

1 0:000> !dumpheap -stat2 Statistics:3 MT    Count    TotalSize Class Name4 6f545468        1           12 System.Collections.Generic.GenericEqualityComparer`1[[System.String, mscorlib]]5 6f544888        1           12System.Security.HostSecurityManager6 6f543d78        1           12 System.Collections.Generic.ObjectEqualityComparer`1[[System.Type, mscorlib]]7 6f5a9b0c        1           16 System.IO.TextReader+SyncTextReader8 ......9 6f545c40        3          806System.Byte[]10 6f542c60       10         2986System.Char[]11 6f5424e4      166         6100System.String12 6f542788        6        17748System.Object[]13 Total 332 objects

G、
!eeheap -gc
我们可以查看托管堆的布局。

1 0:000> !eeheap -gc2 Number of GC Heaps: 1
3 generation 0 starts at 0x024d1018
4 generation 1 starts at 0x024d100c
5 generation 2 starts at 0x024d1000
6 ephemeral segment allocation context: none7 segment     begin  allocated      size8 024d0000  024d1000  024d5ff4  0x4ff4(20468)9 Large object heap starts at 0x034d1000
10 segment     begin  allocated      size11 034d0000  034d1000  034d6666668  0x4558(17752)12 Total Size:              Size: 0x954c (38220) bytes.13 ------------------------------
14 GC Heap Size:    Size: 0x954c (38220) bytes.

H、.hh 命令可以查看命令的帮助文档。



2.2、SOSEX

SOSEX 这款 dll 也是分两个版本的,分别是:32位和64位。
但是说明一下,这个版本只能使用 Net Framework 环境下,Net Core,Net5、Net6、Net7等以上不能使用的
。下载地址就不贴了,上网一找,也不难。SOSEX 是 SOS 非常有力的扩展,提供了非常多的实用函数。


1)、测试代码
Example_1_1_1
2)、简单命令的执行
A、
!sosex.help

查看 SOSEX的帮助命令

1 0:000> !sosex.help2 SOSEX - Copyright 2007-2014 by Steve Johnson - http://www.stevestechspot.com/
3 To report bugs or offer feedback about SOSEX, please email sjjohnson@pobox.com4 Quick Ref:5 --------------------------------------------------
6 bhi       [filename]                                     BuildHeapIndex - Builds an index file forheap objects.7 bpsc      (Deprecated.  Use !mbp instead)8 chi                                                      ClearHeapIndex - Frees all resources used by the heap index and removes it frommemory.9 dlk       [-d]                                           Displays deadlocks between SyncBlocks and/or ReaderWriterLocks10 dumpfd    <FieldAddr>Dumps the properties of a FieldDef structure11 dumpgen   <GenNum> [-free] [-stat] [-type <TYPE_NAME>]   Dumps the contents of the specified generation12                    [-nostrings]13 finq      [GenNum] [-stat]                               Displays objects inthe finalization queue14 frq       [-stat]                                        Displays objects inthe Freachable queue15 gcgen     <ObjectAddr>                                   Displays the GC generation of the specified object
16 gch       [HandleType]... [-stat]                        Lists all GCHandles, optionally filtered by specified handle types17 help      [CommandName]                                  Display thisscreen or details about the specified command18 lhi       [filename]                                     LoadHeapIndex -load the heap index into memory.19 mbc       <SOSEX breakpoint ID | *>Clears the specified or all managed breakpoints20 mbd       <SOSEX breakpoint ID | *>Disables the specified or all managed breakpoints21 mbe       <SOSEX breakpoint ID | *>Enables the specified or all managed breakpoints22 mbl       [SOSEX breakpoint ID]                          Prints the specified or all managed breakpoints23 mbm       <Type/MethodFilter>[ILOffset] [Options]       Sets a managed breakpoint on methods matching the specified filter24 mbp       <SourceFile> <nLineNum>[ColNum] [Options]     Sets a managed breakpoint at the specified source code location25 mdso      [Options]                                      Dumps object references on the stack and in CPU registers inthe current context26 mdt       [TypeName | VarName | MT] [ADDR] [Options]     Displays the fields of an objector type, optionally recursively27 mdv       [nFrameNum]                                    Displays arguments and locals fora managed frame28 mfrag     [-stat] [-mt:<MT>]                             Reports free blocks, the type of objectfollowing the free block, and fragmentation statistics29 mframe    [nFrameNum]                                    Displays or sets the current managed frame for the !mdt and !mdv commands30 mgu       //TODO: Document
31 mk        [FrameCount] [-l] [-p] [-a]                    Prints a stack trace of managed and unmanaged frames32 mln       [expression]                                   Displays the type of managed data located at the specified address or the current instruction pointer33 mlocks    [-d]                                           Lists all managed lockobjects and CriticalSections and their owning threads34 mroot     <ObjectAddr> [-all]                            Displays GC roots for the specified object
35 mt        (no parameters)                                Steps into the managed method at the current position36 mu        [address] [-s] [-il] [-n]                      Displays a disassembly around the current instruction with interleaved source, IL and asm code37 muf       [MD Address | Code Address] [-s] [-il] [-n]    Displays a disassembly with interleaved source, IL and asm code38 mwaits    [-d | LockAddr]                                Lists all waiting threads and, ifknown, the locks they are waiting on39 mx        <Filter String>                                Displays managed type/field/method names matching the specified filter string
40 rcw       [Object or SyncBlock Addr]                     Displays Runtime Callable Wrapper (RCW) COM interop data.41 refs      <ObjectAddr> [-target|-source]                 Displays all references from and to the specified object
42 rwlock    [ObjectAddr | -d]                              Displays all RWLocks or, if provided a RWLock address, details of the specified lock
43 sosexhelp [CommandName]                                  Display thisscreen or details about the specified command44 strings   [ModuleAddress] [Options]                      Search the managed heap or a module forstrings matching the specified criteria45 
46 ListGcHandles -See gch47 
48 Use !help <command> or !sosexhelp <command> formore details about each command.49 You can also use the /? (or -?) option on any command to get help for that command.


B、!strings 我们可以把进程中所有的字符串找出来。

1 0:000> !strings2 Address    Gen    Length   Value3 ---------------------------------------
4 024d1228    0          0   
5 024d1254    0        121   E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\AdvancedDebug.NetFramework.Example_1_1_1\bin\Debug\6 024d1354    0        145   E:\Visual Studio 2022\Source\Projects7 ......8 024d34c0    0          3Nov9 024d34d4    0          3Dec10 024d3698    0          6   zh-CHS11 024d36b4    0          6   zh-CHT12 024d36d0    0          5   zh-CN13 024d3764    0          5   zh-cn14 024d3924    0          5   zh-CN15 ......16 024d3cf8    0          3   936
17 024d3d0c    0          1   3
18 024d3d1c    0          1   2
19 024d3d2c    0          1   0
20 024d3d3c    0          1   0
21 024d3d4c    0         24NLS_CodePage_936_3_2_0_022 024d3e2c    0          8encoding23 024d3e4c    0          6stream24 024d42bc    0          5bytes25 024d42d4    0          5chars26 024d42ec    0          9charCount27 024d430c    0          9charIndex28 024d432c    0          9byteCount29 024d4a9c    0          5count30 024d4ab4    0          6offset31 ---------------------------------------
32 166 strings


C、!finq 可以查看终结器队列。

1 0:000> !finq2 Generation 0:3 Address       Size   Type4 ---------------------------------------------
5 024d1e34        20Microsoft.Win32.SafeHandles.SafePEFileHandle6 024d24d8        44System.Threading.ReaderWriterLock7 024d2638        20Microsoft.Win32.SafeHandles.SafeFileHandle8 024d3d8c        20Microsoft.Win32.SafeHandles.SafeViewOfFileHandle9 024d3da0        20Microsoft.Win32.SafeHandles.SafeFileMappingHandle10 024d4a30        52System.Threading.Thread11 024d4ad0        20Microsoft.Win32.SafeHandles.SafeFileHandle12 7 objects, 196bytes13 
14 Generation 1:15 Address       Size   Type16 ---------------------------------------------
17 0 objects, 0bytes18 
19 Generation 2:20 Address       Size   Type21 ---------------------------------------------
22 0 objects, 0bytes23 
24 TOTAL: 7 objects, 196 bytes


D、!mlocks 判断当前是否有死锁。

1 0:000> !mlocks2 Examining SyncBlocks...3 Scanning forReaderWriterLock instances...4 Scanning forholders of ReaderWriterLock locks...5 Scanning forReaderWriterLockSlim instances...6 Scanning forholders of ReaderWriterLockSlim locks...7 Examining CriticalSections...8 
9 ClrThread  DbgThread  OsThread    LockType    Lock      LockLevel10 ----------------------------------------------------------------------
11 0x1        0          0x1028      thinlock    024d4e90  (recursion:0)



2.3、Net 反编译工具

Net 反编译器可以编译 IL 代码,让 IL 代码转成 C# 代码,这里推荐两款工具。


2.3.1、ILSpy
官网地址:
https://github.com/icsharpcode/ILSpy
镜像地址:
https://sourceforge.net/projects/ilspy.mirror/files/latest/download


2.3.2、DnSpy
这个工具不仅可以可以反编译 C# 代码,还可以对 Net Framework 程序进行调试。
官网地址:
https://github.com/dnSpy/dnSpy/releases


其他下载:
https://filehippo.com/zh/download_dnspy/


2.4、PerfView

这是 CLR 团队调优 CLR 使用的工具,可以实时监控程序的行为,比如:程序的 GC 触发的情况。
官网地址:
https://github.com/microsoft/perfview
微软官网:
https://www.microsoft.com/en-us/download/details.aspx?id=28567
1)、测试代码

Example_1_1_2

2)、使用 Perfview 监控程序。

Perfview使用很简单,先打开 Perfview 软件,然后电机菜单【collect】--->【collect】,打开【Collecting data over a user specified interval】窗口,什么也不用选择,直接点击窗口中的【Start Collection】按钮,开始采集数据。

当 Perfview 开始采集数据的时候,我们打开我们的测试程序【
Example_1_1_2.exe
】,运行到10000,关闭程序,点击【StopCollection】按钮。Perfview 开始生成数据,可以观察状态栏,查看 Perfview 的动作。

还有很多数据,不能一一展示,大家可以自己动手测试下。我截了一张图,表示一下。


三、结束
站在高人的肩膀之上,自己轻松了很多,但是,自己还是一个小学生,Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。

Java替换RequstBody和RequestParam参数的属性

本文主要讲解在Java环境中如何替换RequestBody和RequestParam参数中的属性

背景

近期由于接手的老项目中存在所有接口中新增一个加密串来给接口做一个加密效果(项目历史原因,不方便上Jwt授权这套),所以就研究了一下Http请求链路,发现可以通过
javax.servlet.Filter
去实现

替换RequestParam参数

首先通过继续
HttpServletRequestWrapper
来达到获取和替换RequestParam中的参数信息,接下来我们通过
javax.servlet.Filter
去获取ServletRequest中参数的信息,并且定义对应规则,来实现替换参数
代码示例:

package com.simplemessage.cloudpayservice.infrastructure.config.http;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

/**
 * @CreateAt: 2023/10/24 12:13
 * @ModifyAt: 2023/10/24 12:13
 * @Version 1.0
 */
public class MyRequestWrapper  extends HttpServletRequestWrapper {

    private Map params = new HashMap<>();
    public MyRequestWrapper(HttpServletRequest request, Map newParams) {
        super(request);
        if(request.getParameterMap() != null){
            this.params.putAll(request.getParameterMap());
        }
        if(newParams != null){
            this.params.putAll(newParams);
        }
    }

    /**
     * 获取参数
     * @return
     */
    @Override
    public Map getParameterMap() {
        return params;
    }

    @Override
    public Enumeration getParameterNames() {
        Vector l = new Vector(params.keySet());
        return l.elements();
    }


    @Override
    public String[] getParameterValues(String name) {
        Object v = params.get(name);
        if (v == null) {
            return null;
        } else if (v instanceof String[]) {
            return (String[]) v;
        } else if (v instanceof String) {
            return new String[]{(String) v};
        } else {
            return new String[]{v.toString()};
        }
    }

    /**
     * 根据参数的key获取参数
     * @param name
     * @return
     */
    @Override
    public String getParameter(String name) {
        Object v = params.get(name);
        if (v == null) {
            return null;
        } else if (v instanceof String[]) {
            String[] strArr = (String[]) v;
            if (strArr.length > 0) {
                return strArr[0];
            } else {
                return null;
            }
        } else if (v instanceof String) {
            return (String) v;
        } else {
            return v.toString();
        }
    }
}
package com.simplemessage.cloudpayservice.infrastructure.config.http;

import com.fasterxml.jackson.core.io.JsonEOFException;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.RequestFacade;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @CreateAt: 2023/10/24 12:16
 * @ModifyAt: 2023/10/24 12:16
 * @Version 1.0
 */
@Slf4j
@Component
@WebFilter(filterName = "replaceGetRequestFilter", urlPatterns = {"/*"})
public class ReplaceGetRequestFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        long start = System.currentTimeMillis();
        //获取HttpServletRequest对象
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        //判断当前是否为Get请求
        if ("GET".equalsIgnoreCase(httpServletRequest.getMethod())) {
        	// 获取参数信息
            String param= request.getParameter("param");
            //判断参数是否为空,为空则放行
            if (StringUtils.isEmpty(param)) {
                chain.doFilter(request, response);
                return;
            } else {
                Map<String, String[]> newParameterMap = new HashMap<>();
                // 替换参数(自定义规则)
                String newParama="test";
                newParameterMap.put("param", newParama);
                // 实现参数替换
                MyRequestWrapper myRequestWrapper = new MyRequestWrapper(httpServletRequest, newParameterMap);
                chain.doFilter(myRequestWrapper, response);
            }

        } else {
            try {
                chain.doFilter(request, response);
            } catch (HttpMessageNotReadableException httpMessageNotReadableException) {
                log.error(((RequestFacade) request).getRequestURI() + ", " + httpMessageNotReadableException.getMessage());
            } catch (JsonEOFException jsonEOFException) {
                log.error(((RequestFacade) request).getRequestURI() + ", " + jsonEOFException.getMessage());
            }
        }
        long end = System.currentTimeMillis();
        log.info("{} 接口耗时:{} ms", httpServletRequest.getRequestURI(), (end - start));
    }

    @Override
    public void destroy() {
    }
}

替换RequestBody参数

主要思路就是通过获取Post中请求的输入流信息,解析输入流信息,按照对应的规则进行替换参数信息,最后将对应的流信息包装进行返回
代码示例:

package com.simplemessage.cloudpayservice.infrastructure.config.http;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.List;

/**
 * @version 1.0
 * @createAt: 2023/10/24 12:23:23
 * @modifyAt: 2023/10/24 12:23:23
 */
@RestControllerAdvice
@Slf4j
public class DecryptRequestBodyHandler implements RequestBodyAdvice {
    
    /**
     * 该方法用于判断当前请求,是否要执行beforeBodyRead方法
     * methodParameter方法的参数对象
     * type方法的参数类型
     * aClass 将会使用到的Http消息转换器类类型
     * 注意:此判断方法,会在beforeBodyRead 和 afterBodyRead方法前都触发一次。
     * @return 返回true则会执行beforeBodyRead
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    /**
     * 在Http消息转换器执转换,之前执行
     * inputMessage 客户端请求的信息
     * parameter 参数信息
     * targetType 参数类型
     * converterType Http消息转换器类类型
     *
     * @return 返回 一个自定义的HttpInputMessage
     */
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        // 如果body是空内容直接返回原来的请求
        if (inputMessage.getBody().available() <= 0) {
            return inputMessage;
        }
        // 请求中的header信息
        HttpHeaders headers = inputMessage.getHeaders();     
      
        // 将输入流读出来,注意 body 里面的流只能读一次
        ByteArrayOutputStream requestBodyDataByte = new ByteArrayOutputStream();
        try {
        	//复制流信息
            IOUtils.copy(inputMessage.getBody(), requestBodyDataByte);
        } catch (Exception e) {
            log.error("参数流拷贝失败: ", e.toString());
            return inputMessage;
        }
        ByteArrayOutputStream requestBodyDataByteNew = null;
        try {
            JSONObject body = JSON.parseObject(requestBodyDataByte.toByteArray(), JSONObject.class);
            if (ObjectUtils.isEmpty(body)) {
                return inputMessage;
            }
            //自定义规则西悉尼
            if (body.containsKey("param")) {
                String custId = body.getString("param"); 
                String newParam="";              
                body.put("custId", newParam);
                requestBodyDataByteNew = new ByteArrayOutputStream();
                //拷贝流信息
                IOUtils.copy(new ByteArrayInputStream(body.toJSONString().getBytes()), requestBodyDataByteNew);
            }
        } catch (Throwable e) {
            log.error("流转换异常 ", e.toString());
        }
        // 如果上述发生异常,仍然使用原来的请求内容
        requestBodyDataByte = requestBodyDataByteNew != null ? requestBodyDataByteNew : requestBodyDataByte;
        InputStream rawInputStream = new ByteArrayInputStream(requestBodyDataByte.toByteArray());
        inputMessage.getHeaders().set(HttpHeaders.CONTENT_LENGTH, String.valueOf(rawInputStream.available()));
        return new HttpInputMessage() {
            @Override
            public HttpHeaders getHeaders() {
                return inputMessage.getHeaders();
            }

            @Override
            public InputStream getBody() throws IOException {
                return rawInputStream;
            }
        };
    }

    /**
     * 在Http消息转换器执转换,之后执行
     * body 转换后的对象
     * inputMessage 客户端的请求数据
     * parameter handler方法的参数类型
     * targetType handler方法的参数类型
     * converterType 使用的Http消息转换器类类型
     *
     * @return 返回一个新的对象
     */
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    /**
     * 参数与afterBodyRead相同,不过这个方法body为空的情况
     */
    @Override
    public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}





如有哪里讲得不是很明白或是有错误,欢迎指正
如您喜欢的话不妨点个赞收藏一下吧

前面这些都是问题描述,问题在偏下面

场景:用户电脑上安装了PCL驱动,可通过驱动完成打印。

需求:现在需要提供一种脱离PC端完成文件上传并打印的功能。让用户使用手机或pc未安装驱动时都能打印文件。

目前思路:首先上传文件这一步没有任何问题,开发了相应的H5界面,通过浏览器进行原始文件的上传。

难点在于用户多种多样类型格式的文件,如何转换为打印机能够输出的文件。

现阶段是通过各种各样的转换方法东拼西凑,支持将doc(x)、xls(x)、ppt(x)、txt、jpg/png统一转为PDF文件,然后通过PCL命令LANGUAGE=PDF,传输给打印机,完成PDF文件的输出。

这种方法目前能凑合着用,但是各类型文件转PDF这一步比较麻烦。

现在想了一种新的思路:在服务器上安装我们的PCL驱动,然后通过命令行或者JAVA或者C#等编程语言,来触发打印机打印文件。

尝试一:

通过搜索引擎了解到了JAVA内置的PrintService类,我测试了两种类型的作业:PrinterJob和DocPrinterJob,

- 前者需要提供一个Printable对象,这个对象就是需要你自己把各类型文档的内容先解析一遍,再按照它的标准接口绘制到它的图形里去。------- 还是需要自己对文档进行转换,太麻烦了,被pass了。

- 后者就是把你文档内容原封不动的传给打印机。-----根本没做转换动作,pass。

尝试二:

通过搜索引擎了解到了批处理的【print】命令,测试后发现不行

还试了write /pt 666666.pdf "打印机" " 驱动" "test.prn"命令,它应该是调用了记事本的接口,只能输出txt文件(还会乱码)。其他文件不合适。

尝试三(待实现):

还是通过搜索引擎,看了
操作系统是如何控制打印机打印文件的? - 知乎 (zhihu.com)
这篇文档,了解了文件转PCL一般是驱动程序完成的(回过头来想想也是,所以【尝试一】中,直接调用打印机服务进行打印并没有对进行文件转换,因为是驱动完成的这一步)。

问题在这

所以想咨询园内大佬:有没有什么方法,能够通过非手动的方法,唤起我电脑上的驱动程序,进行文件的转换。用的驱动是【MINOLTA C368SeriesPCL】

扩展:

如果上一步可行,我看到windows电脑上还自带了一个【Microsoft Print To PDF  】的打印机和驱动程序,是否就可以用相同的方式完成各类型文件转PDF的功能???

本人从事打印机相关的软件行业,对打印机、打印驱动这些有一丢丢了解,但又不是很清晰,如果有哪里说的不对,欢迎指正。接受批评。