Winse Blog

走走停停, 熙熙攘攘, 忙忙碌碌, 不知何畏.

WebStart的使用以及如何结合JVMTI

当新技术叠加老功能时总能把人折磨一番,新仇加旧恨,原本的老功能也不是熟到透,然后还得去结合新功能,真的简直要人命。

最近有上新功能,把Swing客户端的代码通过webstart的方式发布给客户用,这样用户只需要点击网页上的链接,就可以使用Swing客户端了。感觉体验上还是厉害不少的,只是感觉啊!现实往往更残酷,我们先避开不谈。

首先简单的介绍下webstart、jnlp的一些知识,然后讲讲怎么结合jvmti、以及过程中遇到问题时定位查找解决的一些小知识点。

JNLP

为了便于借鉴参考,我这里用的是 jre1.8.0_162

签名:

1
jarsigner.exe -keystore Keystore application.jar alias

说说调试:

  • 首先你得安装jre,不然Windows的控制面板没有Java这一项!
  • 然后打开 Java控制面板 - 高级 - 调试 的选项。刚开始调试可以同时把 Java控制台 也显示出来
  • 远程调试 选项在 Java控制面板 - Java - Java运行时环境设置 - 运行时参数 添加!

参考

缓冲:

目录

1
C:\Users\winse\AppData\LocalLow\Sun\Java\Deployment

调出 Java高速缓冲查看器 界面

1
javaws -viewer

证书:

证书是用jre对应目录下的: jre1.8.0_162\lib\security\cacerts

结合JVMTI(仇恨点)

既然都是agent,那么加载时机也同样有两种:启动时(Agent_OnLoad)和运行时Attach(Agent_OnAttach)。

动态loadAgent

修改加载 动态链接库dll 的方式:

默认是不能在程序里面动态修改加载库地址的 JDK-4280189 : loadLibrary() fails to load a shared lib whose path is in java.library.path

  1. 修改环境变量PATH,-Djava.library.path
  2. 运行时动态修改java.library.path:usr_paths/sys_paths
  3. 把dll拷贝到环境变量PATH的一个路径下面

参考

解决 DLL依赖 问题的终极完美方法:

  1. The directory where the executable module for the current process is located.
  2. The current directory.
  3. The Windows system directory. The GetSystemDirectory function retrieves the path of this directory.
  4. The Windows directory. The GetWindowsDirectory function retrieves the path of this directory.
  5. The directories listed in the PATH environment variable.

You might need to use something such as Dependency Walker to trace the set of DLL dependencies.

把所有的库全部按依赖顺序执行一遍 System.loadLibrary !!

com.sun.tools.attach.AttachNotSupportedException: no providers installed

  1. 你没有使用sun jdk
  2. 你使用了sun jdk,并且JAVA_HOME指向了这个jdk,但是你的path下的”java”命令不是这个jdk里面的java,而是操作系统给你默认安装的jre下的,如c:\Program Files\java..

小结

最傻瓜式的点击就能运行是最佳体验,我们暂时不能通过控制面板添加 -agentlib:lib 的方式来初始化JVMTI。最终通过以上添加tools.jar的VirtualMachine.loadAgentLibrary运行时attach方式来实现。

–END

Map入门指南

最近了解了一些Map地图相关的知识点,把学习的资料罗列一下:

坐标体系

说明:

  • WGS84:为一种大地坐标系,也是目前广泛使用的GPS全球卫星定位系统使用的坐标系。标准的Web墨卡托投影坐标系。
  • GCJ02:又称火星坐标系,是由中国国家测绘局制定的地理坐标系统,是由WGS84加密后得到的坐标系。指中国国家测绘局制订的加偏Web墨卡托投影,正式名称为GCJ-02,国内可用的地图多数属于这种坐标系。
  • BD09:为百度坐标系,在GCJ02坐标系基础上再次加密。其中bd09ll表示百度经纬度坐标,bd09mc表示百度墨卡托米制坐标。

地图API

百度

腾讯

高德

国内其他

Google

NOTE: Google的在Java里面用需要指定证书和代理:

网页访问一次,把geo的CA证书保存到本地,然后导入到本地的证书库,加入到应用得启动参数里面:

