Archive for the 'Programming Language' Category

基于文件系统的生产者和消费者问题

周末的时候和team讨论了下如何用最简单的方式,提高数据文件的单位时间传输吞吐量。下面是一个简单的应用场景:

一个目录(DIR1),有很多Producer向这个目录里面放文件,同时有很多的Consumer负责从这个目录里面消费这些文件,插入数据库或者做其他的操作,然后删除或者移走这些文件。

假设条件:

  • 一个文件中转目录DIR1,这个目录位于一个网络存储上
  • 一个生产者,每一秒钟向DIR1里面放一个文件
  • 若干个消费者,假设有8个,其实是8个不同的服务器,都可以访问DIR1,多台服务器可以起负载均衡的作用,任何一台或者几台出问题,整个数据流不会中断
  • 解析一个文件大约需要2-14秒
  • 最后一点:位于网络存储上的目录DIR1,我们认为它是不会出问题的,它不是这里的问题核心

这个场景很普遍,很多公司大概都会用到,尤其是那么比较老的系统(Legacy System),下面是两种方案:

方案一

Consumer循环扫描DIR1,一旦发现有文件,循环解析这些文件,这里有8台服务器,也就是说有8个Consumer一起这样做。代码如下:

	public void run() {
		System.out.println("Created consumer:" + threadName);

		while (true) {
			File file = new File(Constant.STAGING_FOLDER);
			File files[] = file.listFiles();
			for (int i = 0; i < files.length; ++i) {
				File f = files[i];
				parse(Constant.STAGING_FOLDER + "/" + f.getName());
			}

			Commons.sleep();
		}
	}

看起来很简单,可是上面的代码效率非常的差,多个Consumer有很大的几率拿到相同的文件,当某个Consumer尝试去解析一个文件时,却发现这个文件已经被别的Consumer解析过了,并且文件也都删除或者移走了。这样浪费的很多的CPU时间。

可以用下面的方案来替代:

方案二

	public void run() {
		System.out.println("Created consumer:" + threadName);

		while (true) {
			File file = new File(Constant.STAGING_FOLDER);
			File files[] = file.listFiles();

			int nCapacity = files.length > Constant.CAPACITY ? Constant.CAPACITY
					: files.length;
			System.out.println(this.threadName + " found " + nCapacity
					+ " files");

			for (int i = 0; i < nCapacity; ++i) {
				File f = files[i];
				f.renameTo(new File(Constant.TMP_FOLDER + "/" + f.getName()));
			}

			for (int i = 0; i < nCapacity; ++i) {
				parse(Constant.TMP_FOLDER + "/" + files[i].getName());
			}

			Commons.sleep();
		}
	}

它和方案一的不同之处在于:它每次扫描完目录后,最多只取前若干个文件,这里是10个。并且,它不急于去处理文件,而是把文件马上移动到一个临时工作目录,其他的的操作都是相同的。

对于这个方案,有个附加条件:这个临时工作目录tmp,一定要和staging目录在同一个文件系统(filesystem),这样的话,mv操作就只是修改一下inode,几乎瞬间完成。

比较(Benchmarking)

