看似简单的任务-分享二维码图片到微信(分享图片的生成)

1.任务描述

      大体场景是这样的:移动支付中的被扫模式(收款方生成二维码)的二维码分享给好友进行识别支付;考虑到模板样式的多端一致性、多样性、可配置性等特点,所以生成分享图片的功能有java服务端进行实现。刚开始接到任务的时候,想着这么简单的东西,应该很快就能完成,于是保守的估计了一天用来开发此功能;


2.二维码生成方案选型与实现

    QRcode的二维码是当前比较流行的一种二维码编码方式;众多生成算法中当然源出处是不错的选择,但是一看是Japan的,果断的放弃了这种想法;最后选择了功能比较齐全(虽然浪费),应用比较广泛的zxing来生成二维码的原始绘图点。

对应的maven配置如下(我这里选择的是3.3.0版本):


    com.google.zxing
    core
    3.3.0

然后就是调用zxing来生成基准的绘图点了;

二维码生成工具类完整代码如下所示(去package):

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.HashMap;
import java.util.Map;

/**
 * 正方形二维码生成工具类
 * littlehow 2018/5/27
 */
public class QrCodeImageUtils {
    public static final int JPG = BufferedImage.TYPE_INT_RGB;
    public static final int GIF = BufferedImage.TYPE_INT_ARGB;
    /** 扩展信息 */
    private static final Map DEFAULT_HINT = new HashMap();
    /**正方形二维码的默认宽度*/
    private static final int DEFAULT_WIDTH = 300;
    /** 背景色 */
    private static final Color backgroundColor = Color.WHITE;
    /** 二维码颜色 */
    private static final Color contentColor = Color.BLACK;
    /** 图片类型映射 */
    private static final Map IMAGE_TYPE_MAPPING = new HashMap<>();

    static {
        //不设置默认也为L,可以修改
        DEFAULT_HINT.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
        //jpg
        IMAGE_TYPE_MAPPING.put(JPG, "jpeg");
        //gif
        IMAGE_TYPE_MAPPING.put(GIF, "gif");
    }

    /**
     * 创建二维码
     * @param info
     * @return
     */
    public static byte[] createQrCode(String info, InputStream logo, int imageType) {
        if (imageType != JPG && imageType != GIF) {
            throw new RuntimeException("图片类型必须是:QrCodeImageUtils(JPG/PNG)");
        }
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        //进行绘图
        drawImage(info, outputStream, DEFAULT_WIDTH, logo, imageType);
        //返回二进制数据
        return outputStream.toByteArray();
    }

