是否可以检测也使用动态字节码生成的程序?

Is it possible to instrument a program that also uses dynamic bytecode generation?

我正在编写一个 Java 检测程序,它使用内置的 Instrumentation API 和 Javassist (v3.26.0-GA) 来拦截目标程序。此外,我在该程序中实现了一个 REST API 服务,使用 Java Spark 发送 adding/removing 变换器对 starting/stopping 检测的请求,并在检测期间获取拦截的方法时间.

现在,当我尝试 运行 WebGoat(一个开源 Spring 引导应用程序)时,我的 Java 代理程序从 premain 附加,我无法拦截所有方法成功,在日志中,Javassist.

抛出 NotFoundException

这个错误发生在WebGoat中的几个classes都有一个相似的共同事实,即它们与SpringCGLIB有关。下面显示了一些错误。

javassist.NotFoundException: org.owasp.webgoat.hijacksession.cas.HijackSessionAuthenticationProvider$$FastClassBySpringCGLIB$f1f22d
    at javassist.ClassPool.get(ClassPool.java:430)
    at com.sparrow.sptracer.core.transformer.AbstractMethodTransformer.transform(AbstractMethodTransformer.java:87)
    at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:244)
    at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
    at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:541)
    at java.base/java.lang.ClassLoader.defineClass0(Native Method)
    at java.base/java.lang.System.defineClass(System.java:2307)
    at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2439)
    at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2416)
    at java.base/java.lang.invoke.MethodHandles$Lookup.defineClass(MethodHandles.java:1843)
    at java.base/jdk.internal.reflect.GeneratedMethodAccessor44.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:507)
    at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
    at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.apply(AbstractClassGenerator.java:110)
    at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.apply(AbstractClassGenerator.java:108)
    at org.springframework.cglib.core.internal.LoadingCache.call(LoadingCache.java:54)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at org.springframework.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61)
    at org.springframework.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
    at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:134)
    at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319)
    at org.springframework.cglib.reflect.FastClass$Generator.create(FastClass.java:65)
    at org.springframework.cglib.proxy.MethodProxy.helper(MethodProxy.java:135)
    at org.springframework.cglib.proxy.MethodProxy.init(MethodProxy.java:76)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:216)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
    at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:137)
    at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:124)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698)
    at org.owasp.webgoat.hijacksession.cas.HijackSessionAuthenticationProvider$$EnhancerBySpringCGLIB$ae99c75.authenticate(<generated>)
    at org.owasp.webgoat.hijacksession.HijackSessionAssignment.login(HijackSessionAssignment.java:72)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:517)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:584)
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:763)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1651)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:327)
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:115)
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:81)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:122)
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:126)
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:81)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:109)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:149)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:219)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:213)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
    at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:110)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:80)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:211)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:183)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1638)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1638)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1638)
    at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1638)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1638)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:567)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:602)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1610)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1377)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:507)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1580)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1292)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
    at org.eclipse.jetty.server.Server.handle(Server.java:501)
    at org.eclipse.jetty.server.HttpChannel.lambda$handle(HttpChannel.java:383)
    at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:556)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:375)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:273)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105)
    at org.eclipse.jetty.io.ChannelEndPoint.run(ChannelEndPoint.java:104)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:336)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:313)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:129)
    at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:375)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938)
    at java.base/java.lang.Thread.run(Thread.java:833)

javassist.NotFoundException: org.owasp.webgoat.hijacksession.cas.HijackSessionAuthenticationProvider$$EnhancerBySpringCGLIB$ae99c75$$FastClassBySpringCGLIB$c045873
    at javassist.ClassPool.get(ClassPool.java:430)
    at com.sparrow.sptracer.core.transformer.AbstractMethodTransformer.transform(AbstractMethodTransformer.java:87)
    at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:244)
    at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
    at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:541)
    at java.base/java.lang.ClassLoader.defineClass0(Native Method)
    at java.base/java.lang.System.defineClass(System.java:2307)
... similar stacktrace as above
javassist.NotFoundException: org.owasp.webgoat.session.UserSessionData$$FastClassBySpringCGLIB$b6b54bc
    at javassist.ClassPool.get(ClassPool.java:430)
    at com.sparrow.sptracer.core.transformer.AbstractMethodTransformer.transform(AbstractMethodTransformer.java:87)
    at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:244)
    at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
    at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:541)
    at java.base/java.lang.ClassLoader.defineClass0(Native Method)
    at java.base/java.lang.System.defineClass(System.java:2307)
... similar stacktrace as above

[IMPACTRACER-CORE] ERROR [2021-12-24 10:34:20]: NotFoundException on class 'org/owasp/webgoat/session/UserSessionData$$EnhancerBySpringCGLIB$$bbb61fe2$$FastClassBySpringCGLIB$cb52d3': org.owasp.webgoat.session.UserSessionData$$EnhancerBySpringCGLIB$$bbb61fe2$$FastClassBySpringCGLIB$cb52d3
javassist.NotFoundException: org.owasp.webgoat.session.UserSessionData$$EnhancerBySpringCGLIB$$bbb61fe2$$FastClassBySpringCGLIB$cb52d3
    at javassist.ClassPool.get(ClassPool.java:430)
    at com.sparrow.sptracer.core.transformer.AbstractMethodTransformer.transform(AbstractMethodTransformer.java:87)
    at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:244)
    at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
... similar stacktrace as above

我的猜测是WebGoat在其Spring引导环境中使用cglib来动态生成字节码,而相应的class原本不包含在WebGoat的class路径中,所以javassist.ClassPool.get(className) 抛出错误。

我创建 ClassPool 对象的代码如下。

