JDBC中驱动加载的过程分析

作者:kenty  来源:博客园  发布时间:2007-08-20 15:01  阅读:1100 次  原文链接   [收藏]  
 本篇从java.sql.Driver接口、java.sql.DriveManager类以及其它开源数据库的驱动类讨论JDBC中驱动加载的全过程以及JDBC的Framework如何做到“可插拔”的细节。

本篇包含了很多部分的内容。如类加载器、本地方法、对象锁、类锁、按功能或者状态分离锁、安全机制,对这些内容没有深入讨论!详情可以继续关注本博客!我在上篇主要关注驱动管理器的初始化、连接的建立、驱动的注册、驱动的遍列、驱动的取消注册以及DriverManager中的日志操作。

一、Driver接口

//Driver.java

package java.sql;

public interface Driver {

Connection connect(String url, java.util.Properties info) throws SQLException;

boolean acceptsURL(String url) throws SQLException;

DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) throws SQLException;

int getMajorVersion();                                             //返回驱动的主版本号

int getMinorVersion();                                             //返回驱动的次版本号

boolean jdbcCompliant();                              //是否兼容于JDBC标准

}

以上就是JDBC中的Driver接口,它是任何数据库提供商的驱动类必须实现的接口,驱动类必须实现该接口中的所有方法!简单吧!

它之所以是一个接口,就是OO中经常谈到的“依赖倒转原则(DIP-Dependence Inverse Principle)”的具体应用了!在DriverManager类中可以看到:它使用的驱动都是Driver接口,从而依赖与高层,不依赖于实现。这样可以使用JDBC Framework管理和维护不同JDBC提供商的数据库驱动。

JDBC Framework中允许加载多个数据库的驱动!相应地,一般建议数据库提供商的驱动必须小一点,从而保证在加载多个驱动后不会占用太多的内存。

在Driver接口中共有以上六个方法。其中红色的两个相对很重要,它是供DriverManager调用的,其它四个很简单的方法!下面简单讲述前两个方法的意义!

Connection connect(String url, java.util.Properties info) throws SQLException方法是数据库提供商的驱动必须实现的方法,它主要是使用指定的URL和与具体提供商相关的信息建立一个连接。

boolean acceptsURL(String url) throws SQLException方法也是数据库提供商的驱动必须实现的方法,主要判定某个该驱动是否介绍该URL。(一般在建立连接之前调用。详情将DriverManager类)

二、DriverManager类

DriverManager类是整个JDBC的起点!利用它可以创建连接,从而完成后续的操作。在JDBC 中DriverManager是一个相对比较复杂的类,因此我们按其只能分为几类介绍。本篇将DriverManager中的方法分为3类:1.初始化;2.驱动的注册、查询、取消注册;3.建立连接;4.日志相关。

下面就看看它的源代码吧!

//DriverManager.java        1.45 05/11/17

package java.sql;

import sun.misc.Service;

import java.util.Iterator;

class DriverInfo {

Driver         driver;

Class          driverClass;

String         driverClassName;

public String toString() {

return ("driver[className=" + driverClassName + "," + driver + "]");

}

}