1
2
3
keytool -import -rfc -v -alias geo_ca -keystore truststore -file geo.cer

java -Djava.net.useSystemProxies=true -Djavax.net.ssl.trustStore=.\security\truststore 

各坐标系间的转换

Example:

坐标转换代码:

比例尺

百度

1
2
3
map.addControl(new BMap.ScaleControl());
map.enableScrollWheelZoom();   //启用滚轮放大缩小,默认禁用
map.enableContinuousZoom();    //启用地图惯性拖拽,默认禁用

左下角标注的尺寸包括一个数字加一条线段,就是地图上与那条线等长的距离的实际距离为数字表示的长度。假设长度为一厘米,那就是说那一厘米在地图上同等长度实际是20m的距离,比例为1:2000。

在百度地图API中,平面坐标是以最大级别18级为基准的。就是说在18级平面坐标的一个单位就代表了屏幕上的1个像素 (详细的内容后面讲,可以参考百度地图API详解之地图坐标系统)。

Android里面计算百度比例尺的方式:取两个点获取它们的经纬度,然后算两个点之间的距离。

NOTE: 百度地图SDK还提供了标注工具(PushpinTool),测距工具(DistanceTool)。

Google

Bing

深入了解地图 - 瓦片

各种tile的地址路径

腾讯”矢量”地图 - 通过JSON传数据画Canvas

国内百度腾讯网页端的实现

现在的地图基本都是使用瓦片技术,计算步骤如下:

  • 首先,根据投影(墨卡托投影)把 经纬度(度) 转成 平面坐标(m);
  • 然后,更具比例尺把 平面坐标 转成 像素坐标;
  • 最后,根据坐标的平移把窗口内的瓦片从服务端下载并进行展示。

通过JS代码了解地图的实现:

百度

打开一个百度的应用 http://api.map.baidu.com/lbsapi/getpoint/index.html 然后在调试窗口运行转换经纬度的代码,然后进到对应的代码,打断点,然后艰辛进行与混淆的代码死磕!

1
2
3
4
5
6
7
8
9
var projection =new BMap.MercatorProjection();
var point = projection.lngLatToPoint(new BMap.Point(113.338191,23.138992));
point
Q {x: 12616886.99, y: 2631894}

var projection =new BMap.MercatorProjection();
var point = projection.pointToLngLat(new BMap.Pixel(12616886.99,2631894));
point
H {lng: 113.338191, lat: 23.138992}

从代码上看还是不难的,但是里面有一堆魔法数字完全不懂。

如果仅仅获取瓦片 https://github.com/CntChen/tile-lnglat-transform/ 推荐使用这个项目。

这里仅仅是经纬度转换为平面坐标(m)的过程。我们在源码中查找 getTilesUrl 在5901行打个断点,然后在回到网页,移动一下地图。接下来,就可以调试整个过程了。

注意标识的两处,是进行层级缩放、计算出瓦片编号的代码。

腾讯

看过了百度的,再看腾讯的。然鹅并没有觉得轻松啊,两种不同的坐标系,做法差别还是挺大的。不过从命名上看腾讯算学术派的了。

打开 http://lbs.qq.com/javascript_v2/case-run.html#sample-geocoding-reverse ,在 map.qq.com/js/v2.js 的 apiLoad 处打断点进行到真正的map的js文件。

然后查找 fromLatLngToPoint ,再在界面动一下,就可以调试整个过程:

  • fromLatLngToPoint
  • fromPointToLatLng

调式的时刻可以顺便看看整个调用链,会发现:

  • fromDivPixelToLatLng
  • fromLatLngToDivPixel

fromDivPixelToLatLng的条用链,以及数据的传递如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
fromDivPixelToLatLng 
=>

->Rh 转成相对位置转成绝对坐标后,传入到Mc
[
a
P {x: 930, y: -471}
c
ia {width: 1725872.4160540444, height: 794188.9248479041}
b
true
]

->Mc(g, a, h, f)  <->  Gf 缩放后,调用fromPointToLatLng
[
g
Sh {a: P, b: 0.7111111111111111, c: 40.74366543152521, d: true}
a
P {x: 1726802, y: 793718}
h
13
f
true
]