为了测试两中方案的效率差别,我写了一个模拟程序(http://googlestop.com/download/SimConsumer.7z),它有7个class:

  1. App.java - 程序入口
  2. Commons.java - 共享的函数
  3. Constant.java – 配置参数
  4. Producer.java - 生产者,每隔一秒向目录staging里丢一个文件
  5. AbstractConsumer.java – 抽象消费者,定义消费者的一些基本属性和行为
  6. Consumer1.java - 具体消费者,实现方案一
  7. Consumer2.java - 具体消费者,实现方案二

在App.java中,你可以指定调用Consumer1还是Consumer2。

对于前者(Consumer1),staging目录下的文件数目不停的增长,并且如log显示,有很多冲突:一个Consumer准备处理的文件已经被其他的Consumer处理完了,造成了很多无效的操作,由于消费速度更不上生产速度,DIR1被撑爆只是时间的问题。

对于后者(Consumer2),staging目录下的文件几乎马上就会被移动到tmp目录下,大部分时间,文件数都为0。而tmp目录下,在程序稳定后大概保存在20多个文件左右,保持一个动态的平衡。用这种方式,你也会看到很多冲突,但是只会发生在程序刚开始,原因是,刚开始的时候,8个线程几乎是同时去访问staging目录,势必拿到很多相同的文件,待到稳定后,就很少有冲突发生了。

这两种方案都是最基本的,没有借助于第三方工具完成的,成本是最低的,其实还有一些其他的方案,可能会借助一些服务来实现,比如消息分发、数据库等。有时间的话,我继续补充。

NoSuchMethodError of BeanUtils.copyProperty(due to wrong access level)

为了节省开发时间,今天打算做一个数据集合类,可以直接将java中的ResultSet,或者其他Collection派生类的内容copy到该集合中,然后加入自定义的一些方法,比如支持直接导出Excel、CSV、KDF、Image、HTML等。这里借助Apache Commons BeanUtils的和反射(Reflection),将数据库中的一行记录保存为一个对象,然后插入数据集中,结果老是报错如下:

USING CONVERTER org.apache.commons.beanutils.converters.IntegerConverter@1befab0
java.lang.reflect.InvocationTargetException: Cannot set id
at org.apache.commons.beanutils.BeanUtilsBean.copyProperty(BeanUtilsBean.java:449)
at org.apache.commons.beanutils.BeanUtils.copyProperty(BeanUtils.java:129)

… …
Caused by: java.lang.NoSuchMethodException: Property ‘id’ has no setter method
at org.apache.commons.beanutils.PropertyUtilsBean.setSimpleProperty(PropertyUtilsBean.java:1746)
at org.apache.commons.beanutils.BeanUtilsBean.copyProperty(BeanUtilsBean.java:447)

代码段如下:

private void dumpResultSet(ResultSet rs, Class clazz) throws Exception {
	ResultSetMetaData metaData = (ResultSetMetaData) rs.getMetaData();
	int colCnt = metaData.getColumnCount();
	Field[] fields = clazz.getDeclaredFields();

	while (rs.next()) {
		Object newInstance = clazz.newInstance();

		for (int i = 1; i <= colCnt; i++) {
			try {
				Object value = rs.getObject(i);
				for (int j = 0; j < fields.length; j++) {
					Field f = fields[j];
					if (f.getName().equalsIgnoreCase(
							metaData.getColumnName(i).replaceAll("_", ""))) {

						log.info("f.getName:" + f.getName());

						BeanUtils.copyProperty(newInstance, f.getName(),
								value);
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		list.add(newInstance);
	}
}

The root cause: public is required for class UserInfo, or else you’ll get the ‘NoSuchMethodError‘ exception, DO NOT ignore the access level

public class UserInfo {
	private int id;
	private String user;
	private String password;

	public UserInfo() {
	}

	public String getUser() {
		return user;
	}

	public void setUser(String user) {
		this.user = user;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}
}

Eclipse一直提示:NoSuchMethodError,可是UserInfo里面明明有对应的setter方法,最后,才鬼使神差的发现,只要将UserInfo整个类声明为public就可以了。我一直都把注意力放到是否把setter和getter设置为public,却忽略了Bean的访问级别,希望碰到类似问题的朋友可以注意一下。

A XFire error and solution

when i tried to deploy my web service with XFire, i got the following error message:

Exception in thread "main" org.codehaus.xfire.annotations.AnnotationException: Service class cannot be abstract: com.webserviceproject.xifre…

the root cause is mis-matched dependency libs, for my case, i’ve imported xfire-annotation-1.2.6.jar and xfire-annotation-1.1.1.jar at the same time. coz i used the wrong pom.xml as below:

        <dependency>
            <groupId>org.codehaus.xfire</groupId>
            <artifactId>xfire-jaxb2</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.xfire</groupId>
            <artifactId>xfire-spring</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.xfire</groupId>
            <artifactId>xfire-java5</artifactId>
            <version>1.1.1</version>
        </dependency>
       
      <dependency>
            <groupId>org.codehaus.xfire</groupId>
            <artifactId>xfire-all</artifactId>
            <version>1.2.6</version>
        </dependency>

see, actually i’ve imported XFire lib twice(1.1.1 & 1.2.6), that’s the reason why the Java complains, we just need the dependency in green.

good luck.

ASP.net 发布的小问题

Exception Details: System.Data.SqlClient.SqlException: Failed to update database “C:InetpubwwwrootSSRAPP_DATAASPNETDB.MDF” because the database is read-only.

上面的例子通常发生在publish一个新开发的web application的时候,解决方法很简单:即修改App_Data这个目录的Security属性,加入用户”NETWORK SERVICE”,并设置该用户可以对这个目录有Write权限。

C# LDAP Wrapper

Before you try to run the following code, please download Novel LDAP lib:

http://forge.novell.com/modules/xfcontent/downloads.php/ldapcsharp/ldapcsharp/CsharpLDAP-v2.1.10/

using System;
using System.Collections.Generic;
using System.Text;
using Novell.Directory.Ldap;

namespace LDAPUtility
{
    class LDAPUtil
    {
        private string ldapHost = "ssuzdc3";
        private int ldapPort = 389;
        LdapConnection ldapConn = null;

        private void Connect()
        {
            try
            {
                // Creating an LdapConnection instance
                ldapConn = new LdapConnection();

                // Connect function will create a socket connection to the server
                ldapConn.Connect(ldapHost, ldapPort);

                // Bind function with null user dn and password value will perform anonymous bind
                // to LDAP server
                ldapConn.Bind(null, null);
            }
            catch (Exception e)
            {
                // failed to connect to server
            }
        }

        private void Disconnect()
        {
            ldapConn.Disconnect();
            ldapConn = null;
        }

        public string GetSupervisor(string id)
        {
            Connect();

            string boss = "";
            try
            {
                LdapSearchResults lsc = ldapConn.Search("OU=Users,OU=Suzhou,DC=charry,DC=org",
                LdapConnection.SCOPE_ONE,
                "sAMAccountName=" + id,
                null,
                false);

                while (lsc.hasMore())
                {
                    LdapEntry nextEntry = null;
                    try
                    {
                        nextEntry = lsc.next();
                    }
                    catch (LdapException e)
                    {
                        // Exception is thrown, go for next entry
                        continue;
                    }

                    LdapAttribute attribute = nextEntry.getAttribute("manager");
                    boss = attribute.StringValue;
                }
            }
            catch (Exception e)
            {
                // exception
            }

            Disconnect();

            return GetAMAcountName(boss);
        }

        private string GetFullName(string id)
        {
            // CN=Wang, Charry,OU=Users,OU=Suzhou,DC=charry,DC=org
            int end = id.IndexOf("OU=");

            id = id.Substring(3, end - 4);
            id = id.Replace("\", "");

            return id;
        }

        // convert distinguished name to AMAcountName
        public string GetAMAcountName(string id)
        {
            id = GetFullName(id);
            Connect();

            string tmp = "";
            try
            {
                LdapSearchResults lsc = ldapConn.Search("OU=Users,OU=Suzhou,DC=charry,DC=org",
                LdapConnection.SCOPE_ONE,
                "displayName=" + id,
                null,
                false);

                while (lsc.hasMore())
                {
                    LdapEntry nextEntry = null;
                    try
                    {
                        nextEntry = lsc.next();
                    }
                    catch (LdapException e)
                    {
                        // Exception is thrown, go for next entry
                        continue;
                    }

                    LdapAttribute attribute = nextEntry.getAttribute("sAMAccountName");
                    tmp = attribute.StringValue;
                }
            }
            catch (Exception e)
            {
                // exception
            }

            Disconnect();

            return tmp;
        }

        public string GetDisplayName(string id)
        {
            Connect();
            string name = "";
            // get fullname

            try
            {
                LdapSearchResults lsc = ldapConn.Search("OU=Users,OU=Suzhou,DC=charry,DC=org",
                LdapConnection.SCOPE_ONE,
                "sAMAccountName=" + id,
                null,
                false);

                while (lsc.hasMore())
                {
                    LdapEntry nextEntry = null;
                    try
                    {
                        nextEntry = lsc.next();
                    }
                    catch (LdapException e)
                    {
                        // Exception is thrown, go for next entry
                        continue;
                    }

                    
                    LdapAttribute attribute = nextEntry.getAttribute("displayName");
                    name = attribute.StringValue;
                }
            }
            catch (Exception e)
            {
                // exception
            }

            Disconnect();

            return name;
        }

        public string GetEmail(string id)
        {
            Connect();
            string email = "";
            try
            {
                LdapSearchResults lsc = ldapConn.Search("OU=Users,OU=Suzhou,DC=charry,DC=org",
                LdapConnection.SCOPE_ONE,
                "sAMAccountName=" + id,
                null,
                false);

                while (lsc.hasMore())
                {
                    LdapEntry nextEntry = null;
                    try
                    {
                        nextEntry = lsc.next();
                    }
                    catch (LdapException e)
                    {
                        // Exception is thrown, go for next entry
                        continue;
                    }

                    LdapAttribute attribute = nextEntry.getAttribute("mail");
                    email = attribute.StringValue;
                }
            }
            catch (Exception e)
            {
                // exception
            }

            Disconnect();

            return email;
        }

        public static void test()
        {
            string ldapHost = "ssuzdc3";
            int ldapPort = 389;

            try
            {
                // Creating an LdapConnection instance
                LdapConnection ldapConn = new LdapConnection();

                // Connect function will create a socket connection to the server
                ldapConn.Connect(ldapHost, ldapPort);

                // Bind function with null user dn and password value will perform anonymous bind
                // to LDAP server
                ldapConn.Bind(null, null);

                // Searches in the Marketing container and return all child entries just below this
                // container i.e. Single level search
                LdapSearchResults lsc = ldapConn.Search("OU=Users,OU=Suzhou,DC=charry,DC=org",
                LdapConnection.SCOPE_ONE,
                "sAMAccountName=qinick",
                null,
                false);

                while (lsc.hasMore())
                {
                    LdapEntry nextEntry = null;
                    try
                    {
                        nextEntry = lsc.next();
                    }
                    catch (LdapException e)
                    {
                        Console.WriteLine("Error: " + e.LdapErrorMessage);
                        // Exception is thrown, go for next entry
                        continue;
                    }

                    Console.WriteLine("n" + nextEntry.DN);
                    LdapAttributeSet attributeSet = nextEntry.getAttributeSet();
                    System.Collections.IEnumerator ienum = attributeSet.GetEnumerator();
                    while (ienum.MoveNext())
                    {
                        LdapAttribute attribute = (LdapAttribute)ienum.Current;
                        string attributeName = attribute.Name;
                        string attributeVal = attribute.StringValue;
                        Console.WriteLine(attributeName + "value:" + attributeVal);
                    }
                }
                ldapConn.Disconnect();

            }
            catch (Exception e)
            {
                string x = e.Message;
            }

            Console.Read();
        }
    }
}

Problem with JFreechart

一个基于Tomcat的程序里面用到了JFreeChart,偶尔用浏览器访问时,PC端安装的XManager的XServer会自动的打开,然后Tomcat就莫名其妙的挂掉。在没有安装XManager的PC上通常就不会出这种情况。查看一下log,如下:

Can’t connect to X11 window server using ‘:0.0′ as the value of the DISPLAY variable.

搜之,此乃awt的bug。加入以下参数启动即可:

-Djava.awt.handless=true

如果是Tomcat,把上面的参数加到环境变量:CATALINA_OTPS里即可。

Upload the 3rd party artifact to local Archiva

Maven is a powerful build tool, more and more developers migrate to it, so do I. Below is the command for deploy the 3rd artifact to your local Archiva:

mvn deploy:deploy-file -DrepositoryId=internal -Durl=http://ssuzsws02:8080/archiva/repository/internal -DgroupId=com.amd.sws -DartifactId=mysql-connector-java -Dversion=3.0.17 -Dpackaging=jar -Dfile=test.jar

but before you issue this command, you should add the following lines to your local settings.xml

<servers>
  <server>
   <id>internal</id>
   <username>admin</username>
   <password>mypassword</password>
  </server>
</servers>

FYI.: when you upload your jar file to Archiva, you can’t see it immediately, please be patient and wait several minutes, the artifact list will be synced soon.

Hibernate, SocketTimeOutException错误

项目中用到Hibernate,部署的时候发现,过了一段时间后,Hibernate就不能正常工作了,时间很有规律,通常是在部署后的若干个小时。错误日志如下:

** BEGIN NESTED EXCEPTION **

java.net.SocketTimeoutException
MESSAGE: Read timed out

STACKTRACE:

java.net.SocketTimeoutException: Read timed out
    at java.net.SocketInputStream.socketRead0(Native Method)
    at java.net.SocketInputStream.read(Unknown Source)
    at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:1392)
    at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:1539)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:1930)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1168)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1279)
    at com.mysql.jdbc.MysqlIO.sqlQuery(MysqlIO.java:1225)
    at com.mysql.jdbc.Connection.execSQL(Connection.java:2278)
    at com.mysql.jdbc.Connection.execSQL(Connection.java:2237)
    at com.mysql.jdbc.Connection.execSQL(Connection.java:2218)
    at com.mysql.jdbc.Connection.commit(Connection.java:1155)
    at org.hibernate.transaction.JDBCTransaction.commitAndResetAutoCommit(JDBCTransaction.java:139)
    at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:115)
    at com.amd.BizB.test(BizB.java:27)
    at org.apache.jsp.index_jsp._jspService(index_jsp.java:109)
    at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
    at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:393)
    at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:320)
    at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:266)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:230)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:104)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:261)
    at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:844)
    at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:581)
    at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:447)
    at java.lang.Thread.run(Unknown Source)

