范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

SpringBoot结合XXLJOB实现定时任务

  前言
  上篇文章我们介绍了 Quartz 的使用,当时实现了两个简单的需求,不过最后我们总结的时候也提到 Quartz 有不少缺点,代码侵入太严重,所以本篇将介绍 xxl-job 这个定时任务框架。Quartz的不足
  Quartz 的不足:Quartz 作为开源任务调度中的佼佼者,是任务调度的首选。但是在集群环境中,Quartz采用API的方式对任务进行管理,这样存在以下问题:通过调用API的方式操作任务,不人性化。需要持久化业务的 QuartzJobBean 到底层数据表中,系统侵入性相当严重。调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务。Xxl-job介绍
  官方说明:XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
  通俗来讲:XXL-JOB 是一个任务调度框架,通过引入 XXL-JOB 相关的依赖,按照相关格式撰写代码后,可在其可视化界面进行任务的启动,执行,中止以及包含了日志记录与查询和任务状态监控。
  更多详细介绍推荐阅读官方文档。项目实践Spring Boot集成XXL-JOB
  Spring Boot 集成 XXL-JOB 主要分为以下两步:配置运行调度中心(xxl-job-admin)配置运行执行器项目
  xxl-job-admin 可以从源码仓库中下载代码,代码地址有两个:GitHub:github.com/xuxueli/xxl…Gitee:gitee.com/xuxueli0323…
  下载完之后,在 doc/db 目录下有数据库脚本 tables_xxl_job.sql,执行下脚本初始化调度数据库 xxl_job,如下图所示:
  配置调度中心
  将下载的源码解压,用 IDEA 打开,我们需要修改一下 xxl-job-admin 中的一些配置。(我这里下载的是最新版 2.3.1)
  1、修改 application.properties,主要是配置一下 datasource 以及 email,其他不需要改变。### xxl-job, datasource spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver   ### xxl-job, email spring.mail.host=smtp.qq.com spring.mail.port=25 spring.mail.username=1739468244@qq.com spring.mail.from=1739468244@qq.com # 此处不是邮箱登录密码,而是开启SMTP服务后的授权码 spring.mail.password=xxxxx 复制代码
  2、修改 logback.xml,配置日志输出路径,我是在解压的 xxl-job-2.3.1 项目包中新建了一个 logs 文件夹。 复制代码
  然后启动项目,正常启动后,访问地址为:http://localhost:8080/xxl-job-admin,默认的账户为 admin,密码为 123456,访问后台管理系统后台。
  这样就表示调度中心已经搞定了,下一步就是创建执行器项目。创建执行器项目
  本项目与 Quartz 项目用的业务表和业务逻辑都一样,所以引入的依赖会比较多。环境配置
  1、引入依赖:   org.springframework.boot   spring-boot-starter-parent   2.6.3         1.8   1.2.73   5.5.1   8.0.19   1.4.2.Final   1.18.20   1.1.18   1.6.9           org.springframework.boot     spring-boot-starter-web            org.springframework.boot     spring-boot-starter-aop           com.xuxueli     xxl-job-core     2.3.1            com.baomidou     mybatis-plus-boot-starter     3.5.1           com.baomidou     mybatis-plus     3.5.1           mysql     mysql-connector-java     ${mysql.version}     runtime           com.alibaba     druid-spring-boot-starter     ${druid.version}            org.projectlombok     lombok     1.18.20           com.alibaba.fastjson2     fastjson2     2.0.12           org.mapstruct     mapstruct     ${org.mapstruct.version}           org.mapstruct     mapstruct-processor     ${org.mapstruct.version}           cn.hutool     hutool-all     ${hutool.version}           org.springdoc     springdoc-openapi-ui     ${springdoc.version}                     org.springframework.boot       spring-boot-maven-plugin          复制代码
  2、application.yml 配置文件server:   port: 9090  # xxl-job xxl:   job:     admin:       addresses: http://127.0.0.1:8080/xxl-job-admin # 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;     executor:       appname: hresh-job-executor # 执行器 AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册       ip: # 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";       port: 6666 # ### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;       logpath: /Users/xxx/xxl-job-2.3.1/logs/xxl-job # 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;       logretentiondays: 30 # 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;     accessToken: default_token  # 执行器通讯TOKEN [选填]:非空时启用;  spring:   application:     name: xxl-job-practice   datasource:     type: com.alibaba.druid.pool.DruidDataSource     driver-class-name: com.mysql.cj.jdbc.Driver     url: jdbc:mysql://localhost:3306/xxl_job?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false     username: root     password: root  mybatis:   mapper-locations: classpath:mapper/*Mapper.xml   configuration:     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl     lazy-loading-enabled: true 复制代码
  上述 xxl-job 的 logpath 配置与调度中心的输出日志用的是同一个目录,accessToken 也与调度中心的 xxl.job.accessToken 一致。核心类
  1、xxl-job 配置类@Configuration public class XxlJobConfig {    @Value("${xxl.job.admin.addresses}")   private String adminAddresses;   @Value("${xxl.job.executor.appname}")   private String appName;   @Value("${xxl.job.executor.ip}")   private String ip;   @Value("${xxl.job.executor.port}")   private int port;   @Value("${xxl.job.accessToken}")   private String accessToken;   @Value("${xxl.job.executor.logpath}")   private String logPath;   @Value("${xxl.job.executor.logretentiondays}")   private int logRetentionDays;    @Bean   public XxlJobSpringExecutor xxlJobExecutor() {     // 创建 XxlJobSpringExecutor 执行器     XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();     xxlJobSpringExecutor.setAdminAddresses(adminAddresses);     xxlJobSpringExecutor.setAppname(appName);     xxlJobSpringExecutor.setIp(ip);     xxlJobSpringExecutor.setPort(port);     xxlJobSpringExecutor.setAccessToken(accessToken);     xxlJobSpringExecutor.setLogPath(logPath);     xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);     // 返回     return xxlJobSpringExecutor;   } } 复制代码
  2、xxl-job 工具类@Component @RequiredArgsConstructor public class XxlUtil {    @Value("${xxl.job.admin.addresses}")   private String xxlJobAdminAddress;    private final RestTemplate restTemplate;    // 请求Url   private static final String ADD_INFO_URL = "/jobinfo/addJob";   private static final String REMOVE_INFO_URL = "/jobinfo/removeJob";   private static final String GET_GROUP_ID = "/jobgroup/loadByAppName";    /**    * 添加任务    *    * @param xxlJobInfo    * @param appName    * @return    */   public String addJob(XxlJobInfo xxlJobInfo, String appName) {     Map params = new HashMap<>();     params.put("appName", appName);     String json = JSONUtil.toJsonStr(params);     String result = doPost(xxlJobAdminAddress + GET_GROUP_ID, json);     JSONObject jsonObject = JSON.parseObject(result);     Map map = (Map) jsonObject.get("content");     Integer groupId = (Integer) map.get("id");     xxlJobInfo.setJobGroup(groupId);     String xxlJobInfoJson = JSONUtil.toJsonStr(xxlJobInfo);     return doPost(xxlJobAdminAddress + ADD_INFO_URL, xxlJobInfoJson);   }    // 删除job   public String removeJob(long jobId) {     MultiValueMap map = new LinkedMultiValueMap();     map.add("id", String.valueOf(jobId));     return doPostWithFormData(xxlJobAdminAddress + REMOVE_INFO_URL, map);   }    /**    * 远程调用    *    * @param url    * @param json    */   private String doPost(String url, String json) {     HttpHeaders headers = new HttpHeaders();     headers.setContentType(MediaType.APPLICATION_JSON);     HttpEntity entity = new HttpEntity<>(json, headers);     ResponseEntity responseEntity = restTemplate.postForEntity(url, entity, String.class);     return responseEntity.getBody();   }    private String doPostWithFormData(String url, MultiValueMap map) {     HttpHeaders headers = new HttpHeaders();     headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);     HttpEntity> entity = new HttpEntity<>(map, headers);     ResponseEntity responseEntity = restTemplate.postForEntity(url, entity, String.class);     return responseEntity.getBody();   } } 复制代码
  此处我们利用 RestTemplate 来远程调用 xxl-job-admin 中的服务,从而实现动态创建定时任务,而不是局限于通过 UI 界面来创建任务。
  这里我们用到三个接口,都需要我们在 xxl-job-admin 中手动添加,这样在调用接口时,就不需要登录验证了,这就要求在定义接口时加上一个 PermissionLimit并设置 limit 为 false,那么这样就不用去登录就可以调用接口。
  3、修改 JobGroupController,新增 loadByAppName 方法@RequestMapping("/loadByAppName") @ResponseBody @PermissionLimit(limit = false) public ReturnT loadByAppName(@RequestBody Map map) {   XxlJobGroup jobGroup = xxlJobGroupDao.loadByAppName(map);   return jobGroup != null ? new ReturnT(jobGroup)     : new ReturnT(ReturnT.FAIL_CODE, null); } 复制代码
  XxlJobGroupDao 文件以及对应的 xml 文件XxlJobGroup loadByAppName(Map map); 复制代码	 复制代码
  4、修改 JobInfoController,增加 addJob 方法和 removeJob 方法	@RequestMapping("/addJob") 	@ResponseBody 	@PermissionLimit(limit = false) 	public ReturnT addJob(@RequestBody XxlJobInfo jobInfo) { 		return xxlJobService.add(jobInfo); 	}  	@RequestMapping("/removeJob") 	@ResponseBody 	@PermissionLimit(limit = false) 	public ReturnT removeJob(String id) { 		return xxlJobService.remove(Integer.parseInt(id)); 	} 复制代码
  addJob 方法与 JobInfoController 文件中的 add 方法具体逻辑是一样的,只是换个接口名。	@RequestMapping("/add") 	@ResponseBody 	public ReturnT add(XxlJobInfo jobInfo) { 		return xxlJobService.add(jobInfo); 	} 复制代码
  至此,关于调度中心的修改就结束了。
  5、XxlService 创建任务@Service @Slf4j @RequiredArgsConstructor public class XxlService {    private final XxlUtil xxlUtil;    @Value("${xxl.job.executor.appname}")   private String appName;    public void addJob(XxlJobInfo xxlJobInfo) {     xxlUtil.addJob(xxlJobInfo, appName);     long triggerNextTime = xxlJobInfo.getTriggerNextTime();     log.info("任务已添加,将在{}开始执行任务", DateUtils.formatDate(triggerNextTime));   }  } 复制代码业务代码
  1、UserService,包括用户注册,给用户发送欢迎消息,以及发送天气温度通知。@Service @RequiredArgsConstructor @Slf4j public class UserService {    private final UserMapper userMapper;   private final UserStruct userStruct;   private final WeatherService weatherService;   private final XxlService xxlService;    /**    * 假设有这样一个业务需求,每当有新用户注册,则1分钟后会给用户发送欢迎通知.    *    * @param userRequest 用户请求体    */   @Transactional   public void register(UserRequest userRequest) {     if (Objects.isNull(userRequest) || isBlank(userRequest.getUsername()) ||         isBlank(userRequest.getPassword())) {       BusinessException.fail("账号或密码为空!");     }      User user = userStruct.toUser(userRequest);     userMapper.insert(user);      LocalDateTime scheduleTime = LocalDateTime.now().plusMinutes(1L);      XxlJobInfo xxlJobInfo = XxlJobInfo.builder().jobDesc("定时给用户发送通知").author("hresh")         .scheduleType("CRON").scheduleConf(DateUtils.getCron(scheduleTime)).glueType("BEAN")         .glueType("BEAN")         .executorHandler("sayHelloHandler")         .executorParam(user.getUsername())         .misfireStrategy("DO_NOTHING")         .executorRouteStrategy("FIRST")         .triggerNextTime(DateUtils.toEpochMilli(scheduleTime))         .executorBlockStrategy("SERIAL_EXECUTION").triggerStatus(1).build();      xxlService.addJob(xxlJobInfo);   }     public void sayHelloToUser(String username) {     if (StrUtil.isBlank(username)) {       log.error("用户名为空");     }     User user = userMapper.selectByUserName(username);     String message = "Welcome to Java,I am hresh.";     log.info(user.getUsername() + " , hello, " + message);   }     public void pushWeatherNotification() {     List users = userMapper.queryAll();     log.info("执行发送天气通知给用户的任务…");     WeatherInfo weatherInfo = weatherService.getWeather(WeatherConstant.WU_HAN);     for (User user : users) {       log.info(user.getUsername() + "----" + weatherInfo.toString());     }   } } 复制代码
  2、WeatherService,获取天气温度等信息,这里就不贴代码了。
  3、UserController,只有一个用户注册方法@RestController @RequiredArgsConstructor public class UserController {    private final UserService userService;    @PostMapping("/register")   public Result register(@RequestBody UserRequest userRequest) {     userService.register(userRequest);     return Result.ok();   }  } 复制代码任务处理器
  这里演示两种任务处理器,一种是用于处理 UI 页面创建的任务,另一种是处理代码创建的任务。
  1、DemoHandler,仅用作演示,没什么实际含义。@RequiredArgsConstructor @Slf4j public class DemoHandler extends IJobHandler {    @XxlJob(value = "demoHandler")   @Override   public void execute() throws Exception {     log.info("自动任务" + this.getClass().getSimpleName() + "执行");   } } 复制代码
  2、SayHelloHandler,用户注册后再 xxl-job 上创建一个任务,到时间后就调用该处理器。@Component @RequiredArgsConstructor public class SayHelloHandler {    private final UserService userService;    @XxlJob(value = "sayHelloHandler")   public void execute() {     String param = XxlJobHelper.getJobParam();     userService.sayHelloToUser(param);   } } 复制代码
  在最新版本的 xxl-job 中,任务核心类 "IJobHandler" 的 "execute" 方法取消出入参设计。改为通过 "XxlJobHelper.getJobParam" 获取任务参数并替代方法入参,通过 "XxlJobHelper.handleSuccess/handleFail" 设置任务结果并替代方法出参,示例代码如下@XxlJob("demoJobHandler") public void execute() {   String param = XxlJobHelper.getJobParam();    // 获取参数   XxlJobHelper.handleSuccess();                 // 设置任务结果 } 复制代码
  3、WeatherNotificationHandler,每天定时发送天气通知@Component @RequiredArgsConstructor public class WeatherNotificationHandler extends IJobHandler {    private final UserService userService;    @XxlJob(value = "weatherNotificationHandler")   @Override   public void execute() throws Exception {     userService.pushWeatherNotification();   } } 复制代码测试
  1、首先在执行器管理页面,点击新增按钮,弹出新增框。输入AppName (与application.yml中配置的appname保持一致),名称,注册方式默认自动注册,点击保存。
  2、新增任务
  控制台输出:com.msdn.time.handler.DemoHandler        : 自动任务DemoHandler执行 复制代码
  2、利用 postman 来注册用户
  去 UI 任务管理页面,可以看到代码创建的任务。
  1分钟后,控制台输出如下:
  3、在 UI 任务管理页面手动新增任务,用来发送天气通知。
  点击执行一次,控制台输出如下:
  实际应用中,对于手动创建的任务,直接点击启动就可以了。
  这里还有一个问题,如果每次有新用户注册,都会创建一个定时任务,而且只执行一次,那么任务列表到时候就会有很多脏数据,所以我们在执行完发送欢迎通知后,就要删除。所以我们需要修改一下 SayHelloHandler  @XxlJob(value = "sayHelloHandler")   public void execute() {     String param = XxlJobHelper.getJobParam();     userService.sayHelloToUser(param);      long jobId = XxlJobHelper.getJobId();     xxlUtil.removeJob(jobId);   } 复制代码
  重启项目后,比如说明再创建一个名为 hresh2 的用户,然后任务列表就会新增一个任务。
  等控制台输出 sayHello 后,可以发现任务列表中任务 ID 为 20的记录被删除掉了。问题控制台输出邮件注册错误11:01:48.740 logback [RMI TCP Connection(1)-127.0.0.1] WARN  o.s.b.a.mail.MailHealthIndicator - Mail health check failed javax.mail.AuthenticationFailedException: 535 Login Fail. Please enter your authorization code to login. More information in http://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=1001256 复制代码
  原因:xxl-job-admin 项目的 application.properties 文件中关于 spring.mail.password 的配置不对,可能有人配置了自己邮箱的登录密码。
  解决方案:
  总结
  通过对比 Quartz 和 XXL-JOB 的使用,可以发现后者更易上手,代码侵入不严重,且具备可视化界面。这就是推荐新手使用 XXL-JOB 的原因。
  原文链接:https://juejin.cn/post/7172325382320816165
光启技术前三季度净利同比增长146。98齐金钊中国证券报中证网中证网讯(记者齐金钊)日前,光启技术披露2022年第三季度报告,前三季度,公司实现营收8。35亿元,同比增长117归属于上市公司股东的净利润2。98亿元,同比韩深中国企业参与的重要项目,正极大地刺激着印尼的经济复苏继去年百年大党老外讲故事百集融媒体产品,境内外播放量突破16亿之后,老外讲故事迎来第二季海外员工看中国。100位不同国度的海外员工,用最接地气的方式,讲述自己在中国央企和上海企业海华发股份前三季度营收同比增长14。44销售排名位于行业前列中证网讯(王珞)华发股份10月28日晚间发布2022年三季报,公司前三季度实现营业收入328。08亿元,同比增长14。44实现归属于上市公司股东的净利润21。11亿元,同比增长2。中国楼市,一个最新的重要风向信号深圳发布住房项目租赁参考价这是熊猫贝贝的第1366篇原创文章秋日买房攻略继全国率先推出二手房指导价以后,作为中国城市层面过去两年最具有代表性的房住不炒宏观基调的坚定执行者和模范生,又做出了新的示范动作202NBA最新排名篮网3连败东部倒数第2,国王4战皆负紧随湖人北京时间10月28日,NBA常规赛比赛继续进行。在这个比赛日总共进行了4场比赛,国王队在主场迎战灰熊,勇士主场面对热火,快船在客场挑战雷霆队,篮网主场面对独行侠队。篮网125129毁于名字的4款白酒,只因名字太狂不受待见,行家遇见却都成箱囤在我国,起名字一直都是一件很受重视的事,名字里往往带有很多寓意,一个响亮好听的名字也能给别人带来一个好印象。这点在白酒身上更是如此,我们拿白酒送礼宴请,通常都会用一些包装喜庆,名字双11白酒购买指南,行家力荐5款良心酒,都是100纯粮佳酿今年的双11已经开始预售了,各大电商平台也开始进行预热了,对于很多爱喝酒的朋友来说,这可是一个囤酒的好时机。不知各位酒友,你们是否已选好双十一期间要囤什么酒了呢?有很多酒友很苦恼,人生重要建议先起飞,再调整姿势先起飞,再调整姿势?调整好姿势再起飞?这二者的顺序重要吗?以前我一直认为要调整出我满意的姿势我再出发再起飞等我拍照技术好了我再利用我的拍照技术去赚钱等我写作很厉害了我再认真找个平台漫画暗恋说不出口的喜欢画师吉川流该漫画连载于微博,由漫画作家吉川流创作,漫画名字暗恋。学校要组织义卖活动,小鸟身为小组长要负责整理从同学们那里收集来的物品,清点好东西后实在太累了,就趴在办公室的桌子上睡两性情感常这样回你信息的人,说明他不喜欢你,不要自讨苦吃一个经常这样回复你信息的人,很明显说明他不喜欢你,所以不要自讨苦吃。我们每个人在生活中都会遇到各种各样的人,这个时候,我们需要明确自己的身份。不是每个人都能真心和我们做朋友。尤其是国新健康2022年前三季度亏损1。17亿元中证智能财讯国新健康(000503)10月28日披露2022年第三季度报告。2022年前三季度,公司实现营业总收入1。35亿元,同比增长32。18归母净利润亏损1。17亿元,上年同
聚焦数字健康丨数字化加速赋能,如何打造医疗健康产业转型新高地?21世纪经济报道记者季媛媛上海报道经济学人的文章指出,数字医疗将是下一个万亿美元产业。据中研普华产业研究院数据,全球数字医疗市场规模为2309亿美元(折合人民币约15858亿)。其sql强化演练超详尽(带数据)数据准备创建京东数据库createdatabasejingdongcharsetutf8使用京东数据库usejingdong创建一个商品goods数据表createtablegoo关于vivoX90Pro的硬件配置参数预测,大家期待吗?1vivoX90Pro搭载骁龙8Gen2旗舰处理器,采用台积电4nm的制程工艺,性能再次跃升全球首发三星E6发光材料,支持2K的分辨率,细腻显示,重塑视觉感官,支持高频PWM调光4ApacheDoris介绍ApacheDoris是一个基于MPP架构的高性能实时的分析型数据库,以极速易用的特点被人们所熟知,仅需亚秒级响应时间即可返回海量数据下的查询结果,不仅可以支持高并发的点查询场景,岳麓书会丨向经典致敬国学导师刘希彦解读红楼梦里的大智慧11月19日,向经典致敬红楼梦里的大智慧读书分享活动在乐之书店天心店举行。红网时刻新闻11月20日讯(记者蔡娟通讯员唐睿)首届岳麓书会系列活动举办正酣。11月19日,由岳麓书社主办推门见绿移步入园来源经济日报11月18日,河北省邢台市南和区和阳公园,孩子们在滑轮滑。近年来,南和区建成30多个口袋公园,让市民出门有游园,散步闻花香。赵永辉摄(中经视觉)11月12日,重庆市万盛提升露营品质,sanag塞那M13SPro音箱,更小巧更动听随着户外露营陆冲等各类户外运动的兴起,现在越来越多的人投入到户外运动当中去了。而且户外运动确实趣味性很高。而在户外玩的同时,如果有一台合适的户外音箱,那绝对是可以提升整体娱乐性的。北约5艘航母叫阵,俄巡洋舰撂担子跑路,战斗民族名不副实文君剑俄罗斯在乌克兰的军事行动正陷入旷日持久的状态,由于在地中海与美军航母对峙太久,俄军瓦良格号巡洋舰带领的远洋舰队近期不得不返航,日本自卫队声称已经发现该舰队进入东海。(自卫队跟宝马被折翼他说还好跑得快10月26号,小方把一辆宝马车停在杭州一家酒店的机械车库里。他说离开时,刚打开车门,边上的车位就开始上升。宝马停车被折翼车主还好跑得快,不然至少骨折小方这是我当时停好拍的照,我走的原神推荐四个超强幻神角色,输出爆发力强劲,平均伤害突破10万众所周知,原神中的角色定位有所不同,有近战有法师有射手,玩家可以根据自己的喜爱选择更合适的角色。其中近战角色更受玩家喜欢,因为操作简单爆发力足,一般都是主C为主。在众多近战角色中也李健吾先生的两封信读杨绛传(赵彤彤著)知道钱锺书伉俪有位挚友,乃李健吾。钱锺书的围城问世后,李健吾看了,惊喜地说这个做学问的书虫子怎么写起了小说呢?而且是一部讽世之作,一部新儒林外史!此评价谑称其名