->fromPointToLatLng(Ad, e)
[
Ad
P {x: 210.791259765625, y: 96.889404296875}
e
true
]

-> Return value : u
    lat:40.02892889530204
    lng:116.42520904541016

从 坐标计算 经纬度 反过来了:

腾讯的计算过程直接把 转平面坐标和转像素坐标 两个过程合并了。通过 fromLatLngToPoint 得到就是一个 像素坐标 的值,然后通过缩放就可以得到当前层级级别像素坐标

查找瓦片地址的代码,直接在代码里面查找 x= 在37823处代码都打断点,刷新重新加载瓦片就会进到断点。

然后查看调用链,

详细跟踪的话,会发现,每次加载都会计算左上角和右下角两个点的像素坐标 (窗口的bounds)。计算要加载的瓦片时,直接用最大减最小除以256(每个瓦片的像素),就得到要加载瓦片的编号了。

用了几天比较肤浅的跟了下QQ地图的功能,如果没有混淆应该看起来会爽很多。。。没有很深层次的东西,仅仅是一个源码调试过程的记载,一些理论原理的知识请查完文章中的链接。

其他

–END

Java中使用代理-基于Shandowsocks

在开发过程中,时不时需要要代理一下,来访问我们需要的资源,比方说:DEBUG生产集群的应用、还有在Java中翻墙等等。解决了全局的代理能完成我们访问到资源的时刻,又有新的要求,比方说:只有特定的资源走代理等等。

下面把要点简单罗列下,以供参考:

JDK官网的都全部包括了,其他的辅助,看看人家的具体需求。

Java全应用代理(全局)

  • 走HTTP

Shandowsocks转HTTP,前面Docker翻墙安装Kubernate有弄过,参考:Privoxy

也可以直接用Shandowsocks提供的 启用系统代理 -> 系统代理模式 -> 全局模式 来转换,启用HTTP代理功能。(开全局模式,本地会把socks代理转成为一个http的代理)

1
2
3
4
5
-Dhttp.proxyHost=127.0.0.1
-Dhttp.proxyPort=7070
-Dhttps.proxyHost=127.0.0.1
-Dhttps.proxyPort=7070
-Dhttp.nonProxyHosts="localhost|127.0.0.1|192.168.*"
  • http.proxyHost: the host name of the proxy server
  • http.proxyPort: the port number, the default value being 80.
  • http.nonProxyHosts:a list of hosts that should be reached directly, bypassing the proxy. This is a list of patterns separated by ‘|’. The patterns may start or end with a ‘*’ for wildcards. Any host matching one of these patterns will be reached through a direct connection instead of through a proxy.
  • 走Socks
1
-DsocksProxyHost=127.0.0.1 -DsocksProxyPort=7070
  • 使用系统代理
1
-Djava.net.useSystemProxies=true

部分(自动切换)

  • 应用内通过 setProperty 临时 设置
1
2
3
4
System.setProperty("http.proxyHost", proxyHost);
System.setProperty("http.proxyPort", proxyPort);
System.setProperty("https.proxyHost", proxyHost);
System.setProperty("https.proxyPort", proxyPort);

用完之后,取消设置:

1
2
System.clearProperty("http.proxyHost");
...
  • 请求时指定代理:
1
2
3
4
5
SocketAddress addr = new InetSocketAddress("webcache.example.com", 8080);
Proxy proxy = new Proxy(Proxy.Type.HTTP, addr);

URL url = new URL("http://java.example.org/");
URLConnection conn = url.openConnection(proxy);
  • (选择性的)配置哪些访问走代理:ProxySelector

任何请求访问网络之前,会被ProxySelector拦截。根据规则获取一个符合的Proxy(或者Proxy.NO_PROXY),然后通过这个代理去访问网络。

As you can see, with Java SE 5.0, the developer gains quite a bit of control and flexibility when it comes to proxies. Still, there are situations where one would like to decide which proxy to use dynamically, for instance to do some load balancing between proxies, or depending on the destination, in which case the API described so far would be quite cumbersome. That’s where the ProxySelector comes into play.

The best thing about the ProxySelector is that it is plugable! Which means that if you have needs that are not covered by the default one, you can write a replacement for it and plug it in!