最后发现是由于配置文件Hibernate.cfg.xml没有写好,少加了若干的项(注意黑体部分)

<property name=”connection.autocommit”>true</property>

<property name=”connection.url”>jdbc:mysql://ssuzsws01:3306/foo?autoReconnect=true</property>

加了这些选项后,就正常了,希望对遇到类似问题的朋友有帮助。

为什么不用SEH

前些时候,在论坛上看到一个朋友说SEH怎么怎么不好,一定不要用它。其实存在即合理。就像GOTO,不能因为它破坏了程序的流程,就不用它,适当的使用,还是可以事半功倍的。

大家知道SEH是Windows操作系统提供的一种异常处理机制,它和C++无关。在Compiler编译的时候,就把这个机制加入了我们的程序中。在VC下可以用__try, __finally, __except, __leave等关键字来标识。由于SEH可以捕获硬件异常(Hardware Exception)和软件异常(Software Exception),它比C++的异常机制能捕获更多的异常,所以有朋友不喜欢这点,认为它掩盖了错误。其实这种说法是也是合情合理的,毕竟掩盖错误不是最好的解决方案,找出问题的所在才是我们应该做的。可是在现实中,我们不可能找到所有的bug,或者由于时间的关系,来不及修补这个bug,不如先用SEH挡一挡,何尝不可。