public class DriverManager {

// Prevent the DriverManager class from being instantiated.

private DriverManager(){}

以上是其代码的前面部分。主要是包的定义、相关文件的导入、类的定义以及一个私有化的构造器――即该类不可以实例化,只可以调用其静态方法,相当于一个工具类――一个管理驱动的工具类!还有一个就是一个辅助类DriverInfo,它包装了驱动类Driver,包含驱动类的类和驱动类的名称。

下面就开始DriverManager类重要方法的介绍吧!

1.初始化

private static java.util.Vector drivers = new java.util.Vector();    //保存多个驱动的聚集

private static boolean initialized = false;                                              //是否初始化的标记,默认当然是否了

// 真正的初始化方法

static void initialize() {

if (initialized) {    return;     }                                   //已经初始化就返回!(初始化了就算了)

initialized = true;                                                               //设置此标识符,表示已经完成初始化工作

loadInitialDrivers();                                                           //初始化工作主要是完成所有驱动的加载

println("JDBC DriverManager initialized");

}

//初始化方法中完成加载所有系统提供的驱动的方法

private static void loadInitialDrivers() {

String drivers;

try {

drivers = (String) java.security.AccessController.doPrivileged(

new sun.security.action.GetPropertyAction("jdbc.drivers"));

//得到系统属性"jdbc.drivers"对应的驱动的驱动名(这可是需要许可的哦!)。

} catch (Exception ex) {

drivers = null;

}

Iterator ps = Service.providers(java.sql.Driver.class);         //从系统服务中加载驱动

while (ps.hasNext()) {                                                                        //加载这些驱动,从而实例化它们

ps.next();

}

println("DriverManager.initialize: jdbc.drivers = " + drivers);

if (drivers == null) {    return;       }                                       //系统属性未指定驱动则返回

while (drivers.length() != 0) {                                                              //循环过程,讲解见下面

int x = drivers.indexOf(':');

String driver;

if (x < 0) {

driver = drivers;

drivers = "";

} else {

driver = drivers.substring(0, x);

drivers = drivers.substring(x+1);

}

if (driver.length() == 0) {     continue;      }

try {

println("DriverManager.Initialize: loading " + driver);

Class.forName(driver, true, ClassLoader.getSystemClassLoader());

//加载这些驱动,下篇会讲解其细节

} catch (Exception ex) {

println("DriverManager.Initialize: load failed: " + ex);

}

}//end of while

//系统属性"jdbc.drivers"可能有多个数据库驱动,这些驱动的名字是以“:”分隔开的,

//上面的过程就是将此以“:”分隔的驱动,依次遍列,然后调用Class.forName依次加载

}

private static Object logSync = new Object();                //对象锁

//下面是一个辅助方法,用于向日志中写入信息!

public static void println(String message) {

synchronized (logSync) {                        //很重要的一致性编程的方法,见下面

if (logWriter != null) {                                          //设置日志才可以进行下面的写入信息

logWriter.println(message);                      //向logger中写入信息

logWriter.flush();

}

}

}

//以上蓝色的属性和方法,是一致性编程(Concurent Programming)中的重要方法。

//首先明确我们在向日志写入信息的时候,是可以调用其它非写入日志的方法的,

//只是不同的客户不能同时调用该写入方法――一个客户正在写入,其它必须等待写完

//假如我们机械地使用synchronized(this)或synchronized该写入方法时,必然会导致效率低

//一般地,当类的中多个方法可以分为多个不同组,这些组的方法互相之间不干扰时,

//可以为每个组指定一个自己的锁,限制同一个方法被多个客户使用,从而保证该方法的

//一致性,保证无必要的synchronized方法!

//关于一致性编程,请多关注博客中的文章

2.驱动的注册、查询、取消注册

//向DriverManager 注册指定的驱动。驱动这么注册请阅读下篇!

public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {

if (!initialized) {          initialize();   }         //注册前必须初始化了

DriverInfo di = new DriverInfo();                        //创建一个新的驱动信息类

di.driver = driver;

di.driverClass = driver.getClass();

di.driverClassName = di.driverClass.getName();               //以上填入注册驱动的信息

drivers.addElement(di);                                                       //将此驱动信息假如驱动聚集中

println("registerDriver: " + di);

}

//

public static synchronized Driver getDriver(String url) throws SQLException {

println("DriverManager.getDriver( "" + url + " ")");

if (!initialized) {      initialize();        }                 //同样必须先初始化

//本地方法,得到调用此方法的类加载器

ClassLoader callerCL = DriverManager.getCallerClassLoader();

// 遍列所有的驱动信息,返回能理解此URL的驱动

for (int i = 0; i < drivers.size(); i++) {                                                  //遍列驱动信息的聚集

DriverInfo di = (DriverInfo)drivers.elementAt(i);

// 调用者在没有权限加载此驱动时会忽略此驱动

if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {

println("    skipping: " + di);

continue;

}

try {

println("    trying " + di);

if (di.driver.acceptsURL(url)) {                                //驱动能理解此URL时,返回此驱动

println("getDriver returning " + di);

return (di.driver);

}

} catch (SQLException ex) {

// Drop through and try the next driver.

}

}

println("getDriver: no suitable driver");

throw new SQLException("No suitable driver", "08001");

}

//从DriverManager 中取消注册某个驱动。Applet仅仅能够取消注册从它的类加载器加载的驱动

public static synchronized void deregisterDriver(Driver driver) throws SQLException {

ClassLoader callerCL = DriverManager.getCallerClassLoader();

println("DriverManager.deregisterDriver: " + driver);

int i;

DriverInfo di = null;

for (i = 0; i < drivers.size(); i++) {

di = (DriverInfo)drivers.elementAt(i);

if (di.driver == driver) {break;    }                //找到了某个驱动则返回,同时返回i值

}

if (i >= drivers.size()) {                                                         //全部遍列完,度没有找到驱动则返回

println("    couldn't find driver to unload");

return;

}

//找到此驱动,但调用者不能加载此驱动,则抛出异常

if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {

throw new SecurityException();

}

// 在以上所有操作后,可以删除此驱动了

drivers.removeElementAt(i);

}

//得到当前所有加载的JDBC驱动的枚举**

public static synchronized java.util.Enumeration getDrivers() {

java.util.Vector result = new java.util.Vector();

if (!initialized) {     initialize();      }              //该类没有初始化时,必须完成初始化工作

//详情请阅读初始化部分

ClassLoader callerCL = DriverManager.getCallerClassLoader();    //得到当前类的类加载器

for (int i = 0; i < drivers.size(); i++) {                                                           //遍列所有的驱动

DriverInfo di = (DriverInfo)drivers.elementAt(i);                                 //得到某个具体的驱动

// 假如调用者没有许可加载此驱动时,忽略该驱动

if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {

println("    skipping: " + di);

continue;

}

result.addElement(di.driver);                        //将可以加载的驱动加入要返回的结果集

}

return (result.elements());                             //返回结果集

}

private static native ClassLoader getCallerClassLoader();

//获得当前调用者的类装载器的本地方法(关于本地方法JNI请关注本博客后续文章)

// 返回类对象。我们使用DriverManager的本地方法getCallerClassLoader()得到调用者的类加载器

private static Class getCallerClass(ClassLoader callerClassLoader, String driverClassName) {

// callerClassLoader为类加载器,driverClassName为驱动的名称

Class callerC = null;

try {

callerC = Class.forName(driverClassName, true, callerClassLoader);

//使用指定的类装载器定位、加载指定的驱动类,

//true代表该驱动类在没有被初始化时会被初始化,返回此类

}catch (Exception ex) {

callerC = null;           // being very careful

}

return callerC;

}

3.建立连接

在JDBC程序中一般使用DriverManager.getConnection方法返回一个连接。该方法有多个变体,它们都使用了具体驱动类的connect方法实现连接。下面是连接的核心方法。

private static Connection getConnection(String url, java.util.Properties info, ClassLoader callerCL)

throws SQLException {

//当类加载器为null时,必须检查应用程序的类加载器

//其它在rt.jar之外的JDBC驱动类可以从此加载驱动 /*

synchronized(DriverManager.class) {                    //同步当前DriverManger的类

if(callerCL == null) {    callerCL = Thread.currentThread().getContextClassLoader();   }

//得到当前线程的类加载器(此句的真正含义请关注后续线程相关的文章)

}

if(url == null) {    throw new SQLException("The url cannot be null", "08001");   }

println("DriverManager.getConnection( "" + url + " ")");

if (!initialized) {    initialize();         }                 //必须初始化,将默认的驱动加入

// 遍列当前的所有驱动,并试图建立连接

SQLException reason = null;

for (int i = 0; i < drivers.size(); i++) {

DriverInfo di = (DriverInfo)drivers.elementAt(i);

// 假如调用者没有许可加载该类,忽略它

if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {

//当驱动不是被当前调用者的类加载器加载时忽略此驱动

println("    skipping: " + di);

continue;

}

try {

println("    trying " + di);

Connection result = di.driver.connect(url, info);           //调用某个驱动的连接方法建立连接

if (result != null) {                                                //在建立连接后打印连接信息且返回连接

println("getConnection returning " + di);

return (result);

}

} catch (SQLException ex) {

if (reason == null) {    reason = ex;              }                 //第一个错误哦

}

}

//以上过程要么返回连接,要么抛出异常,当抛出异常会给出异常原因,即给reason赋值

//在所有驱动都不能建立连接后,若有错误则打印错误且抛出该异常

if (reason != null)    {

println("getConnection failed: " + reason);

throw reason;

}

//若根本没有返回连接也没有异常,否则打印没有适当连接,且抛出异常

println("getConnection: no suitable driver");

throw new SQLException("No suitable driver", "08001");

}

//以下三个方法是上面的连接方法的变体,都调用了上面的连接方法

public static Connection getConnection(String url, java.util.Properties info) throws SQLException {

ClassLoader callerCL = DriverManager.getCallerClassLoader();

//没有类加载器时就是该调用者的类加载器

return (getConnection(url, info, callerCL));

}

public static Connection getConnection(String url, String user, String password) throws SQLException {

java.util.Properties info = new java.util.Properties();

ClassLoader callerCL = DriverManager.getCallerClassLoader();

if (user != null) {    info.put("user", user);      }

if (password != null) {       info.put("password", password);     }

return (getConnection(url, info, callerCL));

}

public static Connection getConnection(String url) throws SQLException {

java.util.Properties info = new java.util.Properties();

ClassLoader callerCL = DriverManager.getCallerClassLoader();

return (getConnection(url, info, callerCL));

}

4.日志相关

DriverManager中与日志相关的方法有好几个,主要分为已被deprecated的Stream相关的方法,和建议使用的Reader、Writer相关的方法。(对应于java IO的字符流和字节流哦!因为写入日志的信息一般为字符流,所以废弃了与字节流相关的方法)

//常数。允许设置logging stream

final static SQLPermission SET_LOG_PERMISSION = new SQLPermission("setLog");

private static int loginTimeout = 0;

private static java.io.PrintWriter logWriter = null;                                     //写入的字符流

private static java.io.PrintStream logStream = null;                       //写入的字节流

//设置驱动在试图连接(log)时等待的最大时间

public static void setLoginTimeout(int seconds) {     loginTimeout = seconds;    }

public static int getLoginTimeout() {        return (loginTimeout);    }

public static java.io.PrintWriter getLogWriter() {          //得到LogWriter

return logWriter;

}

public static void setLogWriter(java.io.PrintWriter out) {                //设置字符流

SecurityManager sec = System.getSecurityManager();                      //取得安全管理器

if (sec != null) {    sec.checkPermission(SET_LOG_PERMISSION);    }

//检查是否具有日志写入的权限,有权限则继续,否则抛出异常!

logStream = null;

logWriter = out;

}

public static void setLogStream(java.io.PrintStream out) {        //设置字节流

SecurityManager sec = System.getSecurityManager();

if (sec != null) {     sec.checkPermission(SET_LOG_PERMISSION);      }

logStream = out;

if ( out != null )

logWriter = new java.io.PrintWriter(out);                              //将字节流包装为字符流

else

logWriter = null;

}

public static java.io.PrintStream getLogStream() {               //得到字节流

return logStream;

}

}

