SegmentFault 沙湖王最新的文章
2020-08-26T00:35:22+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
根域名服务器只有13台吗
https://segmentfault.com/a/1190000023766712
2020-08-26T00:35:22+08:00
2020-08-26T00:35:22+08:00
沙湖王
https://segmentfault.com/u/shahuwang
0
<p>之前为了做一个分享,查了很多DNS相关的信息, 发现绝大多数关于根域名服务器的数量的说法都是错的。</p><p>第一个说法是根域名服务器只有13台,首先就不止13台,现在实际上有一千多台。另一个说法是根域名只能有13个,因为DNS报文长度的限制导致了只能有13个,实际上这个也是错了,即使是早期未拓展的DNS报文,最长也是可以有15个的。</p><p>可以通过命令 dig +trace segmentfault.com 来查询了解到DNS的解析过程,最先请求的就是根域名了</p><p><img src="/img/bVbLSU2" alt="image.png" title="image.png"></p><p>根域名就是 a~m.root-servers.net. 这13个就是根域名了,然后下面那一列就是根域名对应的服务器IP。</p><p>我感觉所有的DNS服务器应该都是带上了这几个根域名的,DNS的解析过程大致如图:</p><p><img src="/img/bVbLSVI" alt="image.png" title="image.png"></p><p>先请求下自己拿到根域名,然后再去根域名那拿到顶级域名DNS服务器。</p><p>虽然只有13个IP,但实际上背后是有上千台服务器的,甚至于没有人知道到底有多少台根域名服务器,因为你自己架设一个也是没有问题的。</p><p>根域名服务器是采用任播(Anycast)技术部署的,各个地区都可以用一台服务器共享这个IP,接受DNS的请求。</p><p>任播技术的文档不是很多,涉及到BGP,AS号段等知识,我也不是特别懂,所以这里就不多加以介绍了。</p><p><img src="/img/bVbLSK5" alt="image.png" title="image.png"></p><p>另外其实是可以有15个根域名的,如下图的DNS报文说明</p><p><img src="/img/bVbLSXT" alt="image.png" title="image.png"></p><p>当年规定了所有的IPv4设备都必须能接受最小576byte的数据包,然后IP报文的头部占了20字节,UDP头部占8字节,DNS信息占512字节,剩余36字节做预留。而这512字节,实际上也是可以容纳15个根域名的。</p>
为什么使用谷歌的公共DNS 8.8.8.8 反而会让你网速变慢
https://segmentfault.com/a/1190000023765999
2020-08-25T23:41:18+08:00
2020-08-25T23:41:18+08:00
沙湖王
https://segmentfault.com/u/shahuwang
3
<p>有一个外部合作商,有次问我们,为什么我们的接口会概率性超时?但是这个接口是供很多外部合作商使用的,其他人都未反馈这个问题,只有他们反馈了,所以觉得很奇怪。</p><p>后面我就让他们给我提供请求的参数,根据这些参数也没有搜索到对应的请求记录,想想应该是请求并没有到达我们的业务机,可能在公共接入一层,甚至都没到公共接入这一层,这一下子问题就不好查了。</p><p>后来就让他们抓包,把超时的请求的TCP都抓取下来。我用wireshark打开之后,看到超时都是TCP Retransmission 超时重传引起的,然后发现这些请求都跑到了我们公司的香港机房去了,也难怪会超时,毕竟我们的香港机房并没有对国内用户做优化。</p><p>然后我自己用curl把请求指向这个香港的服务器,然后用wireshark抓包,确实会时不时出现超时重传的情况。至此可以确定引起超时的原因是因为请求到了香港机房。</p><p>于是就有两个疑惑了,为什么请求会到了香港机房?为什么会概率性出现而不是必现?</p><p>后来对方的运维根据这个线索,终于意识到是因为他们服务器使用的DNS中有谷歌的公共DNS,从而导致了请求到了我们的香港机房去了。</p><p>当时我就很疑惑,为什么配置了8.8.8.8会导致请求到了我们的香港机房?顺着这个疑惑,我去找了好多文章,总算是大概搞明白了一点了。</p><p>归结起来就是三个疑问:</p><ol><li>为什么使用谷歌的DNS会导致请求到了我们的海外机房</li><li>是根据什么方式和策略让不同地区的人请求到不同的机房去的</li><li>概率性出现是因为DNS的选择策略,那DNS的选择策略是怎么样的?</li></ol><p>在我们国内,由于多个网络运营商,还需要做到让电信的用户到电信机房,联通的用户到联通机房,移动的用户到移动机房,选择运营商又是怎么做到的呢?</p><p>最后搜索到了关于智能DNS的知识,大概如下:</p><p><img src="/img/bVbLNUy" alt="image.png" title="image.png"></p><p>实际上就是智能DNS是根据来源请求的IP地址,来判断所属地区和运营商,然后分配到离用户最近,且运营商一致的机房去。也就是我司这边的域名解析配置了一个智能的DNS,它会根据用户请求的来源IP把用户分配到最佳的机房节点去。</p><p>从DNS请求中是获取不到用户的来源IP地址的,只能通过用户所使用的DNS的IP地址,来判断用户所属的地区和运营商,然后分配到最佳的节点去。</p><p>不过谷歌也开发了一种DNS的扩展协议,EDNS Client Subnet, 可以在DNS请求中携带上用户的真实IP。但是这个要求用户使用的DNS服务支持这个协议,也要求我司使用的DNS服务支持这个协议,才能从中得到用户的真实IP,所以这个运用并不广泛,国内绝大多少网站都是通过用户的DNS服务的IP来判断用户的地理位置和运营商的。</p><p>大多数人使用网络的时候,都默认使用运营商提供的DNS服务器,这些服务器一则与用户地理位置接近,二则运营商也一样。所以根据DNS服务器的IP来判断用户的地理位置和运营商是可行的。</p><p>所以使用谷歌的DNS的时候,由于谷歌的8.8.8.8并没有在国内部署,因此都被我司的智能DNS判定为海外用户,然后指向到海外的机房去了。</p><p>然后这里就涉及到另一个问题了,这些公共的DNS只有一个IP,难道全球就只部署了一台吗?那114.114.114.114这个电信的公共DNS,为什么其他运营商的网络用起来也没什么问题呢?</p><p>实际上公共DNS的部署采取的是任播(Anycast)技术,确切的说是BGP 任播技术,多个主机使用同一个IP地址(该地址即这一组主机的共享单播地址)的一种技术,当发送方发送报文给这个共享单播地址时,报文会根据路由协议路由到这一组主机中离发送方最近的一台。类似于下图:</p><p><img src="/img/bVbLSK5" alt="image.png" title="image.png"></p><p>不同地区的用户访问这个公共DNS的时候,实际访问的服务器是不一样的。另外,作为任播的IP,是只能被动接收请求,而不能主动发起请求的,毕竟主动发起的话,接收方的回复是不知道返回给哪一台服务器好的。</p><p>新的问题就来了,比如114.114.114.114是电信的公共DNS,如果一个移动的用户使用这个DNS,智能DNS是如何分辨出这个用户是移动的而不是电信的呢?就如上面所说的,任播的IP是不能主动发起请求的,公共DNS的服务器为了能够主动发起请求,他们本身还必须配置另一个单播IP,主动发起的请求都使用这个IP。这样子的话,智能DNS就能够分辨运营商了。</p><p>另外,关于多个DNS服务器的选择策略,目前采取的是平滑往返时间,即SRTT策略。</p><p>第一次所有DNS服务器都会随机给一个初始值,然后取最小的那个发起查询请求,根据本次的请求时间,把当前这个DNS的SRTT做一个重算,然后其他的DNS也要有一定量的衰减。</p><p>第二次又选择最小SRTT发起请求,如此循环,大约半小时重置一次,重新随机分配一个初始值。</p><p><img src="/img/bVbLSMb" alt="image.png" title="image.png"></p><p>所以谷歌的公共DNS因为SRTT比较大,导致它被选择的概率是比较小的,也就导致了友商出现请求超时的次数并不是特别多</p>
在spring boot中创建事件监听器以及异步执行的方法
https://segmentfault.com/a/1190000021916824
2020-03-05T00:35:34+08:00
2020-03-05T00:35:34+08:00
沙湖王
https://segmentfault.com/u/shahuwang
1
<p>阅读spring boot如何执行事件监听器的代码,就意识到事件监听机制应该在spring boot中运用很广,而且它的代码形式就是要让开发者来使用的。所以就去搜索了下具体在Spring boot中运行事件监听器要怎么做。</p>
<p>因为阅读代码的过程中,spring boot内置的事件监听器已经教会我怎么在spring boot里添加事件监听器,发布事件,执行对应动作这一个流程了,所以就想看看还有没有其他的知识点。果然还是有的。</p>
<p>首先,一个完整的事件监听过程涉及到四个对象:<br>1、发送事件的主体<br>2、事件的类型定义<br>3、发布事件的发布器<br>4、事件的监听器</p>
<blockquote>如下示例主要来自这篇<a href="https://link.segmentfault.com/?enc=kos%2F0PLWpdAXAWYZf2OhuQ%3D%3D.T6aoPIWxGvAo3X0fFF7EwotMml0hOKkViLMR90VYGguKbmbySL8heiNUcOjEpTI%2F" rel="nofollow">《Spring Boot 事件和监听》</a>
</blockquote>
<p>先来定义一个简单的事件:</p>
<pre><code>package com.shahuwang.bootsample.listener;
import org.springframework.context.ApplicationEvent;
public class BlackListEvent extends ApplicationEvent {
private String address;
public BlackListEvent(Object source, String address){
super(source);
this.address = address;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
</code></pre>
<p>再来定义一个简单的事件监听器:</p>
<pre><code>package com.shahuwang.bootsample.listener;
import org.springframework.context.ApplicationListener;
public class BlackListListener implements ApplicationListener<BlackListEvent> {
@Override
public void onApplicationEvent(BlackListEvent blackListEvent) {
String threaid = Thread.currentThread().getName();
System.out.println(threaid + "收到了黑名单事件:" + blackListEvent.getAddress());
}
}
</code></pre>
<p>然后把监听器注册到spring boot里面:</p>
<pre><code>package com.shahuwang.bootsample;
import com.shahuwang.bootsample.listener.BlackListListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
public class BootsampleApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(BootsampleApplication.class);
app.addListeners(new BlackListListener());
app.run(args);
}
}
</code></pre>
<p>此时,只要在其他地方发布这个事件,马上就会执行对应的代码了。这个事件要怎么发布呢?</p>
<p>如下是一段简单的接口代码,在里面发布了事件:</p>
<pre><code>package com.shahuwang.bootsample.service;
import com.shahuwang.bootsample.listener.BlackListEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicLong;
@RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@Autowired
private ApplicationContext context;
@GetMapping("/greeting")
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
BlackListEvent event = new BlackListEvent(this, "greeting");
context.publishEvent(event);
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
package com.shahuwang.bootsample.service;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
</code></pre>
<p>此时,只要把整个项目运行起来,然后访问:<a href="https://link.segmentfault.com/?enc=64rGKXCmyyAtjdlPiwz0kA%3D%3D.liXo9EAjXYoHH1RKapY8VljSQIeG74YYGCwJuq0R%2FVU%3D" rel="nofollow">http://localhost</a>:8080/greeting 就能看到console打出【收到了黑名单事件】几个字了。</p>
<p>如上的方式定义一个事件监听器还是复杂了点,而且需要写好多个类。Spring boot还支持某个类的方法成为监听器,如下即是:</p>
<pre><code>package com.shahuwang.bootsample.listener;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;
@Component
public class AnnotationListener {
@EventListener
public void processBlacklistEvent(BlackListEvent event){
String threaid = Thread.currentThread().getName();
System.out.println("annotation listener------------------" + threaid);
}
}
</code></pre>
<p>通过加上@Component 和 @EventListener 两个注解,就可以让一个方法成为监听器。</p>
<p>阅读代码的过程中,我看到spring boot的事件执行有两种方式,一种是在主线程里面顺序执行,一种是新起一个线程来执行,似乎是通过读取某个配置来区分。后来搜索到需要加上@Async 这个注解,然而我加上了也没有用,再后来发现还需要在主类上加上@EnableAsync这个注解。</p>
<p>代码如下:</p>
<pre><code>package com.shahuwang.bootsample.listener;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;
@Component
public class AnnotationListener {
@EventListener
@Async
public void processBlacklistEvent(BlackListEvent event){
String threaid = Thread.currentThread().getName();
System.out.println("annotation listener------------------" + threaid);
}
}</code></pre>
<pre><code>package com.shahuwang.bootsample;
import com.shahuwang.bootsample.listener.BlackListListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class BootsampleApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(BootsampleApplication.class);
app.addListeners(new BlackListListener());
app.run(args);
}
}</code></pre>
<p>这样子每次事件到来都是新起一个线程来执行了。</p>
<p>除了上述两种方式注册监听器,还可以通过properties文件里面配置监听器,由spring boot在启动的时候加载。</p>
<p>首先,在src/resources下的application.properties(如果没有就创建一个)加上这么一行:</p>
<pre><code>context.listener.classes=\
com.shahuwang.bootsample.listener.BlackListListener</code></pre>
<p>这个BackListListener 就会在启动的时候,由<code>org.springframework.boot.context.config.DelegatingApplicationListener</code> 加载并注册到spring boot里面了。如果有多个监听器,可以这样写:</p>
<pre><code>context.listener.classes=\
com.shahuwang.bootsample.listener.BlackListListener,\
com.shahuwang.bootsample.listener.BlackListListener2</code></pre>
<p>DelegatingApplicationListener 由spring boot初始化时启动,然后监听ApplicationEnvironmentPreparedEvent事件,收到事件后,就找当前项目的properties文件,并加载里面context.listener.classes对应的监听器</p>
Spring boot 里面的SpringApplicationListeners的功能与作用
https://segmentfault.com/a/1190000021894987
2020-03-02T23:12:28+08:00
2020-03-02T23:12:28+08:00
沙湖王
https://segmentfault.com/u/shahuwang
0
<p>Spring boot里面的Listener和Event,其实是观察者模式的应用。</p>
<p>Spring Boot 里面的SpringApplicationRunListeners,实际上它做的事情,基本上就是一个publisher。</p>
<p><a href="https://link.segmentfault.com/?enc=ZTJqYJZLktqZPcPWAa7J%2Bg%3D%3D.fiPUfn8QSqjNu8hC%2BWaZIR7onGDxDNoZRQ9QT7JIdf05XLcgm2DlG9oG7i%2FkWCkX98Sb5JNo58kUIOHAvgSy%2FE5hZmrtxKCK3gyVuMybuIKkxgtWpXaVftrIAQmUzTWi" rel="nofollow">bootexp</a> 这里面是对Spring boot里面的各种角色简化之后的代码。</p>
<p>对于Spring boot来说,Context是所有操作的核心。</p>
<pre><code>package com.shahuwang.bootexp.listener;
public class AppMain {
public static void main(String [] args){
AppContext context = new AppContext();
IAppListener starting = new StartingListener();
IAppListener started = new StartedListener();
IAppListener running = new RunningListener();
context.addAppListeners(starting);
context.addAppListeners(started);
context.addAppListeners(running);
context.publishEvent(new StartingEvent());
context.publishEvent(new StartedEvent());
context.publishEvent(new RunningEvent());
}
}</code></pre>
<p>如上实例代码,把你想监听的时间注册进去,等事件发布的时候就可以监听到并执行相应的代码。</p>
<p>等有空读下Java的设计模式再回来补全这篇文章,此文先记录下当前阅读Spring Boot源码的进展</p>
windows 下使用wsl直接进入到zsh的方法
https://segmentfault.com/a/1190000021718875
2020-02-10T20:08:25+08:00
2020-02-10T20:08:25+08:00
沙湖王
https://segmentfault.com/u/shahuwang
0
<p>ubuntu下我一直很喜欢用zsh搭配oh-my-zsh,特别是写代码的时候,可以直接显示当前所在的分支,这个功能很是必须。</p>
<p><a href="https://link.segmentfault.com/?enc=LVgCWXCgImBCBgK%2B67pfFw%3D%3D.UtwmnoIAuLtNRTgyIZoOgfJVnz%2Fe55RQHeJa7msKYFBhC%2Fl68BOpdgx7EznqMIP1" rel="nofollow">https://zhuanlan.zhihu.com/p/...</a> 照着这篇文章可以解决wsl下进入zsh会乱码的问题。先下载字体 <code>git clone https://github.com/powerline/fonts.git --depth=1</code> ,里面有一个install.ps1, 右键点击,选择用powershell打开,就会自动安装字体了。</p>
<p>照着流程安装好zsh和oh-my-zsh, 并把zsh设置为默认的shell。</p>
<p>然后打开文件管理器,在你想进入的文件夹里,如图的位置输入 wsl,Enter键敲下,就会自动打开wsl,并进入到zsh里面了</p>
<p><img src="/img/bVbDidq" alt="image.png" title="image.png"></p>
<p>此时可能会出现乱码,右键点击cmd的顶部,选择属性,选择字体,里面已经有刚才安装好的字体了,找到其中的powerline字体,逐个试试,不乱码的那个就行。</p>
<p>另外,我喜欢用byobu,但是安装好byobu之后,在cmd里打开会界面花了,于是我选择 byobu-select-backend, 里面选择screen 这个后端,就不会界面花了。</p>
<p>如果出现<code>Cannot make directory '/var/run/screen': Permission denied</code>, 执行如下命令即可:</p>
<pre><code>sudo /etc/init.d/screen-cleanup start</code></pre>
spring-boot:run 是怎么找到使用了注解@SpringBootApplication的
https://segmentfault.com/a/1190000021690602
2020-02-05T22:54:02+08:00
2020-02-05T22:54:02+08:00
沙湖王
https://segmentfault.com/u/shahuwang
1
<p><a href="https://segmentfault.com/a/1190000021687878">《mvn spring-boot:run 指令是怎么运行起spring boot项目的》</a> 上一篇的文章介绍了这个指令是怎么运行起一个spring boot项目的,但是并未详细说是怎么找到使用了注解@SpringBootApplication并含有main方法的类的,本文就打算说一下这个。</p>
<p>查找主类的代码,在spring-boot-tools/spring-boot-loader-tools的MainClassFinder.java,关键方法是<code>static <T> T doWithMainClasses(File rootFolder, MainClassCallback<T> callback)</code>。</p>
<p>顺着这个方法的代码,注意到spring有一套自用的asm框架,用来解析*.class 文件的,即解析字节码的。再搜了下,Java现在也有一套成熟的asm框架<a href="https://link.segmentfault.com/?enc=zbXCql80le5HE4EvpOBZ7w%3D%3D.SUcgB%2BhlFHsbNUXDYfTFSQgTtowrQAIw2Os1GuBSCDg%3D" rel="nofollow">https://asm.ow2.io/</a>,切面编程说是都需要用到asm。</p>
<p>asm解析后都使用访问者模式来访问解析后的代码,可以判断这个类有没有main方法,使用了哪些类注解等等信息,这样子就能够找到使用了注解@SpringBootApplication并含有main方法的类,然后就能运行起来,让spring boot项目跑起来了。</p>
<p>抽丝剥茧后,我精简了代码,运行起来可以找到一个spring boot项目的主类,代码在 <a href="https://link.segmentfault.com/?enc=YJFIVTgGwNiJNaRhFCkCYw%3D%3D.ie1VgHXn70yt8YJ6qVimUAHBgx%2FIc9y2zk2m3WTVyunB0vsA4NUswbZvQJaANm2YiB8m1s4A83fZzkBzXWT%2BKGsEVjpbWvoV4wIJgxDbJCAjGbfqWEuULgpjaoewGHjmL2iiR%2FscCIKQDnM1lzL1EcdFSbxAfodPGMdCvskccEsPC7b4fajOFj5KtxvwfMnW" rel="nofollow">https://github.com/shahuwang/...</a>, springboot_mainclass_finder 这个tag即是。</p>
<p>这里面的核心就是用asm解析*.class文件,然后获取代码的相关信息。</p>
mvn spring-boot:run 指令是怎么运行起spring boot项目的
https://segmentfault.com/a/1190000021687878
2020-02-05T14:41:33+08:00
2020-02-05T14:41:33+08:00
沙湖王
https://segmentfault.com/u/shahuwang
7
<p>初学spring boot的时候,按照官方文档,都是建立了一个项目之后,然后执行 <code>mvn spring-boot:run</code> 就能把这个项目运行起来,我就很好奇这个指令到底做了什么,以及为什么项目里包含了main方法的那个class,要加一个 <code>@SpringBootApplication</code> 的注解呢?为什么加了这个注解<code>@SpringBootApplication</code>之后,<code>mvn spring-boot:run</code> 指令就能找到这个class并执行它的main方法呢?</p>
<p>首先我注意到,用maven新建的spring boot项目,pom.xml 里面有这么一条配置:</p>
<pre><code> <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build></code></pre>
<p>看来<code>mvn spring-boot:run</code> 指令应该就是这个插件提供的。按照之前写的<a href="https://segmentfault.com/a/1190000020413148">《spring boot源码编译踩坑记》</a>这篇文章把spring boot的源码项目导入IDEA之后,在 <code>spring-boot-project/spring-boot-tools/spring-boot-maven-plugin </code>找到了这个插件的源码。</p>
<p>由于不懂maven插件的开发机制,看不太懂,于是去找了下<a href="https://link.segmentfault.com/?enc=uDU%2BFr7cdSA2d%2Bp3jQQaIw%3D%3D.bxJtofAhx1bDyfrZi86f3TF%2Bpn48iiEZjltdRqN2G25GhuaJVkj4SdOsPXr2DZbHDYLfJCb%2Fpt8qRnkEMuK9gFLGAAcPktqUqAs5b9Xswo4%3D" rel="nofollow">maven的插件开发文档</a>,根据官方的文档,一个maven插件会有很多个目标,每个目标就是一个 Mojo 类,比如 <code>mvn spring-boot:run</code> 这个指令,spring-boot这部分是一个maven插件,run这部分是一个maven的目标,或者指令。</p>
<p>根据maven插件的开发文档,定位到 spring-boot-maven-plugin 项目里的RunMojo.java,就是<code>mvn spring-boot:run</code> 这个指令所运行的java代码。关键方法有两个,一个是 <code>runWithForkedJvm</code>,一个是<code>runWithMavenJvm</code>,如果pom.xml是如上述配置,则运行的是 <code>runWithForkedJvm</code>,如果pom.xml里的配置如下,则运行<code>runWithMavenJvm</code>:</p>
<pre><code> <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>false</fork>
</configuration>
</plugin>
</plugins>
</build></code></pre>
<p><code>runWithForkedJvm</code> 与 <code>runWithMavenJvm</code> 的区别,在于前者是起一个进程来运行当前项目,后者是起一个线程来运行当前项目。</p>
<p>我首先了解的是 <code>runWithForkedJvm</code>:</p>
<pre><code>private int forkJvm(File workingDirectory, List<String\> args, Map<String, String\> environmentVariables)
throws MojoExecutionException {
try {
RunProcess runProcess = new RunProcess(workingDirectory, new JavaExecutable().toString());
Runtime.getRuntime().addShutdownHook(new Thread(new RunProcessKiller(runProcess)));
return runProcess.run(true, args, environmentVariables);
}
catch (Exception ex) {
throw new MojoExecutionException("Could not exec java", ex);
}
}</code></pre>
<p>根据这段代码,<code>RunProcess</code>是由spring-boot-loader-tools 这个项目提供的,需要提供的workingDirectory 就是项目编译后的 *.class 文件所在的目录,environmentVariables 就是解析到的环境变量,args里,对于spring-boot的那些sample项目,主要是main方法所在的类名,以及引用的相关类库的路径。</p>
<p>workingDirectory 可以由maven的 ${project} 变量快速获得,因此这里的关键就是main方法所在的类是怎么找到的,以及引用的相关类库的路径是如何获得的。</p>
<p>找main方法所在的类的实现是在 <code>AbstractRunMojo.java</code> 里面:</p>
<pre><code>mainClass = MainClassFinder.findSingleMainClass(this.classesDirectory, SPRING_BOOT_APPLICATION_CLASS_NAME);</code></pre>
<p><code>MainClassFinder.java</code> 是由spring-boot-loader-tools提供的,找到main方法所在的类主要是如下的代码:</p>
<pre><code>static <T> T doWithMainClasses(File rootFolder, MainClassCallback<T> callback) throws IOException {
if (!rootFolder.exists()) {
return null; // nothing to do
}
if (!rootFolder.isDirectory()) {
throw new IllegalArgumentException("Invalid root folder '" + rootFolder + "'");
}
String prefix = rootFolder.getAbsolutePath() + "/";
Deque<File> stack = new ArrayDeque<>();
stack.push(rootFolder);
while (!stack.isEmpty()) {
File file = stack.pop();
if (file.isFile()) {
try (InputStream inputStream = new FileInputStream(file)) {
ClassDescriptor classDescriptor = createClassDescriptor(inputStream);
if (classDescriptor != null && classDescriptor.isMainMethodFound()) {
String className = convertToClassName(file.getAbsolutePath(), prefix);
T result = callback.doWith(new MainClass(className, classDescriptor.getAnnotationNames()));
if (result != null) {
return result;
}
}
}
}
if (file.isDirectory()) {
pushAllSorted(stack, file.listFiles(PACKAGE_FOLDER_FILTER));
pushAllSorted(stack, file.listFiles(CLASS_FILE_FILTER));
}
}
return null;
}</code></pre>
<p>这里的核心就是利用spring的asm框架,读取class文件的字节码并分析,找到含有main方法的类,然后再判断这个类有没有使用了 <code>@SpringBootApplication</code> 注解,有的话,就属于要执行的代码文件了。如果项目里面有多个含有main方法且被<code>@SpringBootApplication</code> 注解的类的话,我看代码应该是直接选择找到的第一个开运行。</p>
<p>读取依赖的库路径,在spring-boot-maven-plugin里有大量的代码来实现,还是利用maven本身的特性实现的。</p>
<p>根据了解到的这些信息,我新建了一个普通的java项目bootexp,用一段简单的代码来运行起一个spring boot项目,这个spring boot项目就是spring官方给出的<a href="https://link.segmentfault.com/?enc=cOL1Z6WP8gSL7E1esZyWOg%3D%3D.Np1UA4Dh8UdJ75SEVnkPMSBYDxL64WxDFyVol%2BQeRe9a9czj92vo2R7cV6o1xPnj" rel="nofollow"><<Build a Restful Web Service>></a>。我的普通的java项目放在<a href="https://link.segmentfault.com/?enc=qVmLnXnvm9iFQM51lZV%2Fqw%3D%3D.2kkGgQ7wEDMieq9im5mOB67HRnkvWsgYqZR9NU8DRqG9dnyzr949zMdst0PvRBUOhiZn3RlvEXPjFUEhmBzr2w%3D%3D" rel="nofollow">github</a>上,springboot_run_v1 这个tag即为可运行的代码。</p>
<pre><code>package com.shahuwang.bootexp;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.loader.tools.JavaExecutable;
import org.springframework.boot.loader.tools.MainClassFinder;
import org.springframework.boot.loader.tools.RunProcess;
public class Runner
{
public static void main( String[] args ) throws IOException {
String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";
File classesDirectory = new File("C:\\share\\bootsample\\target\\classes");
String mainClass = MainClassFinder.findSingleMainClass(classesDirectory, SPRING_BOOT_APPLICATION_CLASS_NAME);
RunProcess runProcess = new RunProcess(classesDirectory, new JavaExecutable().toString());
Runtime.getRuntime().addShutdownHook(new Thread(new RunProcessKiller(runProcess)));
List<String> params = new ArrayList<>();
params.add("-cp");
params.add("相关库路径")
params.add(mainClass);
Map<String, String> environmentVariables = new HashMap<>();
runProcess.run(true, params, environmentVariables);
}
private static final class RunProcessKiller implements Runnable {
private final RunProcess runProcess;
private RunProcessKiller(RunProcess runProcess) {
this.runProcess = runProcess;
}
@Override
public void run() {
this.runProcess.kill();
}
}
}
</code></pre>
<p>相关库的路径获取,都是spring-boot-maven-plugin这个项目里面的私有方法,所以我这里直接在 bootsample 这个spring boot项目下执行 <code>mvn spring-boot:run -X</code>, 输出classpath,把classpath复制过来即可。执行bootexp这个项目,即可运行起 bootsample 这个spring boot项目了。</p>
<p>所以为什么spring boot的项目,main方法所在的类都要加上注解 @SpringBootApplication 这个疑问也得到了解决。</p>
<p>综上,<code>mvn spring-boot:run</code> 这个指令为什么能运行起一个spring boot项目就没有那么神秘了,这里主要的难点就两个,一个是maven插件的开发,获得项目的配置信息,执行起指令;一个是类加载机制,以及注解分析。</p>
<p>后续继续看maven插件开发的相关信息,以及类加载机制</p>
用ThreadGroup来捕捉线程未捕捉的异常
https://segmentfault.com/a/1190000020507998
2019-09-26T23:49:37+08:00
2019-09-26T23:49:37+08:00
沙湖王
https://segmentfault.com/u/shahuwang
0
<p>决定开始持续更新这个专栏的目的,就是为了记录学习Java过程中各种不懂,或者让我抓狂的东西,搞懂解决之后记录下来,方便自己也方便别人。以前学Java总是不得要领,要说不会吧,基本语法都会;要说会吧,真的是Java的基础库都没搞懂几个,更不要说并发,锁,Collection集合类等内容了。</p>
<p>最近在看Spring Boot的源码,看成熟项目的源码就是能让你开拓很多的眼界,时不时就能让人发出:“原来还能这样做呀!”</p>
<p>Spring Boot的Maven插件这里实现了一个ThreadGroup, 用来捕捉那些没有handler的线程的异常</p>
<pre><code> class IsolatedThreadGroup extends ThreadGroup {
private final Object monitor = new Object();
private Throwable exception;
IsolatedThreadGroup(String name) {
super(name);
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!(ex instanceof ThreadDeath)) {
//这里加锁的意义,看起来就是要用this.exception记住第一个发生的异常,其他的只是输出
synchronized (this.monitor) {
this.exception = (this.exception != null) ? this.exception : ex;
}
getLog().warn(ex);
}
}
void rethrowUncaughtException() throws MojoExecutionException {
// 这里为什么要加锁呢
// 感觉这里加锁的原因只是确保执行的时候this.exception不变,如果是null就是null,即使刚好有个线程发生了异常
synchronized (this.monitor) {
if (this.exception != null) {
throw new MojoExecutionException(
"An exception occurred while running. " + this.exception.getMessage(), this.exception);
}
}
}
}</code></pre>
<p>看了下<a href="https://link.segmentfault.com/?enc=rAVM2moIKkSMOgDTUASwfA%3D%3D.dslVOjPGz5WgU2U2bZgMvhj%2BN4gi%2Bc9SZ3WPHR%2FxLFKz0MOuoLn15bfwJe2Lvah86wN%2F%2F6%2F7KAbgU53x%2Fzl8ZgJu8lA1Iy6hLyLFknpJbGF6ayDMMiSIOqLebYS55xF0ZNQvBIeT%2BMz%2B%2F%2Bkf1fbZ8R1lrv9q1YIaxgrzEfYivmCIwSgLW5Aog8Njn7U%2ByW4T2q9x2EezgbzUJwPeN0YdjA%3D%3D" rel="nofollow">文档</a>,原来可以重写uncaughtException来捕捉那些未被处理的线程异常,之前一直没有好好搞清楚过ThreadGroup,没想的可以这样用。</p>
Java泛型通配符 ? 与 T 的区别
https://segmentfault.com/a/1190000020497160
2019-09-26T00:45:20+08:00
2019-09-26T00:45:20+08:00
沙湖王
https://segmentfault.com/u/shahuwang
2
<p>之前对Java的泛型不太熟悉,没怎么搞明白通配符 ? 的使用,以及 ? 与 T 的区别,导致一直懵懵懂懂,也不太敢用。</p>
<p>网上找了一大堆解释文章,都没有怎么讲清晰,最后发现是Java官方的教程<a href="https://link.segmentfault.com/?enc=1aUgHQYrJLraemgiMbLZBA%3D%3D.%2BpCMAJb7Gh64rkv9xTm7yAK2joXj%2FxtPXHX5FlkI7iIs8dsTOX%2FhDlPPVzo6znIcOZMonxz91tFVWrKXwsQG7Del188b1orGCJFhZxMS%2BoU%3D" rel="nofollow">《Lesson: Generics》</a>,把泛型和通配符的问题讲得非常清楚。</p>
<p>List<T> , 这个 T 是一个形参,可以理解为一个占位符,被使用时,会在程序运行的时候替换成具体的类型,比如替换成String,Integer之类的。</p>
<p>List<?>, 这个 ? 是一个实参,这是Java定义的一种特殊类型,比Object更特殊,就像一个影子。比如List<Object>和List<String>是没有父子关系的,这是两个类型,List<Object>类型和List<String>类型;但是List<?> 是 List<String>的父类。</p>
<p>用数学集合的语言来表述,? 表示了集合【所有Java类型,String,Integer等系统定义的,或者用户定义的Foo等类型】这个整体;而 T 表示了集合【所有Java类型,String,Integer等系统定义的,或者用户定义的Foo等类型】中的一个成员。</p>
<p>正是因为 ? 是个集合,T 是集合中的一个成员,导致我们很容易混淆这两者到底有什么区别。在一些情况下,这两者确实是可以相互替换的,但是另一种情况下就不行了。</p>
<p>?表示了任何的一种类型,那 List<?> 岂不是可以包含 String 和 Integer,但这又和Java的类型系统矛盾了,List里面只能放一种类型。于是乎,对于 <code>List<?> list</code> 是不可能进行 <code>list.add(1)</code> 的,不能对它进行写操作,除了可以 <code>list.add(null)</code> 。</p>
<p>而对于 List<T>, 却是可以进行写操作的:</p>
<pre><code> public static <T extends Number> void addTExtend(List<T> list, T e){
list.add(e);
}
</code></pre>
<p>那List<?>到底有什么意义呢?在我不需要处理数组里的元素的时候,写代码更方便简单。如下两个方法的功能是一样的,但是使用List<?>简单些。</p>
<pre><code> public static void setNullWildcard(List<?> list){
list = null;
}
public static <T> void setNullT(List<T> list){
list = null;
}</code></pre>
<p>List<?>里面的元素,如果你取出来,会都被转化为Object(因为Object是这个集合里所有元素的父),如果你只需要用到这个集合最顶层的父元素的方法,比如List<?>就是Object的方法,List<? extends Number> 就是 Number 的方法,那你也可以使用List<?>以及List<? extends Number> 来简化代码的书写:</p>
<pre><code> public static <T> void printList(List<T> list){
for(T e: list){
System.out.print(e + " ");
}
System.out.println("");
}
public static void printListWildCard(List<?> list){
for(Object e: list){
System.out.print(e + " ");
}
System.out.println("");
}</code></pre>
<p>如上两个方法,功能都是一样的,只是用 List<?> 更简便。</p>
<p>总结 ? 相对于 T 的第一个区别:不关心List里面的元素,或者只需要用到List里面元素的最顶层父元素的方法的时候,可以用List<?>来简化代码的书写。</p>
<p>上面说到了 List<? extends Number>,如下代码,使用 ? 还是 T ,都可以实现同样的功能:</p>
<pre><code> public static void printListWildCardExtend(List<? extends Number> list){
for(Number e: list){
System.out.print(e + " ");
}
System.out.println("");
}
public static <T extends Number> void printListTExtend(List<T> list){
for(Number e: list){
System.out.print(e + " ");
}
System.out.println("");
}</code></pre>
<p>两者都可以通过extends来限定一个类型的子集,但是 T 可以 <code>List<T extends Number & ExtendInterface></code> 即限定为多重继承的,? 却不可以:</p>
<pre><code> public static <T extends Number & ExtendInterface> void printListTExtend(List<T> list){
for(Number e: list){
System.out.print(e + " ");
}
System.out.println("");
}</code></pre>
<p>无法实现 <code>void printListWildCardExtend(List<? extends Number & ExtendInterface> list)</code></p>
<p>总结 ? 与 T 的第二个区别:使用extends限定类型子集的时候,?不能多重继承,T 可以</p>
<pre><code> public static void printListSuperNumber(List<? super Integer> list){
for(Object e: list) {
System.out.print(e + " ");
}
System.out.println("");
}</code></pre>
<p>?是可以限定父集的,但是 T 是做不到这一点的。</p>
<p>总结?与 T的第三个区别:使用super限定父集的时候,? 可以, T 不可以</p>
spring boot源码编译踩坑记
https://segmentfault.com/a/1190000020413148
2019-09-17T22:32:26+08:00
2019-09-17T22:32:26+08:00
沙湖王
https://segmentfault.com/u/shahuwang
1
<p>在Github上下载了<a href="https://link.segmentfault.com/?enc=GRraODrCwCE8mQ%2B0Nf6hWQ%3D%3D.QbvvluK9N56DBmmzVeovIgZThA9LQc91oanUu%2FGsLp6AZ3uCHbxzCF7AqilWru9F" rel="nofollow">Spring Boot</a> 的源码后,折腾了我几天才终于把这货给编译成功,并成功build出IDEA的project出来了,官方埋坑真的是天理不容啊。</p>
<p>我下载的是spring boot 2.1.8,下载后照着网上的文章执行指令:<code>mvnw clean install -DskipTests -Pfast</code> 然后就是半天动都不动,也不知道发生了什么。后面我就加多了个debug日志输出, 还是什么都没有输出。。。</p>
<p>搜了一下 mvnw,原来这家伙全名maven wrapper,它的原理是在maven-wrapper.properties文件中记录你要使用的maven版本,当用户执行mvnw clean 命令时,发现当前用户的maven版本和期望的版本不一致,那么就下载期望的版本,然后用期望的版本来执行mvn命令。也就是这个东西有两个作用,一个是不需要你事先安装maven,一个是保证你build的时候使用的maven保持一致。</p>
<p>后来看到这篇文章<a href="https://link.segmentfault.com/?enc=W00uSoxXGaZwlg8sswGLhQ%3D%3D.7Upt%2FaldCtTS7nonZZ3%2Ba3NJl39mEuLMB88l3OJSTCO68Zomvuc7UFXS%2FSs%2FheFj" rel="nofollow">《学Spring Boot不能装! 🤣不装Maven、Database、Tomcat 才是正常开发模式》</a>,原来mvnw第一次运行会检测$USER_HOME/.m2/wrapper/dists 目录下是否有maven-wrapper.properties中指定的Maven版本,如果没有就自动下载。比如我用的windows,就在目录<code>C:\Users\shahuwang\.m2\wrapper\dists</code>里有一个apache-maven-3.5.4-bin(刚才执行mvnw时生成的),继续打开,来到目录<code>C:\Users\shahuwang\.m2\wrapper\dists\apache-maven-3.5.4-bin\4lcg54ki11c6mp435njk296gm5</code> 然后在里面看到下载了部分的maven。</p>
<p>mvn那样下载的方式特别特别慢,所以我直接用浏览器下载对应版本的maven,<a href="https://link.segmentfault.com/?enc=L694mxdPN%2FxUYIt3wZNU3g%3D%3D.eHivbFsd5IVTIwFrc%2BKWa4pJPADrswcEHijwG%2FjoedAq5zs9QTHaaa6cphSFWU8b0MocWrswJeBHzvL8%2Bh4XYImMLZtjCWqye%2BgYeZ%2BEKSM%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=ZBcjk87UyCtovkWWUoha5Q%3D%3D.ZAcR0IJQIsTI%2BfdBzgYyKIcgtRaaRZAiFUl6IGl6CATlmy1HmUiyR5BKP2hLrciJ0wAXMP%2BgCXm5PltNVHwn7A%3D%3D" rel="nofollow">https://archive.apache.org/di...</a> 3.5.4-bin.zip,然后放到<code>C:\Users\shahuwang\.m2\wrapper\dists\apache-maven-3.5.4-bin\4lcg54ki11c6mp435njk296gm5</code>里即解决问题。注意一定要版本一致,且为zip包。</p>
<p>另外,要注意修改<code>C:\Users\shahuwang\.m2</code>下maven的setting.xml,把官方源修改为阿里云的源,不然会慢到死的。</p>
<p>搞定后,继续执行<code>mvnw clean install -DskipTests -Pfast</code>,中间发现有几次failed,都是继续再执行命令就解决了,看起来应该是网络连接中断的问题,之前网上一直找也没找到解决方案,没想到就是build多几次就解决了。</p>
<p>后面又遇到一个格式化的问题,<code>mvnw spring-javaformat:apply</code> 这个命令就解决了。</p>
<p>最后执行 <code>mvnw idea:idea</code> 创建 IDEA 项目。</p>
<p>然后就遇到了checkstyle的问题,spring boot定义了一个nohttp的规则,不能用http域名,除非在白名单里面。但是呢,它自己的文档里apache.org 这个域名用了http,又没有放到白名单里,然后一直编译都通过不了,只是告诉我违反了规则,又没有告诉我哪里错了。最后,终于搞清楚checkstyle的输出,是放在主目录下target/checkstyle-result.xml 里面,找到里面的error,就知道是哪里的问题了。</p>
<p>最坑爹的是,上述问题我解决了之后,重新下载全新的spring boot工程,执行<code>mvnw clean install -DskipTests -Pfast</code>就一下子通过了,上述问题全部都没有遇到,也难怪我搜索半天也找不到解决方案了,看起来maven还是不太行。</p>
<p>最后的最后,又发现一个新的坑,生成了IDEA项目之后,发现默认的Language Level 就是 Java 5,这哪里受得了呀。网上找到的方法都是加这么一段:</p>
<pre><code> <build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build></code></pre>
<p>经我验证是行不通的,spring boot下面有很多的modules,上面这样写,要逐个module下的pom.xml去修改,太烦人了。最终在这个<a href="https://link.segmentfault.com/?enc=cTaUY7ha23IiyqlFaR%2BE3g%3D%3D.xNMIiXjNml0xiOeF4q6KcHtM3tMlTJ6QacSzC%2Br%2FZovkjTW%2B%2FSc3P56dS5IkQXvvFSMK5YRAxaaZn0DeVceh%2BCdANfnPbO23l4bRMh51QTYGQpJ7is7f1M99LAsPZhLfPTrhySWzcJ8OF36Pjw9DI0iBoLooSSk83%2FeN5CzsLaY%3D" rel="nofollow">问答</a>下找到了解决方案,要使用properties的方式,直接在主目录下的pom.xml 下添加:</p>
<pre><code> <properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties></code></pre>
<p>properties是仅次于project的二级目录,一般情况下都会有的,把中间那两行放进去就行了。之后就完美解决问题了,自动使用java 11了</p>
什么是Java Bean、POJO和DAO
https://segmentfault.com/a/1190000020291377
2019-09-05T00:39:52+08:00
2019-09-05T00:39:52+08:00
沙湖王
https://segmentfault.com/u/shahuwang
4
<p>之前学习总是囫囵吞枣,总是没有把不懂的东西搞懂的精神,导致很多很简单的概念,我都一直没搞懂。今天要一次性把 Bean,POJO和Dao这三个困扰我多年的概念搞懂来。</p>
<p>首先来说Java Bean,它其实是一套编码规范,所谓的编码规范,就类似于你告诉我这是个Java Bean,那我一定知道这个class有哪些特点,我可以用Java Bean规范里的操作来使用这个class,比如Java Bean肯定有一个无参的构造方法,我可以直接 <code>BeanSample sample = new BeanSample()</code> ,关于Java Bean的起源,可以参考这两篇文章<a href="https://link.segmentfault.com/?enc=n8M4AZ%2FQUHbz6FM98HkD1A%3D%3D.CgBXrGHulUbaDp3dNPmyuXczDKx43ddQHLeziM91KUTl3kTPNsNM59Z9OF8uyhwIVHYdFQilrArBj1%2F%2FN%2BTPZhFj5%2FM4IrOdHOJCa6Ay%2Fsom9b0qQFKERDtYqoXFBDxiCrFAA2UHAIQmFXs%2B7AsMy5cnOtzacojvnS3l6suiMjI%3D" rel="nofollow">《Java 帝国之Java bean (上)》</a>和<a href="https://link.segmentfault.com/?enc=OwPBhuabdZmDfjU%2BMr6l3Q%3D%3D.GmKeZ2RAVMAOWr9cXmJ8CpKmgEKmYV7R3wCfmFZ0rTpxmM2Rh1XJ%2Fxz3ehJWZRtakUsdAWFE%2Bu%2Fy8LQFBVQpqS2a6c5rrulZGVxrGPd1SBh9Yb1VUCAn48UknDEYtj2fssS0Ho5513u7%2FHlceYNPax6qRjv%2BPMlRVrdCLChBBBI%3D" rel="nofollow">《Java 帝国之Java bean (下)》</a>。</p>
<p>在这个知乎问题【<a href="https://link.segmentfault.com/?enc=SlIEVrFnA4EIWbPpEfJydA%3D%3D.g1Vdn%2FazggmZ5sgP%2F0oNSOgf7zcgVKVe8YQG3HDK%2BsdtEGmYXaw9hlbeqRyNpVoO2CBgxT6Cyx4DwkBIqkoA5g%3D%3D" rel="nofollow">Java Bean是个什么概念</a>】下的一个回答意简言赅:</p>
<blockquote><ol>
<li>所有属性为private</li>
<li>提供默认构造方法</li>
<li>提供getter和setter</li>
<li>实现serializable接口</li>
</ol></blockquote>
<p>虽然规范里有更多繁复的规定,但是平常大家称谓的Java Bean,只需要符合上述四个条件就可以了。</p>
<p>按照约定实现的Java类,即Java Bean,在一些场景下就变得很方便了,比如使用Java反射机制的时候,就可以很方便利用getter和setter方法了,Java跨机器间通讯的时候,也可以利用Java Bean可序列化的特点,轻松传输Java对象了。</p>
<p>关于POJO,则是说好众多,毕竟最初的定义感觉也不是很规范,就是个Java专家在个会议上提出的,比较好的几个关于POJO的文章,可参考<a href="https://link.segmentfault.com/?enc=R4382KtSP2kdmHaAM2nGpA%3D%3D.cVt0coIhGS3lYc65Idvv3Zjp1zlRZIhGm9xoQJQMS7LJbZBPatJ2F7baDF5zaZKSBjBXmBb9cfavxEN%2B2WTUru7j646xJTlUqgpxnA1QddT03Y%2F9L2v3C0zQwr8fClVs" rel="nofollow">Difference between DTO, VO, POJO, JavaBeans?</a>, 和维基百科上的 <a href="https://link.segmentfault.com/?enc=rxmk9foMQI1PJCo%2BXzd2cg%3D%3D.S1GFJNIehN2pYqSOE23vKITVgi4Rv8am2s5Kc3VX9bZGEUEqqECpvdSkq51N3bbjo2juL5CZHEvL5ixImKmkwA%3D%3D" rel="nofollow">Plain Old Java Object</a></p>
<p>基本上符合下面三无条件的,都可以称之为POJO:</p>
<ol>
<li>不继承任何类</li>
<li>不继承实现任何接口</li>
<li>没有包含注解</li>
</ol>
<p>另外,似乎POJO也不应该包含业务逻辑。</p>
<p>关于Java DAO (DataAccessobjects 数据存取对象)是指位于业务逻辑和持久化数据之间实现对持久化数据的访问。通俗来讲,就是将数据库操作都封装起来。我觉得参考这篇文章即可:<a href="https://link.segmentfault.com/?enc=ikl%2FhmYCKoijw725JXCb6w%3D%3D.A%2BpuWx%2Fb81BzUJLy%2BFOZRlc32aCzwuKDPBwTCRQXvP3u25O%2BaBq8j0H%2BPqeWzHUy" rel="nofollow">《Java DAO 模式》</a>。</p>
<p>实际上DAO就是封装了对数据库访问的类,但是DAO不是单独存在的,而是用DAO模式,目前看到的DAO模式有两个版本,版本一:</p>
<blockquote>1、DAO接口: 把对数据库的所有操作定义成抽象方法,可以提供多种实现。 <br> 2、DAO 实现类:针对不同数据库给出DAO接口定义方法的具体实现。 <br> 3、实体类:用于存放与传输对象数据,比如表示查询到的所有字段存放的的对象<br> 4、数据库连接和关闭工具类:避免了数据库连接和关闭代码的重复使用,方便修改。</blockquote>
<p>版本二会增加一个 DAO 工厂类,可以更方便使用DAO的实现类</p>
Java 的 SPI 机制
https://segmentfault.com/a/1190000020278712
2019-09-04T00:06:58+08:00
2019-09-04T00:06:58+08:00
沙湖王
https://segmentfault.com/u/shahuwang
1
<p>之前写 <a href="https://segmentfault.com/a/1190000020122395">Java注解之如何利用RetentionPolicy.SOURCE生存周期</a>时了解到Java的SPI机制,发现挺神奇的,因此就搜了下相关的知识。这一篇<a href="https://link.segmentfault.com/?enc=tKip1Q5lm8eKxdYwOhy%2FMQ%3D%3D.7WDU3gCvWin4mBeURjypCSGcVVJGZYasBLn05YU64GmyTPI%2FL4q2l6Bgo4arqTmc" rel="nofollow">Java SPI思想梳理</a>基本上把这个讲透了,<a href="https://link.segmentfault.com/?enc=Oz%2Bi2sBh0RGx9gkIMOK0kA%3D%3D.hkkaBnICBskG%2BCHIeVzWZdYUl2bxY9Clgu5N9nyFXZ8UccSDuS%2B%2B0DpGEZUZr5CIyIlF41MtHbe6RjS5OOVCEJy1thb8BpETWYgPX%2BHHKdBi3pywbIKEQpcw%2F8aYTXzl" rel="nofollow">ServiceLoader</a>的文档也提供了更多详尽的知识,包括Module里如何使用SPI都有了。</p>
<p><a href="https://link.segmentfault.com/?enc=zxrcsCYdypGq3%2BZgLQYfpg%3D%3D.HtJl6q7YgBS5AJNaX%2FvoQm5qISIWlF%2B4vSySj77f2e2reQfOoRBiXGo3Sh8E6F9d" rel="nofollow">Java SPI思想梳理</a> 里面有段讲得特别好,正常来说,我们写Java代码,都是调用者——》接口——》接口的实现,如果接口和接口的实现是在一起的,这种一般称之为API。而如果调用者和接口放在一起,这种就是SPI了。</p>
<p>SPI运用范围挺广,以前我就不太了解插件机制是怎么做的,发现SPI就可以用来做插件,还是挺方便的。</p>
<p>ServiceLoader的代码并不算复杂,不过我目前还欠缺功力看懂了。Java的知识实在是庞大,之前学习走偏了,导致一直没有找对方向,希望日拱一卒,能够成为个合格的Java开发者吧</p>
Java注解之Rentention.CLASS的作用
https://segmentfault.com/a/1190000020277644
2019-09-03T21:58:16+08:00
2019-09-03T21:58:16+08:00
沙湖王
https://segmentfault.com/u/shahuwang
0
<p>网上没有找到多少关于Java的注解中生存周期里Retention.CLASS这个级别的文章,找到了这个回答:<a href="https://link.segmentfault.com/?enc=1Yf%2FqxrdexkyY2qpBusW6Q%3D%3D.RQUsf22SsOf7gQUy8%2FXUgLarV8HR5dc0DoG7GRy67aJd8az7Vk1TcOiDJxgjLU%2FErNBRzFxnYLbQYAN6OVBxGh5Mamg%2B33Cluk10TUU5AsaW790wRTu55rYjet3IbL2Bj3HZmwEEaRDeXf%2BwjNZW7g%3D%3D" rel="nofollow">How can I access a RetentionPolicy.CLASS java annotation?</a> ,最重要的一个作用,应该是在生成的class文件里增加更多的标识,给字节码解析和处理工具使用。比如我要给每个class里的字段都自动添加一个setter方法,那就需要修改编译后的class文件,在某些标志位里面插入一段setter方法的二进制</p>
Java注解之如何利用RetentionPolicy.SOURCE生存周期
https://segmentfault.com/a/1190000020122395
2019-08-20T00:04:21+08:00
2019-08-20T00:04:21+08:00
沙湖王
https://segmentfault.com/u/shahuwang
3
<p>上一篇文章简单讲了下<a href="https://segmentfault.com/a/1190000019936605">Java注解的学习之元注解说明</a>,学习了Java注解是如何定义的,怎么使用的,但是并没有介绍Java的注解是怎么起作用的,像Spring Boot里面的那些注解,到底是怎么让程序这样子运行起来的?特别是讲到RetentionPolicy这一块,到底在SOURCE阶段,在CLASS阶段,在RUNTIME阶段有什么差别,注解是如何在这三个阶段起作用的呢?而且,在SOURCE阶段,在CLASS阶段,程序又不运行,那注解到底会用来做些什么呢?</p>
<p>带着这些疑问,我就去了解下了如何让注解起作用,发现RUNTIME阶段的介绍到处都是,但是SOURCE和CLASS阶段就很少文章讲解到了,真的得搜刮好几十篇文章才能成功的把程序跑起来,几乎每一篇文章都少讲了些东西。</p>
<p>本文优先讲的是SOURCE阶段注解如何发挥作用,发现这一块涉及的知识非常多且难,资料还少,另外还发现,Java服务器端编程的人用这个反而不如Android开发的人用得多。对我学习SOURCE阶段的注解帮助最大的是<a href="https://link.segmentfault.com/?enc=AKXRQ2NfEi28oX%2FvbcWgEg%3D%3D.ed3FTq9Mu8y8xqX1efIULPO0JM5Gaeh9XjUOlD6KjWLszBPWnQmqGgh%2FsdVzZV7ieuoKiJ6cfmZ1PVz4glpYpwwVzWBujYfkU%2FN5Fi9MXpqISrABYaRlSlzf8VU48I9m" rel="nofollow">Pluggable Annotation Processing API</a>,<a href="https://link.segmentfault.com/?enc=pqTKwAdm4rDfDh0CI3%2FEEA%3D%3D.yXW0YcTXq6YLY0D8dEeFzqRXAI13hkBt3mTtXyGzDYkVcJ60r6qv3xZ5RY5yEQF2VzDUZ2IsryrL2Hm%2FTsmabWVRLnkb%2F7lR1%2F3UAWzOfSwEvUv5f6Uq6UOe9Rfcd9Vg" rel="nofollow">JSR269插件化注解API</a>以及<a href="https://link.segmentfault.com/?enc=9c58FJ75bknmYTpV8SqFFw%3D%3D.seKZYzxLcmDpkDPTMsDbCUkxEwORuLqXxDbShFuqOx1gytWlNZrx0OjNr5pqGZH%2BDKHac0pDjNUla%2FQVCXp78iDQxyT4e6Z6KOsiB%2FEWQr8%3D" rel="nofollow">JVM进阶 -- 浅谈注解处理器</a>。搜索这方面的资料用“插件化注解处理API”这个关键词能搜得更全。</p>
<p>这几篇文章基本上把SOURCE阶段的注解实现和使用讲了,但是具体细节,比如用javac直接编译运行代码,javac使用jar包,使用maven等三个方式如何运行注解处理器,倒是基本上蜻蜓点水,摸索了我好久才搞定了。关于注解处理器的知识,可以从如上三篇文章了解,本文主要讲注解的定义和运行。</p>
<p>我用的代码是摘抄至 <a href="https://link.segmentfault.com/?enc=LMsf%2FlUC774p3bR%2FUg4Gbw%3D%3D.xJEZjYnzWDGk8IYoTw2fXHTfz4JS4GQKUwN8XAUbam4nkO6Y%2B2U13JUtBIEZPQ6ofSD2Te7AJoG1ZFQ9S9%2BUsSirzWnIhbBK6DQontTJkAA%3D" rel="nofollow">JVM进阶 -- 浅谈注解处理器</a> ,它定义了一个CheckGetter的注解,用来检查一个类里面的字段,哪个没有Getter方法,没有的话编译就报错。不过我的和他的稍稍不同,他的代码定义没有放到package里面,我的放到package里面了,这样子的使用和执行又有了点不同。</p>
<p>首先,定义CheckGetter注解:</p>
<pre><code>package com.shahuwang.processor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface CheckGetter {
}
</code></pre>
<p>注意上面的代码,是放到 package com.shahuwang.processor 里面的,因此,先创建如下结构的文件夹</p>
<pre><code>Processor
—— com
—— shuhuwang
—— processor
—— CheckGetter.java
</code></pre>
<p>注解已经定义好了,现在先来用一下这个注解:</p>
<pre><code>package com.shahuwang.processor;
@CheckGetter
public class TestGetter {
String name;
String first;
public String getName(){
return this.name;
}
}
</code></pre>
<p>这个类有两个字段,但是有一个字段没有设置getter。</p>
<p>下面才是真正重要的代码,就是让CheckGetter这个注解真正能运行起来,发挥作用:</p>
<pre><code>package com.shahuwang.processor;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic;
import javax.lang.model.element.Element;
import java.util.Set;
// 这个地方换了包名就需要改过来,否则processor就不会执行了, 这里是最需要注意的地方,千万注意!!!!!
@SupportedAnnotationTypes("com.shahuwang.processor.CheckGetter")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class CheckGetterProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotatedClass : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(CheckGetter.class))) {
for (VariableElement field : ElementFilter.fieldsIn(annotatedClass.getEnclosedElements())) {
if(!containsGetter(annotatedClass, field.getSimpleName().toString())){
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
String.format("getter not found for '%s.%s'.", annotatedClass.getSimpleName(), field.getSimpleName()));
}
}
}
return false;
}
private static boolean containsGetter(TypeElement typeElement, String name){
String getter = "get" + name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();
for(ExecutableElement executableElement: ElementFilter.methodsIn(typeElement.getEnclosedElements())){
if(!executableElement.getModifiers().contains(Modifier.STATIC) &&
executableElement.getSimpleName().toString().equals(getter) &&
executableElement.getParameters().isEmpty()){
return true;
}
}
return false;
}
}
</code></pre>
<p>上面这段代码要理解的概念有点儿多,实际上我现在也不是很懂,但是本文的目标是先让注解处理器跑起来,所以先不管。这里最重要也是折磨我最惨的地方,就是这一句<code>@SupportedAnnotationTypes("com.shahuwang.processor.CheckGetter")</code>,你定义的注解在哪个package下,这里就要写完整了,如果写错了,注解处理器就不起作用了。</p>
<p>现在在Processor目录下,打开命令行,先编译CheckGetterProcessor.java: <code>javac com/shahuwang/processor/CheckGetterProcessor.java</code>,编译好之后就可以使用了,再执行命令:<code>javac -processor com.shahuwang.processor.CheckGetterProcessor com/shahuwang/processor/TestGetter.java</code>,便可以看到这样的提示:</p>
<blockquote>错误: getter not found for 'TestGetter.first'.</blockquote>
<p>上面这种方式,需要每次执行编译的时候,指定一下processor才能做到检查作用,那如果在团队开发中,希望自动执行一些检查,那可以用SPI 服务提供者发现机制或者maven来实现。</p>
<p>SPI 服务提供者发现机制就是配置META-INF文件夹和里面的文件,告诉Java你要执行某个东西,这个东西的路径在哪里。再把编译好的processor和META-INF打包成jar包,就可以很方便使用了。文件夹和文件结构如下:</p>
<pre><code>processor
-com
-shahuwang
- processor
- CheckGetterProcessor.class
-META-INF
-services
- javax.annotation.processing.Processor
</code></pre>
<p>编译方式和上述一样。</p>
<p>javax.annotation.processing.Processor是一个普通的文本,就是告知java一些关于注解处理器的配置,里面的内容如下:</p>
<pre><code>com.shahuwang.processor.CheckGetterProcessor</code></pre>
<p>就是告知这个注解处理器要用哪些个,如果有多个的话,可以一行一个。</p>
<p>然后在processor目录下,执行指令:jar -cvf processor.jar com META-INF, 形成了一个 processor.jar 的包,此时可以执行指令:</p>
<pre><code>javac -cp processor.jar com/shahuwang/processor/TestGetter.java
</code></pre>
<p>就会自动执行注解处理器检查字段有没有getter方法了。</p>
<p>jar包的这个方法,其实是把注解和注解的实现单独放一块作为一个插件来使用了,maven也是如此的。</p>
<p>现实的开发中,还是用maven最多,先用指令创建一个maven项目:<code>mvn archetype:generate -DgroupId=com.shahuwang -DartifactId=Processor -Dpackage=com.shahuwang.processor</code></p>
<p>然后在pom.xml的<project>层级下添加如下配置:</p>
<pre><code> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<annotationProcessors>
<annotationProcessor>
com.shahuwang.processor.CheckGetterProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build></code></pre>
<p>代码目录结构如下:</p>
<pre><code>Processor
- src
- main
- java
-com
-shahuwang
-processor
-CheckGetter.java
-CheckGetterProcessor.java
- pom.xml</code></pre>
<p>然后Processor目录下执行: <code>mvn clean install</code>, 这个项目就会被安装到本地的maven库里面。</p>
<p>再用maven新建一个TestProcessor项目,项目结构如下:</p>
<pre><code>TestProcessor
- src
- main
- java
-com
-shahuwang
-TestGetter.java
- pom.xml</code></pre>
<p>TestGetter.java 的代码上面有。</p>
<p>修改pom.xml, 先在 <project>下添加:</p>
<pre><code> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<annotationProcessors>
<annotationProcessor>
com.shahuwang.processor.CheckGetterProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build></code></pre>
<p>再在dependencies添加:</p>
<pre><code> <dependency>
<groupId>com.shahuwang</groupId>
<artifactId>Processor</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</code></pre>
<p>也就是引入上面编译好的处理器的包。</p>
<p>现在,在TestProcessor目录下执行:</p>
<pre><code>mvn compile
</code></pre>
<p>就会看到熟悉的提示:</p>
<blockquote>错误: getter not found for 'TestGetter.first'.</blockquote>
<p>三种主流的注解处理器使用方式现在都搞懂怎么使用了。下一篇文章将会关注注解处理器的使用方法,各个注解处理器API的使用</p>
Java注解的学习之元注解说明
https://segmentfault.com/a/1190000019936605
2019-07-31T23:15:11+08:00
2019-07-31T23:15:11+08:00
沙湖王
https://segmentfault.com/u/shahuwang
1
<p>最近在学习Spring Boot,发现真的是一个靠注解搭起来的框架,Spring从使用XML到现在使用注解,大概是终于发现绝大多数人所写的项目,都不需要XML的松耦合,快速上线、快速丢弃、快速接手,在一切都要求快的互联网公司里,XML这样的松耦合浪费太多的学习成本了,划不来。</p>
<p>之前一直没有系统学习过Java的注解,学习起Spring Boot来总是用得不明所以,想想还是从基础学起,方不至于浪费大量的时间去查找Spring Boot各个注解的使用文档。</p>
<p>Java的注解,实际上包含三方面的内容,一个是Java注解本身的定义,一个是Java注解的使用,一个是如何利用Java的反射功能实现功效。本文也将从这三个方面逐一讲解,主要参考文章<a href="https://link.segmentfault.com/?enc=je0g0CK40KHz2fy%2FFM6dww%3D%3D.y0HydFh%2FyInvi7Avfcz1en99NrJIq7ojzzdNLgGK0%2F0gtdDxcwpUJk%2FqXqh26255" rel="nofollow">JAVA 注解的基本原理</a> 和 <a href="https://link.segmentfault.com/?enc=%2FER%2FhWHLtUYhB3suUXNMtw%3D%3D.kcUIVDH%2FIJTQpfgw3h2u97T6wCvUhURz0JD2xyBEo0BUf6h79JjNMLQzqK8nBLmSqMw7btqKt6UoFjf4mIJ3CjZuBTsa9BQNK6LeODl5L4Y%3D" rel="nofollow">Java语法标准</a>。</p>
<p>Java 内置了许多的注解,在<a href="https://link.segmentfault.com/?enc=qeAV3WRpBGOnc3gh36%2FzgA%3D%3D.iEhQM8iOq01l2Cijfy0iXtFuqE9HxzRQkzs5wmk3GRpLYCroFys722zU7iLARwk0qPfYgXrfOhdz6Ca3LIH4U12Sin4b1CAwM95SZl0C0mY%3D" rel="nofollow">Java Annotation</a>可以找到内置的所有注解,我们最常见的注解应该是<a href="https://link.segmentfault.com/?enc=cApVLBThRPOYWHJh16uzbQ%3D%3D.dDKzR197QMcR2SYYg3nSu4yYsWBpPf0wQ%2BhnuH7VkY5bl0xvVeSHJYkwwwJ2VSxvsil79D5TVIt3mpEDyCemECM9kz8V51tQsYpBML3%2BuGo%3D" rel="nofollow">@Deprecated</a>,一旦一个方法使用了这个注解,别人此后再使用这个方法时,就会提示这个方法已经被废弃了,建议不再使用。</p>
<p>那@Deprecated到底是怎么定义的呢?它还能用在什么地方?JDK是如何在编译的时候遇到这个注解就马上发出一个提示的呢?带着这些疑问,一步一步学习Java的注解。</p>
<p>一、如何创建一个注解</p>
<p>所有的注解本身都继承于java.lang.annotation.Annotation, 每一个注解本身就是一个interface,但是注解这种interface有其特殊性,所以,所有的注解都是这样定义的:</p>
<pre><code>publice @interface xxxx {}
</code></pre>
<p>@Deprecated的实现代码如下:</p>
<pre><code>import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
</code></pre>
<p>多了@Documented, @Retention,@Target三个注解,这三个注解分别是什么呢?又有什么作用呢?</p>
<p>用于方法的方法,叫做元方法,所以用于注解的注解,就叫做元注解,这三个都是JDK提供的元注解。</p>
<ol>
<li>@Documented标记这个注解@Deprecated将会出现在Java Doc之中</li>
<li>@Rentention标明这个注解的生命周期,<a href="https://link.segmentfault.com/?enc=S5zcFC47cH4pmjTIz0gN%2BQ%3D%3D.DRwrCPCDHcaYej5bxNjGUFx8bbaXAfZWCGW1Vfr2Q5sP%2B7huft7W3%2BTo336w7IqRRriVczwlk1LR3wTzJKAX7ANkikspd6BJ8vxVBzgR5xXkhTnCB%2Bjyqo%2Bj7dDWqlwl" rel="nofollow">RententionPolicy</a>里面定义了三种生命周期,分别是SOURCE,CLASS,RUNTIME三种,SOURCE表示在编译阶段抛弃,CLASS表示会被记录到class文件里面,但不会出现在vm里面运行,RUNTIME表示在运行期里面存活。</li>
<li>@Target表示这个注解可以用在何处,比如@Deprecated的定义,表明它可以用在构造函数,字段,本地变量,方法,包,参数,类</li>
</ol>
<pre><code>@Deprecated
public class TestDeprecated {
@Deprecated
public String name;
@Deprecated
public TestDeprecated(String name){ }
@Deprecated
public String getName() {
return name;
}
public void testDeprecatedParam(@Deprecated String name, String sex){
@Deprecated
String test = "test";
System.out.println(name + sex + test);
}
}
</code></pre>
<p>如上是@Deprecated在类、字段、构造函数、方法、参数和本地变量的使用方式。不过本地变量和参数的@Deprecated似乎是不起作用的。另外,package级别的注解使用,需要使用package-info.java, 比如package-info.java文件如下:</p>
<p>@Deprecated<br>package com.shahuwang;</p>
<p>这样子就标记了整个包都是要废弃的了。</p>
<p>目前Java 10 提供了 <a href="https://link.segmentfault.com/?enc=lkgtb2VLiJT9v8KEXlqdMQ%3D%3D.C%2FPbxl1QMGCNyE2p1mddM%2FyP3FqSQgmgpESss2RwcppCGi5XBictTucAoeGkI8Ytz6Bz%2BvAvUB0cNHHcLsVUWD0KZ8NA1OGZOlcLg8vqOKE%3D" rel="nofollow">@Documented</a>, <a href="https://link.segmentfault.com/?enc=76YH6V9bYYXTU5gHDjeksA%3D%3D.R97ZZe7NrNsnX8a4x83U7ULzcc2ODxpKA%2FfTlHvpJiA7j9H8J9JX1rB2i9XDbT704F2nC0xrQxqavE4Ifz3nq5orn544d%2FzHcaZrXRyNXX8%3D" rel="nofollow">@Inherited</a>,<a href="https://link.segmentfault.com/?enc=E1cwRPfhGP2wC2hM4KP3Xg%3D%3D.eN3aw41Jf0parwBfzH9LCfjp7iN57xkmC8k3sOjCk6SdNMwUQtKmpBcyHP3CLaqv8JNd3dnPYz84tv4yviJQFWX29f8Wro9NY0JFdFzs6hA%3D" rel="nofollow">@Native</a>,<a href="https://link.segmentfault.com/?enc=TWl8Gul4R195aOYfPj1q%2Bw%3D%3D.PtMgp1RXLhI6lIO1mkNiqm2zflzYb2N7ijN4YPB3jb7Kff9DnNeGOO8PDaaGaQdicU526up0tqG7Lu%2FpnnXOGv3fkDf13PUozKEJMKbYoWU%3D" rel="nofollow">@Repeatable</a>,<a href="https://link.segmentfault.com/?enc=6lSqZsjqhOjR4%2FldjEZ5nQ%3D%3D.AR0iVAUy1mDYqJTeO6%2BQ9X%2B7xYEct5tK2Oq4Z9e%2FWjN5u4qAjuIH70FWn8NHgMuoFp9nT4xlelY3rF1ZEzDW5fDI2%2B9rzdj9Rmsl6XdX%2Fbg%3D" rel="nofollow">@Retention</a>, <a href="https://link.segmentfault.com/?enc=Bq0GXqlTmVYa%2BCz1dmzgWg%3D%3D.laZUvuPjM7nY9u7yU9aMYxkbH0fQeMzKkOFHGFr13PdeZwTtIvjgJhDiep2ukmk17Bms5sbLinmAX2YTIHeZKOefI4pTRstiCRkMiSGsPw0%3D" rel="nofollow">@Target</a> 共六个元注解,除了上面已讲解过的三个,剩余三个的用途:</p>
<ol>
<li>
<p>@Inherited 表示使用了它的注解,再用到类上面时,可被子类继承此注解。</p>
<pre><code> @Inherited
publice @interface Test{}
@Test
public class Parent{}
public class Child extends Parent{}
</code></pre>
<p>如上,Child类也继承了@Test 这个注解,可以使用它的功能。</p>
</li>
<li>@Native 表示定义常量值的字段可以被native代码引用,当native代码和java代码都需要维护相同的常量时,如果java代码使用了@Native标志常量字段,可以通过工具将它生成native代码的头文件。目前对Java的native代码研究比较少,后面再对它进行研究</li>
<li>@Repeatable 表示注解可以重复使用,是Java 8引进的特性。之前注解只能在同一个地方用一次,用了@Repeatable,注解就可以在同一个地方使用多次了,可以参考这篇文章<a href="https://link.segmentfault.com/?enc=JxbSZD5swDx0GTiWA51jKw%3D%3D.uE1EfWPS1DpMX9cKmuVhZ4z4HKMtTvcA4NJoGOT367Ohfz7WPYIR95qUtTumgvQZnyVMOgPz%2FNqKquef%2FQfqsg%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=w8sCYcKjsCV9zDIQlzp4eg%3D%3D.7cf4F21eLpaGpChcfjtuteRmqqXfi9l1Fj%2FVFQEDS6qcCSFPo0lkxnBRcEstjtvWAnXNQXht6AWfPrNhZYZW8w%3D%3D" rel="nofollow">https://blog.csdn.net/aitangy...</a>
</li>
</ol>
如何利用Java NIO实现高性能高并发的http服务器
https://segmentfault.com/a/1190000019389906
2019-06-04T23:49:31+08:00
2019-06-04T23:49:31+08:00
沙湖王
https://segmentfault.com/u/shahuwang
0
<p>在学习Java NIO的过程中,我一直理解不了Java NIO是怎么用来实现高并发的服务器的,网上给出的例子里,基本上没有多少说到这一点的,Tomcat,Jetty这些的源码又太庞大了,导致我无从下手。</p>
<p>后来搜了下才发现,JDK自带了一个httpserver的实现,看了下代码,非常简洁,非常规范,一下子就让我搞懂了Java NIO是怎么实现高并发的了。</p>
<p>NIO是同步非阻塞模型,同步是指用NIO读取数据,需要你的线程一直在运行着,直到数据读写完毕。非阻塞是指监听通道的时候是非阻塞的,比如向通道询问有没有数据可读的时候,可以马上就返回有或者没有。如果有数据,你的线程就可以去处理读数据这部分的逻辑;如果没有数据,你的线程可以去忙其他的事情,这样子单个线程的处理能力就会高很多,不用总是等着数据。</p>
<p>在jdk的httpserver里,处理请求的过程大致如下图:</p>
<p><img src="/img/bVbtwmp?w=779&h=998" alt="图片描述" title="图片描述"></p>
openresty 日志输出的处理
https://segmentfault.com/a/1190000015361258
2018-06-22T18:08:43+08:00
2018-06-22T18:08:43+08:00
沙湖王
https://segmentfault.com/u/shahuwang
7
<p>最近出了个故障,有个接口的请求居然出现了长达几十秒的处理时间,由于日志缺乏,网络故障也解除了,就没法再重现这个故障了。为了可以在下次出现问题的时候能追查到问题,所以需要添加一些追踪日志。<br>添加这些追踪日志,我希望能够达到如下几点:</p>
<p>1、只有请求超过一定时间才记录,不然请求太多,系统扛不住</p>
<p>2、添加的代码可以尽量的少</p>
<p>3、对接口的影响尽量小,比如不影响实际时延,甚至记录日志时出现了错误,也不影响系统正常运行</p>
<p>openresty这套工具,可以在nginx处理请求的每一个阶段介入,编写代码进行逻辑处理。其可介入的流程如下图:<br><img src="/img/bVbcCkf?w=600&h=543" alt="图片描述" title="图片描述"></p>
<p>log Phase这个阶段,就是openresty能处理的最后阶段。到这个阶段的时候,实际上请求的响应已经发送给客户端了。所以使用 log_by_lua (知乎真特么蛋疼啊,左右下划线就自动斜体,还没提供转义功能) </p>
<p>log Phase这个阶段,就是openresty能处理的最后阶段。到这个阶段的时候,实际上请求的响应已经发送给客户端了。另外我也测试过了,即使在这个阶段发生了错误,如 io 错误,也不会影响接口的正常响应,所以使用 log_by_lua 很是符合需求。</p>
<p>好处不止如此, log_by_lua是一个请求的最后处理阶段,那么只要请求正常进行,比如会走到这一步,因此,在这一步,我们就知道了这个请求的耗时了。另外,则是我们的代码里有不少的 ngx.exit ,如果是在业务逻辑处理的时候就记录日志,那么每个出现 ngx.exit 的地方,都需要插入写日志到硬盘的操作,大大增加了代码量。</p>
<p>写日志到硬盘的这一步操作,可以在 log_by_lua 这个阶段来完成,剩下的另一个问题就是每一步记录的日志如何传递到 log_by_lua 这一阶段来了。</p>
<p>我处理的方式是使用ngx.ctx, 每一个请求,都会有自己独立的 ngx.ctx, 这个 ngx.ctx 会贯穿整个请求的始终,简单的log函数如下:</p>
<pre><code>logger.lua
--------------------------
local _M = {}
function _M.log(format, ...)
if ngx.ctx.log_slot == nil then
ngx.ctx.log_slot = {}
end
arg = {...}
local logstr = ""
if arg == nil then
logstr = format
else
logstr = string.format(format, unpack(arg))
end
logstr = logstr .. "\t" .. ngx.now()
table.insert(ngx.ctx.log_slot, logstr)
end
return _M
</code></pre>
<p>到了 log_by_lua 阶段要把追踪日志写入到硬盘里,处理代码如下:</p>
<pre><code>log_slot.lua
---------------------
local request_time = ngx.var.request_time
if request_time < 1 then
return --- 小于1秒的请求不记录
end
local slot = ngx.ctx.log_slot
if slot == nil or type(slot) ~= "table" then
return
end
local logs = table.concat(slot, "\n")
local f = assert(io.open("/logs/trace", "a"))
f:write(logs .. "\n")
f:close()
</code></pre>
<p>log_by_lua 可以用在 http 模块,也可以用在server模块,也能直接精确到location模块,即只到某个请求。所以你可以在nginx.conf 里的http里添加:</p>
<pre><code>http{
log_by_lua_file '/code/log_slot.lua';
}
</code></pre>
<p>也可以在server的配置里添加:</p>
<pre><code>server {
log_by_lua_file '/code/log_slot.lua';
}
</code></pre>
<p>更能直接在某个接口里添加:</p>
<pre><code>/v1/test {
content_by_lua_file '/code/v1/test.lua';
log_by_lua_file '/code/log_slot.lua';
}
</code></pre>
<p>http里添加,则对所有的server; server里添加,则只针对此server;location里添加,就只针对这个接口。</p>
<p>但是,比较坑爹的是,log_by_lua 不像 access log,可以多层级使用。log_by_lua 在某层使用了之后,上层的 log_by_lua 就对此一层无效了。比如 /v1/test 接口添加了 log_by_lua, 那么 http 或者 server 里添加的 log_by_lua 在接受/v1/test接口的请求时都不会被用到。</p>
<p>正是因为这个坑,浪费了我不少的时间来解决。我们的系统里,http 模块是配置了 log_by_lua 的,用来做接口监控,监控返回的错误码,处理的时延等。如果我在 /v1/test 里添加了只针对 /v1/test 的追踪日志,那么接口监控就无法正常运行了。</p>
<p>不过天无绝人之路,我想到了一个处理方法如下:</p>
<pre><code>monitor_log.lua
---------------------
local _M = {}
function _M.monitor_log()
local f = _M.api_monitor_log_func
if f == nil then
f, err = loadfile("/code/monitor.lua")
if f == nil then
ngx.log(ngx.ERR, "/code/monitor.lua, ", err)
--- 如果不存在接口监控,直接给一个空函数
f = function() end
end
_M.api_monitor_log_func = f
end
local status, err = pcall(f)
if not status then
ngx.log(ngx.ERR, "run api monitor /code/monitor.lua failed", err)
end
end
return _M
</code></pre>
<p>修改log_slot.lua代码如下:</p>
<pre><code>local logger = require "code.monitor"
local request_time = ngx.var.request_time
logger.monitor_log()
if request_time < 1 then
return --- 小于1秒的请求不记录
end
local slot = ngx.ctx.log_slot
if slot == nil or type(slot) ~= "table" then
return
end
local logs = table.concat(slot, "\n")
local f = assert(io.open("/logs/trace", "a"))
f:write(logs .. "\n")
f:close()
</code></pre>
<p>如上,就可以进行其他层级的 log_by_lua 代码运行了,皆大欢喜,问题解决了。<br>当系统并发请求较低的时候,worker够用,则使用 log_by_lua 可以说是毫无坏处。当然,一旦 log_by_lua 出现了故障,如死循环,则会长时间占用worker,造成整个系统崩溃掉。</p>