    /**
     * 创建二维码文件
     * @param info
     * @param logo
     * @param imageType
     * @param fileName
     */
    public static void createQrCodeAsFile(String info, InputStream logo, int imageType, String fileName) {
        if (imageType != JPG && imageType != GIF) {
            throw new RuntimeException("图片类型必须是:QrCodeImageUtils(JPG/GIF)");
        }
        OutputStream outputStream;
        try {
            outputStream = new FileOutputStream(fileName);
        } catch (FileNotFoundException e) {
            throw new RuntimeException("指定文件不存在:" + fileName);
        }
        //进行绘图
        drawImage(info, outputStream, DEFAULT_WIDTH, logo, imageType);
        if (outputStream != null) {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 绘图
     * @param info  二维码信息
     * @param outputStream 输出流
     * @param width 宽度和高度(因为是正方形)
     * @param logo  logo图标流
     * @param imageType 图片类型 (jpeg:rgb),(gif:argb)
     */
    public static void drawImage(String info, OutputStream outputStream, int width,
                                  InputStream logo, int imageType) {
        BitMatrix bitMatrix = getBitMatrix(info, width);
        BufferedImage image = new BufferedImage(width, width, imageType);
        Graphics g = image.getGraphics();
        /** 设置背景色 */
        g.setColor(backgroundColor);
        /** 填充 */
        g.fillRect(0, 0, width, width);
        /** 设置内容色 */
        g.setColor(contentColor);
        for(int i = 0; i < width; i++) {
            for(int j = 0; j < width; j++) {
                if(bitMatrix.get(i, j)) {
                    /** 描点 */
                    g.drawRect(i, j, 1, 1);
                }
            }
        }
        /** 绘制logo */
        drawLogo(g, logo, width);
        /** 释放绘图资源 */
        g.dispose();
        try {
            /** 输出图片 */
            ImageIO.write(image, IMAGE_TYPE_MAPPING.get(imageType), outputStream);
        } catch (IOException e) {
            throw new RuntimeException("绘制二维码信息失败", e);
        }
    }



    /**
     * 回执logo
     * @param g
     * @param logoStream
     * @param width
     */
    private static void drawLogo(Graphics g, InputStream logoStream, int width) {
        if (logoStream == null) return;
        try {
            /** logo */
            BufferedImage logo = ImageIO.read(logoStream);
            int logoX = (width - logo.getWidth(null)) / 2;
            int logoY = (width - logo.getHeight(null)) / 2;
            g.drawImage(logo, logoX, logoY, null);
        } catch (Throwable t) {
            throw new RuntimeException("回执logo失败", t);
        }
    }

    /**
     * 获取二维码需要的描点
     * @param info
     * @param width
     * @return
     */
    private static BitMatrix getBitMatrix(String info, int width) {
        QRCodeWriter writer = new QRCodeWriter();
        try {
            return writer.encode(info, BarcodeFormat.QR_CODE, width, width, DEFAULT_HINT);
        } catch (WriterException e) {
            throw new RuntimeException("生成二维码信息异常:" + info, e);
        }
    }

    public static String getImageType(int imageType) {
        return IMAGE_TYPE_MAPPING.get(imageType);
    }
}

3.问题的起源之一:awt抽象窗口工具

       很明显生成二维码只是第一步,将二维码嵌入到html中才是整体模板的完善;这时候明显最方便也是性能最好的方式就是将生成的二维码图片直接用模板引擎写入html模板文件中,这里用的freemarker就不做介绍了;想要这种方式自然而然就想到了使用data:image/gif;base64,xxxxx这样的形式咯;想到就做,于是就将生成的二维码byte数组用base64进行编码后,写入模板中,做到这里感觉任务就要完成了,果然简单极了(其实这个第二天才实现的)。

     接下来只需要将html转换成图片即可,google一下,篇幅比较多的就是用swing、awt这样的java图形化组件来完成这层转换,一看也还好,最后选用的是github上的一个html2image的项目做的这个转换,地址是https://github.com/hkirk/java-html2image/tree/master/html2image ;然后自己在扩展一些方便的方法出来,最后转换出来的图像虽然有一丢丢失真,但勉强还是能用了,于是高兴的想这不就搞定了吗!

     然后我就将工具集成进项目中(springboot),启动后用swagger进行调用,没有像期望的那样返回给我分享图片,拿到的是一个错误信息,大体是因为使用swing渲染html时都需要校验一下headless(若想详细了解请自行google),而springboot项目一般是运行在无图像化界面的服务器上,所以springboot将其设置成了true,好吧,我就将你设置回来:

System.setProperty("java.awt.headless", "false");

再次重启运行,图片如期而至,似乎就要成功了。

       写了这么多,插播一张图片舒缓一下下;



  最危险的时候就是在黎明前夕,问题的暴露就在成功之前;

兴冲冲的将程序打包,发往自己安装了centOS7上的环境中,启动,一切好像都有条不紊的进行着;

打开swagger,准备着自己的最后一波自测;

       当调用获取分享图片接口时,意外还是发生了,因为centOS没有awt的依赖库,因为我安装的环境和真实测试环境一致,也就是说真实测试环境也会面临同样的问题,期间我也按照网上的方式yum了一些包的,最后也没成功,到这里我就不想继续尝试这种方式了,因为这会要求在测试环境上安装,甚至于在预发布和线上环境安装很多原本不需要的工具依赖,只为了实现一个html转图片的功能。所以我放弃了这种方式;一天时间已经结束了!!!

      合上电脑,回家!!!


        


4.SVG-又一种转换图片的思路

        时间总是不起波澜的跑动着,这不,眼睛一眨,第二天了,开始新一天的工作吧! 

        当然办法还是在google里面了,新的一天的开始总是感觉打鸡血的,虽然知道下午的时候鸡血会干!!!

        期间试想过用javascript引擎,canvas绘图,什么能搜的能查的都查了,最后无意间搜canvas的时候发现了对我来说的新  大陆svg;简书上是这样描述svg的 :

    1. SVG是一种使用XML技术描述二维图形的语言,svg是一种矢量图;

    2. 并不属于HTML5专有内容,在HTML5出现之前,就有SVG内容,HTML5只不过提供了SVG原生的内容;

    3. svg文件的扩展名为.svg。    

        既然用html转换图片那么困难,我为什么一定要局限于html呢,svg不也可以用描述的方式生成图片吗;说干就干,简简单单看看svg的介绍,并且打开了https://developer.mozilla.org/kab/docs/Web/SVG  进行了对svg的第一次尝试;因为第一期分享的模板都不是很复杂,所以将html改成svg没有用多少时间的,但这一天已经结束,好吧,不得不说这次的估算严重错误了;合上电脑,等待着第二天的来临;

        svg是编写出来,但如何把svg转成应用广泛的jpg、png这种图片呢,于是新一轮的google又开始了!

       在斑驳的信息中,发现了评价不错的(只看到部分)batik,因为是apache的项目,一时也没有更好的选择,那么就不再花太多时间调研了,直接用吧,反正只是用来转图片的,还真别说,batik转换很快,而且由于是svg转jpg,保真度还是很高的(毕竟  没有复杂的字体);到这一步似乎又接近成功了。

    还是按部就班的启动idea,打开swagger,调用...图片如期而至!!!

    继续重复着上次的最后一次自测,感觉这次终于要成功了,然后又一轮问题出现了!!!


5.问题的起源之二-嵌入式图片协议data

    之前有一个细节一直没说,就是为了留到这里来说;

       java的协议里面根本就没有实现data,也就是说图片内容用data:image/gif;base64这种方式,java根本不认识,会报出类似于data协议不存在或数据有误这样的问题。这可难做了,难道需要我将图片保存在ssd上或者保存在专有文件服务器上?但是这个分享的图片是不需要保存的!想想流程就可怕,生成的二维码字节码要通过文件系统或接口进行文件的保存,然后再生成的时候通过文件系统读出来,这么多无用的io操作,这么多无用的内存复制,太可怕了!!!

      于是乎开始翻看java的协议都是起始于哪里,做过网络编程、写过http实现、或者看过相关代码的人应该都知道,在网络编程中一个至关重要的类java.net.URL,对,就是它主宰这java里面的一切资源获取;于是乎,我战战兢兢的翻开了URL类的源代码,之前编写http的习惯使然,我首先看的是下面这段代码:

public URLConnection openConnection(Proxy proxy)
        throws java.io.IOException {
        if (proxy == null) {
            throw new IllegalArgumentException("proxy can not be null");
        }

        // Create a copy of Proxy as a security measure
        Proxy p = proxy == Proxy.NO_PROXY ? Proxy.NO_PROXY : sun.net.ApplicationProxy.create(proxy);
        SecurityManager sm = System.getSecurityManager();
        if (p.type() != Proxy.Type.DIRECT && sm != null) {
            InetSocketAddress epoint = (InetSocketAddress) p.address();
            if (epoint.isUnresolved())
                sm.checkConnect(epoint.getHostName(), epoint.getPort());
            else
                sm.checkConnect(epoint.getAddress().getHostAddress(),
                                epoint.getPort());
        }
        return handler.openConnection(this, p);
    }

这段代码还是很容易看懂的,关键的一个步骤是handler.openConnection,就是代码的最后一行,这个handler才是最终打开链接的关键性东东!!于是,我又开始找handler是从哪里来的,找啊找,终于找到了java资源加载的关键地方,解开了部分java加载资源的神秘面纱:

static URLStreamHandler getURLStreamHandler(String protocol) {

        URLStreamHandler handler = handlers.get(protocol);
        if (handler == null) {

            boolean checkedWithFactory = false;

            // Use the factory (if any)
            if (factory != null) {
                handler = factory.createURLStreamHandler(protocol);
                checkedWithFactory = true;
            }

            // Try java protocol handler
            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 += "sun.net.www.protocol";

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

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

                    String packagePrefix =
                      packagePrefixIter.nextToken().trim();
                    try {
                        String clsName = packagePrefix + "." + protocol +
                          ".Handler";
                        Class cls = null;
                        try {
                            cls = Class.forName(clsName);
                        } catch (ClassNotFoundException e) {
                            ClassLoader cl = ClassLoader.getSystemClassLoader();
                            if (cl != null) {
                                cls = cl.loadClass(clsName);
                            }
                        }
                        if (cls != null) {
                            handler  =
                              (URLStreamHandler)cls.newInstance();
                        }
                    } catch (Exception e) {
                        // any number of exceptions can get thrown here
                    }
                }
            }

            synchronized (streamHandlerLock) {

                URLStreamHandler handler2 = null;

                // Check again with hashtable just in case another
                // thread created a handler since we last checked
                handler2 = handlers.get(protocol);

                if (handler2 != null) {
                    return handler2;
                }

                // Check with factory if another thread set a
                // factory since our last check
                if (!checkedWithFactory && factory != null) {
                    handler2 = factory.createURLStreamHandler(protocol);
                }

                if (handler2 != null) {
                    // The handler from the factory must be given more
                    // importance. Discard the default handler that
                    // this thread created.
                    handler = handler2;
                }

                // Insert this handler into the hashtable
                if (handler != null) {
                    handlers.put(protocol, handler);
                }

            }
        }