基本上看JDK官网的内容就好了,也参考下 URLs and URIs, Proxies and Passwords

注册自定义的Selector:

1
2
3
4
5
public static void main(String[] args) {
    MyProxySelector ps = new MyProxySelector(ProxySelector.getDefault());
    ProxySelector.setDefault(ps);
    // rest of the application
}

Selector实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
public class MyProxySelector extends ProxySelector {
    // Keep a reference on the previous default
    ProxySelector defsel = null;
    
    /*
     * Inner class representing a Proxy and a few extra data
     */
    class InnerProxy {
        Proxy proxy;
        SocketAddress addr;
        // How many times did we fail to reach this proxy?
        int failedCount = 0;
        
        InnerProxy(InetSocketAddress a) {
            this(a, Proxy.Type.HTTP);
        }
        
        InnerProxy(InetSocketAddress a, Proxy.Type type) {
            addr = a;
            proxy = new Proxy(type, a);
        }
        
        SocketAddress address() {
            return addr;
        }
        
        Proxy toProxy() {
            return proxy;
        }
        
        int failed() {
            return ++failedCount;
        }
    }
    
    /*
     * A list of proxies, indexed by their address.
     */
    HashMap<SocketAddress, InnerProxy> proxies = new HashMap<SocketAddress, InnerProxy>();

    MyProxySelector(ProxySelector def) {
        // Save the previous default
        defsel = def;
        
        // Populate the HashMap (List of proxies)
        InnerProxy i = new InnerProxy(new InetSocketAddress("webcache1.example.com", 8080));
        proxies.put(i.address(), i);
        i = new InnerProxy(new InetSocketAddress("webcache2.example.com", 8080));
        proxies.put(i.address(), i);
        i = new InnerProxy(new InetSocketAddress("webcache3.example.com", 8080));
        proxies.put(i.address(), i);
    }
        
    /*
     * This is the method that the handlers will call.
     * Returns a List of proxy.
     */
    public java.util.List<Proxy> select(URI uri) {
        // Let's stick to the specs. 
        if (uri == null) {
            throw new IllegalArgumentException("URI can't be null.");
        }
        
        /* 这里可以指定你自己的规则/配置
         * If it's a http (or https) URL, then we use our own list.
         */
        String protocol = uri.getScheme();
        if ("http".equalsIgnoreCase(protocol) ||
                "https".equalsIgnoreCase(protocol)) {
            ArrayList<Proxy> l = new ArrayList<Proxy>();
            for (InnerProxy p : proxies.values()) {
                l.add(p.toProxy());
            }
            return l;
        }
        
        /*
         * Not HTTP or HTTPS (could be SOCKS or FTP)
         * defer to the default selector.
         */
        if (defsel != null) {
            return defsel.select(uri);
        } else {
            ArrayList<Proxy> l = new ArrayList<Proxy>();
            l.add(Proxy.NO_PROXY);
            return l;
        }
    }
    
    /*
     * Method called by the handlers when it failed to connect
     * to one of the proxies returned by select().
     */
    public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
        // Let's stick to the specs again.
        if (uri == null || sa == null || ioe == null) {
            throw new IllegalArgumentException("Arguments can't be null.");
        }
        
        /*
         * Let's lookup for the proxy 
         */
        InnerProxy p = proxies.get(sa); 
        if (p != null) {
            /*
             * It's one of ours, if it failed more than 3 times
             * let's remove it from the list.
             */
            if (p.failed() >= 3)
                    proxies.remove(sa);
        } else {
            /*
             * Not one of ours, let's delegate to the default.
             */
            if (defsel != null)
              defsel.connectFailed(uri, sa, ioe);
        }
    }
}

–END

Logstash采集网站的访问日志

最近又重新接触了一下elasticsearch、logstash、kibana,蛮好用的一个日志框架。

同时好久没有更新网站内容、也没怎么关注,虽然有cnzz(umeng)的日志统计功能,但是毕竟是很小一段时间的。要是能够把日志都导出来,就可以用ELK来分析一下自己网站一年来文章的访问情况。

嗯,前阵子买了阿里云的一个VPN服务器,正好可以利用利用。把访问的日志情况通过http发送给logstash,然后存储下来,等过一段时间我们再回来分析分析这些日志。^^