就像我之前的一个项目,程序在一个地方偶尔会Crash掉,而且这个地方如果不能正常执行丝毫不影响整个程序的运作,不会对用户造成损失,在找出问题真正的原因之前,我们完全可以用SEH捕获异常。

 下面的例子也是一个SEH优势的体现

BOOL SafeDiv(INT32 dividend, INT32 divisor, INT32 *pResult)
{
    __try
    {
        *pResult = dividend / divisor;
    }
    __except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ?
             EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
    {
        return FALSE;
    }
    return TRUE;
}

SafeDiv用来做除法操作,它的返回值指出函数是否执行成功。pResult指向最终的结果,如果不用SEH,这类Hardware Exception会导致程序Crash,这里引入了SEH,我们在发现有除零错误的时候,让函数返回FALSE,调用处通过检查函数的返回值就可以判断除法运算是否成功,没必要就因为一个除零错误导致程序Crash掉。

由于SEH是Windows操作系统特有的机制,所以它不适合用在那些跨平台的代码,这种情况不用也罢,既然SEH是一个Windows的一个很好的异常处理机制,我们虽不能滥用它,适当的、合理的使用还是值得推荐的。

P.S.
For details about SEH, check this: http://www.google.com/search?hl=en&q=Programming+Applications+for+Microsoft+Windows