        return handler;

    }

这段代码中有几个关键的东西:1.protocolPathProp = "java.protocol.handler.pkgs";2.protocol(:前面的表示协议,这里就是data); 3.URLStreamHandlerFactory factory(这个最后解决了我的问题,为什么是最后才想到用呢,我也不知道); 4.方法的返回类型URLStreamHandler; 5.String clsName = packagePrefix + "." + protocol + ".Handler";cls = Class.forName(clsName);  

   我首先注意到的就是第4和第5点,当然第5点会用到第1点的内容,因为packagePrefix的由来需要从第1点获取,原来jdk是给我们留下了实现自定义协议的路的(虽然以前也知道可以扩展,并没有自己写过扩展也不知道为什么要这样扩展),看了这段代码后终于知道为什么了,话不多说,开始写吧,扩展我们自己的data协议之路开始;

     又写累了,来张图片充实充实

看似简单的任务-分享二维码图片到微信(分享图片的生成)_第1张图片

    

    继续开工


下面就是简单的实现了handler了,不明白为什么这样实现的同学可以再看看URL类中我贴出的方法!

首先是Handler类,非常简单的一个类,该类必须在data包下,为什么(String clsName = packagePrefix + "." + protocol + ".Handler")应该描述的很清楚吧!

package littlehow.image.data;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;

