1、java的大部分接口的方法都是串行执行的,但是有些业务场景是不需要同步返回结果的,可以把结果直接返回,具体业务异步执行,也有些业务接口是需要并行获取数据,最后把数据聚合在统一返回给前端。

通常我们都是采用多线程的方式来实现上述业务功能,但spring 提供更优雅的方式来实现上述功能,就是@Async 异步注解,在方法上添加@Async,spring就会借助AOP,异步执行方法。

如何启用@Async

spring boot通过@EnableAsync 注解启用@Async异步注解

实现AsyncConfigurer接口,getAsyncExecutor是默认自定义的线程池

/**
* 线程池配置(@Async)
*/
@Slf4j
@EnableAsync
@Configuration
public class SimpleExecutorConfig implements AsyncConfigurer {
/** 线程池维护线程的最少数量 */
@Value("${executor.corePoolSize}")
private Integer corePoolSize; /** 线程池维护线程的最大数量 */
@Value("${executor.maxPoolSize}")
private Integer maxPoolSize; /** 缓冲队列的大小 */
@Value("${executor.queueCapacity}")
private Integer queueCapacity; /** 为每个线程名设置一个前缀(1) */
@Value("${executor.threadNamePrefix}")
private String threadNamePrefix; /** 为每个线程名设置一个前缀(2) */
@Value("${executor.threadNamePrefix_2}")
private String threadNamePrefix_2; @Bean(ExecutorConstant.simpleExecutor_1)
@Override
public Executor getAsyncExecutor() {
//线程池
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(corePoolSize);
taskExecutor.setMaxPoolSize(maxPoolSize);
taskExecutor.setQueueCapacity(queueCapacity);
taskExecutor.setThreadNamePrefix(threadNamePrefix);
taskExecutor.initialize();
return taskExecutor;
} @Bean(ExecutorConstant.simpleExecutor_2)
public Executor asyncExecutor2() {
//线程池
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(corePoolSize);
taskExecutor.setMaxPoolSize(maxPoolSize);
taskExecutor.setQueueCapacity(queueCapacity);
taskExecutor.setThreadNamePrefix(threadNamePrefix_2);
taskExecutor.initialize();
return taskExecutor;
}
}

如何使用@Async

下面是代码:

TestAsyncService类:

@Slf4j
@Service
public class TestAsyncService implements ITestAsyncService { /**
* 异步方法,无返回值
* @return
*/
@Async
@Override
public void asyncFunction_1(){
handleBusinessTime();
log.info("asyncFunction_1 当前线程名称是:{}",Thread.currentThread().getName());
}; /////////////////异步方法,无返回值(指定线程池) start
/**
* 异步方法,无返回值(指定线程池)
* @return
*/
@Async(value = ExecutorConstant.simpleExecutor_2)
@Override
public void asyncFunction_2(){
handleBusinessTime();
log.info("asyncFunction_2 当前线程名称是:{}",Thread.currentThread().getName());
}; @Async(ExecutorConstant.simpleExecutor_2)
@Override
public void asyncFunction_3(){
handleBusinessTime();
log.info("asyncFunction_3 当前线程名称是:{}",Thread.currentThread().getName()); };
/////////////////异步方法,无返回值(指定线程池) end /**
* 异步方法,有返回值
* @return
*/
@Async
@Override
public Future<Integer> asyncReturnDta_1(){
handleBusinessTime();
log.info("asyncReturnDta_1 当前线程名称是:{}",Thread.currentThread().getName());
return new AsyncResult<Integer>(1);
};
/**
* 异步方法,有返回值(指定线程池)
* @return
*/
@Async(ExecutorConstant.simpleExecutor_2)
@Override
public Future<Integer> asyncReturnDta_2(){
handleBusinessTime();
log.info("asyncReturnDta_2 当前线程名称是:{}",Thread.currentThread().getName());
return new AsyncResult<Integer>(1);
}; /**
* 异步方法,有返回值-超时
* @return
*/
@Async
@Override
public Future<Integer> asyncReturnDtaTimeOut(){
handleBusinessTime();
handleBusinessTime();
handleBusinessTime();
handleBusinessTime();
log.info("asyncReturnDta_3 当前线程名称是:{}",Thread.currentThread().getName());
return new AsyncResult<Integer>(1);
}; /**
* 这方法 模拟处理业务或者 去操作数据库 消耗的时间
*/
public static void handleBusinessTime(){
//去数据库查询数据耗时 start
int[] sleepTime = NumberUtil.generateRandomNumber(2000,5000,1);
try {
//Thread.sleep 休眠的时候 相当于 业务操作,或者请求数据库的需要消耗的时间
Thread.sleep(sleepTime[0]);
} catch (InterruptedException e) {
e.printStackTrace();
}
//去数据库查询数据耗时 end
}
}