String name = Descriptor.toJavaName(className);

            try {
                ClassPool cp = ClassPool.getDefault();
                cp.childFirstLookup = true;
                cp.appendClassPath(new LoaderClassPath(loader));

                CtClass cc = cp.get(name);
                Logger.debug("Checking class %s", name);

                CtMethod[] methods = cc.getDeclaredMethods();
                Logger.debug("Altering %d methods in %s", methods.length, wut);
                for (CtMethod m : methods) {
                    // do some code insertion
                }
                bytecode = cc.toBytecode();
                cc.detach();
            } catch (NotFoundException e) {
                Logger.error("NotFoundException on class '%s': %s", className, e.getMessage());
                e.printStackTrace();
            } catch (CannotCompileException e) {
                Logger.error("Cannot compile class '%s': %s", className, e.getMessage());
                e.printStackTrace(System.out);
            } catch (IOException e) {
                Logger.error("IOException while transforming class '%s': %s", className, e.getMessage());
            } catch (Exception ex) {
                Logger.error("Generic exception occurred while transforming class '%s': %s", className, ex.getMessage());
                ex.printStackTrace(System.out);
            }

当我尝试在 localhost:8080 上尝试与 WebGoat 应用程序交互时发生上述错误,试图使用 Java Spark REST API 发送具有特定 ID 的“启动工具”请求,所以每当我发送这个请求时,它都会触发 inst.addTransformer 方法来创建一个新的 Transformer 并将其添加到检测对象中。

导致此错误的原因可能是什么?我假设 WebGoat 本身正在使用一些检测,这意味着我正在检测一个检测过的应用程序,我不知道这是否可能。

如有任何见解,我们将不胜感激。

来自之前的评论:

The unfound classes are dynamic proxies which are heavily used by the Spring Framework in order to implement AOP. Spring can use both JDK dynamic interface proxies and CGLIB proxies, the latter of which is what we are seeing here. Maybe you should simply ignore those types of classes. They are in fact created dynamically, hence the name. But they are rather a result of dynamic (sub-)class generation than of bytecode transformation.

是的,我考虑过忽略那些动态生成的 classes,但我的应用程序的全部目的是在用户与 Web 应用程序交互时捕获每个方法调用(例如单击按钮等)。在这种情况下,是否可以忽略这些类型的动态生成的 classes?我想确保我没有错过任何方法调用。

因为那些 classes 只是动态代理,它们将调用转发到原始方法或调用一些 AOP 或拦截器逻辑 first/instead。无论哪种方式,您都不会错过任何重要的东西,这些代理更像是交换机或路由器,实际的表演发生在其他地方。我建议您简单地尝试一个带有一两个方面的小游乐场项目。

您还询问了如何通过名称检测和忽略动态代理:

  • CGLIB 代理: Spring 的 CGLIB 代理包含 $$FastClassBySpringCGLIB$$$$EnhancerBySpringCGLIB$$ 等子字符串,后跟 8代表 4 个十六进制字节的字符。您可以使用正则表达式进行匹配,只是保持简单并匹配子字符串 BySpringCGLIB$$。如果非 Spring CGLIB 代理也在您的应用程序中的某处使用,您将不得不注意其他命名模式。但如果不过滤它们,您可能会遇到与以前类似的错误,因此您会自动注意到。

  • JDK 代理: 如果您的 Spring 应用程序也恰好使用 JDK 代理,您可以识别它们轻松使用 JRE API 致电 Proxy.isProxyClass(Class). Thanks to Johannes Kuhn 征求意见。

  • JDK 代理(旧答案): 您可以过滤 class 以 [=14] 开头的名称=],通常类似于 com.sun.proxy.$Proxy2(尾随数字不同)。根据 JDK documentation: "代理的不合格名称 class 未指定。 class 以字符串 [=16] 开头的名称的 space =] 但是,保留给代理 classes。 至少对于 Oracle 和可能的 OpenJDK,您可以匹配该命名模式。如果这对所有 JVM 都适用,则由您进行测试,是否有可能在您的环境中使用其他 JVM。我很快尝试使用 Semeru OpenJ9,代理命名模式是相同的,甚至包名 com.sun.proxy。请注意,在更新的 JDK 版本中,JDK 代理将具有完全限定的名称,例如 jdk.proxy2.$Proxy25,因此在例如Java 16 或 17 你不应该依赖包名 com.sun.proxy。添加更多案例或将匹配限制为简单 class 名称中的前导 $Proxy


更新 2022-02-26: 因为这个问题有 activity,我决定添加一些关于 Spring-specific tools 的更多信息它可以确定对象(或 class)是否是 AOP 代理(class),更具体地说,它是 CGLIB 还是 JDK 代理:

看看工具 class AopUtils 及其方便的方法

  • isAopProxy(Object),
  • isCglibProxy(Object),
  • isJdkDynamicProxy(Object).

没有更多的字符串匹配,只需询问Spring。

顺便说一句,在CGLIB中也有一个直接的方法net.sf.cglib.proxy.Proxy.isProxyClass(Class),应该是一样的,但是在Spring中它不起作用,可能是因为Spring使用了CGLIB以非规范的方式。因为 Spring 在其核心中嵌入了一个包重定位的 CGLIB,所以相应的方法 org.springframework.cglib.proxy.Proxy.isProxyClass(Class) 会产生相同的错误结果。因此,如果您在 Spring 内工作,请不要使用这些方法,最好使用 AopUtils.

Here is some example code for your convenience, showing how to determine Spring AOP proxy types (JDK vs. CGLIB proxies) using AopUtils. See also 了解如何配置 Spring 以使用两种代理类型。


顺便说一句,除了 Javassist,您还可以使用 AspectJ 来达到您的目的。这听起来像是一个非常典型的用例。