以上对应于《教你建立简单JDBC程序的》DriverManager.getConnection()方法。

下篇我们将关注:数据库提供商如何注册自己的驱动,即关注Class.forName()方法。以及“可插拔”等概念!
 本篇主要用几个开源数据库的驱动讲述驱动是如何加载的,以及“可插拔”机制等。

由于文章篇幅有限,本文章并不是专门研究开源源代码!因此我们仅研究那些与数据库驱动加载直接相关的方法。在下面的几个开源软件的驱动中,我们主要关注驱动类的getConnection()方法。

一、几个开源数据库的驱动类

以下第一个是smallsql中驱动类SSDriver的源代码:

package smallsql.database;

import java.sql.*;

import java.util.Properties;

public class SSDriver implements Driver {

static SSDriver drv;

static {

try{

drv = new SSDriver();

java.sql.DriverManager.registerDriver(drv);

}catch(Throwable e){}

}

public Connection connect(String url, Properties info) throws SQLException {

if(!acceptsURL(url)) return null;

……

return new SSConnection( (idx > 0) ? url.substring(idx+1) : null);

}

……

}

从上面红色的部分可以看到:这是一个静态语句块(static block),这意味着该语句是在类构造完成前完成的(关于语句块的加载请阅读《Think in java》)。即调用class.forName(“smallsql.database.SSDriver”)语句时,会首先创建一个SSDriver的实例,并且将其向驱动管理器(DriverManager)注册。这样就完成驱动的注册了。