TestAsyncController

@Slf4j
@RestController
@RequestMapping(value = "/v1/async")
public class TestAsyncController { @Autowired
ITestAsyncService testAsyncService; @ApiOperation(value = "调用接口")
@RequestMapping(value = "/test", method = RequestMethod.GET)
public Resp<Integer> test() throws ExecutionException, InterruptedException {
log.info("asyncFunction_1 start");
testAsyncService.asyncFunction_1();
log.info("asyncFunction_1 start"); log.info("asyncFunction_2 start");
testAsyncService.asyncFunction_2();
log.info("asyncFunction_2 end"); log.info("asyncFunction_3 start");
testAsyncService.asyncFunction_3();
log.info("asyncFunction_3 end"); log.info("asyncReturnDta_1 & asyncReturnDta_2 start");
Future<Integer> future = testAsyncService.asyncReturnDta_1();
testAsyncService.asyncReturnDta_2();
log.info("asyncReturnDta_1 & asyncReturnDta_2 end"); Integer resp = future.get();
log.info("future.get() resp:{}",resp);
return Resp.buildDataSuccess(resp);
} @ApiOperation(value = "调用接口-超时")
@RequestMapping(value = "/async_timeOut", method = RequestMethod.GET)
public Resp<Integer> async_timeOut() throws ExecutionException, InterruptedException {
TimeInterval timeInterval = DateUtil.timer();
log.info("asyncReturnDtaTimeOut start");
Future<Integer> future = testAsyncService.asyncReturnDtaTimeOut();
log.info("asyncReturnDtaTimeOut end"); Integer resp = null;
try {
//一秒内 返回不了数据就报错
resp = future.get(1, TimeUnit.SECONDS);
} catch (TimeoutException e) {
resp = -1;//请求超时了,相当于熔断,服务降级
log.error("asyncReturnDtaTimeOut future.get(1, TimeUnit.SECONDS) timeout:",e);
} log.info("future.get() resp:{} 耗时:{}毫秒",resp,timeInterval.intervalRestart());
return Resp.buildDataSuccess(resp);
} }

/v1/async/test 接口:

2021-06-20 21:09:30.490  INFO 14207 --- [nio-8666-exec-7] c.e.m.controller.TestAsyncController     : asyncFunction_1 start
2021-06-20 21:09:30.490 INFO 14207 --- [nio-8666-exec-7] c.e.m.controller.TestAsyncController : asyncFunction_1 start
2021-06-20 21:09:30.490 INFO 14207 --- [nio-8666-exec-7] c.e.m.controller.TestAsyncController : asyncFunction_2 start
2021-06-20 21:09:30.491 INFO 14207 --- [nio-8666-exec-7] c.e.m.controller.TestAsyncController : asyncFunction_2 end
2021-06-20 21:09:30.491 INFO 14207 --- [nio-8666-exec-7] c.e.m.controller.TestAsyncController : asyncFunction_3 start
2021-06-20 21:09:30.491 INFO 14207 --- [nio-8666-exec-7] c.e.m.controller.TestAsyncController : asyncFunction_3 end
2021-06-20 21:09:30.491 INFO 14207 --- [nio-8666-exec-7] c.e.m.controller.TestAsyncController : asyncReturnDta_1 & asyncReturnDta_2 start
2021-06-20 21:09:30.492 INFO 14207 --- [nio-8666-exec-7] c.e.m.controller.TestAsyncController : asyncReturnDta_1 & asyncReturnDta_2 end
2021-06-20 21:09:32.679 INFO 14207 --- [le-1-executor-9] c.e.multi.service.impl.TestAsyncService : asyncFunction_1 当前线程名称是:my-simple-1-executor-9
2021-06-20 21:09:33.454 INFO 14207 --- [le-2-executor-8] c.e.multi.service.impl.TestAsyncService : asyncFunction_3 当前线程名称是:my-simple-2-executor-8
2021-06-20 21:09:33.578 INFO 14207 --- [le-2-executor-9] c.e.multi.service.impl.TestAsyncService : asyncReturnDta_2 当前线程名称是:my-simple-2-executor-9
2021-06-20 21:09:34.101 INFO 14207 --- [e-1-executor-10] c.e.multi.service.impl.TestAsyncService : asyncReturnDta_1 当前线程名称是:my-simple-1-executor-10
2021-06-20 21:09:34.102 INFO 14207 --- [nio-8666-exec-7] c.e.m.controller.TestAsyncController : future.get() resp:1
2021-06-20 21:09:34.357 INFO 14207 --- [le-2-executor-7] c.e.multi.service.impl.TestAsyncService : asyncFunction_2 当前线程名称是:my-simple-2-executor-7

