在项目推进中,如果说第一件事是搭Spring框架的话,那么第二件事情就是在Sring基础上搭建日志框架,我想很多人都知道日志对于一个项目的重要性,尤其是线上Web项目,因为日志可能是我们了解应用如何执行的唯一方式。在18年大环境下,更多的企业使用Springboot和Springcloud来搭建他们的企业微服务项目,此篇文章是博主在实践中用Springboot整合log4j2日志的总结。 目录 Springboot整合log4j2日志全解 常用日志框架 日志门面slf4j 为什么选用log4j2 整合步骤 引入Jar包 配置文件 配置文件模版 配置参数简介 Log4j2配置详解 简单使用 使用lombok工具简化创建Logger类 参考文章 1|0Springboot整合log4j2日志全解 1|1常用日志框架 java.util.logging:是JDK在1.4版本中引入的Java原生日志框架 Log4j:Apache的一个开源项目,可以控制日志信息输送的目的地是控制台、文件、GUI组件等,可以控制每一条日志的输出格式,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。虽然已经停止维护了,但目前绝大部分企业都是用的log4j。 LogBack:是Log4j的一个改良版本 Log4j2:Log4j2已经不仅仅是Log4j的一个升级版本了,它从头到尾都被重写了 1|2日志门面slf4j 上述介绍的是一些日志框架的实现,这里我们需要用日志门面来解决系统与日志实现框架的耦合性。SLF4J,即简单日志门面(Simple Logging Facade for Java),它不是一个真正的日志实现,而是一个抽象层( abstraction layer),它允许你在后台使用任意一个日志实现。 前面介绍的几种日志框架一样,每一种日志框架都有自己单独的API,要使用对应的框架就要使用其对应的API,这就大大的增加应用程序代码对于日志框架的耦合性。 使用了slf4j后,对于应用程序来说,无论底层的日志框架如何变,应用程序不需要修改任意一行代码,就可以直接上线了。 1|3为什么选用log4j2 相比与其他的日志系统,log4j2丢数据这种情况少;disruptor技术,在多线程环境下,性能高于logback等10倍以上;利用jdk1.5并发的特性,减少了死锁的发生; 在这列举一下一些网上其他博文中对它们的性能评测: 可以看到在同步日志模式下, Logback的性能是最糟糕的. log4j2的性能无论在同步日志模式还是异步日志模式下都是最佳的. log4j2优越的性能其原因在于log4j2使用了LMAX,一个无锁的线程间通信库代替了,logback和log4j之前的队列. 并发性能大大提升。 1|4整合步骤 引入Jar包 springboot默认是用logback的日志框架的,所以需要排除logback,不然会出现jar依赖冲突的报错。 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions><!-- 去掉springboot默认配置 --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <!-- 引入log4j2依赖 --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> 配置文件 如果自定义了文件名,需要在application.yml中配置 logging: config: xxxx.xml level: cn.jay.repository: trace 默认名log4j2-spring.xml,就省下了在application.yml中配置 配置文件模版 log4j是通过一个.properties的文件作为主配置文件的,而现在的log4j2则已经弃用了这种方式,采用的是.xml,.json或者.jsn这种方式来做,可能这也是技术发展的一个必然性,因为properties文件的可阅读性真的是有点差。这里给出博主自配的一个模版,供大家参考。 <?xml version="1.0" encoding="UTF-8"?> <!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出--> <!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数--> <configuration monitorInterval="5"> <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN […]
View Details一、日志简介 1.1 日志是什么(WHAT) 日志:记录程序的运行轨迹,方便查找关键信息,也方便快速定位解决问题。 通常,Java程序员在开发项目时都是依赖Eclipse/IDEA等集成开发工具的Debug 调试功能来跟踪解决Bug,但项目发布到了测试、生产环境怎么办?你有可能会说可以使用远程调试,但实际并不能允许让你这么做。 所以,日志的作用就是在测试、生产环境没有 Debug 调试工具时开发和测试人员定位问题的手段。日志打得好,就能根据日志的轨迹快速定位并解决线上问题,反之,日志输出不好,不仅无法辅助定位问题反而可能会影响到程序的运行性能和稳定性。 很多介绍 AOP 的地方都采用日志来作为介绍,实际上日志要采用切面的话是极其不科学的!对于日志来说,只是在方法开始、结束、异常时输出一些什么,那是绝对不够的,这样的日志对于日志分析没有任何意义。如果在方法的开始和结束整个日志,那方法中呢?如果方法中没有日志的话,那就完全失去了日志的意义!如果应用出现问题要查找由什么原因造成的,也没有什么作用。这样的日志还不如不用! 1.2 日志有什么用(WHY) 不管是使用何种编程语言,日志输出几乎无处不再。总结起来,日志大致有以下几种用途: 问题追踪:辅助排查和定位线上问题,优化程序运行性能。 状态监控:通过日志分析,可以监控系统的运行状态。 安全审计:审计主要体现在安全上,可以发现非授权的操作。 1.3 总结 日志在应用程序中是非常非常重要的,好的日志信息能有助于我们在程序出现 BUG 时能快速进行定位,并能找出其中的原因。 作为一个有修养的程序猿,对日志这个东西应当引起足够的重视。 二、日志框架(HOW) 2.1 常用的日志框架 log4j、Logging、commons-logging、slf4j、logback,开发的同学对这几个日志相关的技术不陌生吧,为什么有这么多日志技术,它们都是什么区别和联系呢?且看下文分解: 2.1.1 Logging 这是 Java 自带的日志工具类,在 JDK 1.5 开始就已经有了,在 java.util.logging 包下。通常情况下,这个基本没什么人用了,了解一下就行。 2.1.2 commons-logging commons-logging 是日志的门面接口,它也是Apache 最早提供的日志门面接口,用户可以根据喜好选择不同的日志实现框架,而不必改动日志定义,这就是日志门面的好处,符合面对接口抽象编程。现在已经不太流行了,了解一下就行。 2.1.3 Slf4j slf4j,英文全称为“Simple Logging Facade for Java”,为java提供的简单日志Facade。Facade门面,更底层一点说就是接口。它允许用户以自己的喜好,在工程中通过slf4j接入不同的日志系统。 因此slf4j入口就是众多接口的集合,它不负责具体的日志实现,只在编译时负责寻找合适的日志系统进行绑定。具体有哪些接口,全部都定义在slf4j-api中。查看slf4j-api源码就可以发现,里面除了public final class LoggerFactory类之外,都是接口定义。因此slf4j-api本质就是一个接口定义。 2.1.4 Log4j Log4j 是 Apache 的一个开源日志框架,也是市场占有率最多的一个框架。 注意:log4j 在 2015.08.05 这一天被 Apache 宣布停止维护了,用户需要切换到 Log4j2上面去。 下面是官宣原文: On August 5, 2015 the Logging Services Project Management Committee announced that Log4j 1.x had reached end of life. For […]
View Details1.SLF4J(Simple logging Facade for Java) 意思为简单日志门面,它是把不同的日志系统的实现进行了具体的抽象化,只提供了统一的日志使用接口,使用时只需要按照其提供的接口方法进行调用即可,由于它只是一个接口,并不是一个具体的可以直接单独使用的日志框架,所以最终日志的格式、记录级别、输出方式等都要通过接口绑定的具体的日志系统来实现,这些具体的日志系统就有log4j,logback,java.util.logging等,它们才实现了具体的日志系统的功能。 如何使用SLF4J? 既然SLF4J只是一个接口,那么实际使用时必须要结合具体的日志系统来使用,我们首先来看SLF4J和各个具体的日志系统进行绑定时的框架原理图: 其实slf4j原理很简单,他只提供一个核心slf4j api(就是slf4j-api.jar包),这个包只有日志的接口,并没有实现,所以如果要使用就得再给它提供一个实现了些接口的日志包,比 如:log4j,common logging,jdk log日志实现包等,但是这些日志实现又不能通过接口直接调用,实现上他们根本就和slf4j-api不一致,因此slf4j又增加了一层来转换各日志实现包的使 用,当然slf4j-simple除外。其结构如下: slf4j-api(接口层) | 各日志实现包的连接层( slf4j-jdk14, slf4j-log4j) | 各日志实现包 所以,结合各日志实现包使用时提供的jar包情况为: SLF4J和logback结合使用时需要提供的jar:slf4j-api.jar,logback-classic.jar,logback-core.jar SLF4J和log4j结合使用时需要提供的jar:slf4j-api.jar,slf4j-log412.jar,log4j.jar SLF4J和JDK中java.util.logging结合使用时需要提供的jar:slf4j-api.jar,slf4j-jdk14.jar SLF4J和simple(SLF4J本身提供的一个接口的简单实现)结合使用时需要提供的jar:slf4j-api.jar,slf4j-simple.jar 当然还有其他的日志实现包,以上是经常会使用到的一些。 注意,以上slf4j和各日志实现包结合使用时最好只使用一种结合,不然的话会提示重复绑定日志,并且会导致日志无法输出。 slf4j-api.jar:对外提供统一的日志调用接口,该接口具体提供的调用方式和方法举例说明: public class Test { private static final Logger logger = LoggerFactory.getLogger(Tester.class); //通过LoggerFactory获取Logger实例 public static void main(String[] args) { //接口里的统一的调用方法,各具体的日志系统都有实现这些方法 logger.info("testlog: {}", "test"); logger.debug("testlog: {}", "test"); logger.error("testlog: {}", "test"); logger.trace("testlog: {}", "test"); logger.warn("testlog: {}", "test"); } } 如果系统中之前已经使用了log4j做日志输出,想使用slf4j作为统一的日志输出,该怎么办呢? 如果之前系统中是单独使用log4j做为日志输出的,这时再想使用slf4j做为日志输出时,如果系统中日志比较多,此时更改日志输出方法肯定是不太现实的,这个时候就可以使用log4j-over-slf4j.jar将使用log4j日志框架输出的日志路由到slf4j上来统一采用slf4j来输出日志。 为什么要使用SLF4J? slf4j是一个日志接口,自己没有具体实现日志系统,只提供了一组标准的调用api,这样将调用和具体的日志实现分离,使用slf4j后有利于根据自己实际的需求更换具体的日志系统,比如,之前使用的具体的日志系统为log4j,想更换为logback时,只需要删除log4j相关的jar,然后加入logback相关的jar和日志配置文件即可,而不需要改动具体的日志输出方法,试想如果没有采用这种方式,当你的系统中日志输出有成千上万条时,你要更换日志系统将是多么庞大的一项工程。如果你开发的是一个面向公众使用的组件或公共服务模块,那么一定要使用slf4的这种形式,这有利于别人在调用你的模块时保持和他系统中使用统一的日志输出。 slf4j日志输出时可以使用{}占位符,如,logger.info("testlog: {}", "test"),而如果只使用log4j做日志输出时,只能以logger.info("testlog:"+"test")这种形式,前者要比后者在性能上更好,后者采用+连接字符串时就是new 一个String 字符串,在性能上就不如前者。 2.log4j(log for java) Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。 如何使用? 引入jar,使用log4j时需要的jar为:log4j.jar。 定义配置文件log4j.properties或log4j.xml 在具体的类中进行使用: 在需要日志输出的类中加入:private static final Logger logger = Logger.getLogger(Tester.class); //通过Logger获取Logger实例 在需要输出日志的地方调用相应方法即可:logger.debug(“System […]
View Details一、说明 如果是使用slf4j规范的,请先引用:
1 2 3 4 5 6 |
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.11.0</version> </dependency> |
二、测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LogTest { @Test public void test() { Logger log = LoggerFactory.getLogger(LogTest.class); log.warn("this is message {}", 1); Exception ex = new Exception("this is a message."); log.error("a new exeception", ex); log.trace("trace message."); log.info("info message."); for (int i = 0; i < 120000; i++) log.debug("debug message:{}={}", "line", i); } } |
三、配置 在Maven项目的resources目录下,或者Java项目的src下,新建log4j2.xml文件。这里要注意,如果是使用的log4j1版本,请添加log4j.properties文件并配置,但是在log4j2中已经废弃了log4j.properties文件的使用,使用的是log4j2.xml。参考如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="UTF-8"?> <configuration status="ON"> <appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> <RollingFile name="RollingFile" fileName="logs/app.log" filePattern="logs/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz"> <PatternLayout pattern="%d{yyyy.MM.dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/> <SizeBasedTriggeringPolicy size="5 MB" /> </RollingFile> </appenders> <loggers> <root level="DEBUG"> <appender-ref ref="Console"/> <appender-ref ref="RollingFile"/> </root> </loggers> </configuration> |
说明:上面配置的最低Level为DEBUG,同时使用了Console和RollingFile两种输出方式。所以输出的日志,一方面输出到控制台,一方面输出到logs/app.log中,当app.log达到5MB时,就会自动生成gz压缩文件到/logs下,并清空app.log,继续输出到app.log,以此循环。 四、运行并输出日志 宋兴柱(Sindrol):转载内容,请标明出处,谢谢!源文来自 宝贝云知识分享:https://www.dearcloud.cn from:https://www.cnblogs.com/songxingzhu/p/8867817.html
View DetailsMap 接口中键和值一一映射. 可以通过键来获取值。 给定一个键和一个值,你可以将该值存储在一个 Map 对象。之后,你可以通过键来访问对应的值。 当访问的值不存在的时候,方法就会抛出一个 NoSuchElementException 异常。 当对象的类型和 Map 里元素类型不兼容的时候,就会抛出一个 ClassCastException 异常。 当在不允许使用 Null 对象的 Map 中使用 Null 对象,会抛出一个 NullPointerException 异常。 当尝试修改一个只读的 Map 时,会抛出一个 UnsupportedOperationException 异常。 序号 方法描述 1 void clear( ) 从此映射中移除所有映射关系(可选操作)。 2 boolean containsKey(Object k) 如果此映射包含指定键的映射关系,则返回 true。 3 boolean containsValue(Object v) 如果此映射将一个或多个键映射到指定值,则返回 true。 4 Set entrySet( ) 返回此映射中包含的映射关系的 Set 视图。 5 boolean equals(Object obj) 比较指定的对象与此映射是否相等。 6 Object get(Object k) 返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。 7 int hashCode( ) 返回此映射的哈希码值。 8 boolean isEmpty( ) 如果此映射未包含键-值映射关系,则返回 true。 9 Set keySet( ) 返回此映射中包含的键的 Set 视图。 10 Object put(Object k, Object v) 将指定的值与此映射中的指定键关联(可选操作)。 11 void putAll(Map m) […]
View Details版权声明:这可是本菇凉辛辛苦苦原创的,转载请一定带上我家地址,不要忘记了哈 . https://blog.csdn.net/u011314442/article/details/90140532
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import tk.mybatis.mapper.entity.Example; import com.github.pagehelper.PageHelper; ... @Override public List<RepaymentPlan> listRepaymentPlan(Integer start) { Example example = new Example(RepaymentPlan.class); // 排序 example.orderBy("id"); // 条件查询 example.createCriteria() .andNotEqualTo("repayStatus", 3) .andLessThanOrEqualTo("shouldRepayDate", new Date()); // 分页 PageHelper.startPage(start, 20); // 每次查询20条 return repaymentPlanMapper.selectByExample(example); } |
2. PageHelper 使用详解见文章:分页插件pageHelpler的使用(ssm框架中)服务器端分页 3. 更多关于 Example 的使用说明见文章: java 查询功能实现的八种方式 MyBatis : Mapper 接口以及 Example 使用实例、详解 4. 当只是查询数据,不需要返回总条数时可选择此方法:
1 |
PageHelper.startPage(第几页, 20,false); // 每次查询20条 |
当数据量极大时,可以快速查询,忽略总条数的查询,减少查询时间。 以下是该方法原码实现: ————————————————- 2019.5.13 后记 : 1)分页的写法 下图中黄框中的写法运行 比红框中 快,不知道是不是插件本身也会有费时: 2)再补充一种分页方式,mybatis 自带的 RowBounds:
1 2 3 4 5 6 7 8 9 10 |
public List<RepayPlan> listRepayPlan(int start) { // 查询所有未还款结清且应还日期小于当前时间的账单 Example example = new Example(RepayPlan.class); example.orderBy("id "); // 按id排序 example.createCriteria() .andNotEqualTo("repayStatus", 3) .andLessThanOrEqualTo("shouldRepayDate", new Date()); RowBounds rowBounds = new RowBounds(start, 20); // 每次查询20条 return epaymentPlanMapper.selectByExampleAndRowBounds(example,rowBounds); } |
推荐用 RowBounds :mybatis 自带的,且速度快 。个人运行,后 2 种分页明显比 PageHelper 快。 from:https://cloud.tencent.com/developer/article/1433161
View Details在开发过程中遇到一个问题,服务器经过排序返回后的字符串数据使用fastjson解析后,数据顺序发生变化,引起业务异常。 解决办法: 1、解析时增加参数不调整顺序 JSONObject respondeBodyJson = JSONObject.parseObject(jsonStr, Feature.OrderedField); 2、初始化json对象为有序对象: JSONObject retObj = new JSONObject(true); 这样生成的json对象就与放入数据时一致。 3、使用Gson解析 JsonObject returnData = new JsonParser().parse(replyString).getAsJsonObject(); ———————————————— 版权声明:本文为CSDN博主「long2010110」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/long2010110/article/details/81537820
View Details场景 两个不同的类,其中一部分的属性相同。 要把其中一个对象的一些属性赋值给另一个对象。 最原始的方式是依次调用两个对象的set和get方法,挨个赋值。 但是spring提供了BanUtils的方法copyPrpperties可以实现。 注: 博客: https://blog.csdn.net/badao_liumang_qizhi 关注公众号 霸道的程序猿 获取编程相关电子书、教程推送与免费下载。 实现 引入包
1 |
import org.springframework.beans.BeanUtils; |
然后调用
1 |
BeanUtils.copyProperties(kqDkszJl,kqDksz); |
其中左边是要取值的对象,右边是要赋值的对象。 博客园: https://www.cnblogs.com/badaoliumangqizhi/ 关注公众号 霸道的程序猿 获取编程相关电子书、教程推送与免费下载。 from:https://www.cnblogs.com/badaoliumangqizhi/p/13786384.html
View Details转换字符串示例:
1 2 |
String array2 = "{'i':'2','b':'3'}"; JSONObject parseObject = JSON.parseObject(array2); |
结果:
1 |
{"b":"3","i":"2"} |
我们会发现顺序与原来的字符串顺序不一致。 通过DEBUG去com.alibaba.fastjson.parser.DefaultJSONParser的下述方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
public Object parse(Object fieldName) { final JSONLexer lexer = this.lexer; switch (lexer.token()) { case SET: lexer.nextToken(); HashSet<Object> set = new HashSet<Object>(); parseArray(set, fieldName); return set; case TREE_SET: lexer.nextToken(); TreeSet<Object> treeSet = new TreeSet<Object>(); parseArray(treeSet, fieldName); return treeSet; case LBRACKET: JSONArray array = new JSONArray(); parseArray(array, fieldName); if (lexer.isEnabled(Feature.UseObjectArray)) { return array.toArray(); } return array; case LBRACE: //重点就是此行的lexer.isEnabled(Feature.OrderedField)=false JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return parseObject(object, fieldName); // case LBRACE: { // Map<String, Object> map = lexer.isEnabled(Feature.OrderedField) // ? new LinkedHashMap<String, Object>() // : new HashMap<String, Object>(); // Object obj = parseObject(map, fieldName); // if (obj != map) { // return obj; // } // return new JSONObject(map); // } case LITERAL_INT: Number intValue = lexer.integerValue(); lexer.nextToken(); return intValue; case LITERAL_FLOAT: Object value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal)); lexer.nextToken(); return value; |
重点就是此行的lexer.isEnabled(Feature.OrderedField)=false,打开JSONObject的源码构造方法可以发现当ordered参数值为false时使用的是HashMap存放数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public JSONObject(int initialCapacity, boolean ordered){ if (ordered) { map = new LinkedHashMap<String, Object>(initialCapacity); } else { map = new HashMap<String, Object>(initialCapacity); } } |
hashmap是数组加链表结构,根据key的hash算法确定在数组中的位置,当发生hash冲突的时候,根据二叉树或者红黑树构成链表。所以是有序的,key确定,位置也就确定了。 如果要实现转换前的数据顺序与转换后的数据顺序一致,可以使用如下方式:
1 2 3 |
String array2 = "{'i':'2','b':'3'}"; JSONObject parseObject = JSON.parseObject(array2, Feature.OrderedField); |
此时会使用LinkedHashMap,LinkedHashMap的内部维持了一个双向链表,保存了数据的插入顺序,遍历时,先得到的数据便是先插入的。 from:https://blog.csdn.net/h363659487/article/details/103880710
View Detailsspring-boot继承mybatis启动时,警告如下: 2018-09-10 15:00:14.721 WARN tk.mybatis.spring.mapper.ClassPathMapperScanner --No MyBatis mapper was found in '[com.kevin]' package. Please check your configuration. 使用的tk的开源项目进行mybatis集成,百度了很多解决方案,最终看到一位前辈介绍:doScan()会扫描启动类同级目录下的mapper接口,但是合理的目录结果绝对不允许所有的mapper都在启动类目录下,所以在启动类目录下添加了一个伪mapper,如下: 再重新启动服务,就不会出现如上warn信息了…… from:https://my.oschina.net/kevin2kelly/blog/2046324 ========================================================================================= 光子:通过以上兄弟的方法,我的警告确实没有了。但经过dev-tools热启动后还是会提醒: 无法获取实体类com.w3cnet.doctoradvice.entity.HisvMzbrJzxx对应的表名 …… 还需要安装一下:Mapper Spring Boot Starter ,贴上地址:
1 2 |
// https://mvnrepository.com/artifact/tk.mybatis/mapper-spring-boot-starter compile group: 'tk.mybatis', name: 'mapper-spring-boot-starter', version: '2.1.5' |
折腾了几次,终于完美解决…… 参考:https://blog.csdn.net/zwrlj527/article/details/91824220
View Details