从上面的蓝色的代码可以看出:驱动的连接方法返回的是一个具体的SSConnection对象。而在前面研究的Driver接口中返回的是Connection接口,这是不茅盾的,SSConnection对象实现了Connection接口。

再下面一个是HSqlD中的驱动类的源代码:

package org.hsqldb;

import java.sql.*;

import java.util.Properties;

import org.hsqldb.jdbc.jdbcConnection;

import org.hsqldb.persist.HsqlDatabaseProperties;

import org.hsqldb.persist.HsqlProperties;

public class jdbcDriver implements Driver {

public Connection connect(String url, Properties info) throws SQLException {

return getConnection(url, info);

}

public static Connection getConnection(String url, Properties info) throws SQLException {

HsqlProperties props = DatabaseURL.parseURL(url, true);

if (props == null) {

throw new SQLException(Trace.getMessage(Trace.INVALID_JDBC_ARGUMENT));

} else if (props.isEmpty()) {

return null;

}

props.addProperties(info);

return new jdbcConnection(props);

}

static {

try {

DriverManager.registerDriver(new jdbcDriver());

} catch (Exception e) {}

}

}

蓝色的依然是建立连接的部分,依然返回某个具体的实现java.sql.Connection接口的对象。是设计模式中的哪个工厂啊(一般工厂、抽象工厂、工厂方法还是什么都不是啊)!

