本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

暂无数据

springboot通过mybatis-plus中的Interceptor自定义一个拦截器实现打印完整sql(包含值)日志的功能

发布于2020-11-19 20:49     阅读(1499)     评论(0)     点赞(9)     收藏(5)


1、实现功能结果如下图所示,用红框框住的就是实现的打印功能,而下边带?号的是原始的:

2、具体springboot+mybatis项目的搭建请看:spring-boot+mybatis搭建一个后端restfull服务https://blog.csdn.net/sunxiaoju/article/details/109607322,此文就是在此基础上延伸出来的。

3、要实现此功能需要加入如下依赖:

  1. <dependency>
  2. <groupId>com.baomidou</groupId>
  3. <artifactId>mybatis-plus-boot-starter</artifactId>
  4. <version>3.4.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.slf4j</groupId>
  8. <artifactId>slf4j-api</artifactId>
  9. <version>1.7.30</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.slf4j</groupId>
  13. <artifactId>slf4j-log4j12</artifactId>
  14. <version>1.7.30</version>
  15. <scope>test</scope>
  16. </dependency>
  17. <dependency>
  18. <groupId>org.springframework.boot</groupId>
  19. <artifactId>spring-boot-starter-log4j2</artifactId>
  20. </dependency>

3、将spring-boot-starter-logging从spring-boot-starter-web中排除,如:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-web</artifactId>
  4. <exclusions>
  5. <exclusion>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-logging</artifactId>
  8. </exclusion>
  9. </exclusions>
  10. </dependency>

