• java JDBC系列


    1. JDBC介绍

      java语言连接数据库,JDBC通过载入不同的数据库驱动程序进而于不同的数据库进行连接。

    2. 数据库驱动载入

      使用文本编辑器需要配置classpath环境变量,环境变量路径配置驱动地址;使用IDEA在选择需要加载驱动的 模块->打开模块设置(Open Module Settings)->库(Libraries)-> 加载数据库驱动(+号)。

    3. 数据库表的设计可以使用PowerDesign工具

    4. JDBC编程六步

      (1)注册数据库驱动;(2)连接数据库;(3)创建执行SQL对象;(4)执行SQL语句;(5)查询语句处理查询结果集;(6)释放资源;

    5. jdbc实例,执行insert,delete,update,select语句

      处理插入语句

    public class JDBCTest{
        public static void main(String[] args){
            Connection conn = null;
            Statement stat = null;
            try{
                Driver driver = new com.mysql.cj.jdbc.Driver();   //多态,父类型引用指向子类型对象
                //1. 注册数据库驱动
                DriverManager.registerDriver(driver);
                String url = "jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false";
                String user = "root";
                String password = "123456";
                //2. 连接数据库
                conn = DriverManager.getConnection(url,user,password);
                System.out.println(conn);
                //3. 获取执行SQL对象
                stat = conn.createStatement();
                //4. 执行SQL语句
                String sql = "insert into dept_bak(deptno,dname,loc) value(60,'销售部','北京')";
                int result = stat.executeUpdate(sql);
                System.out.println(result == 1 ? "执行成功":"执行失败");
            }catch(SQLException e){
                e.printStackTrace();
            }finally{
                //6. 释放资源
                //释放资源放在finally中,不管前面是否执行成功,都释放资源
                //释放资源依次从下往上释放(stat,conn),释放资源也需要处理异常
                if(stat != null){
                    try{
                        stat.close();
                    
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
                if(conn != null){
                    try{
                        conn.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
                
            }    
        }
    }

      注册数据库驱动使用registerDriver()方法,但是这种方法已经不常用,后面使用反射会更加常用。

      处理更新,删除语句

    public class jdbcTest02{
        public static void main(String[] args){
            Connection conn = null;
            Statement stat = null;
            try{
                //1. 注册驱动
                DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
                //2. 连数据库
                conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456");
                //3. 获取执行SQL对象
                stat = conn.createStatement();
                //4. 执行SQL语句
                //String sql = "delete from dept_bak where deptno = 50";
                String sql = "update dept_bak set dname = '研发部' where deptno = 60";
                int result = stat.executeUpdate(sql);
                System.out.println(result == 1 ? "执行成功":"执行失败");
                
            }catch(SQLException e){
                e.printStackTrace();
            }finally{
                //6. 释放资源
                if(stat != null){
                    try{
                        stat.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
                if(conn != null){
                    try{
                        conn.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }

      其中insert,update,delete都不需要对查询结果集处理,只需根据返回结果(SQL语句执行后数据改变的条数对应返回结果数)来判断SQL语句是否执行成功。

      使用反射,并且把数据库等字符串信息写入.properties文件中,使用资源绑定器读取配置文件。

    public class jdbcTest04{
        public static void main(String[] args){
            ResourceBundle rb = ResourceBundle.getBundle("db");
            String dbdriver = rb.getString("driver");
            String url = rb.getString("url");
            String user = rb.getString("user");
            String password = rb.getString("password");
            Connection conn = null;
            Statement stat = null;
            try{
                Class.forName(dbdriver);
                conn = DriverManager.getConnection(url,user,password);
                stat = conn.createStatement();
                String sql = "update dept_bak set dname = '研发部2',loc = '天津' where deptno = 60";
                int result = stat.executeUpdate(sql);
                System.out.println(result == 1? "执行成功":"执行失败");
                
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                if(stat != null){
                    try{
                        stat.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
                if(conn != null){
                    try{
                        conn.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
            }
            
            
            
        }
    }
    db.properties

    driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false user=root password=123456

      使用资源绑定器对代码部分修改较少

      执行select语句,处理查询结果集

    public class jdbcTest05{
        public static void main(String[] args){
            Connection conn = null;
            Statement stat = null;
            ResultSet rs = null;
            try{
                Class.forName("com.mysql.cj.jdbc.Driver");
                conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456");
                stat = conn.createStatement();
                String sql = "select empno as a,ename,sal from emp";
                rs = stat.executeQuery(sql);   //执行select语句
                
                //1. 使用getString(数据库字段名) 遍历
    /*            while(rs.next()){    //next()遍历查询结果集
                    String empno = rs.getString("empno");
                    String ename = rs.getString("ename");
                    String sal = rs.getString("sal");
                    System.out.println(empno + " " + ename + " " + sal);
                }
    */        
                //2. 使用getString(第几个字段)遍历
    /*            while(rs.next()){
                    String empno = rs.getString(1);
                    String ename = rs.getString(2);
                    String sal = rs.getString(3);
                    System.out.println(empno + " " + ename + " " + sal);
                }
    */            
                //3. 遍历具体字段getInt(),getString(),getDouble()
    /*            while(rs.next()){
                    int empno = rs.getInt("empno");
                    String ename = rs.getString("ename");
                    double sal = rs.getDouble("sal");
                    System.out.println(empno + " " + ename + " " + (sal + 100));  //可以进行数据运算
                }
    */
                //4. 可以遍历别名字段
                while(rs.next()){
                    String empno = rs.getString("a");  //使用别名,遍历查询结果集
                    String ename = rs.getString("ename");
                    String sal = rs.getString("sal");
                    System.out.println(empno + " " + ename + " " + sal);
                }
                
                
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                if(rs != null){
                    try{
                        rs.close();
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                }
                if(stat != null){
                    try{
                        stat.close();
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                }
                if(conn != null){
                    try{
                        conn.close();
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }

      与update,delete,insert语句不同的是,select语句需要处理查询结果集,处理查询结果集通常有4种方式来获取字段的值,同时在最后需要关闭查询结果集的异常。

    6. SQL注入问题

      SQL注入:用户输入的信息中可能含有SQL语句的关键字,并且这些关键字参与SQL语句的编译过程,导致SQL语句的查询原意扭曲,进而达到SQL注入。

      使用用户登录模拟SQL注入问题

    public class jdbcTest06{
        public static void main(String[] args){
            //初始化用户界面
            Map<String,String> loginInfo = initUI();
            //用户登录
            boolean loginFlag = login(loginInfo);
            
            System.out.println(loginFlag? "登录成功":"登录失败");
        }
        
        public static Map<String,String> initUI(){
            System.out.println("请输入用户名:");
            Scanner s = new Scanner(System.in);
            String username = s.nextLine();
            System.out.println("请输入密码:");
            String password = s.nextLine();
            Map<String,String> map = new HashMap();
            map.put("username",username);
            map.put("password",password);
            return map;
        }
        
        public static boolean login(Map<String,String> userloginInfo){
            boolean flag = false;
            String getusername = userloginInfo.get("username");
            String getpassword = userloginInfo.get("password");
            Connection conn = null;
            Statement stat = null;
            ResultSet rs = null;
            
            try{
                Class.forName("com.mysql.cj.jdbc.Driver");
                conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456");
                stat = conn.createStatement();
                String sql = "select * from t_user where loginname='" + getusername + "' and loginpwd = '"+getpassword+"'";
                rs = stat.executeQuery(sql);
                if(rs.next()){
                    flag = true;
                }
                
                
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                if(rs != null){
                    try{
                        rs.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
                if(stat != null){
                    try{
                        stat.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
                if(conn != null){
                    try{
                        conn.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
            }    
            return flag;
        }
    }

      在t_user表有两条数据{“zhangsan”:"123","jack","123"},当使用数据库中的用户名和密码匹配时可以正常登录,使用数据库中没有的数据也可以登录失败,但是当使用用户名:fdsa,密码:fdsa' or '1'='1,也可以登录成功,这是因为用户名和密码正好完成了SQL语句的拼接(select * from t_user where loginname = 'fdsa' and loginpwd = 'fdsa' or '1'='1' or  '1' = '1';),执行了非法的SQL编译。

    7. 解决SQL注入问题

      使用prepareStatement创建预编译数据库对象

    public class jdbcTest07{
        public static void main(String[] args){
            
            Map<String,String> userloginInfo = InitUI();
            
            boolean result = useLogin(userloginInfo);
            
            System.out.println(result? "登录成功":"登录失败");
        }
        
        public static Map<String,String> InitUI(){
            System.out.println("请输入用户名:");
            Scanner s = new Scanner(System.in);
            String username = s.nextLine();
            System.out.println("请输入密码:");
            String password = s.nextLine();
            Map<String,String> map = new HashMap<>();
            map.put("username",username);
            map.put("password",password);
            return map;
        }
        
        public static boolean useLogin(Map<String,String> map){
            boolean flag = false;
            Connection conn = null;
            PreparedStatement ps = null;
            ResultSet rs = null;
            String username = map.get("username");
            String password = map.get("password");
            try{
                Class.forName("com.mysql.cj.jdbc.Driver");
                conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456");
                String sql = "select * from t_user where loginname = ? and loginpwd = ?";   //?表示占位符,用来接收值
                ps = conn.prepareStatement(sql);   //创建预编译数据库操作对象
                ps.setString(1,username);    //对占位符1传入值
                ps.setString(2,password);    //对占位符2传入password,loginname和loginpwd都是字符串,所以使用setString(),如果是Int,则使用setInt()
                rs = ps.executeQuery();
                if(rs.next()){
                    flag = true;
                }
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                if(rs != null){
                    try{
                        rs.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
                if(ps != null){
                    try{
                        ps.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
                if(conn != null){
                    try{
                        conn.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
            }
            return flag;
        }
    }

      使用prepareStatement解决SQL注入问题原理:先编译SQL语句框架,然后填入变量值,就算有SQL关键字,也不会使关键字参与SQL编译。

      使用prepareStatement执行insert,update,delete语句

    public class jdbcTest08{
        public static void main(String[] args){
            Connection conn = null;
            PreparedStatement ps= null;
            
            try{
                Class.forName("com.mysql.cj.jdbc.Driver");
                conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456");
                
            /*    String sql = "insert into dept_bak(deptno,dname,loc) values(?,?,?)";
                ps = conn.prepareStatement(sql);
                ps.setInt(1,70);
                ps.setString(2,"研发二部");
                ps.setString(3,"北京");   */
                
            /*    String sql = "update dept_bak set dname = ?,loc = ? where deptno = ?";
                ps = conn.prepareStatement(sql);
                ps.setString(1,"研发三部");
                ps.setString(2,"上海");
                ps.setInt(3,70);    */
                
                String sql = "delete from dept_bak where deptno = ?";
                ps = conn.prepareStatement(sql);
                ps.setInt(1,70);
                
                int result = ps.executeUpdate();
                System.out.println(result);
                
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                if(ps != null){
                    try{
                        ps.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
                if(conn != null){
                    try{
                        conn.close();
                    }catch(SQLException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    8. createStatement和prepareStatement比较

      在一些场景中可能需要使用createStatement来实现某些功能,例如排序等场景

    public class jdbcTest02 {
        public static void main(String[] args) {
            //1. 使用PreparedStatement不能满足排序场景
            /*System.out.println("请输入asc或desc进行升序或降序:");
            Scanner scanner = new Scanner(System.in);
            String str = scanner.nextLine();
            Connection conn = null;
            PreparedStatement ps = null;
            ResultSet rs = null;
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456");
                String sql = "select * from emp order by ename ?";
                ps = conn.prepareStatement(sql);
                ps.setString(1,str);
                rs = ps.executeQuery();
                if(rs.next()){
                    System.out.println(rs.getString("ename"));
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if(rs != null){
                    try{
                        rs.close();
                    }catch (SQLException e){
                        e.printStackTrace();
                    }
                }
                if(ps != null){
                    try {
                        ps.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
                if(conn != null){
                    try {
                        conn.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }*/
            //2. 使用Statement满足升序排序场景
            System.out.println("请输入asc或desc进行升序或降序:");
            Scanner scanner = new Scanner(System.in);
            String str = scanner.nextLine();
            Connection conn = null;
            Statement stat = null;
            ResultSet rs = null;
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456");
                stat = conn.createStatement();
                String sql = "select * from emp order by ename " + str;
                rs = stat.executeQuery(sql);
                while(rs.next()){
                    System.out.println(rs.getString("ename"));
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if(rs != null){
                    try{
                        rs.close();
                    }catch (SQLException e){
                        e.printStackTrace();
                    }
                }
                if(stat != null){
                    try {
                        stat.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
                if(conn != null){
                    try {
                        conn.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

      在排序场景中我们需要asc或desc来参与SQL语句的编译,如果使用prepareStatement那么把排序当作字符串处理'asc'或'desc',这样SQL语句是不能执行的,所以此时可以使用createStatement来满足业务需求,所以并不是说所有情况下都需使用prepareStatement,针对不同场景选择不同的数据库执行对象。

      createStatement和prepareStatement对比:

      (1)prepareStatement可解决SQL注入问题;

      (2)createStatement是编译一次执行一次,preparedStatement编译一次,可执行多次,效率更高;

      (3)preparedStatement会在编译阶段对类型的进行安全检查;

      当业务层面需要SQL注入的情况(需要SQL语句拼接)使用createStatement,其他情况基本使用prepareStatement。

    9. jdbc事务

      jdbc事务是自动提交的,执行一次,提交一次

    public class jdbcTest03 {
        public static void main(String[] args) {
            Connection conn = null;
            PreparedStatement ps = null;
    
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456");
                String sql = "update dept_bak set dname = ? where deptno = ?";
                ps = conn.prepareStatement(sql);
                ps.setString(1,"研发5部");
                ps.setInt(2,60);
                int result = ps.executeUpdate();    //在此处打断点,当执行完这步,数据库中数据已经更新
    
                ps.setString(1,"研发6部");
                ps.setInt(2,60);
                result = ps.executeUpdate();
    
                System.out.println( result == 1 ? "执行成功":"执行失败");
    
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }finally {
                if(ps != null){
                    try {
                        ps.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
                if(conn != null){
                    try {
                        conn.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

      如果在调用executeUpdate()处打断点,当执行完语句,数据库中值已经更新,可以看出事务是自动提交的,这显然不符合业务需求,如果是转账的操作那么账户1已经减去了一部分钱,而账户2还没有收到。

      模拟转账场景

    public class jdbcTest04 {
        public static void main(String[] args) {
            Connection conn = null;
            PreparedStatement ps = null;
    
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456");
                String sql = "update t_act set balance = ? where actno = ?";
                ps = conn.prepareStatement(sql);
                conn.setAutoCommit(false);    //开启事务,关闭自动提交
    
                ps.setDouble(1,10000);
                ps.setInt(2,111);
                int result = ps.executeUpdate();
    
        //        String str = null;    //模拟异常,由于jdbc是自动提交事务的,如果转账过程中发生异常,那么此时111账户余额已经为10000,但是222账户还没有增加
        //        str.toString();
    
                ps.setDouble(1,10000);
                ps.setInt(2,222);
                result += ps.executeUpdate();
                conn.commit();     //当两边都执行成功,转账成功后,提交事务
    
                System.out.println(result == 2? "转账成功":"转账失败");
    
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                if(conn != null){
                    try {
                        conn.rollback();     // 当执行异常时,回滚
                    } catch (SQLException ex) {
                        ex.printStackTrace();
                    }
                }
                e.printStackTrace();
            } finally {
                if(ps != null){
                    try {
                        ps.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
                if(conn != null){
                    try {
                        conn.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
    
        }
    }

      在转账场景中,如果转账过程中发生异常,那么账户1已经减去了10000块,而账户2并没有增加。

      所有我们要在建立连接后开启事务,关闭自动提交(conn.setAutoCommit(false)),当我们的SQL语句都执行成功后才提交(conn.commit()),如果当中发生异常那么必须回滚(conn.rollback())。

    10. jdbc中常用方法的封装

      对jdbc中一些常用的方法我们可以封装到一个类中,这样我们在其他类中可以更方便的调用

    public class DBUtil {
        //可以直接使用类名调用方法,不需要new对象,所以构造方法私有化
        private DBUtil(){}
    
        static{
            try{
                Class.forName("com.mysql.cj.jdbc.Driver");    //可能会多次连接数据库,但是注册驱动只注册一次,静态代码块在类加载时执行,且只执行一次
            }catch(ClassNotFoundException e){
                e.printStackTrace();
            }
    
        }
    
        //获取数据库连接对象
        public static Connection getConnection() throws SQLException{     //使用抛出异常,不用捕获异常,因为在主方法中已经进行捕获
            return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydatabase?serverTimezone=UTC&useSSL=false","root","123456");
        }
    
        //关闭资源
        public static void close(Connection conn,Statement stat,ResultSet rs){
            if(rs != null){
                try{
                    rs.close();
                }catch(SQLException e){
                    e.printStackTrace();
                }
            }
            if(stat != null){
                try{
                    stat.close();
                }catch(SQLException e){
                    e.printStackTrace();
                }
            }
            if(conn != null){
                try{
                    conn.close();
                }catch(SQLException e){
                    e.printStackTrace();
                }
            }
        }
    }

      使用DBUtil工具类实现模糊查询

    public class jdbcTest05 {
        public static void main(String[] args) {
            Connection conn = null;
            PreparedStatement ps = null;
            ResultSet rs = null;
            try {
                conn = DBUtil.getConnection();
                String sql = "select * from emp where ename like ?";
                ps = conn.prepareStatement(sql);
                ps.setString(1,"_A%");    //模糊查询,ename中第二个字母是'A'
                rs = ps.executeQuery();
                while(rs.next()){
                    System.out.println(rs.getString("ename"));
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                DBUtil.close(conn,ps,rs);
            }
        }
    }

    11. 行级锁

      select ename,job,sal from emp where job = 'manager' for update;

      当在select语句之后添加for update之后就表示给查询处理的整行完整数据添加了行级锁,当添加了行级锁之后,事务2就不能对数据进行修改了,必须等待事务1提交之后才能进行修改数据

      使用两个事务1,2模拟行级锁

    /**
     * 演示行级锁
     * 事务1
     */
    public class jdbcTest06 {
        public static void main(String[] args) {
            Connection conn = null;
            PreparedStatement ps = null;
            ResultSet rs = null;
            try{
                conn = DBUtil.getConnection();
                conn.setAutoCommit(false);
                String sql = "select ename,job,sal from emp_bak where job = ? for update";
                ps = conn.prepareStatement(sql);
                ps.setString(1,"MANAGER");
                rs = ps.executeQuery();
                while (rs.next()){
                    System.out.println(rs.getString("ename") + " "+ rs.getString("job") + " "+ rs.getDouble("sal"));
                }
                conn.commit();  //添加行级锁后,在此处打断点,模拟事务1还没执行完查询,那么事务2(jdbcTest07)就不能进行更新,没有返回值,当事务1执行完成后,事务2才执行更新
            }catch (Exception e){
                if(conn != null){
                    try {
                        conn.rollback();
                    } catch (SQLException ex) {
                        ex.printStackTrace();
                    }
                }
                e.printStackTrace();
            }finally {
                DBUtil.close(conn,ps,rs);
            }
        }
    }
    /**
     * 演示行级锁
     * 事务2
     */
    public class jdbcTest07 {
        public static void main(String[] args) {
            Connection conn = null;
            PreparedStatement ps = null;
    
            try {
                conn = DBUtil.getConnection();
                conn.setAutoCommit(false);
                String sql = "update emp_bak set sal = sal * 1.1 where job = ?";
                ps = conn.prepareStatement(sql);
                ps.setString(1,"MANAGER");
                int result = ps.executeUpdate();
                System.out.println(result);
                conn.commit();
            } catch (Exception e) {
                if(conn != null){
                    try {
                        conn.rollback();
                    } catch (SQLException ex) {
                        ex.printStackTrace();
                    }
                }
                e.printStackTrace();
            }finally {
                DBUtil.close(conn,ps,null);
            }
        }
    }
  • 相关阅读:
    使用Visual Studio自带工具dumpbin解析PE文件
    Python内置函数—bytearray
    .gitignore
    Git本地仓库基本操作
    推荐一款编辑神器UltraEdit
    APK文件结构
    在QQ浏览器中使用微信读书辅助工具“小悦记”插件
    Vue动画
    自定义指令的学习
    Vue过滤器的基本使用
  • 原文地址:https://www.cnblogs.com/homle/p/15087724.html
Copyright © 2020-2023  润新知