从日志上可以看出,都是异步执行的


/v1/async/async_timeOut 接口:

2021-06-20 21:20:58.886  INFO 14427 --- [nio-8666-exec-1] c.e.m.controller.TestAsyncController     : asyncReturnDtaTimeOut start
2021-06-20 21:20:58.890 INFO 14427 --- [nio-8666-exec-1] c.e.m.controller.TestAsyncController : asyncReturnDtaTimeOut end
2021-06-20 21:20:59.899 ERROR 14427 --- [nio-8666-exec-1] c.e.m.controller.TestAsyncController : asyncReturnDtaTimeOut future.get(1, TimeUnit.SECONDS) timeout: java.util.concurrent.TimeoutException: null
at java.util.concurrent.FutureTask.get(FutureTask.java:205) ~[na:1.8.0_231]
at com.example.multi.controller.TestAsyncController.async_timeOut(TestAsyncController.java:69) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_231]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_231]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_231]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_231]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197) [spring-web-5.3.8.jar:5.3.8]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141) [spring-web-5.3.8.jar:5.3.8]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106) [spring-webmvc-5.3.8.jar:5.3.8]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894) [spring-webmvc-5.3.8.jar:5.3.8]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) [spring-webmvc-5.3.8.jar:5.3.8]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) [spring-webmvc-5.3.8.jar:5.3.8]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1063) [spring-webmvc-5.3.8.jar:5.3.8]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) [spring-webmvc-5.3.8.jar:5.3.8]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) [spring-webmvc-5.3.8.jar:5.3.8]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) [spring-webmvc-5.3.8.jar:5.3.8]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:626) [tomcat-embed-core-9.0.46.jar:4.0.FR]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) [spring-webmvc-5.3.8.jar:5.3.8]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) [tomcat-embed-core-9.0.46.jar:4.0.FR]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) [tomcat-embed-websocket-9.0.46.jar:9.0.46]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) [spring-web-5.3.8.jar:5.3.8]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.8.jar:5.3.8]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) [spring-web-5.3.8.jar:5.3.8]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.8.jar:5.3.8]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) [spring-web-5.3.8.jar:5.3.8]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.8.jar:5.3.8]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) [tomcat-embed-core-9.0.46.jar:9.0.46]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.46.jar:9.0.46]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_231]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_231]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.46.jar:9.0.46]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_231] 2021-06-20 21:20:59.900 INFO 14427 --- [nio-8666-exec-1] c.e.m.controller.TestAsyncController : future.get() resp:-1 耗时:1014毫秒
2021-06-20 21:21:12.105 INFO 14427 --- [le-1-executor-1] c.e.multi.service.impl.TestAsyncService : asyncReturnDta_3 当前线程名称是:my-simple-1-executor-1

从日志上看出,如果future.get(1, TimeUnit.SECONDS) 到了超时时间,直接抛出超时异常,走主线程后续代码。

比较适合 规定时间范围内要返回数据(超时可以根据业务场景,返回一个默认值,或者返回值值就是空的)的业务场景

@Async原理+部分源码@Async原理+源码

原理:是通过spring aop + 线程池的方式来实现的

源码:

源码的方法是:AsyncExecutionInterceptor.invoke



107行:是获取一个线程池

108行:如果没有设置线程池抛出异常

113行:是创建一个线程对象 他的run方法执行invocation.proceed()【走实际业务代码】

121和124行:走的是统一的异常处理 主要是调用handleUncaughtException方法,SimpleExecutorConfig 实现了AsyncConfigurer接口它就有getAsyncUncaughtExceptionHandler方法,可以重写这个方法,实现自定义的异常处理

下图是AsyncConfigurer接口可以实现的方法:

doSubmit方法:

实际上就是调用线程池的submit方法:

具体代码,我上传到gitee,大家感兴趣可以clone 传送门~

@Async源码参考于:

https://www.cnblogs.com/dennyzhangdd/p/9026303.html

