网站首页 > 技术文章 正文
自定义一个简单的 ORM 框架
前面我们通过跟踪源码的方式剖析了Hibernate 和MyBatis 两个框架是如何应用 ORM 思想的,接下来我们自己定义一个简单的 ORM 框架(名为 MiniORM),希望能通过这种方式让大家亲自零距离的去应用一下 ORM。
4.1MiniORM 框架的结构设计
1.第一层为配置层:
?miniORM.cfg.xml 是框架的核心配置文件,主要用来设置数据库连接信息和映射配置文件路径信息
?Xxx.mapper.xml 是框架的映射配置文件,主要用来设置类和表之间以及属性和字段之间的映射关系
?Xxx.java 是带有映射注解的实体类,主要用来设置类和表之间以及属性和字段之间的映射关系,和 Xxx.mapper.xml 的作用一样,只不过采用的是注解方式,两者二选一
2.第二层为解析层:
?Dom4jUtil 类用来解析 miniORM.cfg.xml 和Xxx.mapper.xml 两个配置文件的数据
?AnnotationUtil 类用来解析实体类中的映射注解
3.第三层为封装层:
?ORMConfig 类用来封装和存储从 miniORM.cfg.xml 文件中解析得到的数据
?Mapper 类用来封装和存储从 Xxx.mapper.xml 或实体类中解析得到的映射数据
4.第四层为功能层:
?ORMSession 类主要用来从 ORMConfig 和 Mapper 中获取相关数据,然后生成 sql 语句, 最后通过对 JDBC 的封装最终实现增删改查功能
4.2MiniORM 框架的代码实现
1.pom.xml
<groupId>cn.itcast.framework.miniorm</groupId>
<artifactId>MiniORM</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
<build>
<finalName>MiniORM</finalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<compilerArguments>
<bootclasspath>${JAVA_HOME}/jre/lib/rt.jar</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
MiniORM 框架依赖 dom4j 和 jdk1.8, 编译时会打成 jar 包并 install 到本地仓库中,如下图所示:
2.miniORM.cfg.xml 是框架的核心配置文件,主要用来设置数据库连接信息和映射配置文件路径信息,源码如下所示:
<?xml version='1.0' encoding='utf-8'?> <orm-factory> <!--数据库连接数据--> <property name="connection.url">jdbc:mysql://localhost:3306/test</property> <property name="connection.driverClass">com.mysql.jdbc.Driver</property> <property name="connection.username">root</property> <property name="connection.password">123</property> <!--采用 xml 配置映射数据--> <mapping resource="cn/itcast/orm/test/entity/Book.mapper.xml"/> <!--采用实体类注解配置映射数据--> <entity package="cn.itcast.orm.test.entity"/> </orm-factory>
3.Xxx.mapper.xml 是框架的映射配置文件,主要用来设置类和表之间以及属性和字段之间的映射关系,以Book.mapper.xml 为例,源码如下所示:
<?xml version='1.0' encoding='UTF-8'?> <!--实体类和表之间的映射关系配置--> <orm-mapping> <class name="cn.itcast.orm.test.entity.Book" table="t_book"> <id name="id" column="bid"/> <property name="name" column="bname"/> <property name="author" column="author"/> <property name="price" column="price"/> </class> </orm-mapping>
4.当然 MiniORM 框架也支持在实体类上以注解方式去配置映射关系,以 Book.java 为例,源码如下所示:
import cn.itcast.orm.annotation.ORMColumn; import cn.itcast.orm.annotation.ORMId; import cn.itcast.orm.annotation.ORMTable; //实体类:图书@ORMTable(name = "t_book") public class Book { @ORMId @ORMColumn(name = "bid") private int id; //主键 @ORMColumn(name="bname") private String name; //图书名字 @ORMColumn(name="author") private String author; //图书作者 @ORMColumn(name="price") private double price; //图书价格 ... ... }
实体类中的@ORMTable、@ORMId、@ORMColumn 是我们自定义的三个注解,@ORMTable 用来设置当前类和哪个表对应,@ORMColumn 用来设置当前属性和表中哪个字段对应, @ORMId 用来设置哪个属性对应的字段是主键。
5.Dom4jUtil 类是一个基于 Dom4j 的工具类, 主要用来解析 miniORM.cfg.xml 和Xxx.mapper.xml,源码如下所示:
public class Dom4jUtil {
/**
*通过文件的路径获取 xml 的 document 对象
*
*@param path 文件的路径
*@return 返回文档对象
*/
public static Document getXMLByFilePath(String path) { if (null == path) {
return null;
}
Document document = null; try {
SAXReader reader = new SAXReader(); document = reader.read(new File(path));
} catch (Exception e) { e.printStackTrace();
}
return document;
}
/**
*获得某文档中某元素内某属性的值和元素的文本信息
*
*@param documentxml 文档对象
*@param elementName 元素名
*@param attrName属性名
*@return 返回一个 Map 集合
*/
public static Map<String, String> Elements2Map(Document document, String elementName,
String attrName) {
List<Element> propList = document.getRootElement().elements(elementName); Map<String, String> propConfig = new HashMap<>();
for (Element element : propList) {
String key = element.attribute(attrName).getValue(); String value = element.getTextTrim(); propConfig.put(key, value);
}
return propConfig;
}
/**
*针对 mapper.xml 文件,获得映射信息并存到 Map 集合中
*@param document xml 文档对象
*@return 返回一个 Map 集合
*/
public static Map<String, String> Elements2Map(Document document) { Element classElement = document.getRootElement().element("class"); Map<String, String> mapping = new HashMap<>();
Element idElement = classElement.element("id"); String idKey = idElement.attribute("name").getValue();
String idValue = idElement.attribute("column").getValue();
mapping.put(idKey, idValue);
List<Element> propElements = classElement.elements("property"); for (Element element : propElements) {
String propKey = element.attribute("name").getValue(); String propValue = element.attribute("column").getValue(); mapping.put(propKey, propValue);
}
return mapping;
}
/**
*针对 mapper.xml 文件,获得主键的映射信息并存到 Map 集合中
*
*@param document xml 文档对象
*@return 返回一个 Map 集合
*/
public static Map<String, String> ElementsID2Map(Document document) { Element classElement = document.getRootElement().element("class"); Map<String, String> mapping = new HashMap<>();
Element idElement = classElement.element("id"); String idKey = idElement.attribute("name").getValue();
String idValue = idElement.attribute("column").getValue(); mapping.put(idKey, idValue);
return mapping;
}
/**
*获得某文档中某元素内某属性的值
*
*@param documentxml 文档对象
*@param elementName 元素名
*@param attrName属性名
*@return 返回一个 Set 集合
*/
public static Set<String> Elements2Set(Document document, String elementName, String attrName) { List<Element> mappingList = document.getRootElement().elements(elementName);
Set<String> mappingSet = new HashSet<>(); for (Element element : mappingList) {
String value = element.attribute(attrName).getValue(); mappingSet.add(value);
}
return mappingSet;
}
/**
*获得某文档中某元素内某属性的值
*
*@param documentxml 文档对象
*@param elementName 元素名
*@param attrName属性名
*@return 返回一个 Set 集合
*/
public static String getPropValue(Document document, String elementName, String attrName) { Element element = (Element) document.getRootElement().elements(elementName).get(0); return element.attribute(attrName).getValue();
}
}
6.AnnotationUtil 类主要用来通过反射技术解析实体类中的注解,从而获得映射数据,源码如下所示:
public class AnnotationUtil {
/*
得到的类名
*/
public static String getClassName(Class clz) { return clz.getName();
}
/*
得到ORMTable 注解中的表名
*/
public static String getTableName(Class clz) {
if (clz.isAnnotationPresent(ORMTable.class)) {
ORMTable ormTable = (ORMTable) clz.getAnnotation(ORMTable.class); return ormTable.name();
} else {
System.out.println("缺少 ORMTable 注解"); return null;
}
}
/*
得到主键属性和对应的字段
*/
public static Map<String, String> getIdMapper(Class clz) { boolean flag = true;
Map<String, String> map = new HashMap<>(); Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(ORMId.class)) { flag = false;
String fieldName = field.getName();
if (field.isAnnotationPresent(ORMColumn.class)) {
ORMColumn ormColumn = field.getAnnotation(ORMColumn.class); String columnName = ormColumn.name();
map.put(fieldName, columnName); break;
} else {
System.out.println("缺少 ORMColumn 注解");
}
}
}
if (flag) {
System.out.println("缺少 ORMId 注解");
}
return map;
}
/*
得到类中所有属性和对应的字段
*/
public static Map<String, String> getPropMapping(Class clz) { Map<String, String> map = new HashMap<>(); map.putAll(getIdMapper(clz));
Field[] fields = clz.getDeclaredFields(); for (Field field : fields) {
if (field.isAnnotationPresent(ORMColumn.class)) {
ORMColumn ormColumn = field.getAnnotation(ORMColumn.class); String fieldName = field.getName();
String columnName = ormColumn.name(); map.put(fieldName, columnName);
}
}
return map;
}
/*
获得某包下面的所有类名
*/
public static Set<String> getClassNameByPackage(String packagePath) { Set<String> names = new HashSet<>();
String packageFile = packagePath.replace(".", "/");
String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); if (classpath == null) {
classpath = Thread.currentThread().getContextClassLoader().getResource("/").getPath();
}
try {
classpath = java.net.URLDecoder.decode(classpath, "utf-8");
} catch (UnsupportedEncodingException e) { e.printStackTrace();
}
File dir = new File(classpath + packageFile); if (dir.exists()) {
File[] files = dir.listFiles(); for (File f : files) {
String name = f.getName();
if (f.isFile() && name.endsWith(".class")) {
name = packagePath + "." + name.substring(0, name.lastIndexOf(".")); names.add(name);
}
}
} else {
System.out.println("包路径不存在");
}
return names;
}
}
7.Mapper 类用来封装并存储从 Xxx.mapper.xml 中或从实体类中解析得到的映射信息,哪个表和哪个类映射,哪个字段和哪个属性映射等等,源码如下所示:
public class Mapper { private String className; //类名private String tableName; //表名 private Map<String,String> idMapper=new HashMap(); //主键字段和属性private Map<String,String> propMapping=new HashMap(); //非主键字段和属性 ... ... }
8.ORMConfig 类主要用来存储 miniORM.cfg.xml 配置文件中的信息和 Mapper 映射信息,该类内部会使用 Dom4jUtil、AnnotationUtil 工具类去解析数据,源码如下所示:
public class ORMConfig { public static String classpath; //类路径public static File cfgFile; //核心配置文件 public static Map<String, String> propConfig; //核心配置文件数据public static Set<String> mappingSet; //映射配置文件 public static Set<String> entitySet; //实体类 public static List<Mapper> mapperList; //解析出来的 Mapper // 从 classpath 中加载框架的核心配置文件 miniORM.cfg.xml static { classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); if (classpath == null) { classpath = Thread.currentThread().getContextClassLoader().getResource("/").getPath(); } try { classpath = java.net.URLDecoder.decode(classpath, "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } cfgFile = new File(classpath + "miniORM.cfg.xml"); if (cfgFile.exists()) { Document document = Dom4jUtil.getXMLByFilePath(cfgFile.getPath()); propConfig = Dom4jUtil.Elements2Map(document, "property", "name"); mappingSet = Dom4jUtil.Elements2Set(document, "mapping", "resource"); entitySet = Dom4jUtil.Elements2Set(document, "entity", "package"); } else { cfgFile = null; System.out.println("未找到核心配置文件 miniORM.cfg.xml"); } } //从 propConfig 获得信息,连接数据库 private Connection getConnection() throws ClassNotFoundException, SQLException { String url = propConfig.get("connection.url"); String driverClass = propConfig.get("connection.driverClass"); String username = propConfig.get("connection.username"); String password = propConfig.get("connection.password"); Class.forName(driverClass); Connection connection = DriverManager.getConnection(url, username, password); connection.setAutoCommit(true); return connection; } //从 mappingSet 中挨个解析 mapper.xml 配置文件,获得实体类和表之间的映射信息 //从 entitySet 中挨个解析实体类中的注解,获得实体类和表之间的映射信息 private void getMapping() throws ClassNotFoundException { mapperList = new ArrayList<>(); for (String xmlPath : mappingSet) { Document document = Dom4jUtil.getXMLByFilePath(classpath + xmlPath); Map<String, String> mapping = Dom4jUtil.Elements2Map(document); String className = Dom4jUtil.getPropValue(document, "class", "name"); String tableName = Dom4jUtil.getPropValue(document, "class", "table"); Map<String, String> id_id = Dom4jUtil.ElementsID2Map(document); Mapper mapper = new Mapper(); mapper.setClassName(className); mapper.setTableName(tableName); mapper.setIdMapper(id_id); mapper.sePropMapping(mapping); mapperList.add(mapper); } for (String packagePath : entitySet) { Set<String> nameSet = AnnotationUtil.getClassNameByPackage(packagePath); for (String name : nameSet) { Class clz = Class.forName(name); String className = AnnotationUtil.getClassName(clz); String tableName = AnnotationUtil.getTableName(clz); Map<String, String> id_id = AnnotationUtil.getIdMapper(clz); Map<String, String> mapping = AnnotationUtil.getPropMapping(clz); Mapper mapper = new Mapper(); mapper.setClassName(className); mapper.setTableName(tableName); mapper.setIdMapper(id_id); mapper.sePropMapping(mapping); mapperList.add(mapper); } } } public ORMSession buildORMSession() throws Exception { //从 propConfig 获得信息,连接数据库 Connection connection = getConnection(); //从 mappingSet 中挨个解析 mapper.xml 配置文件,获得实体类和表之间的映射信息 getMapping(); //创建 ORMSession 对象 return new ORMSession(connection); } }
9.ORMSession 类主要用来从 ORMConfig 和 Mapper 中获取相关数据,然后生成 sql 语句,最后通过对 JDBC 的封装最终实现增删改查功能,源码如下所示:
public class ORMSession { private Connection connection; public ORMSession(Connection conn) { this.connection = conn; } //保存数据 public void save(Object entity) throws Exception { String insertSQL = ""; //1. 从 ORMConfig 中获得保存有映射信息的集合 List<Mapper> mapperList = ORMConfig.mapperList; //2. 遍历集合,从集合中找到和 entity 参数相对应的 mapper 对象 for (Mapper mapper : mapperList) { if (mapper.getClassName().equals(entity.getClass().getName())) { String tableName = mapper.getTableName(); String insertSQL1 = "insert into " + tableName + "( "; String insertSQL2 = " ) values ( "; //3. 得到当前对象所属类中的所有属性 Field[] fields = entity.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); //4. 遍历过程中根据属性得到字段名 String columnName = mapper.getPropMapper().get(field.getName()); //5. 遍历过程中根据属性得到它的值 String columnValue = field.get(entity).toString(); //6. 拼接 sql 语句 insertSQL1 += columnName + ","; insertSQL2 += "'" + columnValue + "',"; } insertSQL = insertSQL1.substring(0, insertSQL1.length() - 1) + insertSQL2.substring(0, insertSQL2.length() - 1) + " )"; break; } } // 把 sql 语句打印到控制台 System.out.println("MiniORM-save: " + insertSQL); //7. 通过 JDBC 发送并执行 sql PreparedStatement statement = connection.prepareStatement(insertSQL); statement.executeUpdate(); statement.close(); } //根据主键进行数据删除 delete from 表名 where 主键 = 值 public void delete(Object entity) throws Exception { String delSQL = "delete from "; //1. 从 ORMConfig 中获得保存有映射信息的集合 List<Mapper> mapperList = ORMConfig.mapperList; //2. 遍历集合,从集合中找到和 entity 参数相对应的 mapper 对象 for (Mapper mapper : mapperList) { if (mapper.getClassName().equals(entity.getClass().getName())) { // 3. 得到我们想要的 mapper 对象,并得到表名String tableName = mapper.getTableName(); delSQL += tableName + " where "; // 4. 得到主键的字段名和属性名 Object[] idProp = mapper.getIdMapper().keySet().toArray(); //idProp[0] Object[] idColumn = mapper.getIdMapper().values().toArray(); //idColumn[0] // 5. 得到主键的值 Field field = entity.getClass().getDeclaredField(idProp[0].toString()); field.setAccessible(true); String idVal = field.get(entity).toString(); // 6. 拼接 sql delSQL += idColumn[0].toString() + " = " + idVal; // 把 sql 语句打印到控制台 System.out.println("MiniORM-delete: " + delSQL); break; } } //7. 通过 JDBC 发送并执行 sql PreparedStatement statement = connection.prepareStatement(delSQL); statement.executeUpdate(); statement.close(); } // 根据主键进行查询 select * from 表名 where 主键字段 = 值 public Object findOne(Class clz, Object id) throws Exception{ String querySQL = "select * from "; //1. 从 ORMConfig 中得到存有映射信息的集合 List<Mapper> mapperList=ORMConfig.mapperList; //2. 遍历集合拿到我们想要的 mapper 对象 for (Mapper mapper : mapperList) { if (mapper.getClassName().equals(clz.getName())) { // 3. 获得表名 String tableName = mapper.getTableName(); //4. 获得主键字段名 Object[] idColumn = mapper.getIdMapper().values().toArray(); //idColumn[0] //5. 拼接 sql querySQL += tableName + " where " + idColumn[0].toString() + " = " + id; break; } } System.out.println("MiniORM-findOne:" +querySQL); //6. 通过 jdbc 发送并执行 sql, 得到结果集 PreparedStatement statement=connection.prepareStatement(querySQL); ResultSet rs=statement.executeQuery(); //7. 封装结果集,返回对象 if(rs.next()){ // 查询到一行数据 // 8.创建一个对象,目前属性的值都是初始值Object obj=clz.newInstance(); // 9. 遍历 mapperList 集合找到我们想要的 mapper 对象 for(Mapper mapper:mapperList){ if (mapper.getClassName().equals(clz.getName())) { //10. 得到存有属性-字段的映射信息 Map<String,String> propMap = mapper.getPropMapper(); //11. 遍历集合分别拿到属性名和字段名Set<String> keySet = propMap.keySet(); for(String prop:keySet){ //prop 就是属性名 String column = propMap.get(prop); //column 就是和属性对应的字段名 Field field = clz.getDeclaredField(prop); field.setAccessible(true); field.set(obj,rs.getObject(column)); } break; } } //12. 释放资源statement.close(); rs.close(); //13. 返回查询出来的对象 return obj; }else { // 没有查到数据 return null; } } //关闭连接,释放资源 public void close() throws Exception{ if(connection!=null){ connection.close(); connection = null; } } }
4.3MiniORM 框架的测试使用
我们自定义的 MiniORM 框架主要用来体现 ORM 思想,并不是为了开发一个成熟的持久层框架出来,因此很多逻辑并不完善,很多情况也未去考虑,请各位理解。接下来我们就测试一下该框架。
1.pom.xml
<groupId>cn.itcast.orm</groupId> <artifactId>TestMiniORM</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>cn.itcast.framework.miniorm</groupId> <artifactId>MiniORM</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.36</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies> <build> <resources> <resource> <directory>src/main/java</directory> <includes> <!--为了编译时能加载包中的 xml 文件--> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> <resource> <!--为了编译时能加载 resources 中的 xml 文件--> <directory>src/main/resources</directory> <includes> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources> </build>
我们前面把 MiniORM 框架打成 jar 包并 install 到了本地 Maven 仓库中,因此在使用该框架时需要从本地仓库进行加载。
2.miniORM.cfg.xml
<?xml version='1.0' encoding='utf-8'?> <orm-factory> <!--数据库连接--> <property name="connection.url">jdbc:mysql://localhost:3306/test</property> <property name="connection.driverClass">com.mysql.jdbc.Driver</property> <property name="connection.username">root</property> <property name="connection.password">123</property> <!--采用 xml 配置映射数据--> <mapping resource="cn/itcast/orm/test/entity/Book.mapper.xml"/> <!--采用注解配置映射数据--> <entity package="cn.itcast.orm.test.entity"/> </orm-factory>
这是框架的核心配置文件,我们既采用了 xml 方式配置映射数据,也采用了注解方式在实体
类中配置映射数据,最后可以二选一分别进行功能测试。
3.实体类和映射配置文件
@ORMTable(name = "t_book") public class Book { @ORMId @ORMColumn(name = "bid") private int id; //主键 @ORMColumn(name="bname") private String name; //图书名字 @ORMColumn(name="author") private String author; //图书作者 @ORMColumn(name="price") private double price; //图书价格 ... ... } <?xml version='1.0' encoding='UTF-8'?> <!--实体类和表之间的映射关系配置--> <orm-mapping> <class name="cn.itcast.orm.test.entity.Book" table="t_book"> <id name="id" column="bid"/> <property name="name" column="bname"/> <property name="author" column="author"/> <property name="price" column="price"/> </class> </orm-mapping>
注意:对于同一个表或实体类,不需要既进行 xml 配置,又进行注解配置,二选一即可,这
里同时进行配置只是为了测试方便。
测试类
public class BookDao { private ORMConfig config; @Before public void init() { config = new ORMConfig(); } @Test public void testSave() throws Exception { ORMSession session = config.buildORMSession(); Book book = new Book(); book.setId(1); book.setName("降龙十八掌"); book.setAuthor("不知道"); book.setPrice(9.9); session.save(book); session.close(); } @Test public void testDelete() throws Exception { ORMSession session = config.buildORMSession(); Book book = new Book(); book.setId(1); session.delete(book); session.close(); } @Test public void testFindOne() throws Exception { ORMSession session = config.buildORMSession(); Book book = (Book) session.findOne(Book.class, 1); System.out.println(book); session.close(); }
?我们调用 ORMSession 类的 save 方法完成了数据保存功能,由 MiniORM 框架生成 sql语句,运行效果如下图所示:
?我们调用 ORMSession 类的 findOne 方法完成了数据查询功能,由 MiniORM 框架生成sql语句,运行效果如下图所示:
?我们调用 ORMSession 类的 delete 方法完成了数据删除功能,由 MiniORM 框架生成 sql语句,运行效果如下图所示:
- 上一篇: 卧槽!原来 IOC 这么简单
- 下一篇: 什么是IOC?教你手撸一个IOC容器
猜你喜欢
- 2024-09-22 MyBatis模拟实现
- 2024-09-22 成都校区*精品*Dom4J解析XML的范例浅析
- 2024-09-22 Jenkins Nested View插件XXE漏洞(CVE-2021-21680)分析
- 2024-09-22 Java互联网架构-Spring IOC底层源码分析
- 2024-09-22 XML文件
- 2024-09-22 Dom4j解析xml的小demo
- 2024-09-22 java全栈CMS:AOP+Freemarke代码自动生成4
- 2024-09-22 专科生逆袭入职阿里P6,成人生赢家。网友:我应该也可以
- 2024-09-22 在IDEA的maven项目中连接使用MySQL8.0方法教程
- 2024-09-22 Spring:IOC 控制反转
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)