4、将原来的mybatis-spring-boot-starter去掉,最后pom.xml文件为:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <modelVersion>4.0.0</modelVersion>
  6. <groupId>com.best</groupId>
  7. <artifactId>spring-boot-mybatis</artifactId>
  8. <version>1.0-SNAPSHOT</version>
  9. <properties>
  10. <java.version>1.8</java.version>
  11. <fastjson.version>1.2.74</fastjson.version>
  12. <spring.version>5.3.5.RELEASE</spring.version>
  13. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  14. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  15. </properties>
  16. <parent>
  17. <groupId>org.springframework.boot</groupId>
  18. <artifactId>spring-boot-starter-parent</artifactId>
  19. <version>2.3.5.RELEASE</version>
  20. </parent>
  21. <dependencies>
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-starter-web</artifactId>
  25. <exclusions>
  26. <exclusion>
  27. <groupId>org.springframework.boot</groupId>
  28. <artifactId>spring-boot-starter-logging</artifactId>
  29. </exclusion>
  30. </exclusions>
  31. </dependency>
  32. <dependency>
  33. <groupId>org.springframework.boot</groupId>
  34. <artifactId>spring-boot-starter-log4j2</artifactId>
  35. </dependency>
  36. <dependency>
  37. <groupId>org.springframework.boot</groupId>
  38. <artifactId>spring-boot-starter-test</artifactId>
  39. <scope>test</scope>
  40. </dependency>
  41. <dependency>
  42. <groupId>org.springframework.boot</groupId>
  43. <artifactId>spring-boot-starter-jdbc</artifactId>
  44. <exclusions>
  45. <!-- 排除 tomcat-jdbc 以使用 HikariCP -->
  46. <exclusion>
  47. <groupId>org.apache.tomcat</groupId>
  48. <artifactId>tomcat-jdbc</artifactId>
  49. </exclusion>
  50. </exclusions>
  51. </dependency>
  52. <dependency>
  53. <groupId>org.mybatis.spring.boot</groupId>
  54. <artifactId>mybatis-spring-boot-starter</artifactId>
  55. <version>2.1.3</version>
  56. </dependency>
  57. <dependency>
  58. <groupId>mysql</groupId>
  59. <artifactId>mysql-connector-java</artifactId>
  60. <version>8.0.22</version>
  61. </dependency>
  62. <dependency>
  63. <groupId>com.alibaba</groupId>
  64. <artifactId>druid</artifactId>
  65. <version>1.2.1</version>
  66. </dependency>
  67. <dependency>
  68. <groupId>com.alibaba</groupId>
  69. <artifactId>fastjson</artifactId>
  70. <version>${fastjson.version}</version>
  71. </dependency>
  72. <dependency>
  73. <groupId>org.projectlombok</groupId>
  74. <artifactId>lombok</artifactId>
  75. <version>1.18.10</version>
  76. </dependency>
  77. <dependency>
  78. <groupId>com.baomidou</groupId>
  79. <artifactId>mybatis-plus-boot-starter</artifactId>
  80. <version>3.4.0</version>
  81. </dependency>
  82. <dependency>
  83. <groupId>org.slf4j</groupId>
  84. <artifactId>slf4j-api</artifactId>
  85. <version>1.7.30</version>
  86. </dependency>
  87. <dependency>
  88. <groupId>org.slf4j</groupId>
  89. <artifactId>slf4j-log4j12</artifactId>
  90. <version>1.7.30</version>
  91. <scope>test</scope>
  92. </dependency>
  93. </dependencies>
  94. <build>
  95. <plugins>
  96. <plugin>
  97. <groupId>org.apache.maven.plugins</groupId>
  98. <artifactId>maven-compiler-plugin</artifactId>
  99. <version>3.8.0</version>
  100. <configuration>
  101. <source>1.8</source>
  102. <target>1.8</target>
  103. </configuration>
  104. </plugin>
  105. </plugins>
  106. <resources>
  107. <resource>
  108. <directory>src/main/java</directory>
  109. <includes>
  110. <include>**/dao/*Mapper.xml</include>
  111. </includes>
  112. </resource>
  113. </resources>
  114. </build>
  115. </project>

5、既然是打印日志,那么肯定需要日志框架,这里使用的是log4j2,那么就需要再resources中新建一个log4j2.xml文件,文件内容如下:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
  3. <!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
  4. <!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
  5. <configuration status="WARN" monitorInterval="30">
  6. <properties>
  7. <property name="LOG_HOME">../../../../build/logs</property>
  8. </properties>
  9. <!-- 先定义所有的appender -->
  10. <appenders>
  11. <!-- 这个输出控制台的配置 -->
  12. <Console name="Console" target="SYSTEM_OUT">
  13. <!-- 控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
  14. <ThresholdFilter level="trace" onMatch="ACCEPT" onMismatch="DENY"/>
  15. <!-- 这个都知道是输出日志的格式 -->
  16. <PatternLayout charset="UTF-8" pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
  17. </Console>
  18. <!-- 文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用 -->
  19. <!-- append为TRUE表示消息增加到指定文件中,false表示消息覆盖指定的文件内容,默认值是true -->
  20. <RollingFile name="DaoFileTrace" fileName="${LOG_HOME}/test.log"
  21. filePattern="${LOG_HOME}/$${date:yyyy-MM}/DaoFileTrace-%d{yyyy-MM-dd}-%i.log">
  22. <ThresholdFilter level="trace" onMatch="ACCEPT" onMismatch="DENY"/>
  23. <PatternLayout>
  24. <MarkerPatternSelector defaultPattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n">
  25. <PatternMatch key="spark" pattern="%m%n"/>
  26. </MarkerPatternSelector>
  27. </PatternLayout>
  28. <Policies>
  29. <TimeBasedTriggeringPolicy/>
  30. <SizeBasedTriggeringPolicy size="100 MB"/>
  31. </Policies>
  32. </RollingFile>
  33. <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
  34. <RollingFile name="RollingFileInfo" fileName="${LOG_HOME}/info.log"
  35. filePattern="${LOG_HOME}/${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">
  36. <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
  37. <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
  38. <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
  39. <Policies>
  40. <TimeBasedTriggeringPolicy/>
  41. <SizeBasedTriggeringPolicy size="100 MB"/>
  42. </Policies>
  43. </RollingFile>
  44. <RollingFile name="RollingFileWarn" fileName="${LOG_HOME}/warn.log"
  45. filePattern="${LOG_HOME}/${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log">
  46. <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
  47. <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
  48. <Policies>
  49. <TimeBasedTriggeringPolicy/>
  50. <SizeBasedTriggeringPolicy size="100 MB"/>
  51. </Policies>
  52. <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件,这里设置了20 -->
  53. <DefaultRolloverStrategy max="20"/>
  54. </RollingFile>
  55. <RollingFile name="RollingFileError" fileName="${LOG_HOME}/error.log"
  56. filePattern="${LOG_HOME}/${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log">
  57. <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
  58. <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
  59. <Policies>
  60. <TimeBasedTriggeringPolicy/>
  61. <SizeBasedTriggeringPolicy size="100 MB"/>
  62. </Policies>
  63. </RollingFile>
  64. </appenders>
  65. <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
  66. <loggers>
  67. <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
  68. <logger name="org.springframework" level="INFO"></logger>
  69. <logger name="com.alibaba.druid.*" level="INFO" additivity="false">
  70. <AppenderRef ref="Console"/>
  71. </logger>
  72. <logger name="org.mybatis" level="INFO" additivity="false">
  73. <AppenderRef ref="Console" />
  74. <AppenderRef ref="DaoFileTrace" />
  75. </logger>
  76. <logger name="com.best.common.sqllog" level="DEBUG" additivity="false">
  77. <AppenderRef ref="Console" />
  78. <AppenderRef ref="DaoFileTrace" />
  79. </logger>
  80. <logger name="org.apache.ibatis.logging" level="INFO" additivity="false">
  81. <AppenderRef ref="Console" />
  82. <AppenderRef ref="DaoFileTrace" />
  83. </logger>
  84. <root level="all">
  85. <appender-ref ref="Console"/>
  86. <appender-ref ref="RollingFileInfo"/>
  87. <appender-ref ref="RollingFileWarn"/>
  88. <appender-ref ref="RollingFileError"/>
  89. </root>
  90. </loggers>
  91. </configuration>

6、在添加完log4j2.xml文件后,就需要让springboot程序可以加载该配置文件,那么只需要再application.yml中加入:

  1. logging:
  2. config: classpath:log4j2.xml

如下图所示:

7、经过以上配置,在springboot中就可以使用log4j2框架来打印日志了。

8、开始创建拦截器,创建自定义拦截器必须实现Interceptor接口,那么我们新建一个SqlLoggerInterceptor类并实现Interceptor接口,并且通过@Intercepts和@Signature注解来配置拦截的方法和对象,这里我们拦截的是Executor,也就是mybatis具体执行sql语句的对象,而Executor对象中有如下方法:

  1. public interface Executor {
  2. ResultHandler NO_RESULT_HANDLER = null;
  3. int update(MappedStatement var1, Object var2) throws SQLException;
  4. <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException;
  5. <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;
  6. <E> Cursor<E> queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException;
  7. List<BatchResult> flushStatements() throws SQLException;
  8. void commit(boolean var1) throws SQLException;
  9. void rollback(boolean var1) throws SQLException;
  10. CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4);
  11. boolean isCached(MappedStatement var1, CacheKey var2);
  12. void clearLocalCache();
  13. void deferLoad(MappedStatement var1, MetaObject var2, String var3, CacheKey var4, Class<?> var5);
  14. Transaction getTransaction();
  15. void close(boolean var1);
  16. boolean isClosed();
  17. void setExecutorWrapper(Executor var1);
  18. }

不过我们经常用到的就只有update和两个query,因此在@Intercepts内写上3个Signature,如:

  1. @Intercepts({
  2. //type指定代理的是那个对象,method指定代理Executor中的那个方法,args指定Executor中的query方法都有哪些参数对象
  3. //由于Executor中有两个query,因此需要两个@Signature
  4. @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),//需要代理的对象和方法
  5. @Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
  6. @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })//需要代理的对象和方法
  7. })

由此可以看出update、query、query中的args中的对象是和Executor中的update、query、query方法中的参数是一一对应的,

9、实现Interceptor接口中的方法,主要有三个方法分别是plugin、setProperties、intercept,而我们主要做的就是intercept方法,plugin方法一般固定即可,setProperties是在外部传近来的值,如:

  1. @Override
  2. public Object plugin(Object target) {
  3. return Plugin.wrap(target, this);
  4. }
  5. @Override
  6. public void setProperties(Properties properties) {
  7. System.out.println(properties);
  8. }

10、最后在intercept填写拦截后要进行的处理逻辑,代码如下:

  1. package com.best.common.sqllog;
  2. import org.apache.ibatis.cache.CacheKey;
  3. import org.apache.ibatis.executor.Executor;
  4. import org.apache.ibatis.mapping.BoundSql;
  5. import org.apache.ibatis.mapping.MappedStatement;
  6. import org.apache.ibatis.mapping.ParameterMapping;
  7. import org.apache.ibatis.plugin.*;
  8. import org.apache.ibatis.reflection.MetaObject;
  9. import org.apache.ibatis.session.Configuration;
  10. import org.apache.ibatis.session.ResultHandler;
  11. import org.apache.ibatis.session.RowBounds;
  12. import org.apache.ibatis.type.TypeHandlerRegistry;
  13. import org.apache.logging.log4j.LogManager;
  14. import org.apache.logging.log4j.Logger;
  15. import java.lang.reflect.InvocationTargetException;
  16. import java.text.DateFormat;
  17. import java.util.Date;
  18. import java.util.List;
  19. import java.util.Locale;
  20. import java.util.Properties;
  21. /**
  22. * @author:sunxj
  23. * @date:2020-11-10 23:16:22
  24. * @description:打印sql语句
  25. */
  26. @Intercepts({
  27. //type指定代理的是那个对象,method指定代理Executor中的那个方法,args指定Executor中的query方法都有哪些参数对象
  28. //由于Executor中有两个query,因此需要两个@Signature
  29. @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),//需要代理的对象和方法
  30. @Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
  31. @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })//需要代理的对象和方法
  32. })
  33. public class SqlLoggerInterceptor implements Interceptor {
  34. Logger logger = LogManager.getLogger(SqlLoggerInterceptor.class.getName());
  35. @Override
  36. public Object intercept(Invocation invocation) throws Throwable {
  37. MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
  38. Object parameter = null;
  39. if (invocation.getArgs().length > 1) {
  40. //获得查询方法的参数,比如selectById(Integer id,String name),那么就可以获取到四个参数分别是:
  41. //{id:1,name:"user1",param1:1,param2:"user1"}
  42. parameter = invocation.getArgs()[1];
  43. }
  44. String sqlId = mappedStatement.getId();//获得mybatis的*mapper.xml文件中映射的方法,如:com.best.dao.UserMapper.selectById
  45. //将参数和映射文件组合在一起得到BoundSql对象
  46. BoundSql boundSql = mappedStatement.getBoundSql(parameter);
  47. //获取配置信息
  48. Configuration configuration = mappedStatement.getConfiguration();
  49. Object returnValue = null;
  50. logger.debug("/*---------------java:"+sqlId+"[begin]---------------*/");
  51. //通过配置信息和BoundSql对象来生成带值得sql语句
  52. String sql = geneSql(configuration, boundSql);
  53. //打印sql语句
  54. logger.debug("==> sql:"+sql);
  55. //先记录执行sql语句前的时间
  56. long start = System.currentTimeMillis();
  57. try {
  58. //开始执行sql语句
  59. returnValue = invocation.proceed();
  60. } catch (InvocationTargetException | IllegalAccessException e) {
  61. e.printStackTrace();
  62. }
  63. //记录执行sql语句后的时间
  64. long end = System.currentTimeMillis();
  65. //得到执行sql语句的用了多长时间
  66. long time = (end - start);
  67. //以毫秒为单位打印
  68. logger.debug("<== sql执行历时:"+time+"毫秒");
  69. //返回值,如果是多条记录,那么此对象是一个list,如果是一个bean对象,那么此处就是一个对象,也有可能是一个map
  70. return returnValue;
  71. }
  72. /**
  73. * 如果是字符串对象则加上单引号返回,如果是日期则也需要转换成字符串形式,如果是其他则直接转换成字符串返回。
  74. * @param obj
  75. * @return
  76. */
  77. private static String getParameterValue(Object obj) {
  78. String value;
  79. if (obj instanceof String) {
  80. value = "'" + obj.toString() + "'";
  81. } else if (obj instanceof Date) {
  82. DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
  83. value = "'" + formatter.format(obj) + "'";
  84. } else {
  85. if (obj != null) {
  86. value = obj.toString();
  87. } else {
  88. value = "";
  89. }
  90. }
  91. return value;
  92. }
  93. /**
  94. * 生成对应的带有值得sql语句
  95. * @param configuration
  96. * @param boundSql
  97. * @return
  98. */
  99. public static String geneSql(Configuration configuration, BoundSql boundSql) {
  100. Object parameterObject = boundSql.getParameterObject();//获得参数对象,如{id:1,name:"user1",param1:1,param2:"user1"}
  101. List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();//获得映射的对象参数
  102. String sql = boundSql.getSql().replaceAll("[\\s]+", " ");//获得带问号的sql语句
  103. if (parameterMappings.size() > 0 && parameterObject != null) {//如果参数个数大于0且参数对象不为空,说明该sql语句是带有条件的
  104. TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
  105. if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {//检查该参数是否是一个参数
  106. //getParameterValue用于返回是否带有单引号的字符串,如果是字符串则加上单引号
  107. sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));//如果是一个参数则只替换一次,将问号直接替换成值
  108. } else {
  109. MetaObject metaObject = configuration.newMetaObject(parameterObject);//将映射文件的参数和对应的值返回,比如:id,name以及对应的值。
  110. for (ParameterMapping parameterMapping : parameterMappings) {//遍历参数,如:id,name等
  111. String propertyName = parameterMapping.getProperty();//获得属性名,如id,name等字符串
  112. if (metaObject.hasGetter(propertyName)) {//检查该属性是否在metaObject中
  113. Object obj = metaObject.getValue(propertyName);//如果在metaObject中,那么直接获取对应的值
  114. sql = sql.replaceFirst("\\?", getParameterValue(obj));//然后将问号?替换成对应的值。
  115. } else if (boundSql.hasAdditionalParameter(propertyName)) {
  116. Object obj = boundSql.getAdditionalParameter(propertyName);
  117. sql = sql.replaceFirst("\\?", getParameterValue(obj));
  118. }
  119. }
  120. }
  121. }
  122. return sql;//最后将sql语句返回
  123. }
  124. @Override
  125. public Object plugin(Object target) {
  126. return Plugin.wrap(target, this);
  127. }
  128. @Override
  129. public void setProperties(Properties properties) {
  130. //properties可以通过
  131. System.out.println(properties);
  132. }
  133. }