启动Logstash收集服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
~/logstash-6.1.2/bin/logstash -e '
input { 
  http { 
    port => 20000 
    response_headers => {
      "Access-Control-Allow-Origin" => "*"
      "Content-Type" => "application/json"
      "Access-Control-Allow-Headers" => "Origin, X-Requested-With, Content-Type, Accept"
    }
  } 
} 
filter {
  if [message] =~ /^\s*$/ {
    drop { }
  }
  
  json {
    source => "message"
  }
  json {
    source => "location"
    target => "location"
  }
  mutate {
    remove_field => [ "headers" ]
  }
}
output { 
  file { 
    path => "winse-accesslog-%{+YYYY-MM-dd}.log"
    codec => json_lines 
  } 
} 
'

页面发送访问日志记录

1
2
3
4
5
6
7
8
9
10
11
12
$.ajax({
  type: "POST",
  url: "http://SERVER:PORT",
  data: JSON.stringify({
    title: document.title,
    location: JSON.stringify(location),
    referrer: document.referrer,
    userAgent: navigator.userAgent
  }),
  contentType: "application/json; charset=utf-8",
  dataType: "json"
});

–END

Gitalk on Octopress

以前有添加过 多说 ,步骤都类似的。其实就是调用一个第三方的服务,把评论的数据存储在第三方。

可以先看看 gitalk 的文档 ,分四步:

  • 注册一个github 的 OAuth Apps
  • 添加div容器
  • 加入css,js依赖
  • 调用javascript显示

配置

注册一个github应用

_layouts/post.html 的 Comments 下添加一个 gitalk-container 的节点:

(粘贴后把大括号和百分号之间的空格去掉)

1
2
3
4
5
6
7
8
{ % if site.disqus_short_name and page.comments == true % }
  <section>
    <h1>Comments</h1>
<!-- gitalk评论 start -->
    <div id="gitalk-container"></div> 
<!-- gitalk评论 end -->
  </section>
{ % endif % }

_includes 目录下增加一个 gitalk.html 的页面,添加依赖并添加初始化代码:

这里clientID,clientSecret对应第一步注册应用的id和secret。

在官网文档给的例子上调整了一下: id, body, createIssueManually。代码里面是通过 labels + id 来查询对应的issue:查询Issue源码

(粘贴后把大括号和百分号之间的空格去掉)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{ % if site.disqus_short_name and page.comments != false % }

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css">
<script src="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js"></script>

<script>

var gitalk = new Gitalk({
  clientID: 'c14f68eac6330d15d984',
  clientSecret: '73b7c1fffa98e299ff0cdd332821201933858e6e',
  repo: 'winse.github.com',
  owner: 'winse',
  admin: ['winse'],
  id: location.pathname,
  labels: ['Gitalk'],
  body: "http://winseliu.com" + location.pathname,
  createIssueManually: true,
  
  // facebook-like distraction free mode
  distractionFreeMode: false
})

gitalk.render('gitalk-container')

</script>

{ % endif % }

然后在同一级目录的 after_footer.html 新增一条 这个新页面一个引用(粘贴后把大括号和百分号之间的空格去掉):

1
{ % include gitalk.html % }

初始化

其实就是在对应的repo下面建一个repo,注意下 labels 规则就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
username = "winse" # GitHub 用户名

# https://github.com/settings/tokens
new_token = ""  # GitHub Token
repo_name = "winse.github.com" # 存放 issues

sitemap_url = "sitemap.xml" # sitemap
kind = "Gitalk"

# 可以结合git的状态,added的调用命令创建一个issue

# 除了使用Token,也可以手动输入密码
curl -H "Accept: application/json" -X POST -d '{"body": "http://winseliu.com/blog/2017/11/20/sed-debug-sedsed/", "labels": ["Gitalk", "/blog/2017/11/20/sed-debug-sedsed/"], "title": "gitalk 测试" }' -u $username https://api.github.com/repos/$username/$repo_name/issues
Enter host password for user 'winse':

OR

# https://developer.github.com/v3/auth/#basic-authentication
curl -u username:token https://api.github.com/user

参考

–END