API Hook的问题,卡住了:(

这两天研究如何在我自己的进程中,获取IE或者FireFox中的网页内容,也就是监视用户上网信息。需要通过API Hook的方式拦截IE(拦截FireFox也是一个道理)用的API。

 刚开始的思路是,我想Hook MSHTML.dll 中的API,用Depends看了一下,发现只有寥寥几个导出函数,诸如ShowHTMLDialog(), 这些都不能获取网页内容。那么再换个DLL呢,于是Depends一下,ShDocVw.dll,导出函数倒是不少,大部分名字都是N/A,只有几个如OpenURL(), AddUrlToFavorites(),对我也没什么用。

上面的方法不行,我只能再向底层靠了,准备Hook Ws2_32.dll,Depends这个DLL,你可以看到,N多导出函数,让人赏心悦目,其中recv就是我要Hook的API,写了一个Demo程序,然后打开IE,运行这个Demo,发现毫无反应,我开始怀疑是我的API Hook的方式不对,后来我用同样的方法,发现可以拦截住user32.dll的MessageBoxA,百思不得其解。Google一下,发现有人和我遇到类似的问题:

Hook MSN Messenger之socket通訊的鳥事

于是这次我挂到wsock32.dll,果然,这次有反应了。总算前进了一小步,离我要获得网页内容还有很多距离呢,我的recv函数中也就是my_recv内容如下:

int PASCAL FAR my_recv (SOCKET s, char FAR * buf, int len, int flags)
{
        OutputDebugString(_T(“In my_recv”));
 
        OutputDebugString(buf);

        SaveLog(buf, len);

        int nReturn = 0;
        myJmp.SetHookOff();
        nReturn = recv(s, buf, len, flags);
        myJmp.SetHookOn();

        return (nReturn);
}

打印出buf的内容,用DebugView可以看到,很多都是乱码。不知道别人都怎么分析recv到的数据流的,怎么才能从中获取文本内容。接下来,保存也是一个问题,SaveLog的第二个参数是buf的长度,我觉得应该填recv的返回值,它是实际返回的数据的长度。但是把SaveLog放到recv后面,就不能保存到文件中,放到前面就可以,没办法,这里我只能用len。

打开文件如下图:

2007-07-07_00-14-02.jpg

最后还有个问题是:卸载这个钩子的时候,偶尔会让IE崩溃。下面是我的一个demo,有兴趣的可以帮忙调试看看上面的若干个问题该怎么解决。写的仓促,代码很乱,我也没去测试Release是否有问题,暂且这么着吧。

Downlaod Demo

Page 1 of 3123