草庐IT

Fireasy3 揭秘 -- 使用 SourceGeneraor 实现动态代理(AOP)

Fireasy.Net 2023-03-28 原文

目录

  • Fireasy3 揭秘 -- 依赖注入与服务发现
  • Fireasy3 揭秘 -- 自动服务部署
  • Fireasy3 揭秘 -- 使用 SourceGeneraor 改进服务发现
  • Fireasy3 揭秘 -- 使用 SourceGeneraor 实现动态代理(AOP)
  • Fireasy3 揭秘 -- 使用 Emit 构建程序集
  • Fireasy3 揭秘 -- 代码编译器及适配器
  • Fireasy3 揭秘 -- 使用缓存提高反射性能
  • Fireasy3 揭秘 -- 动态类型及扩展支持
  • Fireasy3 揭秘 -- 线程数据共享的实现
  • Fireasy3 揭秘 -- 配置管理及解析处理
  • Fireasy3 揭秘 -- 数据库适配器
  • Fireasy3 揭秘 -- 解决数据库之间的语法差异
  • Fireasy3 揭秘 -- 获取数据库的架构信息
  • Fireasy3 揭秘 -- 数据批量插入的实现
  • Fireasy3 揭秘 -- 使用包装器对数据读取进行兼容
  • Fireasy3 揭秘 -- 数据行映射器
  • Fireasy3 揭秘 -- 数据转换器的实现
  • Fireasy3 揭秘 -- 通用序列生成器和雪花生成器的实现
  • Fireasy3 揭秘 -- 命令拦截器的实现
  • Fireasy3 揭秘 -- 数据库主从同步的实现
  • Fireasy3 揭秘 -- 大数据分页的策略
  • Fireasy3 揭秘 -- 数据按需更新及生成实体代理类
  • Fireasy3 揭秘 -- 用对象池技术管理上下文
  • Fireasy3 揭秘 -- Lambda 表达式解析的原理
  • Fireasy3 揭秘 -- 扩展选择的实现
  • Fireasy3 揭秘 -- 按需加载与惰性加载的区别与实现
  • Fireasy3 揭秘 -- 自定义函数的解析与绑定
  • Fireasy3 揭秘 -- 与 MongoDB 进行适配
  • Fireasy3 揭秘 -- 模块化的实现原理

  实现 AOP(面向切面编程)的方式有很多种,但无外乎静态纺织和动态编织两种。

  动态编织 在 Fireasy 2 中使用 Emit 实现了动态编织。Emit 的缺点很明显,首先就是要求对 IL 语言必须比较熟悉且考虑较全面,其次由于是动态编织,需要使用内存缓存来对代理类进行管理。
  静态编织 是指在代码编译时就将拦截器相关的代码插入到代码内部,运行时不耗用时间和内存空间。常用的有 MSBuild(如PostSharp) 和 Code Analyzers(本文中用到的 ISourceGenerator)。

  首先,定义拦截器的接口,Initialize/InitializeAsync 方法用于首次初始化,Intercept/InterceptAsync 用于拦截方法的执行和属性的访问。如下:

    /// <summary>
    /// 提供对类成员进行拦截的方法。
    /// </summary>
    public interface IInterceptor
    {
        /// <summary>
        /// 使用上下文对象对当前的拦截器进行初始化。
        /// </summary>
        /// <param name="context">包含拦截定义的上下文。</param>
        void Initialize(InterceptContext context);

        /// <summary>
        /// 将自定义方法注入到当前的拦截点。
        /// </summary>
        /// <param name="info">拦截调用信息。</param>
        void Intercept(InterceptCallInfo info);
    }

    /// <summary>
    /// 提供对类成员进行拦截的异步方法。
    /// </summary>
    public interface IAsyncInterceptor
    {
        /// <summary>
        /// 使用上下文对象对当前的拦截器进行初始化。
        /// </summary>
        /// <param name="context">包含拦截定义的上下文。</param>
        ValueTask InitializeAsync(InterceptContext context);

        /// <summary>
        /// 将自定义方法注入到当前的拦截点。
        /// </summary>
        /// <param name="info">拦截调用信息。</param>
        ValueTask InterceptAsync(InterceptCallInfo info);
    }

  InterceptCallInfo 是拦截的调用信息,其中,Arguments 属性是调用的入参,ReturnValue 属性是返回的值。如下:

    /// <summary>
    /// 用于通知客户端的拦截信息。无法继承此类。
    /// </summary>
    public sealed class InterceptCallInfo
    {
        /// <summary>
        /// 获取或设置定义的类型。
        /// </summary>
        public Type? DefinedType { get; set; }

        /// <summary>
        /// 获取或设置当前被拦截的方法或属性。
        /// </summary>
        public MemberInfo? Member { get; set; }

        /// <summary>
        /// 获取或设置方法的返回类型。
        /// </summary>
        public Type? ReturnType { get; set; }

        /// <summary>
        /// 获取或设置当前被拦截的目标对象。
        /// </summary>
        public object? Target { get; set; }

        /// <summary>
        /// 获取或设置拦截的类型。
        /// </summary>
        public InterceptType InterceptType { get; set; }

        /// <summary>
        /// 获取或设置方法的参数数组。
        /// </summary>
        public object[]? Arguments { get; set; }

        /// <summary>
        /// 获取或设置方法的返回值。
        /// </summary>
        public object? ReturnValue { get; set; }

        /// <summary>
        /// 获取或设置触发的异常信息。
        /// </summary>
        public Exception? Exception { get; set; }

        /// <summary>
        /// 获取或设置取消 Before 事件之后调用基类的方法。
        /// </summary>
        public bool Cancel { get; set; }

        /// <summary>
        /// 获取或设置是否中断后继拦截器的执行。
        /// </summary>
        public bool Break { get; set; }
    }

  最后定义一个特性 InterceptAttribute 用于指定类或方法上的拦截器类型,只需要在类、属性或方法上指定即可:

