数据源配置:
#datasource #Introductions: https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8 #https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_DruidDataSource%E5%8F%82%E8%80%83%E9%85%8D%E7%BD%AE #初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 default:0 spring.datasource.druid.initial-size=2 #最大连接池数量。default=8+ spring.datasource.druid.max-active=20 #最小连接池数量。maxIdle已经废弃 spring.datasource.druid.min-idle=10 #获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁 spring.datasource.druid.max-wait=60000 #是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。default=false spring.datasource.druid.pool-prepared-statements=false #要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,会存在Oracle下PSCache占用内存过多的问题,可以把这个数据配置大一些,比如100.default=-1 spring.datasource.druid.max-pool-prepared-statement-per-connection-size=-1 #用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow,testOnBorrow,testOnReturn,testWhileIdle都不会起作用。这个可以不配置 #spring.datasource.druid.validation-query=select 'x' #单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void. setQueryTImeout(int seconds)方法,mysql实现的不是很合理,不建议在mysql下配置此参数 #spring.datasource.druid.validation-query-timeout=60 #申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。default=true spring.datasource.druid.test-on-borrow=false #归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。default=false spring.datasource.druid.test-on-return=false #建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。default=false spring.datasource.druid.test-while-idle=true #连接池中的minIdle数据以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。default=false spring.datasource.druid.keep-alive=true #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 default=1分钟 #有两个含义: # (1)Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接 # (2)testWhileIdle的判断依据,详细看testWhileIdle属性的说明 spring.datasource.druid.time-between-eviction-runs-millis=60000 #池中的连接保持空闲而不被驱逐的最小时间,单位是毫秒 spring.datasource.druid.min-evictable-idle-time-millis=300000 #合并多个DruidDataSource的监控数据 spring.datasource.druid.use-global-data-source-stat=true #spring.datasource.druid.filters=#配置多个英文逗号分隔 # 配置StatFilter spring.datasource.druid.filter.stat.enabled=true spring.datasource.druid.filter.stat.db-type=mysql spring.datasource.druid.filter.stat.log-slow-sql=true spring.datasource.druid.filter.stat.slow-sql-millis=2000 spring.datasource.druid.filter.stat.merge-sql=true # 配置WallFilter spring.datasource.druid.filter.wall.enabled=true spring.datasource.druid.filter.wall.db-type=mysql spring.datasource.druid.filter.wall.config.delete-allow=false spring.datasource.druid.filter.wall.config.drop-table-allow=false spring.datasource.druid.filter.wall.config.create-table-allow=false spring.datasource.druid.filter.wall.config.alter-table-allow=false spring.datasource.druid.filter.wall.config.truncate-allow=false
4. SQL合并配置
当你程序中存在没有参数化的sql执行时,sql统计的效果会不好。比如:
select * from t where id = 1 select * from t where id = 2 select * from t where id = 3
在统计中,显示为3条sql,这不是我们希望要的效果。StatFilter提供合并的功能,能够将这3个SQL合并为如下的SQL
select * from t where id = ?
配置StatFilter的mergeSql属性
<bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter"> <property name="mergeSql" value="true" /> </bean>
https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatFilter
spring.datasource.druid.filters=config,stat,wall,log4j 配置监控统计拦截的filters,去掉后监控界面SQL无法进行统计,’wall’用于防火墙
WebStatFilter用于采集web-jdbc关联监控的数据。【只是提供一个展示界面,及展示界面本身按一定维度统计的数据】
用来展示已经统计到的监控数据
https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_%E9%85%8D%E7%BD%AEWebStatFilter
监控配置【展示一些Filter已经存放的数据】,主要决定怎么展示
# WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter spring.datasource.druid.web-stat-filter.enabled= #是否启用StatFilter默认值false spring.datasource.druid.web-stat-filter.url-pattern= spring.datasource.druid.web-stat-filter.exclusions= spring.datasource.druid.web-stat-filter.session-stat-enable= spring.datasource.druid.web-stat-filter.session-stat-max-count= spring.datasource.druid.web-stat-filter.principal-session-name= spring.datasource.druid.web-stat-filter.principal-cookie-name= spring.datasource.druid.web-stat-filter.profile-enable= # StatViewServlet配置,说明请参考Druid Wiki,配置_StatViewServlet配置 spring.datasource.druid.stat-view-servlet.enabled= #是否启用StatViewServlet(监控页面)默认值为false(考虑到安全问题默认并未启动,如需启用建议设置密码或白名单以保障安全) spring.datasource.druid.stat-view-servlet.url-pattern= spring.datasource.druid.stat-view-servlet.reset-enable= spring.datasource.druid.stat-view-servlet.login-username= spring.datasource.druid.stat-view-servlet.login-password= spring.datasource.druid.stat-view-servlet.allow= spring.datasource.druid.stat-view-servlet.deny= # Spring监控配置,说明请参考Druid Github Wiki,配置_Druid和Spring关联监控配置 spring.datasource.druid.aop-patterns= # Spring监控AOP切入点,如x.y.z.service.*,配置多个英文逗号分隔
如何配置 Filter
你可以通过 spring.datasource.druid.filters=stat,wall,log4j ...
的方式来启用相应的内置Filter,不过这些Filter都是默认配置。如果默认配置不能满足你的需求,你可以放弃这种方式,通过配置文件来配置Filter,下面是例子。
# 配置StatFilter
spring.datasource.druid.filter.stat.enabled=true
spring.datasource.druid.filter.stat.db-type=h2
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=2000
# 配置WallFilter
spring.datasource.druid.filter.wall.enabled=true
spring.datasource.druid.filter.wall.db-type=h2
spring.datasource.druid.filter.wall.config.delete-allow=false
spring.datasource.druid.filter.wall.config.drop-table-allow=false
# 其他 Filter 配置不再演示
目前为以下 Filter 提供了配置支持,请参考文档或者根据IDE提示(spring.datasource.druid.filter.*
)进行配置。
- StatFilter
- WallFilter
- ConfigFilter
- EncodingConvertFilter
- Slf4jLogFilter
- Log4jFilter
- Log4j2Filter
- CommonsLogFilter
要想使自定义 Filter 配置生效需要将对应 Filter 的 enabled
设置为 true
,Druid Spring Boot Starter 默认禁用 StatFilter,你也可以将其 enabled
设置为 true
来启用它。
https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
http://120.26.192.168/druid/datasource.html
如果你用druid,看监控数据库,NotEmptyWaitCount数量多,或者可以考虑加大连接池的MaxActive数量。
数据库处理不过来导致execute queryTimeout了
有一个Druid连接池的错误,错误信息是这样的“The last packet successfully received from the server was 3,984,663 milliseconds ago. The last packet sent successfully to the server was 37 milliseconds ago.”,连接池缺省配置的testWhileIdle,按理来说不应该发生这样的错误。阿里应用一直没问题,但是外部的用户偶尔零星反馈过来,多年来都是如此,我一直很担心。
最近几天有一个阿里云的用户遇到这样的问题,直接通过阿里云客服找过来支持,经过多次在对方生产环境测试验证,终于找到原因,是用法的问题,不是Druid的问题,确认了这个之后,多年的担心可放下了。
问题是这样的,申请连接后间隔长时间再createStatememt执行Sql,而testWhileIdle只在getConnectin()时发挥作用。代码示例如下:
Connection conn = druidDataSource.getConnection();
// 这里做耗时很长的事情,比如一个Hive大任务,跑数个小时
Statement stmt = conn.createStatement();
stmt.execute(); // 这里抛错
Druid内置提供了四种LogFilter(Log4jFilter、Log4j2Filter、CommonsLogFilter、Slf4jLogFilter),用于输出JDBC执行的日志。这些Filter都是Filter-Chain扩展机制中的Filter,所以配置方式可以参考这里:Filter配置
1. 别名映射
在druid-xxx.jar!/META-INF/druid-filter.properties文件中描述了这四种Filter的别名
druid.filters.log4j=com.alibaba.druid.filter.logging.Log4jFilter druid.filters.log4j2=com.alibaba.druid.filter.logging.Log4j2Filter druid.filters.slf4j=com.alibaba.druid.filter.logging.Slf4jLogFilter druid.filters.commonlogging=com.alibaba.druid.filter.logging.CommonsLogFilter druid.filters.commonLogging=com.alibaba.druid.filter.logging.CommonsLogFilter
他们的别名分别是log4j、log4j2、slf4j、commonlogging和commonLogging。其中commonlogging和commonLogging只是大小写不同。
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> ... ... <property name="filters" value="stat,log4j" /> </bean>
2. loggerName配置
LogFilter都是缺省使用四种不同的Logger执行输出,看实现代码:
public abstract class LogFilter { protected String dataSourceLoggerName = "druid.sql.DataSource"; protected String connectionLoggerName = "druid.sql.Connection"; protected String statementLoggerName = "druid.sql.Statement"; protected String resultSetLoggerName = "druid.sql.ResultSet"; }
你可以根据你的需要修改,在log4j.properties文件上做配置时,注意配置使用相关的logger。
2. 配置输出日志
缺省输入的日志信息全面,但是内容比较多,有时候我们需要定制化配置日志输出。
<bean id="log-filter" class="com.alibaba.druid.filter.logging.Log4jFilter"> <property name="resultSetLogEnabled" value="false" /> </bean> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> ... <property name="proxyFilters"> <list> <ref bean="log-filter"/> </list> </property> </bean>
参数 | 说明 |
dataSourceLogEnabled | 所有DataSource相关的日志 |
connectionLogEnabled | 所有连接相关的日志 |
connectionLogErrorEnabled | 所有连接上发生异常的日志 |
statementLogEnabled | 所有Statement相关的日志 |
statementLogErrorEnabled | 所有Statement发生异常的日志 |
resultSetLogEnabled | |
resultSetLogErrorEnabled | |
connectionConnectBeforeLogEnabled | |
connectionConnectAfterLogEnabled | |
connectionCommitAfterLogEnabled | |
connectionRollbackAfterLogEnabled | |
connectionCloseAfterLogEnabled | |
statementCreateAfterLogEnabled | |
statementPrepareAfterLogEnabled | |
statementPrepareCallAfterLogEnabled | |
statementExecuteAfterLogEnabled | |
statementExecuteQueryAfterLogEnabled | |
statementExecuteUpdateAfterLogEnabled | |
statementExecuteBatchAfterLogEnabled | |
statementCloseAfterLogEnabled | |
statementParameterSetLogEnabled | |
resultSetNextAfterLogEnabled | |
resultSetOpenAfterLogEnabled | |
resultSetCloseAfterLogEnabled |
4. log4j.properties配置
如果你使用log4j,可以通过log4j.properties文件配置日志输出选项,例如:
log4j.logger.druid.sql=warn,stdout log4j.logger.druid.sql.DataSource=warn,stdout log4j.logger.druid.sql.Connection=warn,stdout log4j.logger.druid.sql.Statement=warn,stdout log4j.logger.druid.sql.ResultSet=warn,stdout
5. 输出可执行的SQL
Java启动参数配置方式
-Ddruid.log.stmt.executableSql=true
logFilter参数直接配置
<bean id="log-filter" class="com.alibaba.druid.filter.logging.Log4jFilter"> <property name="statementExecutableSqlLogEnable" value="true" /> </bean>
https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_LogFilter
MySql+Mybatis+Druid:sql injection violation, multi-statement not allow
做一个批量update的操作 ,sqlmap如下:
<update id="updateBatch" parameterType="java.util.List"> <foreach collection="list" item="item" index="index" open="" close="" separator=";"> update device_bd_token <set> access_token=#{item.accessToken} </set> where device_id = #{item.deviceId} </foreach> </update>
刚开始以为是连接数据库的url上没有加上支持批量的参数,然后就改了下:
jdbc.url=jdbc:mysql://192.168.11.107:3306/alarm_db?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
结果还是同样的错误!但是在命令行直接执行又是没问题的,这就很奇怪了!
仔细看日志,好像是Druid的WallFilter.check()抛出来的,那就是说是Druid在做预编译的时候,给抛出的异常,还没有到mysql的服务器。
最终的解决办法是这样的:
<bean id="dataSourceOne" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="proxyFilters"> <list> <ref bean="stat-filter" /> <ref bean="wall-filter"/> </list> </property> </bean>
<bean id="wall-filter" class="com.alibaba.druid.wall.WallFilter"> <property name="config" ref="wall-config" /> </bean> <bean id="wall-config" class="com.alibaba.druid.wall.WallConfig"> <property name="multiStatementAllow" value="true" /> </bean>
配置一个multiStatementAllow参数就可以了。
看下源码的处理:
也就是说,只要把config的multiStatementAllow设置为true就可以避免出现这样的错误了!
Druid配置的时候还有一个大坑就是,不要同时配置filters和proxyFilters,filter都是内置的,想通过proxyFilters来定制的话,就不要配置filters。
DruidDataSource继承了DruidAbstractDataSource,
可以看出来,既可以配置filters,也可以配置proxyFilters,不同的是,filters是字符串别名,proxyFilters是类。
我们继续看一下这些字符串的值应该是啥样的:
原来在这里:
这就是druid内置的所有的filter了,去掉前缀druid.filters就是别名了。
[ERROR] session ip change too many (WebSessionStat.java:266)的原因及不完整解决办法
最近将项目放到公网,结果反复刷新页面后,出现了大量的[ERROR] session ip change too many (WebSessionStat.java:266)错误,如下图
仔细查找,原来是alibaba druid提示的,具体来说,是druid监控session时,记录访问IP提示的。
如果只想看解决方法,对原因无兴趣,请直接跳到本文末尾,下面是产生的原因。
首先打开druid的源代码,找到com.alibaba.druid.support.http.stat.WebSessionStat类,可以看到输出错误的源代码
public void addRemoteAddress(String ip) { if (remoteAddresses == null) { this.remoteAddresses = ip; return; } if (remoteAddresses.contains(ip)) { return; } if (remoteAddresses.length() > 256) { LOG.error("session ip change too many"); return; } remoteAddresses += ';' + ip; }
很明显,是由于每次访问的ip不一样,然后remoteAddresses += ';' + ip;多次累加,导致remoteAddresses超过了256位长度。可是为什么同一个session每次ip会不一样,ip又是怎么获取的,这需要继续跟踪源代码。
跟踪源代码可以看到com.alibaba.druid.util.DruidWebUtils的public static String getRemoteAddr(HttpServletRequest request)方法中,是这么获取IP的:
package com.alibaba.druid.util; import javax.servlet.GenericServlet; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; public class DruidWebUtils { public static String getRemoteAddr(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; }
可以看到,druid获得ip的方法是request.getHeader("x-forwarded-for");,这个方法会获取到ip:port,而国内由于公网IP极其匮乏,导致绝大部分人上网都是通过地址映射以后来上网,这就导致了每次请求的ip、端口都可能不一样,那么自然会被WebSessionStat.addRemoteAddress()方法累加到remoteAddresses,导致remoteAddresses越来越长,最终超过256位长度,触发LOG.error("session ip change too many");语句。
知道原因,下面就是修改方法:
方法一:关闭druid的session监控。
在web.xml配置druid的地方,将sessionStatEnable设为false即可,如下:
方法二:修改druid的源码。
下载或反编译druid的源码,将LOG.error("session ip change too many");注释掉,或者将if条件长度改大即可。
方法三:等。等阿里巴巴官方修改相关代码。
参考文献:http://www.oschina.net/question/579092_243246
---------------------
作者:zhu19774279
来源:CSDN
原文:https://blog.csdn.net/zhu19774279/article/details/50392813
版权声明:本文为博主原创文章,转载请附上博文链接!
配置WebStatFilter
WebStatFilter用于采集web-jdbc关联监控的数据。
web.xml配置
<filter> <filter-name>DruidWebStatFilter</filter-name> <filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class> <init-param> <param-name>exclusions</param-name> <param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value> </init-param> </filter> <filter-mapping> <filter-name>DruidWebStatFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
exclusions配置
经常需要排除一些不必要的url,比如*.js,/jslib/*等等。配置在init-param中。比如:
<init-param> <param-name>exclusions</param-name> <param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value> </init-param>
sessionStatMaxCount配置
缺省sessionStatMaxCount是1000个。你可以按需要进行配置,比如:
<init-param> <param-name>sessionStatMaxCount</param-name> <param-value>1000</param-value> </init-param>
sessionStatEnable配置
你可以关闭session统计功能,比如:
<init-param> <param-name>sessionStatEnable</param-name> <param-value>false</param-value> </init-param>
principalSessionName配置
你可以配置principalSessionName,使得druid能够知道当前的session的用户是谁。比如:
<init-param> <param-name>principalSessionName</param-name> <param-value>xxx.user</param-value> </init-param>
根据需要,把其中的xxx.user修改为你user信息保存在session中的sessionName。
注意:如果你session中保存的是非string类型的对象,需要重载toString方法。
principalCookieName
如果你的user信息保存在cookie中,你可以配置principalCookieName,使得druid知道当前的user是谁
<init-param> <param-name>principalCookieName</param-name> <param-value>xxx.user</param-value> </init-param>
根据需要,把其中的xxx.user修改为你user信息保存在cookie中的cookieName
profileEnable
druid 0.2.7版本开始支持profile,配置profileEnable能够监控单个url调用的sql列表。
<init-param> <param-name>profileEnable</param-name> <param-value>true</param-value> </init-param>
结果展示
https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_%E9%85%8D%E7%BD%AEWebStatFilter
DruidDataSource配置兼容DBCP,但个别配置的语意有所区别。
配置 | 缺省值 | 说明 |
---|---|---|
name | 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。如果没有配置,将会生成一个名字,格式是:"DataSource-" + System.identityHashCode(this). 另外配置此属性至少在1.0.5版本中是不起作用的,强行设置name会出错。详情-点此处。 | |
url | 连接数据库的url,不同数据库不一样。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto |
|
username | 连接数据库的用户名 | |
password | 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里 | |
driverClassName | 根据url自动识别 | 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName |
initialSize | 0 | 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 |
maxActive | 8 | 最大连接池数量 |
maxIdle | 8 | 已经不再使用,配置了也没效果 |
minIdle | 最小连接池数量 | |
maxWait | 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 | |
poolPreparedStatements | false | 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 |
maxPoolPreparedStatementPerConnectionSize | -1 | 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 |
validationQuery | 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 | |
validationQueryTimeout | 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法 | |
testOnBorrow | true | 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 |
testOnReturn | false | 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 |
testWhileIdle | false | 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 |
keepAlive | false (1.0.28) |
连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。 |
timeBetweenEvictionRunsMillis | 1分钟(1.0.14) | 有两个含义: 1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 |
numTestsPerEvictionRun | 30分钟(1.0.14) | 不再使用,一个DruidDataSource只支持一个EvictionRun |
minEvictableIdleTimeMillis | 连接保持空闲而不被驱逐的最小时间 | |
connectionInitSqls | 物理连接初始化的时候执行的sql | |
exceptionSorter | 根据dbType自动识别 | 当数据库抛出一些不可恢复的异常时,抛弃连接 |
filters | 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall |
|
proxyFilters | 类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系 |
如果表格无法完全展示,请查看图片:
https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8
来源:https://www.cnblogs.com/softidea/p/6110830.html