红色的同样是一个静态语句块,同样完成该驱动的注册。

两个开源数据库的驱动竟然如此相似,是不是其它的就不一样呢!看下面是mckoi中的驱动类:

package com.mckoi;

public class JDBCDriver extends com.mckoi.database.jdbc.MDriver {

static {

com.mckoi.database.jdbc.MDriver.register();

}

public JDBCDriver() {

super();

// Or we could move driver registering here...

}

}

红色的部分又是完成相同的工作――在类装载完成前向驱动管理器注册驱动,只不过这次是调用MDriver的register方法罢了。那么该驱动建立连接是否也一样呢,当然一样啦!不信你看下面的代码:

package com.mckoi.database.jdbc;

……

public class MDriver implements Driver {

……

private static boolean registered = false;

public synchronized static void register() {

if (registered == false) {

try {

java.sql.DriverManager.registerDriver(new MDriver());

registered = true;

}catch (SQLException e) {

e.printStackTrace();

}

}

}

public Connection connect(String url, Properties info) throws SQLException {

if (!acceptsURL(url)) {      return null;    }

DatabaseInterface db_interface;

int row_cache_size;

int max_row_cache_size;

……

MConnection connection = new MConnection(url, db_interface, row_cache_size, max_row_cache_size);

……

return connection;

}

}