/**
 * @author littlehow
 * @description
 * @createTime 2018/05/28
 **/
public class Handler extends URLStreamHandler {
    @Override
    protected URLConnection openConnection(URL u) throws IOException {
        return new DataConnection(u);
    }
}

 然后再看看我们的DataConnection类,为了方便,我也将该类放入data包中,这个类放在哪里没有做强制规定的;

package littlehow.image.data;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Base64;

/**
 * @author littlehow
 * @description
 * @createTime 2018/05/28
 **/
public class DataConnection extends URLConnection {
    public DataConnection(URL u) {
        super(u);
    }

    @Override
    public void connect() throws IOException {
        connected = true;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        String data = url.toString();
        data = data.replaceFirst("^.*;base64,", "");
        byte[] bytes = Base64.getDecoder().decode(data);
        return new ByteArrayInputStream(bytes);
    }
}

下面就是注册我们的协议了,可以自己写一个注册类,为了方便,我也将注册类放入data包下:

package littlehow.image.data;

/**
 * @author littlehow
 * @description data协议注册类
 * @createTime 2018/05/28
 **/
public class DataProtocolRegister {
    static boolean registered = false;
    /**
     * 进行data协议的注册,一次注册就可以了
     */
    public synchronized static void register() {
        if (registered) {
            return;//已经注册过了
        }
        //获取handler所在的包
        String pkgName = Handler.class.getPackage().getName();
        String pkg = pkgName.substring(0, pkgName.lastIndexOf('.'));
        //注册如URL类中规定的系统属性中,并且包之间用|隔开
        String protocolHandlers = System.getProperty("java.protocol.handler.pkgs", "");
        if (!protocolHandlers.isEmpty()) {
            protocolHandlers += "|";
        }
        protocolHandlers += pkg;
        System.setProperty("java.protocol.handler.pkgs", protocolHandlers);
        registered = true;
    }
}

 这个注册协议在项目启动后注册,或者在调用转换utils类里面用静态代码块进行注册;


