zoukankan      html  css  js  c++  java
  • Spring JDBC最佳实践(1)

    原文地址:https://my.oschina.net/u/218421/blog/38513

    Spring提供了两种使用JDBC API的最佳实践,一种是以JdbcTemplate为核心的基于Template的JDBC的使用方式,另一种则是在JdbcTemplate基础之上的构建的基于操作对象的JDBC的使用方式。

    基于Template的JDBC的使用方式
    该使用方式的最初设想和原型,需要追溯到Rod Johnson在03年出版的Expert One-on-One J2EE Design and Development,在该书的Practical Data Access(数据访问实践)中,Rod针对JDBC使用中的一些问题提出了一套改进的实践原型,并最终将该原型完善后在Spring框架中发布。

    JDBC的尴尬
    JDBC作为Java平台的访问关系数据库的标准,其成功是 有目共睹的。几乎所有java平台的数据访问,都直接或者间接的使用了JDBC,它是整个java平台面向关系数据库进行数据访问的基石。
    作为一个标准,无疑JDBC是很成功的,但是要说JDBC在使用过程当中多么的受人欢迎,则不尽然了。JDBC主要是面向较为底层的数据库操作,所以在设计的过程当中 ,比较的贴切底层以提供尽可能多的功能特色。从这个角度来说,JDBC API的设计无可厚非。可是,过于贴切底层的API的设计,对于开发人员则未必是一件好事。即使执行一个最简单的查询,开发人员也要按照API的规矩写上一大堆雷同的代码,如果不能合理的封装使用JDBC API,在项目中使用JDBC访问数据所出现的问题估计会使人发疯!
    对于通常的项目开发来说,如果层次划分很明确,数据访问逻辑一般应该在DAO层中实现。根据功能模块的划分,可能每个开发人员都会分得或多或少的实现相应的DAO的任务,假设开发人员A在分得了DAO实现任务后进行开发,他或许开发了如下所示的代码:

    package com.google.spring.jdbc;
    
    import java.sql.Connection;
    import java.sql.SQLException;
    import java.sql.Statement;
    
    import javax.sql.DataSource;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    public class DaoWithA implements IDao
    {
    
        private final Log logger = LogFactory.getLog(DaoWithA.class);
        private DataSource dataSource = null;
        
        public DataSource getDataSource()
        {
            return dataSource;
        }
    
        public void setDataSource(DataSource dataSource)
        {
            this.dataSource = dataSource;
        }
    
        @Override
        public int updateSomething(String sql)
        {
            int count;
            Connection conn = null;
            Statement stmt = null;
            try
            {
                conn = getDataSource().getConnection();
                stmt = conn.createStatement();
                count = stmt.executeUpdate(sql);
                stmt.close();
                stmt = null;
            } 
            catch (SQLException e)
            {
                throw new RuntimeException(e);
                
            }
            finally
            {
                if(stmt!=null)
                {
                    try
                    {
                        stmt.close();
                    } 
                    catch (SQLException ex)
                    {
                        logger.warn("fail to close statement:"+ex);
                    }
                }
                if(conn!=null)
                {
                    try
                    {
                        conn.close();
                    } 
                    catch (Exception ex)
                    {
                        logger.warn("failed to close Connection:"+ex);
                    }
                }
            }
            return count;
        }
    
    }

    而B所负责的DAO的实现中,可能也有类似的更新的操作。无疑,B也要像A这样,在他的DAO实现类中写一大堆同样的JDBC代码,类似的情况还可能扩展到C、D等开发人员。如果每个开发人员都能严格的按照JDBC的编程规范开发还好,但是事实是,一个团队中的开发人员是有差别的。
    这其实只是API的使用过程中的一个插曲,当你看到应用程序中成百的使用JDBC实现类的时候,会发现如下的问题:
    1、Statement使用完没有关闭,而是想着让Connection关闭的时候一并关闭,可是并非所有的驱动程序都有这样的行为。
    2、创建了多个ResultSet或者Statement,只清理了最外层的,忽视了里层的。

    3、忘记关闭Connection。
    JDBC规范在指定数据库访问异常的时候也没有能够进行的很彻底:
    1、将异常类型定义为SQLException是一个值得商榷的地方。
    2、SQLExcpetion没有采用将具体的异常情况子类化,以进一步抽象不同的数据访问的情况,而是采用ErrorCode的方式来区分访问过程中所出现的不同异常情况,其实这也没什么,只要能区分出具体的错误就行,但是JDBC规范却把ErrorCode的规范留给了数据库提供商,这导致了不同的数据库供应商对应了不同的ErrorCode,进而应用程序在捕获到SQLException后,还要看当前用的是什么数据库。
    针对以上问题,Spring提供了相应的解决方案帮助我们提高开发效率!

    为了解决JDBC API在实际使用中的各种尴尬的局面,spring提出了org.springframework.jdbc.core.JdbcTemplate作为数据访问的Helper类。JdbcTemplate是整个spring数据抽象层提供的所有JDBC API最佳实践的基础,框架内其它更加方便的Helper类以及更高层次的抽象,全部的构建于JdbcTemplate之上。抓住了JdbcTemplate,就抓住了spring框架JDBC API最佳实践的核心。
    概括的说,JdbcTemplate主要关注一下两个事情:
    1、封装所有的基于JDBC的数据访问的代码,以统一的格式和规范来使用JDBC API。所有的基于JDBC API的数据访问全部通过JdbcTemplate,从而避免了容易出错的数据访问方式。
    2、对SQLException所提供的异常信息在框架内进行统一的转译,将基于JDBC的数据访问异常纳入Spring自身的异常层次之中,统一了数据接口的定义,简化了客户端代码对数据访问异常的处理。
    Spring主要是通过模板方法对基于JDBC的数据访问代码进行统一的封装,所以我们可先看下模板方法:
    模板方法主要是用于对算法的行为或者逻辑进行封装,即如果多个类中存在相似的算法逻辑或者行为逻辑,可以将这些逻辑提取到模板方法中实现,然后让相应的子类根据需要实现某些自定义的逻辑。
    举个例子,所有的汽车,不管是宝马还是大众,他们的驾驶流程基本上是固定的。实际上,除了少数的实现细节有所不同之外,大部分的流程是相同的,基本上是如下所示的流程说明:
    1、点火启动
    2、踩刹车,挂前进的档位(不同的车在这一步会存在差异)
    3、放下手动控制器(手刹)
    4、踩油门启动车辆运行
    此时,我们可以声明一个模板方法类,将确定的行为以模板的形式定义,而将不同的行为留给相应的子类来实现:

    package com.google.spring.jdbc;
    
    public abstract class Vehicle
    {
        
        public final void drive()
        {
            startTheEnginee();//启动
            putIntoGear(); //前进
            looseHandBrake();//放下手刹
            stepOnTheGasAndGo();//踩油门前进
        }
        
        protected abstract void putIntoGear();
        
        private void startTheEnginee()
        {
            
        }
        
        private void looseHandBrake()
        {
            
        }
        
        private void stepOnTheGasAndGo()
        {
            
        }
    }

    drive()方法就是我们的模板方法,它被声明为final,表示该类是不能被子类重写的,车辆的自动挡和手动挡是不同的,所以留给了子类去实现:

    package com.google.spring.jdbc;
    
    public class VehicleAT extends Vehicle
    {
    
        @Override
        protected void putIntoGear()
        {
            //挂前进档位
    
        }
    
    }
    package com.google.spring.jdbc;
    
    public class VehicleMT extends Vehicle
    {
    
        @Override
        protected void putIntoGear()
        {
            //踩离合器 挂前进档位
    
        }
    
    }

    这样,每个子类实现特有的逻辑就可以了。

     JdbcTemplate的演化
    如果回头看一下最初的使用JDBC API进行数据访问的代码。就会发现,不管这些代码是谁负责的,也不管数据访问的逻辑如何,除了小部分的差异之外,所有的这些代码几乎都是按照同一个流程走下来的,如下:
    1、conn=getDataSource().getConnection();
    2、stmt=conn.createStatement()或者ps=conn.prepareStatement();
    3、stmt.executeUpdate(sql)或者ps.executeUpdate()  或者进行相应的查询。
    4、stmt.close()  stmt=null
    5、catch处理数据库访问异常
    6、关闭数据库连接避免连接泄露导致系统崩溃
    对于多个DAO中充斥着几乎相同的JDBC API的使用代码,我们也可以采用模板方法,多这些代码进行重构,避免因个人操作不当所出现的种种问题,我们要做的,就是将一些公共的行为提取到模板方法中去,而特有的操作,比如每次执行不同的更新,或者对不同的查询结果进行不同的处理,则放入具体的子类中,这样,我们就有个JdbcTemplate的雏形:

    package com.google.spring.jdbc;
    
    import java.sql.Connection;
    import java.sql.SQLException;
    import java.sql.Statement;
    
    import javax.sql.DataSource;
    
    import org.springframework.dao.DataAccessException;
    
    
    public abstract class JdbcTemplate
    {
        private DataSource dataSource;
        
        
        public DataSource getDataSource()
        {
            return dataSource;
        }
    
    
        public void setDataSource(DataSource dataSource)
        {
            this.dataSource = dataSource;
        }
    
    
        public final Object execute(String sql)
        {
            Connection conn = null;
            Statement stmt = null;
            try
            {
                conn = this.getDataSource().getConnection();
                stmt = conn.createStatement();
                Object retValue = this.executeWithStatement(stmt, sql);
                return retValue;
            }
            catch (SQLException e)
            {
                throw new RuntimeException(e);
            }
            finally
            {
                closeStatement(stmt);
                closeConnection(conn);
            }
        }
        
        protected abstract Object executeWithStatement(Statement stmt,String sql);
        
        private final DataAccessException translateSQLException(SQLException e)
        {
            DataAccessException dataAccessException = null;
            //进行相应的转译
            return dataAccessException;
        }
        
        private final void closeStatement(Statement stmt)
        {
            //关闭Statement
        }
        
        private final void closeConnection(Connection conn)
        {
            //关闭Connection
        }
    }

    这样处理之后,JDBC代码的使用有了规范。但是,只使用模板方法还不足以提供方便的Helper类。顶着abstract的帽子,每次使用都要进行相应的子类化,这也太不靠谱了。所以,spring中的JdbcTemplate除了引入了模板方法之外,还引入了相应的Callback,避免了每次都子类化,比如,当引入了StatementCallback接口以后:

    package com.google.spring.jdbc;
    
    import java.sql.Statement;
    
    public interface StatementCallback
    {
        public Object doWithStatement(Statement stmt) throws SQLException;
    }

    这样这个真正的Helper类就存在了,如下所示:

    package com.google.spring.jdbc;
    
    import java.sql.Connection;
    import java.sql.SQLException;
    import java.sql.Statement;
    
    import javax.sql.DataSource;
    
    import org.springframework.dao.DataAccessException;
    
    
    public  class JdbcTemplate
    {
        private DataSource dataSource;
        
        
        public DataSource getDataSource()
        {
            return dataSource;
        }
    
    
        public void setDataSource(DataSource dataSource)
        {
            this.dataSource = dataSource;
        }
    
    
        public final Object execute(StatementCallback callback)
        {
            Connection conn = null;
            Statement stmt = null;
            try
            {
                conn = this.getDataSource().getConnection();
                stmt = conn.createStatement();
                Object retValue = callback.doWithStatement(stmt);
                return retValue;
            }
            catch (SQLException e)
            {
                throw new RuntimeException(e);
            }
            finally
            {
                closeStatement(stmt);
                closeConnection(conn);
            }
        }
        
        
        
        private final DataAccessException translateSQLException(SQLException e)
        {
            DataAccessException dataAccessException = null;
            //进行相应的转译
            return dataAccessException;
        }
        
        private final void closeStatement(Statement stmt)
        {
            //关闭Statement
        }
        
        private final void closeConnection(Connection conn)
        {
            //关闭Connection
        }
    }

    要在相应的DAO中使用该JdbcTemplate,只需要根据情况提供参数和相应的callback就可以了,如下所示:

    final String sql = "update";
            JdbcTemplate jdbcTemplate = new JdbcTemplate();
            BasicDataSource dataSource = new BasicDataSource();
            jdbcTemplate.setDataSource(dataSource);
            //对dataSource进行setter操作
            StatementCallback callback = new StatementCallback()
            {
                public Object doWithStatement(Statement stmt) throws SQLException
                {
                    return new Integer(stmt.executeUpdate(sql)) ;
                }
            };
            
            jdbcTemplate.execute(callback);

    这样,开发人员只需要关注与数据访问逻辑相关的东西,JDBC底层的细节不需要再考虑了。
    上述是spring中JdbcTemplate的中心思想,实际上,JdbcTemplate在实现上要考虑很多的东西,继承层次如下:

     org.springframework.jdbc.core.JdbcOperations接口定义了JdbcTemplate可以使用的JDBC操作集合,该接口提供的操作声明,从查询到更新无所不有。
    JdbcTemplate的直接父类是JdbcAccessor,这是一个抽象类,主要为子类提供一些公用的属性:

     DataSource:javax.sql.DataSource是JDBC2.0之后引入的接口定义,用来替代java.sql.DriverManager的数据库连接方式,它的角色可以看做是JDBC的连接工厂,所以,基本上现在它应该作为获取数据库资源的统一接口。

    SQLExceptionTranslator:JdbcTemplate委托此类进行异常的转译。

    JdbcTemplate中的模板方法可分为如下的四组:
    面向Connection的模板方法:
    通过ConnectionCallback接口所公开的Connection进行数据访问

    import java.sql.Connection;
    import java.sql.SQLException;
    
    import org.springframework.dao.DataAccessException;
    
    public interface ConnectionCallback
    {
        Object doInConnection(Connection con) throws SQLException, DataAccessException;
    }
    public Object execute(ConnectionCallback action) throws DataAccessException {
            Assert.notNull(action, "Callback object must not be null");
    
            Connection con = DataSourceUtils.getConnection(getDataSource());
            try {
                Connection conToUse = con;
                if (this.nativeJdbcExtractor != null) {
                    // Extract native JDBC Connection, castable to OracleConnection or the like.
                    conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
                }
                else {
                    // Create close-suppressing Connection proxy, also preparing returned Statements.
                    conToUse = createConnectionProxy(con);
                }
                return action.doInConnection(conToUse);         }
            catch (SQLException ex) {
                // Release Connection early, to avoid potential connection pool deadlock
                // in the case when the exception translator hasn't been initialized yet.
                DataSourceUtils.releaseConnection(con, getDataSource());
                con = null;
                throw getExceptionTranslator().translate("ConnectionCallback", getSql(action), ex);
            }
            finally {
                DataSourceUtils.releaseConnection(con, getDataSource());
            }
        }

    可以随意操作Connection。
    面向Statement的模板方法:
    该模板方法主要处理基于SQL的数据访问请求。该组模板方法通过org.springframework.jdbc.core.StatementCallback回调接口,对外公开java.sql.Statement的操作句柄。该方式缩小了回调接口内的权限范围,但是提高了API使用上的安全性和便捷性。
    面向PreparedStatement的模板方法:
    对于使用包含查询参数的SQL请求来说,使用PreparedStatement可以让我们免于SQL注入的攻击,而在使用PreparedStatement之前,需要根据传入的包含参数的SQL对其进行创建,所以,面向PreparedStatement的模板方式会通过org.springframework.jdbc.core.PreparedStatementCreator的回调接口公开Connection以允许PreparedStatement的创建。PreparedStatement创建之后,会公开org.springframework.jdbc.core.PreparedStatementCallback回调接口,以支持其使用PreparedStatement进行数据访问。
    面向CallableStatement的模板方法:
    JDBC支持使用CallableStatement进行数据库存储过程的访问,面向CallableStatement的的模板方法会通过org.springframework.jdbc.core.CallableStatementCreator公开的Connection用于创建调用存储过程的CallableStatement。之后,再通过org.springframework.jdbc.core.CallableStatementCallback公开的CallableStatement的操作句柄,实现基于存储过程的数据访问。
    每一组的模板方法都 有一个核心的方法实现,其它的属于同一组的重载的模板方法,会调用这个核心的方法来完成最终的工作。以面向Statement的模板方法为例,使用StatementCallback回调接口作为方法参数的execute方法是这组的核心代码:

    public Object execute(StatementCallback action) throws DataAccessException {
            Assert.notNull(action, "Callback object must not be null");
    
            Connection con = DataSourceUtils.getConnection(getDataSource());
            Statement stmt = null;
            try {
                Connection conToUse = con;
                if (this.nativeJdbcExtractor != null &&
                        this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
                    conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
                }
                stmt = conToUse.createStatement();
                applyStatementSettings(stmt);
                Statement stmtToUse = stmt;
                if (this.nativeJdbcExtractor != null) {
                    stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
                }
                Object result = action.doInStatement(stmtToUse);
                handleWarnings(stmt);
                return result;
            }
            catch (SQLException ex) {
                // Release Connection early, to avoid potential connection pool deadlock
                // in the case when the exception translator hasn't been initialized yet.
                JdbcUtils.closeStatement(stmt);
                stmt = null;
                DataSourceUtils.releaseConnection(con, getDataSource());
                con = null;
                throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
            }
            finally {
                JdbcUtils.closeStatement(stmt);
                DataSourceUtils.releaseConnection(con, getDataSource());
            }
        }

    其它模板方法会根据自身的签名,构建相应的StatementCallback实例以调用回调接口中公开的方法,例如:

    public void execute(final String sql) throws DataAccessException {
            if (logger.isDebugEnabled()) {
                logger.debug("Executing SQL statement [" + sql + "]");
            }
    
            class ExecuteStatementCallback implements StatementCallback, SqlProvider {
                public Object doInStatement(Statement stmt) throws SQLException {
                    stmt.execute(sql);
                    return null;
                }
                public String getSql() {
                    return sql;
                }
            }
            execute(new ExecuteStatementCallback());
        }

    同一组内的模板方法,可以根据使用的方便性进行增加,只要在实现的时候,将相应的条件加以对应,改组的回调接口进行封装,最终调用当前组的核心模板方法即可。
    下面来逐一看下这些方法:
    public void execute(final String sql):

    public void execute(final String sql) throws DataAccessException {
            if (logger.isDebugEnabled()) {
                logger.debug("Executing SQL statement [" + sql + "]");
            }
    
            class ExecuteStatementCallback implements StatementCallback, SqlProvider {
                public Object doInStatement(Statement stmt) throws SQLException {
                    stmt.execute(sql);
                    return null;
                }
                public String getSql() {
                    return sql;
                }
            }
            execute(new ExecuteStatementCallback());
        }

    根据传入的静态SQL语句进行更新,无返回值,使用的是Statement

     public Object execute(String callString, CallableStatementCallback action) throws DataAccessException:

    public Object execute(String callString, CallableStatementCallback action) throws DataAccessException {
            return execute(new SimpleCallableStatementCreator(callString), action);
        }

    内部调的是

    public Object execute(CallableStatementCreator csc, CallableStatementCallback action)
                throws DataAccessException {
    
            Assert.notNull(csc, "CallableStatementCreator must not be null");
            Assert.notNull(action, "Callback object must not be null");
            if (logger.isDebugEnabled()) {
                String sql = getSql(csc);
                logger.debug("Calling stored procedure" + (sql != null ? " [" + sql  + "]" : ""));
            }
    
            Connection con = DataSourceUtils.getConnection(getDataSource());
            CallableStatement cs = null;
            try {
                Connection conToUse = con;
                if (this.nativeJdbcExtractor != null) {
                    conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
                }
                cs = csc.createCallableStatement(conToUse);
                applyStatementSettings(cs);
                CallableStatement csToUse = cs;
                if (this.nativeJdbcExtractor != null) {
                    csToUse = this.nativeJdbcExtractor.getNativeCallableStatement(cs);
                }
                Object result = action.doInCallableStatement(csToUse);
                handleWarnings(cs);
                return result;
            }
            catch (SQLException ex) {
                // Release Connection early, to avoid potential connection pool deadlock
                // in the case when the exception translator hasn't been initialized yet.
                if (csc instanceof ParameterDisposer) {
                    ((ParameterDisposer) csc).cleanupParameters();
                }
                String sql = getSql(csc);
                csc = null;
                JdbcUtils.closeStatement(cs);
                cs = null;
                DataSourceUtils.releaseConnection(con, getDataSource());
                con = null;
                throw getExceptionTranslator().translate("CallableStatementCallback", sql, ex);
            }
            finally {
                if (csc instanceof ParameterDisposer) {
                    ((ParameterDisposer) csc).cleanupParameters();
                }
                JdbcUtils.closeStatement(cs);
                DataSourceUtils.releaseConnection(con, getDataSource());
            }
        }

    JdbcTemplate在此提供了一个内部类SimpleCallableStatementCreator:

    private static class SimpleCallableStatementCreator implements CallableStatementCreator, SqlProvider {
    
            private final String callString;
    
            public SimpleCallableStatementCreator(String callString) {
                Assert.notNull(callString, "Call string must not be null");
                this.callString = callString;
            }
    
            public CallableStatement createCallableStatement(Connection con) throws SQLException {
                return con.prepareCall(this.callString);
            }
    
            public String getSql() {
                return this.callString;
            }
        }

    根据传入的sql语句创建一CallableStatement

    public Object execute(String sql, PreparedStatementCallback action) throws DataAccessException

    public Object execute(String sql, PreparedStatementCallback action) throws DataAccessException {
            return execute(new SimplePreparedStatementCreator(sql), action);
        }

    可知其内部调用了
    execute(new SimplePreparedStatementCreator(sql), action)方法。

    public Object execute(PreparedStatementCreator psc, PreparedStatementCallback action)
                throws DataAccessException {
    
            Assert.notNull(psc, "PreparedStatementCreator must not be null");
            Assert.notNull(action, "Callback object must not be null");
            if (logger.isDebugEnabled()) {
                String sql = getSql(psc);
                logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
            }
    
            Connection con = DataSourceUtils.getConnection(getDataSource());
            PreparedStatement ps = null;
            try {
                Connection conToUse = con;
                if (this.nativeJdbcExtractor != null &&
                        this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativePreparedStatements()) {
                    conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
                }
                ps = psc.createPreparedStatement(conToUse);
                applyStatementSettings(ps);
                PreparedStatement psToUse = ps;
                if (this.nativeJdbcExtractor != null) {
                    psToUse = this.nativeJdbcExtractor.getNativePreparedStatement(ps);
                }
                Object result = action.doInPreparedStatement(psToUse);
                handleWarnings(ps);
                return result;
            }
            catch (SQLException ex) {
                // Release Connection early, to avoid potential connection pool deadlock
                // in the case when the exception translator hasn't been initialized yet.
                if (psc instanceof ParameterDisposer) {
                    ((ParameterDisposer) psc).cleanupParameters();
                }
                String sql = getSql(psc);
                psc = null;
                JdbcUtils.closeStatement(ps);
                ps = null;
                DataSourceUtils.releaseConnection(con, getDataSource());
                con = null;
                throw getExceptionTranslator().translate("PreparedStatementCallback", sql, ex);
            }
            finally {
                if (psc instanceof ParameterDisposer) {
                    ((ParameterDisposer) psc).cleanupParameters();
                }
                JdbcUtils.closeStatement(ps);
                DataSourceUtils.releaseConnection(con, getDataSource());
            }
        }

    在创建PreparedStatementCreator实现类的时候,JdbcTemplate为其默认提供了一个SimplePreparedStatementCreator内部静态类,可根据传入的SQL语句创建一个PreparedStatement  代码如下:

    private static class SimplePreparedStatementCreator implements PreparedStatementCreator, SqlProvider {
    
            private final String sql;
    
            public SimplePreparedStatementCreator(String sql) {
                Assert.notNull(sql, "SQL must not be null");
                this.sql = sql;
            }
    
            public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
                return con.prepareStatement(this.sql);
            }
    
            public String getSql() {
                return this.sql;
            }
        }

    其它的模仿方法与此类似,可以触类旁通!

  • 相关阅读:
    网络设备snmp配置syslog方法。
    Oracle19c安装手册Windows及mysql-oracle转换
    Oracle11g--hxss安装
    VMWare ESXI 6.5 的部署及简单使用的说明
    windos下redis的启动与安装
    二、fidder 和 python结合进行 get post 请求发送
    一、fidder工具使用
    Python学习二《数据类型介绍》
    Python学习一《基础规范》
    Charles设置为代理服务
  • 原文地址:https://www.cnblogs.com/dyh004/p/11550642.html
Copyright © 2011-2022 走看看