11、此拦截写好之后还需要springboot使用,如果是通过@Component扫描是无法正常注入到IOC容器中的,因此需要通过@Configuration和@Bean的方式进行,新建一个配置BatisPlusConfig类,代码如下:

  1. package com.best.db;
  2. import com.best.common.sqllog.SqlLoggerInterceptor;
  3. import org.apache.ibatis.plugin.Interceptor;
  4. import org.apache.ibatis.session.SqlSessionFactory;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import org.springframework.transaction.annotation.EnableTransactionManagement;
  8. import java.util.Properties;
  9. /**
  10. * @author:sunxj
  11. * @date:2020-11-11 22:18:55
  12. * @description:mybatis插件配置类
  13. */
  14. @Configuration
  15. public class BatisPlusConfig {
  16. @Bean
  17. public String myInterceptor(SqlSessionFactory sqlSessionFactory) {
  18. //实例化插件
  19. SqlLoggerInterceptor sqlInterceptor = new SqlLoggerInterceptor();
  20. //创建属性值
  21. Properties properties = new Properties();
  22. properties.setProperty("prop1","value1");
  23. //将属性值设置到插件中
  24. sqlInterceptor.setProperties(properties);
  25. //将插件添加到SqlSessionFactory工厂
  26. sqlSessionFactory.getConfiguration().addInterceptor(sqlInterceptor);
  27. return "interceptor";
  28. }
  29. }

12、到此配置完成,启动springboot然后在浏览器中输入:http://localhost:8080/best/getUser1即可打印如下图所示:

 

 

 

原文链接:https://blog.csdn.net/sunxiaoju/article/details/109660493



所属网站分类: 技术文章 > 博客

作者:gogogo

链接:http://www.javaheidong.com/blog/article/984/161c8ca3609695e799ea/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

9 0
收藏该文
已收藏

评论内容:(最多支持255个字符)