好,似乎准备工作都完成了,接下来就可以再进行一轮自测了;

再自测环境又遭遇了一轮失败!!!

扎心了,真的扎心了,这个功能到底要折磨我多久啊!!!

失败的原因还是报找不到data协议,我真的醉了,我的data协议不是好好的吗,在idea测试都没问题,确实也调用了啊!

此时的我真的是一头雾水!!!

但是没办法,已经做到这一步了,难道要半途而废,当然不可能,我已经按照URL的设定来了,为什么还没找到协议;

不得已,我又翻来覆去的看URL获取URLStreamHandler这个方法,我的设置系统参数肯定不会有错,那么错误在哪里呢,继续往下读代码,handler是由class.newInstance获取的,有个判断就是class是否为空,好像有点感觉了!!!


6.问题的起源之三-springboot

    class为空了!!!我惊讶的猜测到!但毕竟只是猜测,要实际看看才知道,说做就做;

   开启远程debug模式(此处就不多说了,google一下就ok啦)!

  在String clsName = packagePrefix + "." + protocol +".Handler";代码处进行断点!

 ....

最后发现我的包名类名完全正确,就是会出现ClassNotFoundException!!!

看似简单的任务-分享二维码图片到微信(分享图片的生成)_第2张图片


看到这里我其实大概知道答案了!!!

fuck的springboot!!!

这里其实又涉及到了一个类加载的知识,因篇幅问题,只简单的介绍一下,但是我还是建议不了解这块机制的同学可以看看深入了解java虚拟机一书,里面有对类的生命周期以及类加载器有详细的描述!

首先URL类中加载我的handler类用的是Class.forName,而该方法的中用的类加载器用的是所在类的类加载器,也就是用的是加载URL类的类加载器进行加载,而java.开头的java核心类是用虚拟器内置加载器(引导类加载器)进行加载的,所以加载我的类,自然也会用引导类加载器进行加载咯!!,引导类加载我的类,开玩笑吧!!!怎么可能!它能知道我的类在哪里?所以很明显,这种方式是加载不到我的类的,而且引导类加载器也不会加载应用类的,所以自然会抛异常!

    这个不要紧啊,没看到还捕获了异常吗,异常处不是又有获取系统类加载器进行加载吗!!!yes,系统类加载器就是appclassloader,也就是加载应用程序类的类加载器;所以idea里面是可以的!!!那为什么放到centOS里面就不行了,难道又是因为环境问题,这个想法刚一出现就被否定了,当然不可能!

    突然想到了springboot的fat-jar中的类加载方式!!问题的根源终于找到了,spring把我的类打入到了jar in jar了!!!