从以上三个开源数据库驱动的源代码可以看出:在调用Class.forName(“XXXDriver”)时,完成了将具体的驱动程序向JDBC API中驱动管理器的注册,该注册方法在类构造完成前完成,一般使用静态语句块。在调用DriverManager的getConnection方法时,一般先在已注册的驱动中查找可以了解此URL的驱动,然后调用该驱动的connect方法,从而建立连接,返回的连接都是一个实现java.sql.Connection接口的具体类。下面是该过程的时序图。

二、JDBC中驱动加载的时序图

以上是JDBC中驱动加载的时序图。时序图主要有以下7个动作:

1.         客户调用Class.forName(“XXXDriver”)加载驱动。

2.         此时此驱动类首先在其静态语句块中初始化此驱动的实例,

3.         再向驱动管理器注册此驱动。

4.         客户向驱动管理器DriverManager调用getConnection方法,

5.         DriverManager调用注册到它上面的能够理解此URL的驱动建立一个连接,

6.         在该驱动中建立一个连接,一般会创建一个对应于数据库提供商的XXXConnection连接对象,

7.         驱动向客户返回此连接对象,不过在客户调用的getConnection方法中返回的为一个java.sql.Connection接口,而具体的驱动返回一个实现java.sql.Connection接口的具体类。

以上就是驱动加载的全过程。由此过程我们可以看出JDBC的其它一些特点。

三、JDBC的架构

在《教你建立简单JDBC程序》一篇中,讲述了一般JDBC的几个步骤。通过本篇的介绍,我将此程序分为以下几部分:

上图中,蓝色的即为本章前面介绍的JDBC驱动加载的细节部分。看看下面的部分:左面的很明显吧!是java.sql包中的接口吧!它是抽象的!右边呢?通过驱动管理器DriverManager得到的是一个实现java.sql.Connection接口的具体类吧!(不知道啊!前面不是讲过了吗!)因此我们可以可以注意到左右分别是抽象的和具体的。(这种抽象和具体的连接是由java的RTTI支持的,不懂可以阅读《Think in java》)。在接下来的结果集的处理rs也是抽象的吧!

因此,在写JDBC程序时,即使我们使用不同数据库提供商的数据库我们只要改变驱动类的地址,和具体连接的URL及其用户名和密码,其它几乎不用任何修改,就可以完成同样的工作!方便吧!

这意味着什么呢?我们其实是在针对抽象接口编程,只要知道接口的调用顺序,以及其中的主要方法,我们就可以迅速学会JDBC编程了!

同时,我们只要对不同数据库提供商的驱动类使用Class.forName(“XXXDriver”)就可以加载驱动,其细节你根本不用关注――JDBC Framework已经全为你搞定了!应用程序对于不同提供商的数据库是无需太多改动的,因而其是“可插拔”的!整个J2EE就是一个高层的API――抽象接口,用户可以使用不同的产品提供商提供的产品,使用统一的API,从而便于程序员学习!

谢谢大家!到此结束!

