手写orm框架

作者: z七夜 | 来源:发表于2018-06-17 09:07 被阅读29次

    写在前面

    ORM框架,关系映射框架,只需要提供持久化类和数据库表之间的关系,就可以在运行时候进行数据持久化,通常javaweb使用hibernate或者mybatis,本文就是写了一个简易版的orm框架。。只是实现了部分功能

    功能介绍

    1.可以根据表结构自动生成实体类
    2.可直接插入,修改删除对象,不需要写sql语句
    3.在查询时候需要自己写SQL语句,支持多行,单行,某一个字段的查询
    4.支持自定义实体类查询,当在进行连表查询的时候,得到的数据是自定义的,可自定义一个实体类进行接收
    5.连接使用了连接池,效率相对高一点

    1.接口设计

    • Query接口:符合查询,对外提供服务的核心类
    • QueryFactory类:管理query对象
    • TypeConvertor接口:负责数据类型转换
    • TableContext类:负责获取数据库的表结构和类结构的关系,并可以根据表结构生成类结构,
    • DBManager类:根据配置信息,维持连接对象的管理
      工具类
      JdbcUtils 用来封装通用的jdbc操作
      StringUtils 用来封装常用的字符串操作
      ReflectUtils 用来封装反射相关的操作
      PathUtils 用来操作本地文件路径相关的操作
      JavaFileUtils
      核心bean
    • COlumnInfo 封装表中一个字段的信息
    • Configuration 封装整个项目的配置信息
    • TableInfo: 封装一张表的信息
    • JavaFeildInfo:用于封装一个属性和get set方法的数据(在自动生成java文件的时候用)

    2.具体实现

    1.query接口设计

    框架封装了常用的几个方法

    //将某个实体类持久化到数据库中
    public void insert(Object object);
    //从数据库中删除某个数据
     public void delete(Object object)
    //从数据库中删除某条数据,指定id
     public void delete(Class clazz,Object id) ;
    //更新某条数据
    public void update(Object object,String[] fieldNames);
    //根据传入的sql语句进行 查询数据,某个类型的数据
     public List queryRows(String sql,Class clazz,Object[] params);
    //查找某一列的数据
     public Object queryValue(String sql,Object[] params);
    //执行增删改sql语句的方法
    public int executeDML(String sql,Object[] params);
    

    2.数据类型转换器

    因为不知道项目用的什么数据库,所以数据类型也不一样,类型转换器是一个接口,提供了实现,现在只提供了mysql数据类型转换器

    2.2.1

    转换器有两个方法,一个是java数据类型转换成数据库数据类型,另一个是数据库类型转换成java数据类型,因为此项目需要根据数据库表生成java文件,所以只实现了数据库类型转换成java数据类型,如果需要另外转换,只需要反过来即可

    3.Configuration封装配置信息

    配置文件是db.properties,在src下

    driver = com.mysql.jdbc.Driver //数据库驱动
    username = root  //用户名
    password = 123456 //密码
    useDB = mysql  //声明使用的数据库
    url = jdbc:mysql://localhost:3306/test // 连接url
    packageName = jk.zmn.sorm.pojo  //声明自动生成的实体类放在当前项目哪个包下,可不存在
    

    这个类主要是用来封装用户的配置信息,将配置信息保存起来,使用更方便,配置类即是将这个属性都封装起来

    4.DBManager类

    这个类主要是用来,读取用户的配置信息然后将信息封装到configuration类中,管理数据库连接,等功能


    2.3.1 封装配置信息

    图2.3.1封装配置信息

    2.3.2

    图2.3.2是管理数据库连接,后期将会增加连接池功能

    4.TableContext类

    此类用来连接数据库,得到库中的表,并封装起来,并且根据表信息自动生成java文件,当生成java实体类的时候,需要使用map将实体类的Class文件和表信息对应存储起来,用于数据持久化,后面会讲到

    5.自动生成java实体类实现

    约定:如何根据表信息生成java类呢,我们先看看平时手动生成的java实体类是什么样子的,如表user,或_user 或t_user 再看看字段,id,name,或者t_id,t_name,我们自己写实体类会写成什么样子呢,Class User 属性写成 id,name或tname,tid, 那我们就先来个约定,如表名user,生成的类就叫User,首字母大写,去掉下划线,字段也是一样,只需要去掉下划线,get和set方法是getId 或者setName,首字母大写,
    思路: 上面我们已经得到了数据库中所有的表信息,也有了生成实体类的约定,那到底怎么生成呢,1.我们确定类名,根据表名得到得到类名, 2.属性如何生成呢,还有get set方法,我们将一个属性和一个getset 方法封装在一个类中,根据表中的字段信息,来生成一个字段对应的属性的相关信息,然后将所有字段的信息整合在这个类中,具体实现一下,我们还缺一些东西

    5.1 ColumnInfo 类

    封装表中一个字段的信息,什么意思,
    封装了三个属性,

      /**
        * 字段名称
        */
        private String name;
    
        /**
         * 字段数据类型
         */
        private String dataType;
    
        /**
         * 字段键类型 0普通键,1主键 2外键
         */
        private int keyType;
    

    5.2TableInfo

    封装一张表的信息

      /**
         * 表名
         */
        private String name;
        /**
         * 存放字段信息, 字段名和字段信息
         */
        private Map<String,ColumnInfo> columns;
        /**
         * 主键信息
         */
        private ColumnInfo onlyPriKey;
        /**
         * 联合主键
         */
        private List<ColumnInfo> priKeys;
    

    5.3JavaFeildInfo

    用于封装一个属性和get set方法的数据,最后拼接类文件的时候用

     /**
         * 属性信息 private String feild
         */
        private String feildInfo;
        /**
         * 封装get方法
         */
        private String getFeildInfo;
        /**
         * 封装set方法
         */
        private String setFeildInfo;
    

    有了上面的基本数据,就可以进行操作了

    5.4 开始生成文件

    上面介绍了tablecontext类,用来得到数据库中的表,使用map封装起来,附上源码,接着下面看

    
        /**
         * 表名为key,表信息对象为value
         */
        public static Map<String,TableInfo> tables = new HashMap<String,TableInfo>();
    
        /**
         * 将po的class对象和表信息对象关联起来,便于重用!
         */
        public static  Map<Class,TableInfo>  poClassTableMap = new HashMap<Class,TableInfo>();
    
        private TableContext(){}
    
        static {
            try {
                //初始化获得表的信息
                Connection con = DBManager.getConnection();
                DatabaseMetaData dbmd = con.getMetaData();
    
                ResultSet tableRet = dbmd.getTables(null, "%","%",new String[]{"TABLE"});
    
                while(tableRet.next()){
                    String tableName = (String) tableRet.getObject("TABLE_NAME");
    
                    TableInfo ti = new TableInfo(tableName,
                            new HashMap<String, ColumnInfo>(),new ArrayList<ColumnInfo>());
                    tables.put(tableName, ti);
    
                    ResultSet set = dbmd.getColumns(null, "%", tableName, "%");  //查询表中的所有字段
                    while(set.next()){
                        ColumnInfo ci = new ColumnInfo(set.getString("COLUMN_NAME"),
                                set.getString("TYPE_NAME"), 0);
                        ti.getColumns().put(set.getString("COLUMN_NAME"), ci);
                    }
    
                    ResultSet set2 = dbmd.getPrimaryKeys(null, "%", tableName);  //查询t_user表中的主键
                    while(set2.next()){
                        ColumnInfo ci2 = (ColumnInfo) ti.getColumns().get(set2.getObject("COLUMN_NAME"));
                        ci2.setKeyType(1);  //设置为主键类型
                        ti.getPriKeys().add(ci2);
                    }
    
                    if(ti.getPriKeys().size()>0){  //取唯一主键。。方便使用。如果是联合主键。则为空!
                        ti.setOnlyPriKey(ti.getPriKeys().get(0));
                    }
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
    
    
            //生成java文件
            updatePoFile();
    
    
            //将对应的类和表封装起来
            loadTablePoToMap();
    
        }
    
        /**
         *  根据数据库表结构生成pojo类
         */
        public static void updatePoFile(){
            JavaFileUtils.createJavaFileToPackage();
        }
    
    
        /**
        *  加载完数据库中的表,生成pojo之后,把对应的类和对应的表关联起来
        * @date 2018/6/15 19:44
        * @param
        * @return void
        */
        public static void loadTablePoToMap(){
    
            for (TableInfo tableInfo : tables.values()){
                //实体类
                Class<?> aClass = null;
                try {
                    aClass = Class.forName(DBManager.getConfiguration().getPackageName()
                            +"."+StringUtils.UpFirstString(tableInfo.getName()));
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                poClassTableMap.put(aClass,tableInfo);
            }
    
    
        }
    
    
    

    5.5 使用javafileutils生成java文件

    讲一下思路:得到了所有的表信息,如何拼接成一个类
    1.首先要先得到类名
    根据表信息,得到java文件的类名,
    2.得到所有的属性和get set方法
    得到表的一个字段信息,将字段的数据类型转换成java数据类型,将字段名转换成类的属性名,拼接字符串,就得到了一个定义属性的字符串,然后封装get和set方法,将这一个属性的信息,封装在FieldInfo中,循环将所有的属性都封装完毕,
    3.拼装整个类文件
    将包名,和类名和属性一起封装起来,然后使用io流将文件输出,问题来了,输出到哪里,指定了包,包不存在,使用PathUtils,将包名穿进去,就会自动创建包,

    附源码,
    PathUtils

      String str=packageName; //"jk.zmn.auto.dfd";
            str = str.replace(".","\\");
            File file = new File("");
            String canonicalPath = null;
            try {
                canonicalPath = file.getCanonicalPath();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            canonicalPath += "\\src\\"+str;
    //        String resource = Thread.currentThread().getContextClassLoader().getResource("/").getPath();
    //        resource = resource.substring(1);
    //        System.out.println(resource);
            File packageFile = new File(canonicalPath);
            if (!packageFile.exists()){
                packageFile.mkdirs();
            }
    
            return canonicalPath;
        }
    

    JavaFileutils

    private static Configuration configuration = DBManager.getConfiguration();
        /**
         * private String name;
         * <p>
         * private String dataType;
         * <p>
         * private int keyType;
         */
        public static javaFeildInfo createJavaFeild(ColumnInfo columnInfo, TypeConvertorHandler convertorHandler) {
    
            //将字段数据类型转换成java数据类型
            String javaType = convertorHandler.JdbcType2JavaType(columnInfo.getDataType());
    
            String columnName = columnInfo.getName().toLowerCase();
            javaFeildInfo feildInfo = new javaFeildInfo();
            //生成属性语句
            feildInfo.setFeildInfo("\tprivate " + javaType + " " + StringUtils.trimUnderLine(columnName) + ";\n");
    
            StringBuilder sb = new StringBuilder();
            sb.append("\tpublic " + javaType + " " + "get" + StringUtils.UpFirstString(columnName) + "() {\n");
    
            sb.append("\t\treturn " + columnName + ";\n");
    
            sb.append("\t}\n");
    
            feildInfo.setGetFeildInfo(sb.toString());
    
    
            StringBuilder sb1 = new StringBuilder();
    
            sb1.append("\tpublic void " + "set" + StringUtils.UpFirstString(columnName) + "(" + javaType + " " + columnName + ") {\n");
    
            sb1.append("\t\t this." + columnName + " = " + columnName + ";\n");
    
            sb1.append("\t}\n");
    
            feildInfo.setSetFeildInfo(sb1.toString());
    
            return feildInfo;
        }
    
    
        public static void createJavaFile(TableInfo tableInfo, TypeConvertorHandler typeConvertorHandler){
    
            //得到所有的列信息
            Map<String, ColumnInfo> columns = tableInfo.getColumns();
    
            ArrayList<javaFeildInfo> javaFeildInfos = new ArrayList<>();
            Collection<ColumnInfo> values = columns.values();
    
            //生成所有的java属性信息和get set方法
            for (ColumnInfo columnInfo : values){
                javaFeildInfo javaFeild = createJavaFeild(columnInfo, typeConvertorHandler);
                javaFeildInfos.add(javaFeild);
            }
    
            StringBuilder sb = new StringBuilder();
    
            sb.append("package "+configuration.getPackageName()+";\n\n");
            sb.append("import java.sql.*;\n");
            sb.append("import java.util.*;\n\n");
            sb.append("public class "+StringUtils.UpFirstString(tableInfo.getName())+" {\n\n");
    
            for (javaFeildInfo javaFeildInfo: javaFeildInfos){
                sb.append(javaFeildInfo.getFeildInfo());
            }
            sb.append("\n");
            for (javaFeildInfo javaFeildInfo: javaFeildInfos){
                sb.append(javaFeildInfo.getGetFeildInfo());
            }
            for (javaFeildInfo javaFeildInfo: javaFeildInfos){
                sb.append(javaFeildInfo.getSetFeildInfo());
            }
    
            sb.append("}\n");
            //System.out.println(sb.toString());
    
            String classInfo = sb.toString();
    
            String filePathFromPackage = PathUtils.getFilePathFromPackage(configuration.getPackageName());
            File file = new File(filePathFromPackage, StringUtils.UpFirstString(tableInfo.getName()) + ".java");
            BufferedOutputStream bufferedOutputStream=null;
            try {
                bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file));
    
                bufferedOutputStream.write(classInfo.getBytes(),0,classInfo.getBytes().length);
    
                bufferedOutputStream.flush();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    bufferedOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("表"+tableInfo.getName()+"对应的类"+StringUtils.UpFirstString(tableInfo.getName())+"已自动生成..");
        }
    
    
        public static void createJavaFileToPackage(){
    
            Map<String, TableInfo> tables = TableContext.tables;
            TypeConvertorHandler convertorHandler = null;
            if (configuration.getUseDB().equalsIgnoreCase("mysql")){
                convertorHandler = new MysqlConvertorHandler();
            }
            for (TableInfo tableInfo:tables.values()){
                createJavaFile((tableInfo),convertorHandler);
            }
        }
    

    6.查询实现

    如今实体类已经有了,那么如何对数据进行操作呢
    ,上面我们定义了一个Query接口,不同数据库查询可能不一样,所以做一个mysqlQuery的实现类,做mysql的查询

    简单介绍添加方法,
    传入实体类,如何持久化到数据库呢,还记得我们前面在得到数据库表,生成java文件的时候将,实体类的Class对象和表名存储起来吗,现在用到了哦

    得到该类的Class对象,从map中取出表名,拼接sql语句,通过反射,得到该对象的所有的属性,判断是否为空,将不为空的属性插入,那么该类的属性的值怎么获取呢,还是反射,根据属性的get和set方法,反射调用该方法,得到属性的值,将值和sql一起传入执行sql语句的方法,就完成持久化了

     // insert into logs(a,b) values (?,?)
            //得到类对应的表信息
            Class<?> aClass = object.getClass();
            TableInfo tableInfo = TableContext.poClassTableMap.get(aClass);
            StringBuilder sb = new StringBuilder("insert into "+tableInfo.getName()+"(");
            //得到属性
            Field[] fields = aClass.getDeclaredFields();
    
            ArrayList<Object> fieldValueList = new ArrayList<>();
            for (Field field : fields){
                String name = field.getName();
                Object value = ReflectUtils.invokeGet(object, name);
                if (value!=null){
                    sb.append(name+",");
                    fieldValueList.add(value);
                }
            }
            //将最后一个,换成)
            sb.setCharAt(sb.length()-1,')');
            sb.append(" values(");
    
            for (int i=0;i<fieldValueList.size();i++){
                sb.append("?,");
            }
    
            sb.setCharAt(sb.length()-1,')');
    
            executeDML(sb.toString(),fieldValueList.toArray());
    

    其他方法

        /**
        *  删除一个对象
        * @date 2018/6/15 16:54
        * @param object 要移除的对象
        * @return void
        */
        public void delete(Object object){
    
            Class<?> aClass = object.getClass();
    
            TableInfo tableInfo = TableContext.poClassTableMap.get(aClass);
            //得到表的主键
            ColumnInfo onlyPriKey = tableInfo.getOnlyPriKey();
    
            String sql = "delete from "+tableInfo.getName()+" where "+onlyPriKey.getName()+"=?";
    
            //反射调用get方法,得到属性的值
            Object o = ReflectUtils.invokeGet(object, onlyPriKey.getName());
            executeDML(sql,new Object[]{o});
    
        }
        /**
        *  删除类 对应的表中的数据,删除该id的对象
        * @date 2018/6/15 16:55
        * @param clazz  类对象
        * @param id  主键
        * @return void
        */
        public void delete(Class clazz,Object id) {
            // delete from logs where id=?
    
            TableInfo tableInfo = TableContext.poClassTableMap.get(clazz);
            ColumnInfo onlyPriKey = tableInfo.getOnlyPriKey();
            String sql = "delete from "+tableInfo.getName()+" where "+onlyPriKey.getName()+"=?";
            executeDML(sql.toString(),new Object[]{id});
        }
    
        /**
        *  更新对象字段的信息
        * @date 2018/6/15 16:57
        * @param object  对象
        * @param fieldNames  多个字段
        * @return void
        */
        public void update(Object object,String[] fieldNames){
    
            //obj{"uanme","pwd"}-->update 表名  set uname=?,pwd=? where id=?
            Class c = object.getClass();
            List<Object> params = new ArrayList<Object>();   //存储sql的参数对象
            TableInfo tableInfo = TableContext.poClassTableMap.get(c);
            ColumnInfo priKey = tableInfo.getOnlyPriKey();   //获得唯一的主键
            StringBuilder sql  = new StringBuilder("update "+tableInfo.getName()+" set ");
    
            for(String fname:fieldNames){
                Object fvalue = ReflectUtils.invokeGet(object,fname);
                params.add(fvalue);
                sql.append(fname+"=?,");
            }
            sql.setCharAt(sql.length()-1, ' ');
            sql.append(" where ");
            sql.append(priKey.getName()+"=? ");
    
            params.add(ReflectUtils.invokeGet(object,priKey.getName()));    //主键的值
    
            executeDML(sql.toString(), params.toArray());
        }
    
    
        /**
        *   根据参数,查询指定的数据,多行记录,单行记录可直接get(0)
        * @date 2018/6/15 16:59
        * @param sql  sql语句
        * @param clazz   类对象
        * @param params   sql语句参数
        * @return java.util.List
        */
        public List queryRows(String sql,Class clazz,Object[] params){
            Connection connection = DBManager.getConnection();
            PreparedStatement ps = null;
            List<Object> rows = new ArrayList<Object>();
            ResultSet resultSet = null;
            try {
                ps = connection.prepareStatement(sql);
                JdbcUtils.handlerParams(ps,params);
                resultSet = ps.executeQuery();
                //得到返回结果又多少列
                ResultSetMetaData metaData = resultSet.getMetaData();
    
                while (resultSet.next()){
    
                    Object o = clazz.newInstance();
    
                    for (int i=0;i<metaData.getColumnCount();i++){
                        //得到每一列的名称
                        String columnLabel = metaData.getColumnLabel(i + 1);
                        Object columnValue = resultSet.getObject(i + 1);
                        ReflectUtils.invokeSet(o, columnLabel, columnValue);
                    }
    
                    rows.add(o);
                }
    
                return rows;
    
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                DBManager.close(connection,ps);
            }
            return null;
        }
    
    
    
        /**
        *   查询某个字段的数据
        * @date 2018/6/15 17:05
        * @param sql  sql语句
        * @param params   参数
        * @return java.lang.Object 封装查询到的数据
        */
        public Object queryValue(String sql,Object[] params){
    
            Connection connection = DBManager.getConnection();
            PreparedStatement ps = null;
            ResultSet resultSet = null;
            Object o =null;
            try {
                ps = connection.prepareStatement(sql);
                JdbcUtils.handlerParams(ps,params);
                resultSet = ps.executeQuery();
                while (resultSet.next()){
                    o = resultSet.getObject(1);
                }
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }finally {
                DBManager.close(connection,ps);
            }
            return o;
        }
    
     /**
        * 执行sql语句
        * @date 2018/6/15 16:50
        * @param sql  sql语句
        * @param params  参数
        * @return int  SQL影响的行数
         *
        */
        public int executeDML(String sql,Object[] params){
    
            Connection connection = DBManager.getConnection();
            int count = 0;
    
            PreparedStatement ps = null;
            try {
                ps = connection.prepareStatement(sql);
    
                JdbcUtils.handlerParams(ps,params);
                System.out.println(ps);
    
                count = ps.executeUpdate();
            } catch (SQLException e) {
                e.printStackTrace();
            }finally {
                DBManager.close(connection,ps);
            }
            System.out.println("count:"+count);
            return count;
        }
    

    以上,基础的增删改查就可以实现了

    7.项目修改

    1.工厂模式得到Query对象,

    private static QueryFactory queryFactory = new QueryFactory();
    
        private static Class c;
    
        static {
            if (DBManager.getConfiguration().getUseDB().equalsIgnoreCase("mysql")){
    
                try {
                    c = Class.forName("jk.zmn.sorm.core.MySqlQuery");
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        }
        /*
         构造器私有化
         */
        private QueryFactory(){
        }
    
        public static Query createQuery(){
            try {
                return (Query) c.newInstance();
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    

    2.添加连接池功能
    当对数据库操作很频繁的时候,如果每次都新建链接,关闭链接,很耗资源,使用连接池保存链接,提高项目的效率

    /**
         * 连接池对象
         */
        private List<Connection> pool;
    
        /**
         * 最大连接数
         */
        private static final int POOL_MAX_SIZE = DBManager.getConfiguration().getPoolMaxSize();
        /**
         * 最小连接池
         */
        private static final int POOL_MIN_SIZE = DBManager.getConfiguration().getPoolMinSize();
    
    
        /**
         * 初始化连接池,使池中的连接数达到最小值
         */
        public void initPool() {
            if(pool==null){
                pool = new ArrayList<Connection>();
            }
    
            while(pool.size()<DBPool.POOL_MIN_SIZE){
                pool.add(DBManager.getConnection());
                System.out.println("初始化池,池中连接数:"+pool.size());
            }
        }
    
    
        /**
         * 从连接池中取出一个连接
         * @return
         */
        public synchronized Connection getConnection() {
            int last_index = pool.size()-1;
            Connection conn = pool.get(last_index);
            pool.remove(last_index);
    
            return conn;
        }
    
        /**
         * 将连接放回池中
         * @param conn
         */
        public synchronized void close(Connection conn){
    
            if(pool.size()>=POOL_MAX_SIZE){
                try {
                    if(conn!=null){
                        conn.close();
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }else {
                pool.add(conn);
            }
        }
    
    
        public DBPool() {
            initPool();
        }
    

    3.测试

    如此一来,项目大致就结束了,直接打成jar包,由别的项目引入
    新建项目,导入此jar包

    添加


    image.png
    image.png
    image.png

    这里的count是影响数据库的行数

    查询


    image.png image.png

    演示到这里
    源码看这里:https://gitee.com/zhangqiye/SORM
    QQ群:552113611

    相关文章

      网友评论

      • SteveGuRen:看到你的连接池实现里面DBManager.getConnection()获取的竟然是物理连接,我就忍不住提醒一下了,这个还不是连接池,简单的都不算是。后面的连接池直接就通过close把IO资源释放掉了,DBManager.getConnection()获取到的是物理连接。。。。
        SteveGuRen:@z七夜 你的实现里面,接口Connection是直接由com.mysql.jdbc.ConnectionImpl实例生成的,带有网络IO资源,ConnectionImpl里面的close是直接关闭IO。Druid里面的物理连接就是直接从驱动里面获取的Connection连接,然后你的连接池是直接调用这个close方法。。。所以不能算是连接池,都已经直接关闭网络IO了,怎么重用喔。。
        z七夜:@邓慧智 而且close也是把链接放在了连接池里面。超过得才会真正的销毁
        z七夜:@邓慧智 啥是物理链接
      • cae06e66914f:怎么没有,pom.xml配置
        z七夜:@思想在远行 这是javase项目,没有jar,只有数据库连接jar包

      本文标题:手写orm框架

      本文链接:https://www.haomeiwen.com/subject/iqjbeftx.html