也就是内部jar中,系统类加载器根据classpath是找不到我的类的,所以肯定没办法加载到我的类了!!!看到这里似乎已经知道解决方案了,将handler类单独出来进行加入classpath,然后就能加载,尝试一下,确实成功了!!但喜欢优雅实现的我真的要用这种不优雅的方式吗!!!我还抱怨为什么URL的实现不能学DriverManager的实现用上下文类加载器呢!!!那样我不就可以让我的handler优雅的进入了吗!!

    期间我有想过扩展spring-boot-plugin的打包,但很快就否定了!可行性差!然后又想着写个东西将springboot打好的包进行二次打包!想想也不可行,只有侵入maven的生命周期才可行!难道真的没有优雅的方式能解决这个问题了吗?

    这里主要提供几个概念可以帮助快速进行类加载的世界!!

   1.类的加载时双亲委派机制,什么意思呢!就是说当一个类加载器获得一个加载类的任务的时候,他先判断是否加载过该类了,如果加载过了当然就没下面什么事情了,但是如果没有加载过呢,那么它会尝试叫父加载器来加载该类,如果父加载器为空,那么会用引导类加载器来加载,如果还加载不了的话,才会轮到自己加载,这个是大部分的类加载过程!

  2.为什么1说双亲委派机制是大部分类的加载过程呢,也就是说还有类的加载不是这种机制,当然!还是有比较多打破该加载机制的类加载模式,如我上面提到的DriverManager的内部实现,DriverManager的类加载器是引导类加载器,那它怎么加载JDBC生产商实现的驱动类呢,这里就打破了双亲委派,引入了上下文类加载器;

  3.如JDBC,JNDI这些spi的加载模式、如OSGI这样的加载方式都是打破了双亲委派机制的!很多热部署、插件化的东东都需要用到上下文类加载器或OSGI模式的加载机制!比较经典的就是tomcat类加载器加载jsp,idea、eclipse这样的工具等;

   所以想要深入了解类加载机制,请详读深入理解java虚拟机一书!!!可以让你对java更底层的实现细节有进一步的了解!!!

 

  突然发现话题跑偏了,言归正传,为什么springboot可以加载到我的类而系统类加载器加载不到呢!!因为springboot也注册了自己的jar协议,该协议就是读jar in jar的,再配合springboot自己的类加载器org.springframework.boot.loader.LaunchedURLClassLoader;这样它就能轻松的加载自己jar in jar中的类,而且它还做了读取缓存,读过的jar就不需要再读了!!但是苦了我了啊,我的handler你给我放出来!!放出来啊!!


7.解决问题-URLStreamHandlerFactory

   前面都说了,路好像都堵死了,但是真的堵死了吗!!我居然忽略了前面的一个重要东东URLStreamHandlerFactory,

它可以直接生产一个URLStreamHandler,要是我自己实现一个这个呢,是不是就能解决问题呢!

    还是那句话,说干就干,劈哩叭啦一顿敲打,工厂出来了!!

package littlehow.image.data;

import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;

/**
 * @author littlehow
 * @description
 * @createTime 2018/05/29
 **/
public class HandlerFactory implements URLStreamHandlerFactory {
    @Override
    public URLStreamHandler createURLStreamHandler(String protocol) {
        if ("data".equals(protocol)){
            return new Handler();
        }
        return null;
    }
}