spring boot使用@Async异步注解的更多相关文章

  1. Spring Boot -- Spring Boot之@Async异步调用、Mybatis、事务管理等

    这一节将在上一节的基础上,继续深入学习Spring Boot相关知识,其中主要包括@Async异步调用,@Value自定义参数.Mybatis.事务管理等. 本节所使用的代码是在上一节项目代码中,继续 ...

  2. spring boot:使用async异步线程池发送注册邮件(spring boot 2.3.1)

    一,为什么要使用async异步线程池? 1,在生产环境中,有一些需要延时处理的业务场景: 例如:发送电子邮件, 给手机发短信验证码 大数据量的查询统计 远程抓取数据等 这些场景占用时间较长,而用户又没 ...

  3. spring boot(17)-@Async异步

    验证码的异步机制 上一篇讲过可以用邮件发验证码,通常我们在某网站发验证码时,首先会提示验证码已发送,请检查邮箱或者短信,这就是图中的1和3.然而此时查看邮箱或短信可能并没有收到验证码,往往要过几秒种才 ...

  4. Spring Boot (18) @Async异步

    通常我们在某网站发送邮件验证码时,首先会提示验证码已发送,然而此时可能没有收到验证码,过几秒种才真正的收到.如果是同步会先验证发送是否成功然后再通知,如果是异步可以先通知用户已发送,并释放请求,然后再 ...

  5. Spring Boot中@Async的作用

    在Spring中,@Async这个注解用于标记的异步的方法.方法上一旦标记了这个方法,当其它线程调用这个方法时,就会开启一个新的线程去异步处理业务逻辑. 此注解的使用说明: 1.此注解可以用在方法上, ...

  6. spring boot的一些常用注解

    spring boot的一些常用注解: 使用@SpringBootApplication注释: 许多Spring Boot开发人员喜欢他们的应用程序使用自动配置,组件扫描,并能够在其“应用程序类”上定 ...

  7. @Async异步注解与SpringBoot结合使用

    当你在service层需要启动异步线程去执行某些分支任务,又不希望显式使用Thread等线程相关类,只想专注于实现业务逻辑代码开发,可以使用@Async异步注解. 1. 使用@Async 异步注解 C ...

  8. Spring Boot整合MyBatis(非注解版)

    Spring Boot整合MyBatis(非注解版),开发时采用的时IDEA,JDK1.8 直接上图: 文件夹不存在,创建一个新的路径文件夹 创建完成目录结构如下: 本人第一步习惯先把需要的包结构创建 ...

  9. Spring Boot 实战 —— MyBatis(注解版)使用方法

    原文链接: Spring Boot 实战 -- MyBatis(注解版)使用方法 简介 MyBatis 官网 是这么介绍它自己的: MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过 ...

随机推荐

  1. Nacos使用 MySQL 8.0 提示Public Key Retrieval is not allowed

    原因如下(参考官网给出的连接选项): 如果用户使用了 sha256_password 认证,密码在传输过程中必须使用 TLS 协议保护,但是如果 RSA 公钥不可用,可以使用服务器提供的公钥:可以在连 ...

  2. Pycharm集成码云,图文手把手教学!

    Pycharm集成码云 码云(http://gitee.com)是开源中国推出的代码托管平台,支持 Git 和 SVN,提供免费的私有仓库托管 可以通过码云保管你的代码,每次修改完代码提交,就是一个版 ...

  3. 逆向工程初步160个crackme-------4

    crackme–3因为涉及到浮点数操作以及一些指令和寄存器(由于本人对浮点指令不了解),所以先隔过去分析后面的程序. 工具:1. 按钮事件地址转换工具E2A 2. PEID 3. Ollydbg 首先 ...

  4. 安装MySQL后,需要调整的10个性能配置项

    本文翻译自 Percona 官方博客,适用于 MySQL 5.6 及 5.7 版本. 作者:Stephane Combaudon 原文: https://www.percona.com/blog/20 ...

  5. 关于Aborted connection告警日志的分析

    前言: 有时候,连接MySQL的会话经常会异常退出,错误日志里会看到"Got an error reading communication packets"类型的告警.本篇文章我们 ...

  6. QFNU 10-09 training

    1.F - Three displays 题意:就是给出了两个数组,然后第一组数中找到i,j,k满足i<j<k,第二组数中找到a[i],a[j],a[k],满足a[i]<a[j]&l ...

  7. 在Visual Studio 中使用git——文件管理-下(六)

    在Visual Studio 中使用git--什么是Git(一) 在Visual Studio 中使用git--给Visual Studio安装 git插件(二) 在Visual Studio 中使用 ...

  8. Packing data with Python

    Defining how a sequence of bytes sits in a memory buffer or on disk can be challenging from time to ...

  9. Promise解析(待完成)

    Promise是一种异步操作的解决方案,将写法复杂的传统的回调函数和监听事件的异步操作,用同步代码的形式表达出来.避免了多级异步操作的回调函数嵌套. 1.主要用于异步计算 2.可以将异步操作队列化,按 ...

  10. 通俗易懂的JS之Proxy

    与掘金文章同步,地址:https://juejin.cn/post/6964398933229436935 什么是代理模式 引入一个现实生活中的案例 我们作为用户需要去如何评估一个房子的好坏.如何办理 ...