[InterceptAttribute(typeof(SampleInterceptor))]

  接下来,我们定义好注入代码的原型,以便下一步生成代码。比如方法的切面示例:

public override string Test(string str)
{
    //定义由 InterceptAttribute 指定的拦截器实例
    var interceptors = new List<IInterceptor> { new SampleInterceptor() };
    var info = new InterceptCallInfo();
    //拦截的对象为当前对象
    info.Target = this;
    //方法的入参
    info.Arguments = new object[] { str };
    //当前拦截的成员(方法)
    info.Member = ((MethodInfo)MethodBase.GetCurrentMethod()).GetBaseDefinition();

    try
    {
        //初始化
        _Initialize(interceptors, info);
        //通知拦截器方法即将被调用
        _Intercept(interceptors, info, InterceptType.BeforeMethodCall);

        //如果拦截器告知要取消执行
        if (info.Cancel)
        {
            //返回值,如果方法为 void,那么直接 return
            //在拦截器中可以给 ReturnValue 进行赋值,然后 Cancel = true
            return info.ReturnValue == null ? default : (string)info.ReturnValue;
        }
        //调用父方法
        info.ReturnValue = base.Test(str);
        //通知拦截器方法已调用完成
        _Intercept(interceptors, info, InterceptType.AfterMethodCall);
    }
    catch (System.Exception exp)
    {
        info.Exception = exp;
        //通知拦截器,有异常抛出
        _Intercept(interceptors, info, InterceptType.Catching);
        throw exp;
    }
    finally
    {
        //任何时候,都会走到这一步
        _Intercept(interceptors, info, InterceptType.Finally);
    }
    //返回值
    return info.ReturnValue == null ? default : (string)info.ReturnValue;
}

  属性的切面也差不多,只是对 get 和 set 分别处理。我们来看一下 _Initialize 方法要实现的目的:

private void _Initialize(List<IInterceptor> interceptors, InterceptCallInfo callInfo)
{
    if (!this._initMarks.Contains(callInfo.Member))
    {
        for (int i = 0; i < interceptors.Count; i++)
        {
            InterceptContext context = new InterceptContext(callInfo.Member, this);
            interceptors[i].Initialize(context);
        }
        this._initMarks.Add(callInfo.Member);
    }
}

  它的目的是让拦截器只调用一次 Initialize 方法。而 Intercept 方法用于通知拦截器进行拦截,在拦截器里给定 Break = true 时,后续的拦截器将不会被调用。

private void _Intercept(List<IInterceptor> interceptors, InterceptCallInfo callInfo, InterceptType interceptType)
{
    callInfo.InterceptType = interceptType;
    callInfo.Break = false;
    for (int i = 0; i < interceptors.Count; i++)
    {
        if (callInfo.Break)
        {
            break;
        }

        interceptors[i].Intercept(callInfo);
    }
}

  接下来,我们用 ISourceGenretor 来实现代码的生成。

  上篇 使用 SourceGeneraor 改进服务发现 已经讲解过 ISourceGenerator 的用法了,需要分别定义一个 ISyntaxContextReceiverISourceGenerator 的实现。
  定义 DynamicProxySyntaxReceiver 类,用来接收语法节点,并进行分析,找出可以拦截的方法和属性,以及拦截器等内容。这里,我着重讲解一下 AnalyseClassSyntax 方法的实现,如下:

        /// <summary>
        /// 分析类型语法。
        /// </summary>
        /// <param name="model"></param>
        /// <param name="syntax"></param>
        private void AnalyseClassSyntax(SemanticModel model, ClassDeclarationSyntax syntax)
        {
            var typeSymbol = (ITypeSymbol)model.GetDeclaredSymbol(syntax)!;
            if (typeSymbol.IsSealed)
            {
                return;
            }

            var interceptorMetadataOfClass = FindInterceptorMetadata(typeSymbol);

            var metadata = new ClassMetadata(typeSymbol);

            //获取所有成员
            foreach (var memberSymbol in typeSymbol.GetMembers())
            {
                //如果不是方法或属性
                if (memberSymbol.Kind != SymbolKind.Method && memberSymbol.Kind != SymbolKind.Property)
                {
                    continue;
                }

                if (memberSymbol is IMethodSymbol method)
                {
                    //构造器需要重载,所以也要记录下来
                    if (method.MethodKind == MethodKind.Constructor)
                    {
                        metadata.AddConstructor(method);
                        continue;
                    }

                    if (method.MethodKind != MethodKind.Ordinary)
                    {
                        continue;
                    }
                }

                //方法定义为 virtual 并且是公共的
                if (!memberSymbol.IsVirtual || memberSymbol.DeclaredAccessibility != Accessibility.Public)
                {
                    continue;
                }

                //查找方法上的 InterceptAttribute 特性
                if (memberSymbol.GetAttributes().Any(s => s.AttributeClass!.ToDisplayString() == InterceptorAttributeName))
                {
                    var interceptorMetadataOfMember = FindInterceptorMetadata(memberSymbol);
                    if (interceptorMetadataOfMember != null)
                    {
                        metadata.AddMember(memberSymbol, interceptorMetadataOfMember);
                    }
                }
                //没找到方法上的特性,则使用类上定义的 InterceptAttribute 特性
                else if (interceptorMetadataOfClass != null)
                {
                    var hasIgnoreThrowExpAttr = HasIgnoreThrowExceptionAttribute(memberSymbol);
                    metadata.AddMember(memberSymbol, interceptorMetadataOfClass.Clone(!hasIgnoreThrowExpAttr));
                }
            }

            if (metadata.IsValid)
            {
                _metadata.Add(FindUsings(syntax, metadata));
            }
        }

  FindInterceptorMetadata 方法用于查找在类或方法、属性上定义的 InterceptAttribute 特性,并记录下拦截器的类型。如下:

        /// <summary>
        /// 获取拦截器的元数据。
        /// </summary>
        /// <param name="symbol"></param>
        /// <returns></returns>
        private InterceptorMetadata? FindInterceptorMetadata(ISymbol symbol)
        {
            var types = new List<ITypeSymbol>();
            foreach (AttributeData classAttr in symbol.GetAttributes().Where(s => s.AttributeClass!.ToDisplayString() == InterceptorAttributeName))
            {
                var interceptorType = GetInterceptorType(classAttr.ConstructorArguments[0].Value);
                if (interceptorType != null)
                {
                    types.Add(interceptorType);
                }
            }

            if (!types.Any())
            {
                return null;
            }

            var hasIgnoreThrowExpAttr = HasIgnoreThrowExceptionAttribute(symbol);

            return new InterceptorMetadata(types, !hasIgnoreThrowExpAttr);
        }

  在 ClassMetadata 里,定义了可被拦截的方法、属性,以及构造器、引用的命名空间等信息,如下:

    /// <summary>
    /// 类的元数据。
    /// </summary>
    public class ClassMetadata
    {
        /// <summary>
        /// 初始化 <see cref="ClassMetadata"/> 类的新实例。
        /// </summary>
        /// <param name="type">类型符号。</param>
        public ClassMetadata(ITypeSymbol type)
        {
            Type = type;
        }

        /// <summary>
        /// 获取类型符号。
        /// </summary>
        public ITypeSymbol Type { get; }

        /// <summary>
        /// 获取命名空间。
        /// </summary>
        public string Namespace => Type.ContainingNamespace.ToDisplayString();

        /// <summary>
        /// 获取类型的全名。
        /// </summary>
        public string TypeFullName => Type.ToDisplayString();

        /// <summary>
        /// 获取代理类的名称。
        /// </summary>
        public string ProxyTypeName => $"{Type.Name}_proxy_";

        /// <summary>
        /// 获取代理类的全名。
        /// </summary>
        public string ProxyTypeFullName => $"{Namespace}.{ProxyTypeName}";

        /// <summary>
        /// 获取源代码名称。
        /// </summary>
        public string SourceCodeName => Type.ToDisplayString().Replace(".", "_") + ".cs";

        /// <summary>
        /// 获取构造函数列表。
        /// </summary>
        public List<IMethodSymbol> Constructors { get; } = new();

        /// <summary>
        /// 获取可拦截的方法。
        /// </summary>
        public Dictionary<IMethodSymbol, InterceptorMetadata> Methods { get; } = new();

        /// <summary>
        /// 获取可拦截的属性。
        /// </summary>
        public Dictionary<IPropertySymbol, InterceptorMetadata> Properties { get; } = new();

        /// <summary>
        /// 获取引用的命名空间列表。
        /// </summary>
        public List<string> Usings { get; } = new();

        /// <summary>
        /// 添加可拦截的成员。
        /// </summary>
        /// <param name="symbol"></param>
        /// <param name="metadata"></param>
        public void AddMember(ISymbol symbol, InterceptorMetadata metadata)
        {
            if (symbol is IMethodSymbol method)
            {
                Methods.Add(method, metadata);
            }
            else if (symbol is IPropertySymbol property && property.Parameters.Count() == 0) //忽略索引器
            {
                Properties.Add(property, metadata);
            }
        }

        /// <summary>
        /// 添加构造方法。
        /// </summary>
        /// <param name="symbol"></param>
        public void AddConstructor(IMethodSymbol symbol)
        {
            Constructors.Add(symbol);
        }

        /// <summary>
        /// 添加引用的命名空间列表。
        /// </summary>
        /// <param name="usings"></param>
        public void AddUsings(IEnumerable<string> usings)
        {
            Usings.AddRange(usings);
        }

        /// <summary>
        /// 若有可拦截的方法或属性,则此元数据有效。
        /// </summary>
        public bool IsValid => Methods.Any() || Properties.Any();
    }

  接下来,在 DynamicProxyGenerator 类的 Execute 方法里,获取到以上记录到的元数据,分别创建 DynamicProxyClassBuilder 对象来生成代码,如下:

        void ISourceGenerator.Execute(GeneratorExecutionContext context)
        {
            var mappers = new Dictionary<string, string>();

            if (context.SyntaxContextReceiver is DynamicProxySyntaxReceiver receiver)
            {
                var metadatas = receiver.GetMetadatas();
                metadatas.ForEach(s =>
                {
                    context.AddSource(s.SourceCodeName, new DynamicProxyClassBuilder(s).BuildSource());
                    mappers.Add(s.TypeFullName, s.ProxyTypeFullName);
                });

                //代码生成完毕后,还需要在部署器中,将父类和代理类添加到 Container 容器中
                if (mappers.Count > 0)
                {
                    context.AddSource("DynamicProxyServicesDeployer.cs", BuildDeploySourceCode(mappers));
                }
            }
        }

  DynamicProxyClassBuilder 类的核心的实现,在这里,将对构造函数进行重载,对所有的方法、属性进行切面代码的生成。由于篇幅有限,这里只贴上类的构建方法,其他的对照原型进行参悟,也是比较容易理解的。如下:

        /// <summary>
        /// 生成源代码。
        /// </summary>
        /// <returns></returns>
        public SourceText BuildSource()
        {
            var sb = new StringBuilder();
            foreach (var u in _metadata.Usings)
            {
                sb.AppendLine(u.ToString());
            }

            sb.AppendLine("using System.Reflection;");

            sb.AppendLine($@"
namespace {_metadata.Namespace}
{{
    public class {_metadata.ProxyTypeName} : {_metadata.TypeFullName}, IDynamicProxyImplemented
    {{
        private List<System.Reflection.MemberInfo> _initMarks = new ();
        
        {BuildConstructors()}
        {BuildInitializeMethod()}
        {BuildInterceptMethod()}
        {BuildMethods()}
        {BuildProperties()}
    }}
}}");

            return SourceText.From(sb.ToString(), Encoding.UTF8);
        }

  那么同样的,项目编译后,会生成一个 __DynamicProxyServicesDeployer 的部署器,目的是往 Container 里添加代理映射。如下:

[assembly: ServicesDeployAttribute(typeof(__DynamicProxyNs.__DynamicProxyServicesDeployer))]

internal class __DynamicProxyServicesDeployer : IServicesDeployer
{
    void IServicesDeployer.Configure(IServiceCollection services)
    {
        Container.TryAdd(typeof(DependencyInjectionTests.TestDynamicProxyClass), typeof(TestDynamicProxyClass_proxy_));
        Container.TryAdd(typeof(DynamicProxyTests.TestProxy), typeof(TestProxy_proxy_));
        Container.TryAdd(typeof(ObjectActivatorTests.TestServiceProxy2), typeof(TestServiceProxy2_proxy_));
    }
}

  单元测试就不再赘述了,可以去这里查看 动态代理单元测试

  最后,奉上 Fireasy 3 的开源地址:https://gitee.com/faib920/fireasy3 ,欢迎大家前来捧场。

  本文相关代码请参考:
  https://gitee.com/faib920/fireasy3/src/libraries/Fireasy.Common/DynamicProxy
  https://gitee.com/faib920/fireasy3/src/libraries/Fireasy.Common.Analyzers/DynamicProxy
  https://gitee.com/faib920/fireasy3/tests/Fireasy.Common.Tests/DynamicProxyTests.cs

  更多内容请移步官网 http://www.fireasy.cn 。

有关Fireasy3 揭秘 -- 使用 SourceGeneraor 实现动态代理(AOP)的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  5. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  6. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  7. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  8. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  9. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  10. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

随机推荐