感觉好像离成功很近了,然后在启动后我调用URL的setURLStreamHandlerFactory方法进行设置,结果异常如期而至!!!

看看该方法的具体实现:

public static void setURLStreamHandlerFactory(URLStreamHandlerFactory fac) {
        synchronized (streamHandlerLock) {
            if (factory != null) {
                throw new Error("factory already defined");
            }
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                security.checkSetFactory();
            }
            handlers.clear();
            factory = fac;
        }
    }

从源码中可以看出url的factory变量只能被设置一次,而我的到的错误信息就是可恶的factory already defined!!!


           到底是谁,到底是谁,到底是谁占用了我的那仅有的一次机会,我的天哪,真的太折磨人了,没办法,断点看看咯,Tomcat,居然是Tomcat,好吧,就是TomcatURLStreamHandlerFactory,居然是你,这个类所在的包是org.apache.catalina.webresources,那好我到要看看你是何方神圣咯!慢慢看它的源码,不得不惊叹,大神就是大神,做事总给人留下后路,当看到下面这段代码时,我知道我其实已经成功了(它里面居然有个应用handler工厂,也就是用户自定义的工厂):

public URLStreamHandler createURLStreamHandler(String protocol) {

        // Tomcat's handler always takes priority so applications can't override
        // it.
        if (WAR_PROTOCOL.equals(protocol)) {
            return new Handler();
        } else if (CLASSPATH_PROTOCOL.equals(protocol)) {
            return new ClasspathURLStreamHandler();
        }

        // Application handlers
        for (URLStreamHandlerFactory factory : userFactories) {
            URLStreamHandler handler =
                factory.createURLStreamHandler(protocol);
            if (handler != null) {
                return handler;
            }
        }

        // Unknown protocol
        return null;
    }
非常简洁的一个:
public void addUserFactory(URLStreamHandlerFactory factory) {
        userFactories.add(factory);
    }

接下来就重写我的注册了 show time:

package littlehow.image.data;


import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLStreamHandlerFactory;

/**
 * @author littlehow
 * @description 处理特殊链接
 * @createTime 2018/04/29
 **/
public class DataProtocolRegister {
    private static boolean registered = false;
    private static final String tomcatFactory = "TomcatURLStreamHandlerFactory";

    /**
     * 进行注册
     */
    public static void register() {
        if (!registered) {
            synchronized (DataProtocolRegister.class) {
                if (registered) return;
                URLStreamHandlerFactory factory = getUrlFactory();
                if (factory.getClass().getName().endsWith(tomcatFactory)) {
                    //注册自定义的factory
                    registFactory(factory);
 registered = true;
                }
               
            }
        }
    }

    /**
     * 注册自定义的handler
     * @param factory
     */
    private static void registFactory(URLStreamHandlerFactory factory) {
        try {
            Method method = factory.getClass().getDeclaredMethod("addUserFactory", URLStreamHandlerFactory.class);
            method.invoke(factory, new HandlerFactory());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取注册进入URL的factory实例
     * @return
     */
    private static URLStreamHandlerFactory getUrlFactory() {
        try {
            Field factoryField = URL.class.getDeclaredField("factory");
            factoryField.setAccessible(true);
            URLStreamHandlerFactory factory = (URLStreamHandlerFactory)factoryField.get(null);
            return factory;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

8.写在后面

     平时看起来无关紧要的底层知识积累,在某些时候非常重要!!!

    所以不要总是说了解那么多原理做什么,不要做一件只知其然而不知其所以然的程序员!!

    评估不是每次都非常准确的,确实会有超过你能力或时间范围的事情发生,不要着急,慢慢抽丝剥茧!


然后必须配上一个高兴的图片咯!!!

看似简单的任务-分享二维码图片到微信(分享图片的生成)_第3张图片


littlehow 写于2018/05/30

你可能感兴趣的:(java,ClassLoader,springboot类加载机制,html转图片,URL类分解,java自定义协议)