mybatis plug 只查id_MyBatis Generator的一个问题引发对插件的修改
在使用mybatis.generator插件自动生成mapper.xml的时候发现一个问题:默认生成的dao接口为mapper结尾
mapper结尾
当然我们知道在不同的ORM框架中名称表示不同,例如:mybatis中称为Mapper,spring Data JPA中称为Repository,但是习惯用***Dao结尾表示数据访问层接口的应该怎么办?
其实mybatis generator支持修改这个后缀:通过generatorConfig.xml配置文件添加table标签的mapperName属性,但是修改后会存在另一个问题:生成的xml由原本的Mapper结尾变成了Dao结尾,也就是只能跟设置的mapperName属性一致,网上搜索了相关问题,只发现一个通过修改插件源码中的calculateMyBatis3XmlMapperFileName方法的解决方案。
接下来说下我的处理过程,主要涉及下面几点:
generator插件的使用
maven创建自定义插件
插件的调试(远程调试)
generator源码的修改
先说下MyBatis Generator插件的使用
1.pom.xml添加依赖
org.mybatis.generator
mybatis-generator-maven-plugin
1.3.4
${basedir}/src/main/resources/generator/mybatis/generatorConfig.xml
true
true
2.generatorConfig.xml的配置示例
/p>
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.connectionURL}"
userId="${jdbc.userId}"
password="${jdbc.password}">
targetProject="src/main/java">
targetProject="src/main/java">
targetProject="src/main/java" type="XMLMAPPER">
enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false" mapperName="UserDao">
generatorConfig.xml的配置可以参考《MyBatis从入门到精通》第5章
3.执行mybatis-generator:generate命令即可生成配置的table对应代码
创建一个简单的maven插件
参考maven实战第17章
了解插件的基本实现以及插件的运行入口类对接下来的源码调试修改有所帮助
1.插件本身也是maven项目,区别的地方是打包方式必须为maven-plugin
首先pom.xml需要导入两个依赖:
maven-plugin-api包含插件开发必须的类
maven-plugin-annotations提供注解支持
com.test
maven-plugin-demo
1.0-SNAPSHOT
maven-plugin-demo
maven-plugin
org.apache.maven
maven-plugin-api
3.5.4
org.apache.maven.plugin-tools
maven-plugin-annotations
3.5.2
provided
2.为插件编写目标:创建一个类继承AbstractMojo并实现execute()方法,Maven称为Mojo(maven old java object与Pojo对应),实际上我们执行插件命令时会执行对应的Mojo中的execute()方法
@Mojo(name = "hi")
public class Helloworld extends AbstractMojo {
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
System.out.println("Hello World!");
}
}
@Mojo(name = "hi")定义了插件的目标名称,执行插件时通过groupId:artifactId:version:名称,例如:上面我们定义的插件执行命令为com.test:maven-plugin-demo:1.0-SNAPSHOT:hi
其他的注解还有@Parameter用于读取参数配置等
3.使用插件
首先我们把创建的自定义插件项目install到本地仓库
然后其他项目就可以在pom.xml中使用标签引入插件了
com.test
maven-plugin-demo
1.0-SNAPSHOT
IntelliJIdea可以在Maven Projects插件栏看到我们引入的插件而直接运行,也可以通过com.test:maven-plugin-demo:1.0-SNAPSHOT:hi命令运行
自定义插件运行结果
插件代码的调试
远程调试步骤:①服务端建立监听②使用相同代码的客户端打断点建立连接并调试
1.maven提供mvnDebug命令行模式启动,默认8000端口号,mvnDebug groupId:artifactId:version:名称,执行mvnDebug com.test:maven-plugin-demo:1.0-SNAPSHOT:hi
启动远程调试
2.可以在当前项目下通过remote连接,module选择当前插件项目
remote配置
3.然后就可以打断点debug了
debug
调试MBG并修改源码实现我们想要的效果(接口Dao结尾xml以Mapper结尾)
MBG项目结构
2.比较重要的是plugin和core两个工程,而且plugin依赖core工程
plugin依赖core
3.在plugin工程中可以找到以Mojo结尾的项目入口类,那么我们就可以在execute()打上断点调试
@Mojo(name = "generate",defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class MyBatisGeneratorMojo extends AbstractMojo {
public void execute() throws MojoExecutionException {
{
//......
try {
ConfigurationParser cp = new ConfigurationParser(
project.getProperties(), warnings);
/**
* 解析后返回Configuration对象,对应XML中的generatorConfiguration根标签
* Configuration对象中的List contexts对象则对应XML中配置的多个context标签
* Context类对象中的ArrayList tableConfigurations则对应XML配置的多个table标签
* 根据它们之间的包含关系,可以看到TableConfiguration类中就有mapperName属性
*/
Configuration config = cp.parseConfiguration(configurationFile);
// ShellCallback作用于IDE执行环境的支持:主要是文件创建,已存在文件时是否支持覆盖,java文件支持合并,以及文件创建完成提醒IDE刷新project
ShellCallback callback = new MavenShellCallback(this, overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
callback, warnings);
/**
* 执行generate生成mapper
* MavenProgressCallback:log日志打印执行过程,verbose:默认false不打印
* contextsToRun:参数配置,限制哪些context应该被执行
* fullyqualifiedTables:参数配置,限制哪些table应该被生成
*/
myBatisGenerator.generate(new MavenProgressCallback(getLog(),
verbose), contextsToRun, fullyqualifiedTables);
} catch (XMLParserException e) {
for (String error : e.getErrors()) {
getLog().error(error);
}
throw new MojoExecutionException(e.getMessage());
}
//......
}
}
Configuration config = cp.parseConfiguration(configurationFile);首先是generatorConfig.xml配置文件的加载和解析,点进方法里边可以看到是使用Document读取XML文档的方式,也就是需要解析到我们的配置文件
涉及到XML配置的首先想到都是要先读取解析XML,我们在《Spring源码深度解析》、《MyBatis技术内幕》都可以看到先从XML配置文件的解析开始
使用Document读取XML文档的简单流程:
//使用DocumentBuilder
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
//得到Document对象, builder.parse可以接收InputStream/file或者url
Document doc = builder.parse(file);
Element root = doc.getDocumentElement();//获取root根节点对象
NodeList nodelist = root.getChildNodes();
接下来我们进入parseConfiguration()方法,可以发现他把解析工作交给了MyBatisGeneratorConfigurationParser类去解析:
public class ConfigurationParser {
private Configuration parseConfiguration(InputSource inputSource) throws IOException, XMLParserException {
//......略
Configuration config;
Element rootNode = document.getDocumentElement();
DocumentType docType = document.getDoctype();
if (rootNode.getNodeType() == Node.ELEMENT_NODE
&& docType.getPublicId().equals(
XmlConstants.IBATOR_CONFIG_PUBLIC_ID)) {
config = parseIbatorConfiguration(rootNode);
} else if (rootNode.getNodeType() == Node.ELEMENT_NODE
&& docType.getPublicId().equals(
XmlConstants.MYBATIS_GENERATOR_CONFIG_PUBLIC_ID)) {
//DTD文档PUBLIC:根据generatorconfig.xml的文档头部定义的PUBLIC区分使用MyBatis文档方式解析
config = parseMyBatisGeneratorConfiguration(rootNode);
} else {
throw new XMLParserException(getString("RuntimeError.5")); //$NON-NLS-1$
}
return config;
}
private Configuration parseMyBatisGeneratorConfiguration(Element rootNode)
throws XMLParserException {
MyBatisGeneratorConfigurationParser parser = new MyBatisGeneratorConfigurationParser(
extraProperties);
//继续执行解析操作
return parser.parseConfiguration(rootNode);
}
}
进到MyBatisGeneratorConfigurationParser的parseConfiguration方法,可以发现是一层层的标签解析并封装到Configuration类对应的属性中,我们可以通过以下顺序找到mapperName:
parseConfiguration()->parseContext()->parseTable()
protected void parseTable(Context context, Node node) {
TableConfiguration tc = new TableConfiguration(context);
context.addTableConfiguration(tc);
//获取mapperName属性并设置到TableConfiguration对象中
String mapperName = attributes.getProperty("mapperName");
if (stringHasValue(mapperName)) {
tc.setMapperName(mapperName);
}
}
通过Configuration config = cp.parseConfiguration(configurationFile);我们了解到XML配置文件会解析封装为Configuration对象,而且也找到了解析读取mapperName属性的地方
接下来可以看执行生成的主流程myBatisGenerator.generate(new MavenProgressCallback(getLog(), verbose), contextsToRun, fullyqualifiedTables);
public class MyBatisGenerator {
public void generate(ProgressCallback callback, Set contextIds,
Set fullyQualifiedTableNames, boolean writeFiles) throws SQLException,
IOException, InterruptedException {
//......
// now run the introspections...
for (Context context : contextsToRun) {
//连接数据库并读取保存table信息,等待后面的generateFiles生成文件
context.introspectTables(callback, warnings,
fullyQualifiedTableNames);
}
// now run the generates
for (Context context : contextsToRun) {
//生成GeneratedJavaFile/GeneratedXmlFile对象,用于后面生成文件
context.generateFiles(callback, generatedJavaFiles,
generatedXmlFiles, warnings);
}
// now save the files
if (writeFiles) {
callback.saveStarted(generatedXmlFiles.size()
+ generatedJavaFiles.size());
for (GeneratedXmlFile gxf : generatedXmlFiles) {
projects.add(gxf.getTargetProject());
writeGeneratedXmlFile(gxf, callback);
}
for (GeneratedJavaFile gjf : generatedJavaFiles) {
projects.add(gjf.getTargetProject());
//获取java文件内容source = gjf.getFormattedContent()可以看interfaze类中拼接内容的方法
writeGeneratedJavaFile(gjf, callback);
}
for (String project : projects) {
shellCallback.refreshProject(project);
}
}
callback.done();
}
}
其中先通过introspectTables方法获取表的信息,然后再执行generateFiles生成GeneratedJavaFile保存要生成的文件结构,然后再通过writeGeneratedJavaFile获取文件内容以及编码等信息在目录下生成文件。
表示对IntrospectedTable表示不太理解,搜了一篇介绍IntrospectedTable是提供扩展的基础类,配置文件context标签上设置的runtime对应的就是不同的IntrospectedTable的实现,接下来我们观察代码时也会看到这点。
先看context.introspectTables方法,里边主要是获取了数据库连接Connection,以及调用List tables = databaseIntrospector .introspectTables(tc);方法:
public class DatabaseIntrospector {
public List introspectTables(TableConfiguration tc)
throws SQLException {
// 获取列信息
Map> columns = getColumns(tc);
removeIgnoredColumns(tc, columns);
calculateExtraColumnInformation(tc, columns);
applyColumnOverrides(tc, columns);
calculateIdentityColumns(tc, columns);
List introspectedTables = calculateIntrospectedTables(
tc, columns);
// ......略
return introspectedTables;
}
private List calculateIntrospectedTables(
TableConfiguration tc,
Map> columns) {
boolean delimitIdentifiers = tc.isDelimitIdentifiers()
|| stringContainsSpace(tc.getCatalog())
|| stringContainsSpace(tc.getSchema())
|| stringContainsSpace(tc.getTableName());
List answer = new ArrayList();
for (Map.Entry> entry : columns
.entrySet()) {
ActualTableName atn = entry.getKey();
//过滤一些没有指定的不必要的信息
FullyQualifiedTable table = new FullyQualifiedTable(
//......略
delimitIdentifiers, context);
//创建IntrospectedTable并返回
IntrospectedTable introspectedTable = ObjectFactory
.createIntrospectedTable(tc, table, context);
for (IntrospectedColumn introspectedColumn : entry.getValue()) {
introspectedTable.addColumn(introspectedColumn);
}
calculatePrimaryKey(table, introspectedTable);
enhanceIntrospectedTable(introspectedTable);
answer.add(introspectedTable);
}
return answer;
}
}
可以看到,getColumns(tc)方法通过访问数据库获取到列信息,然后可以发现createIntrospectedTable创建IntrospectedTable的方法:
public class ObjectFactory {
public static IntrospectedTable createIntrospectedTable(
TableConfiguration tableConfiguration, FullyQualifiedTable table,
Context context) {
IntrospectedTable answer = createIntrospectedTableForValidation(context);
answer.setFullyQualifiedTable(table);
answer.setTableConfiguration(tableConfiguration);
return answer;
}
public static IntrospectedTable createIntrospectedTableForValidation(Context context) {
String type = context.getTargetRuntime();
if (!stringHasValue(type)) {
type = IntrospectedTableMyBatis3Impl.class.getName();
} else if ("Ibatis2Java2".equalsIgnoreCase(type)) { //$NON-NLS-1$
type = IntrospectedTableIbatis2Java2Impl.class.getName();
} else if ("Ibatis2Java5".equalsIgnoreCase(type)) { //$NON-NLS-1$
type = IntrospectedTableIbatis2Java5Impl.class.getName();
} else if ("Ibatis3".equalsIgnoreCase(type)) { //$NON-NLS-1$
type = IntrospectedTableMyBatis3Impl.class.getName();
} else if ("MyBatis3".equalsIgnoreCase(type)) { //$NON-NLS-1$
type = IntrospectedTableMyBatis3Impl.class.getName();
} else if ("MyBatis3Simple".equalsIgnoreCase(type)) { //$NON-NLS-1$
type = IntrospectedTableMyBatis3SimpleImpl.class.getName();
}
IntrospectedTable answer = (IntrospectedTable) createInternalObject(type);
answer.setContext(context);
return answer;
}
}
createIntrospectedTableForValidation方法中通过runtime的设置,会使用不同的IntrospectedTable实现,我们之前配置文件中的是targetRuntime="MyBatis3",对应会使用IntrospectedTableMyBatis3Impl这个实现类,接下来的generateFiles流程就是用的IntrospectedTableMyBatis3Impl里边的方法
context.generateFiles生成GeneratedJavaFile/GeneratedXmlFile对象,用于后面生成文件,可以说这里边即将构建生成的就是最终的文件结构,后面的writeFile生成文件也只是读取里边的信息生成文件而已:
public void generateFiles(ProgressCallback callback,
List generatedJavaFiles,
List generatedXmlFiles, List warnings)
throws InterruptedException {
//......略
if (introspectedTables != null) {
for (IntrospectedTable introspectedTable : introspectedTables) {
callback.checkCancel();
//这里的initialize/calculateGenerators/getGeneratedJavaFiles方法都是调用runtime对应实现类里边的方法
introspectedTable.initialize();
introspectedTable.calculateGenerators(warnings, callback);
generatedJavaFiles.addAll(introspectedTable
.getGeneratedJavaFiles());
generatedXmlFiles.addAll(introspectedTable
.getGeneratedXmlFiles());
generatedJavaFiles.addAll(pluginAggregator
.contextGenerateAdditionalJavaFiles(introspectedTable));
generatedXmlFiles.addAll(pluginAggregator
.contextGenerateAdditionalXmlFiles(introspectedTable));
}
}
generatedJavaFiles.addAll(pluginAggregator
.contextGenerateAdditionalJavaFiles());
generatedXmlFiles.addAll(pluginAggregator
.contextGenerateAdditionalXmlFiles());
}
主要分析里边调用的IntrospectedTableMyBatis3Impl的3个方法(initialize,calculateGenerators,getGeneratedJavaFiles)
首先是初始化initialize:
public void initialize() {
//设置java客户端接口的属性
calculateJavaClientAttributes();
//设置model实体类的属性
calculateModelAttributes();
//设置XML
calculateXmlAttributes();
//......
}
protected void calculateJavaClientAttributes() {
//......
sb.setLength(0);
sb.append(calculateJavaClientInterfacePackage());
sb.append('.');
sb.append(fullyQualifiedTable.getDomainObjectName());
sb.append("DAO"); //$NON-NLS-1$
setDAOInterfaceType(sb.toString());//DAO接口的名称!
sb.setLength(0);
sb.append(calculateJavaClientInterfacePackage());
sb.append('.');
if (stringHasValue(tableConfiguration.getMapperName())) {//设置了Mapper
sb.append(tableConfiguration.getMapperName());
} else {
sb.append(fullyQualifiedTable.getDomainObjectName());
sb.append("Mapper"); //$NON-NLS-1$
}
setMyBatis3JavaMapperType(sb.toString());
}
这里我们可以发现Mapper的设置,以及产生一个疑问:DAOInterfaceType明明单独设置了接口是DAO为什么生成的时候却变成跟下面的Mapper同样的结尾?
接着看calculateGenerators方法
public class IntrospectedTableMyBatis3Impl extends IntrospectedTable {
@Override
public void calculateGenerators(List warnings, ProgressCallback progressCallback) {
//生成javaClientGenerator
AbstractJavaClientGenerator javaClientGenerator = calculateClientGenerators(warnings, progressCallback);
}
protected AbstractJavaClientGenerator calculateClientGenerators(List warnings, ProgressCallback progressCallback) {
AbstractJavaClientGenerator javaGenerator = createJavaClientGenerator();
return javaGenerator;
}
protected AbstractJavaClientGenerator createJavaClientGenerator() {
if (context.getJavaClientGeneratorConfiguration() == null) {
return null;
}
String type = context.getJavaClientGeneratorConfiguration()
.getConfigurationType();
AbstractJavaClientGenerator javaGenerator;
if ("XMLMAPPER".equalsIgnoreCase(type)) { //$NON-NLS-1$
javaGenerator = new JavaMapperGenerator();
} else if ("MIXEDMAPPER".equalsIgnoreCase(type)) { //$NON-NLS-1$
javaGenerator = new MixedClientGenerator();
} else if ("ANNOTATEDMAPPER".equalsIgnoreCase(type)) { //$NON-NLS-1$
javaGenerator = new AnnotatedClientGenerator();
} else if ("MAPPER".equalsIgnoreCase(type)) { //$NON-NLS-1$
javaGenerator = new JavaMapperGenerator();
} else {
javaGenerator = (AbstractJavaClientGenerator) ObjectFactory
.createInternalObject(type);
}
return javaGenerator;
}
}
javaClientGenerator标签配置客户端代码,我们使用的是XMLMAPPER单独生成XML和接口文件的方式,对应代码这里会使用JavaMapperGenerator这个生成器,在getGeneratedJavaFiles方法中我们主要看到调用javaGenerator.getCompilationUnits();
public class JavaMapperGenerator extends AbstractJavaClientGenerator {
@Override
public List getCompilationUnits() {
progressCallback.startTask(getString("Progress.17", //$NON-NLS-1$
introspectedTable.getFullyQualifiedTable().toString()));
CommentGenerator commentGenerator = context.getCommentGenerator();
//使用的是MyBatis3JavaMapperType,而不是DAOInterfaceType
FullyQualifiedJavaType type = new FullyQualifiedJavaType(
introspectedTable.getMyBatis3JavaMapperType());
//......
return answer;
}
}
这里发现在java客户端代码生成器里边统一使用的都是MyBatis3JavaMapperType,猜测是这里写死了值导致的,代码改成introspectedTable.getDAOInterfaceType()后再install执行插件,果然变成了DAO结尾:
修改为DAOInterfaceType后
我们也可以通过writeGeneratedJavaFile生成文件时获取文件名的方法找到原因:
public String getFileName() {return compilationUnit.getType().getShortNameWithoutTypeArguments() + ".java";}
调用FullyQualifiedJavaType的baseShortName属性,就是上面通过构造方法传参解析出来的
最后了解了MyBatis Generator的工作流程我们也可以参考有mapperName的地方添加多一个daoName,实现接口文件和xml文件各自的自定义属性:
①最开始解析XML的parseTable添加String daoName = attributes.getProperty("daoName");
②introspectedTable.initialize();中的calculateJavaClientAttributes方法参考Mapper修改DAO
③给本地项目中XML文档也添加个daoName属性resources\org\mybatis\generator\config\xml\mybatis-generator-config_1_0.dtd
④install后执行效果:
添加自定义daoName属性
总结补充:修改core项目源码后调试时需要重新install一次,不然远程调试不会生效
mybatis plug 只查id_MyBatis Generator的一个问题引发对插件的修改相关推荐
- mybatis plug 只查id_Mybatis一对多/多对多查询时只查出了一条数据
问题描述: 如果三表(包括了关系表)级联查询,主表和明细表的主键都是id的话,明细表的多条数据只能查询出来第一条/最后一条数据. 三个表,权限表(Permission),权限组表(Permission ...
- mybatis plug 只查id_mybatis-plugin的几种常用的方法
一.逻辑删除 1.实体类添加 /** * 0 正常 1 删除 */ @TableLogic private Integer deleted; 2.配置文件添加 mybatis-plus.global- ...
- Mybatis 详细的创建流程及创建第一个Mybatis增删改查程序 CRUD
1.idea新建Maven项目Mybatis-study 将项目里的src文件夹删掉 依次将此项目作为父项目 2.在Mybatis-study中新建模块mybatis-01 在myba ...
- 事务中mybatis通过id查不到但是通过其他条件可以查到_40打卡 MyBatis 学习
第57次(mybatis) 学习主题:mybatis 学习目标: 1 掌握框架的概念 2 掌握mybatis环境搭建 对应视频: http://www.itbaizhan.cn/course/id/8 ...
- SSM框架——Mybatis增删改查
目录 目录 环境配置 增删改查的实现 查询全部 查询单个ID 添加用户 修改用户 删除用户 增删改查-使用注解开发 思路流程:搭建环境-->导入Mybatis--->编写代码---> ...
- mybatis增删改查快速实现!!!
Mybatis 简介 ** 1.什么是Mybatis ** MyBatis是一款优秀的基于java的持久层框架,它内部 封装了jdbc,使开发者只需要关注sql语句本身. 参考文档 :https:// ...
- ”一个馒头引发的血案“|记Mybatis之BindingException异常的产生及解决过程
一. 业务场景 前几天壹哥带学生做一个项目,需要更新数据库中的车辆信息表,具体需求是要根据指定车辆的设备id(编号和设备ID均非主键)来更新车辆信息.壹哥要求学生们用Mybatis进行实现,所以就在对 ...
- 一只公鸡5块钱,一只母鸡3块钱,3只小鸡一块钱,一个农夫用100块钱买100只鸡(不许解方程),怎么实现?编写java程序。...
一只公鸡5块钱,一只母鸡3块钱,3只小鸡一块钱,一个农夫用100块钱买100只鸡(不许解方程),怎么实现?编写java程序. public class A {public static void ma ...
- 一道经典的面试题:一只公鸡5块钱,一只母鸡3块钱,3只小鸡一块钱,一个农夫用100块钱买100只鸡(编写java程序)...
一只公鸡5块钱,一只母鸡3块钱,3只小鸡一块钱,一个农夫用100块钱买100只鸡(不许解方程),怎么实现?编写java程序. public class BuyChicken { public stat ...
最新文章
- Asp.net core 学习笔记 ( Web Api )
- 用 HTTPS 就安全了?HTTPS 会被抓包吗?
- POJ1195Mobile phones
- sql 找到最近的值_数据分析——SQL查询(常用函数)
- HttpContext.Cache属性
- ubuntu使用redis和宝塔面板
- php 连接数据库 pod,PHP PDO类解决数据库连接问题
- caffe 在 windows 使用
- .NET生成漂亮桌面背景
- 依赖注入通俗解释_我如何向团队解释依赖注入
- SVN或其他网盘类软件同步图标不显示的异常
- 2021 最新 IDEA集成Gitee、Gitee迁移GitHub【图文讲解】
- Linux学习笔记-最基础的常用shell命令
- 20200119:(leetcode)回文数(3种解法)
- 对jfinal中enjoy的理解
- 写给测试小白:怎么快速找到bug?怎么写测试用例?
- 【转】Android游戏框架AndEngine使用入门
- 搜狗输入法 linux 卸载,ubuntu彻底卸载搜狗拼音输入法
- 【TI-AM5728】(1)开发环境搭建
- 2014 360校园招聘技术类笔试题
热门文章
- 剑指offer——面试题7:用两个栈实现队列
- Latex中的常用公式模板
- symbol lookup error: undefined symbol:PySlice_Unpack
- python : 正确复制列表的方法
- C#并行和多线程编程
- Codeforces 754A(搜索)
- PHP+MYSQL 出现乱码的解决方法
- 针对ASP.NET页面实时进行GZIP压缩优化的几款压缩模块的使用简介及应用测试!(附源码)...
- solr6.6.2之拼音联想
- 运维学习之自动化安装系统的配置