JDBC中驱动加载的过程分析相关推荐

  1. JDBC驱动加载机制详解以及spi机制

    首先有两个问题: 1.java连接数据库时是否真的需要加载驱动? 2.JDBC如何区分多个驱动? 以下摘自:https://blog.csdn.net/buqutianya/article/detai ...

  2. 老调重弹:JDBC系列之驱动加载原理全面解析)

    前言 最近在研究Mybatis框架,由于该框架基于JDBC,想要很好地理解和学习Mybatis,必须要对JDBC有较深入的了解.所以便把JDBC 这个东东翻出来,好好总结一番,作为自己的笔记,也是给读 ...

  3. 老调重弹:JDBC系列 之 驱动加载原理全面解析

    前言 最近在研究Mybatis框架,由于该框架基于JDBC,想要很好地理解和学习Mybatis,必须要对JDBC有较深入的了解.所以便把JDBC 这个东东翻出来,好好总结一番,作为自己的笔记,也是给读 ...

  4. HarmonyOS镜像,HarmonyOS驱动加载过程分析

    HDF(硬件驱动程序基础)驱动程序框架为驱动器开发人员提供驱动程序框架功能,包括驱动器加载,驱动服务管理和驱动程序消息传递机制.它旨在构建一个统一的驱动器架构平台,为驾驶开发人员提供更准确,更高效的开 ...

  5. (DT系列四)驱动加载中, 如何取得device tree中的属性

    转载于: http://blog.csdn.net/lichengtongxiazai/article/details/38941933 本文以At91rm9200平台为例,从源码实现的角度来分析驱动 ...

  6. mysql驱动如何编写_解读MySQL驱动加载逻辑

    解读MySQL驱动加载逻辑 我们很早之前就知道最基础的JDBC编写,先执行Class.forName方法,加载MySQL驱动.但是为什么加载过驱动后,后续的接口层的调用就会自动切换到MySQL的相关代 ...

  7. linux设备和驱动加载的先后顺序

    点击打开链接 Linux驱动先注册总线,总线上可以先挂device,也可以先挂driver,那么究竟怎么控制先后的顺序呢. Linux系统使用两种方式去加载系统中的模块:动态和静态. 静态加载:将所有 ...

  8. WinCE流驱动加载的控制

    前段时间整理了< WinCE下调试串口的动态复用>,基本实现了调试串口与普通功能串口之间的动态切换.其中实现的方法有点欠缺,在重新烧录或者升级系统后,导致系统无法正常启动.这算是个BUG. ...

  9. 征途2无法显示服务器列表,解决win10系统玩征途2提示“DriverCommlnit驱动加载失败”的方法...

    征途2是一款众所周知的角色扮演类游戏,它在玩法上延续了征途1的总体风格,但是在画面和音效上比征途1的要提高了很多,其独特的玩法吸引了众多的玩家.但是又部分用户反映,在win10系统上运行征途2的游戏时 ...

最新文章

  1. Could not load file or assembly App_Licenses.dll的问题
  2. 爬虫之常用数据解析方法
  3. Python语言学习:Python常用自带库(imageio、pickle)简介、使用方法之详细攻略
  4. 前端学习(1526):heroes案例 效果演示
  5. windows环境下nginx的入门配置跳转tomcat
  6. java三元组的快速转置_矩阵压缩——三元组以及矩阵快速转置程序实现
  7. WPF如何给窗口设置透明png的图片背景
  8. 灰度拉伸python_灰度变换之灰度线性拉伸(算法1)
  9. idea redis图形化_5.13redis图形化工具---idea中配置redis密码
  10. WordPress更改“固定链接”后 ,页面出现404的解决方法
  11. 20190618每日一句
  12. unity UI 之text and image
  13. 2017山东省ACM省赛总结(校史首金!!)
  14. [题解]luogu_P3593_[NOIP2017]逛公园(最短路相关计数
  15. 基于信息增益率的决策树特征选择算法(C4.5)及其python实现
  16. 抖音运营详细教程,算法解读、平台规则、热门涨粉......丨国仁网络
  17. 浅谈一下量化交易与程序化交易
  18. 自学考试-“软件开发工具”
  19. 关于软考证书专项扣除填报抵扣个税
  20. scrapy爬取京东图书的数据

热门文章

  1. mysql多表联查分页_sqlserver多表联合查询和多表分页查询的代码讲解
  2. 运维企业专题(10)RHCS集群工具——FENCE搭建、高可用服务配置详解
  3. 提升篇——SELINUX相关介绍
  4. http status 404 – 未找到_HTTP状态码
  5. C语言各类型变量所占的字节数
  6. php在dw中设置按钮圆角,Dreamweaver怎么用CSS制作圆角按钮?
  7. makefile ifneq多个判断条件_一文入门Makefile
  8. java 读取url https_如何获取URL链接是http还是https
  9. python中def _init_是什么意思_Python中self和__init__的含义与使用
  10. 多态时:成员特点,成员变量