最近研究微信的公众平台开发,需要和微信的服务器进行数据读取,简单研究了下jdk自带的HttpUrlConnection类(URLConnection的子类),简单实现了一下微信的access_token获取。

该地址是get方法请求即可,先贴代码,doGet:

private final String ACCESS_TOKEN_WEIXIN = "https://api.weixin.qq.com/cgi-bin/token";
	
	public String doGet(String grantType, String appId, String secret){
		String url = ACCESS_TOKEN_WEIXIN+"?grant_type="+grantType+
				"&appid="+appId+"&secret="+secret;
		HttpURLConnection conn = null;
		InputStream is = null;
		InputStreamReader reader = null;
		BufferedReader br = null;
		String str = "";
		try {
			URL weiUrl = new URL(url);
			conn = (HttpURLConnection)weiUrl.openConnection();
			conn.setRequestProperty("connection", "Keep-Alive");
			conn.connect();
			is = conn.getInputStream();
			reader = new InputStreamReader(is, "UTF-8");
			br = new BufferedReader(reader);
			String readLine = "";
			while((readLine=br.readLine())!=null){
				str+=readLine+"\n";
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			try{
				if(br!=null){
					br.close();
				}
				if(conn!=null){
					conn.disconnect();
				}
			}catch(Exception e1){
				e1.printStackTrace();
			}
		}
		return str;
	}



我将InputStream流和InputStreamReader流都单独定义引用是为了测试流关闭的问题,最后只关闭最外层流,因为外层流关闭的时候就将包裹的流一并关闭了。见下面的BufferedReader类的close方法:

public void close() throws IOException {
        synchronized (lock) {
            if (in == null)
                return;
            in.close();
            in = null;
            cb = null;
        }
    }

其中,in就是包裹的流。


下面贴post方法的代码,然后综合比较一下,代码:

public String doPost(String grantType, String appId, String secret){
		HttpURLConnection conn = null;
		InputStream is = null;
		InputStreamReader reader = null;
		BufferedReader br = null;
		String str = "";
		try {
			URL url = new URL(ACCESS_TOKEN_WEIXIN);
			conn = (HttpURLConnection)url.openConnection();
			conn.setRequestMethod("POST");
			conn.setDoOutput(true);//默认为false的,所以需要设置
			conn.setUseCaches(false);
			conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
			conn.connect();
			
			OutputStream outStream = conn.getOutputStream();
			DataOutputStream out = new DataOutputStream(outStream);
			String params = "grant_type="+grantType+
					"&appid="+appId+"&secret="+secret;
			out.writeBytes(params);
			out.close();
			
			is = conn.getInputStream();
			reader = new InputStreamReader(is, "UTF-8");
			br = new BufferedReader(reader);
			String readLine = "";
			while((readLine=br.readLine())!=null){
				str+=readLine+"\n";
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			try{
				if(br!=null){
					br.close();
				}
				if(conn!=null){
					conn.disconnect();
				}
			}catch(Exception e1){
				e1.printStackTrace();
			}
		}
		return str;
	}



两相比较:

1.思路是一样的:首先建立链接对象(URL)->由链接对象得到代理对象(HttpUrlConnection)->设置一些链接参数->启动链接通道(conn.connect();)->进行输出\输入操作

2.get方法参数是带在链接上的,所以没有单独赋值操作,直接在启动链接通道后得到输入流即可得到返回值

3.post方法参数不带在链接上,需要单独赋值(赋值是使用输出流来进行的),需要将输出打开(conn.setDoOutput(true);)因为URLConnection默认输出是false;而input是默认打开的,故不用重复设置。

4.读取返回值方式一样。


在研究过程中,对于URLConnection怎样得到的InputStream比较好奇,查看源码为:

public InputStream getInputStream() throws IOException {
        throw new UnknownServiceException("protocol doesn't support input");
    }



所以很迷惑,是jdk1.7 。等下来研究出来再补上来。

共勉!

-----------------------------------------------------------------

紧跟上面,对于jdk怎样实现的getInputStream和getOutputStream以及其他一些方法,经过研究,终于找到了。接下来释疑:

需要注意到:

URL url = new URL(ACCESS_TOKEN_WEIXIN);
			conn = (HttpURLConnection)url.openConnection();


这里得到HttpUrlConnection的时候是使用了强转得到的,故跟踪到URL类的openConnection方法内->

public URLConnection openConnection() throws java.io.IOException {
        return handler.openConnection(this);
    }


这里的handler定义在URL类的上方->

transient URLStreamHandler handler;



然后去寻找handler的赋值方法getURLStreamHandler,发现如下代码->

if (handler == null) {
                String packagePrefixList = null;

                packagePrefixList
                    = java.security.AccessController.doPrivileged(
                    new sun.security.action.GetPropertyAction(
                        protocolPathProp,""));
                if (packagePrefixList != "") {
                    packagePrefixList += "|";
                }

                // REMIND: decide whether to allow the "null" class prefix
                // or not.
                packagePrefixList += JDK_PACKAGE_PREFIX;

                StringTokenizer packagePrefixIter =
                    new StringTokenizer(packagePrefixList, "|");

                while (handler == null &&
                       packagePrefixIter.hasMoreTokens()) {

                    String packagePrefix =
                      packagePrefixIter.nextToken().trim();

                    // do not try to instantiate the JDK gopher handler
                    // unless the system property had been explicitly set
                    if (protocol.equalsIgnoreCase(GOPHER) &&
                        packagePrefix.equals(JDK_PACKAGE_PREFIX) &&
                        !enableGopher) {
                            continue;
                    }
                    try {
                        String clsName = packagePrefix + "." + protocol +
                          ".Handler";
                        Class cls = null;
                        try {
                            cls = Class.forName(clsName);



其中

JDK_PACKAGE_PREFIX

在类中定义的是:

private static final String JDK_PACKAGE_PREFIX =  "sun.net.www.protocol";

再结合下面的


String clsName = packagePrefix + "." + protocol +
                          ".Handler";

基本可以定位到handler类的实现类在sun.net.www.protocol.http.Handler。然后百度搜索之后找到源码(jdk下没有sun包的源码)->

sun.net.www.protocol.http包的视图  Handler.java的源码 ->

protected java.net.URLConnection openConnection(URL u)
    throws IOException {
        return openConnection(u, (Proxy)null);
    }

    protected java.net.URLConnection openConnection(URL u, Proxy p)
        throws IOException {
        return new HttpURLConnection(u, p, this);
    }



可以看到返回值是HttpURLConnection,但是发现该类中并没有引入java.net.HttpURLConnection类,故猜测在Handler的同包下有一个同名类,果然发现该类,

源码 ->

public class HttpURLConnection extends java.net.HttpURLConnection



这是关建行,该类继承了java.net.HttpURLConnection类。所以返回该类可以由java.net.HttpURLConnection类的父类URLConnection接到,然后强转成java.net.HttpURLConnection类。设计十分巧妙->

@Override
    public synchronized OutputStream getOutputStream() throws IOException {

        try {
            if (!doOutput) {
                throw new ProtocolException("cannot write to a URLConnection"
                               + " if doOutput=false - call setDoOutput(true)");
            }

            if (method.equals("GET")) {
                method = "POST"; // Backward compatibility
            }
            if (!"POST".equals(method) && !"PUT".equals(method) &&
                "http".equals(url.getProtocol())) {
                throw new ProtocolException("HTTP method " + method +
                                            " doesn't support output");
            }

            // if there's already an input stream open, throw an exception
            if (inputStream != null) {
                throw new ProtocolException("Cannot write output after reading input.");
            }

            if (!checkReuseConnection())
                connect();

......

这就是getOutputStream的实现地方了,正好doOutput属性的设置也在这里起到了作用。艾玛,终于找到实现的地方了,其他方法原理一致了。

稍微整理下:整个的类流转过程就是:sun.net.www.protocol.http.HttpURLConnection转化成java.net.URLConnection,然后强转成java.net.HttpURLConnection。

晕乎~