-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
1062 lines (1062 loc) · 613 KB
/
search.xml
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[从0开始用SpringCloud搭建微服务系统【五】]]></title>
<url>%2F2020%2F04%2F30%2F%E4%BB%8E0%E5%BC%80%E5%A7%8B%E7%94%A8SpringCloud%E6%90%AD%E5%BB%BA%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%B3%BB%E7%BB%9F%E3%80%90%E4%BA%94%E3%80%91%2F</url>
<content type="text"><![CDATA[调用链监控如果能跟踪每个请求,中间请求经过哪些微服务,请求耗时,网络延迟,业务逻辑耗时等。我们就能更好地分析系统瓶颈、解决系统问题。因此链路跟踪很重要。 链路追踪目的:解决错综复杂的服务调用中链路的查看。排查慢服务。 市面上链路追踪产品,大部分基于google的Dapper论文。 链路追踪要考虑的几个问题 探针的性能消耗。尽量不影响 服务本尊。 易用。开发可以很快接入,别浪费太多精力。 数据分析。要实时分析。维度足够。 SleuthSleuth是Spring cloud的分布式跟踪解决方案。 span(跨度),基本工作单元。一次链路调用,创建一个span,span用一个64位id唯一标识。包括:id,描述,时间戳事件,spanId,span父id。 span被启动和停止时,记录了时间信息,初始化span叫:root span,它的span id和trace id相等。 trace(跟踪),一组共享“root span”的span组成的树状结构 称为 trace,trace也有一个64位ID,trace中所有span共享一个trace id。类似于一颗 span 树。 annotation(标签),annotation用来记录事件的存在,其中,核心annotation用来定义请求的开始和结束。 CS(Client Send客户端发起请求)。客户端发起请求描述了span开始。 SR(Server Received服务端接到请求)。服务端获得请求并准备处理它。SR-CS=网络延迟。 SS(Server Send服务器端处理完成,并将结果发送给客户端)。表示服务器完成请求处理,响应客户端时。SS-SR=服务器处理请求的时间。 CR(Client Received 客户端接受服务端信息)。span结束的标识。客户端接收到服务器的响应。CR-CS=客户端发出请求到服务器响应的总时间。 其实数据结构是一颗树,从root span 开始。 使用在每个需要被监控的系统的pom中引入: 12345<!-- 引入sleuth依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> 启动后,日志中可以看到类似于:[account-service,,,]的数据,说明如下: 1[服务名称,traceId(一条请求调用链中 唯一ID),spanID(基本的工作单元,获取数据等),是否让zipkin收集和展示此信息] zipkinzipkin是twitter开源的分布式跟踪系统。 原理收集系统的时序数据,从而追踪微服务架构中系统延时等问题。还有一个友好的界面。 由4个部分组成:Collector(采集器)、Storage(存储器)、Restful API(接口)、Web UI 原理sleuth收集跟踪信息通过http请求发送给zipkin server,zipkin将跟踪信息存储,以及提供RESTful API接口,zipkin ui通过调用api进行数据展示。 默认内存存储,可以用mysql,ES等存储。 使用在每个需要被监控的系统的pom中引入: 12345<!-- zipkin --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency> 在每个需要监听的服务的配置文件中加入: 12345678spring: #zipkin zipkin: base-url: http://localhost:9411/ #采样比例1 sleuth: sampler: rate: 1 根据zipkin官网中的Quickstart,启动zipkin。 监控告警使用 Spring Boot2.x Actuator 监控应用在项目的 POM 中引入 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency> Endpoints可以通过/actuator查看所有的暴露端点。 暴露配置123456789management: endpoints: web: exposure: include: '*' # 通过HTTP暴露所有的端点 base-path: '/actuator' jmx: exposure: include: '*' # 通过JMX暴露所有的端点 health端点health端点会聚合你程序的健康指标,来检查程序的健康情况。端点公开的应用健康信息取决于:management.endpoint.health.show-details=always show-details的值: never 不展示详细信息,up或者down的状态,默认配置 when-authorized 详细信息将会展示给通过认证的用户。授权的角色可以通过 management.endpoint.health.roles配置 always 对所有用户暴露详细信息 metrics端点metrics端点用来返回当前应用的各类重要度量指标,比如:内存信息、线程信息、垃圾回收信息、tomcat、数据库连接池等。 调用:"http://localhost:8080/actuator/metrics/{requiredMetricName}" 获得各个指标。如:http://localhost:8080/actuator/metrics/jvm.memory.max loggers端点loggers 端点暴露了我们程序内部配置的所有logger的信息。 调用:http://localhost:8080/actuator/loggers获得所有的logger信息。 可以调用对应的POST接口,修改日志级别等。 info端点info端点可以用来展示你程序的信息。我理解过来就是一些程序的基础信息。并且你可以按照自己的需求在配置文件 application.properties中个性化配置。 beans端点beans端点会返回Spring 容器中所有bean的别名、类型、是否单例、依赖等信息。 heapdump端点访问:http://localhost:8080/actuator/heapdump会自动生成一个 Jvm 的堆文件 heapdump。我们可以使用 JDK 自带的 Jvm监控工具,VisualVM 打开此文件查看内存快照。 threaddump 端点主要展示了线程名、线程ID、线程的状态、是否等待锁资源、线程堆栈等信息。就是可能查看起来不太直观。访问:http://localhost:8080/actuator/threaddump shutdown端点这个端点属于操作控制类端点,可以优雅关闭 Spring Boot 应用。要使用这个功能首先需要在配置文件中开启:1234management: endpoint: shutdown: enabled: true 由于 shutdown 接口默认只支持 POST 请求。 env端点获取应用所有可用的环境属性报告 mappings端点获取应用所有Spring Web的控制器映射关系报告 configprops端点获取应用中配置的属性信息报告 SpringCloud Admin健康检查Admin客户端在要被监控的客户端添加pom引用: 123456789<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.2.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> 在配置文件中如下配置: 12345spring: boot: admin: client: url: http://localhost:9030 Admin服务端新建一个项目,引入: 12345<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.2.1</version> </dependency> 然后添加注解@EnableAdminServer 从0开始用SpringCloud搭建微服务系统【一】 从0开始用SpringCloud搭建微服务系统【二】 从0开始用SpringCloud搭建微服务系统【三】 从0开始用SpringCloud搭建微服务系统【四】 从0开始用SpringCloud搭建微服务系统【五】]]></content>
<categories>
<category>Spring</category>
<category>Spring Cloud</category>
</categories>
<tags>
<tag>微服务</tag>
<tag>Spring Cloud</tag>
</tags>
</entry>
<entry>
<title><![CDATA[从0开始用SpringCloud搭建微服务系统【四】]]></title>
<url>%2F2020%2F04%2F27%2F%E4%BB%8E0%E5%BC%80%E5%A7%8B%E7%94%A8SpringCloud%E6%90%AD%E5%BB%BA%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%B3%BB%E7%BB%9F%E3%80%90%E5%9B%9B%E3%80%91%2F</url>
<content type="text"><![CDATA[容错基本的容错模式有: 主动超时: 限流:限制最大并发数 熔断:错误数达到阈值时,类似保险丝熔断 隔离:隔离不同的依赖调用或者隔离不同的线程 降级:服务降低 容错理念: 凡是依赖都可能会失败 凡是资源都有限制 CPU/Memory/Threads/Queue 网络并不可靠 延迟是应用稳定性杀手 Netflix HystrixHystrix实现了 超时机制和断路器模式。 Hystrix是Netflix开源的一个类库,用于隔离远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。主要有以下几点功能: 为系统提供保护机制。在依赖的服务出现高延迟或失败时,为系统提供保护和控制。 防止雪崩。 包裹请求:使用HystrixCommand(或HystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中运行。 跳闸机制:当某服务失败率达到一定的阈值时,Hystrix可以自动跳闸,停止请求该服务一段时间。 资源隔离:Hystrix为每个请求都的依赖都维护了一个小型线程池,如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定。防止级联失败。 快速失败:Fail Fast。同时能快速恢复。侧重点是:(不去真正的请求服务,发生异常再返回),而是直接失败。 监控:Hystrix可以实时监控运行指标和配置的变化,提供近实时的监控、报警、运维控制。 回退机制:fallback,当请求失败、超时、被拒绝,或当断路器被打开时,执行回退逻辑。回退逻辑我们自定义,提供优雅的服务降级。 自我修复:断路器打开一段时间后,会自动进入“半开”状态,可以进行打开,关闭,半开状态的转换。 Hystrix设计原理Hystrix的工作原理可以可以参考官网的How it Works。如果要看中文版,可以参考:Hystrix工作原理。 信号量隔离与线程隔离默认情况下hystrix使用线程池控制请求隔离 线程池隔离技术,是用 Hystrix 自己的线程去执行调用;而信号量隔离技术,是直接让 tomcat 线程去调用依赖服务。信号量隔离,只是一道关卡,信号量有多少,就允许多少个 tomcat 线程通过它,然后去执行。 信号量隔离主要维护的是Tomcat的线程,不需要内部线程池,更加轻量级。 优点 不足 适用 信号量隔离 轻量,无额外开销 不支持任务排队和主动超时不支持异步调用 受信客户高扇出(网关)高频高速调用(cache) 线程池隔离 支持排队和超时支持异步调用 线程调用会产生额外的开销 不受信客户有限扇出 使用Hystrix独立使用独立使用Hystrix可以参考How To Use。 如果想要中文,可以参考:Hystrix都停更了,我为什么还要学? 与Resttemplate整合引入依赖spring-cloud-starter-netflix-hystrix。 在Service中: 12345678910111213@Servicepublic class GreetingService { @HystrixCommand(fallbackMethod = "defaultGreeting") public String getGreeting(String username) { return new RestTemplate() .getForObject("http://localhost:9090/greeting/{username}", String.class, username); } private String defaultGreeting(String username) { return "Hello User!"; }} 在Application中: 1234567@SpringBootApplication@EnableCircuitBreakerpublic class RestConsumerApplication { public static void main(String[] args) { SpringApplication.run(RestConsumerApplication.class, args); }} @EnableCircuitBreaker注释将扫描类路径以查找任何兼容的Circuit Breaker实现。 与Feign整合在集成Feign的基础上修改如下: 1234567891011121314@FeignClient(name = "statistics-service", fallback = StatisticsServiceClientFallback.class)public interface StatisticsServiceClient { @RequestMapping(method = RequestMethod.PUT, value = "/statistics/{accountName}") void updateStatistics(@PathVariable("accountName") String accountName, Account account);}@Component@Slf4jpublic class StatisticsServiceClientFallback implements StatisticsServiceClient { @Override public void updateStatistics(String accountName, Account account) { log.error("Error during update statistics for account: {}", accountName); }} 然后在配置文件中打开: 1feign.hystrix.enabled=true 使用fallbackFactory检查具体错误在一些场景中,简单的触发在 FeignClient 加入 Fallback 属性即可,而另外有一些场景需要访问导致回退触发的原因,那么这个时候可以在 FeignClient 中加入 FallbackFactory 属性即可; 定义一个类,实现FallbackFactory 123456789101112131415161718192021222324@Componentpublic class WebError implements FallbackFactory<ConsumerApi> { @Override public ConsumerApi create(Throwable cause) { return new ConsumerApi() { @Override public String getById(Integer id) { //针对不同异常返回响应 if(cause instanceof InternalServerError) { System.out.println("InternalServerError"); return "远程服务报错"; }else if(cause instanceof RuntimeException) { return "请求时异常:" + cause; }else { return "都算不上"; } return null; } }; }} @Feign中的使用@FeignClient(name = "statistics-service", fallbackFactory = WebError.class) 主要配置项 配置项(前缀hystrix.command.*.) 含义 execution.isolation.strategy 线程“THREAD”或信号量“SEMAPHORE”隔离(Default: THREAD) execution.isolation.thread.timeoutInMilliseconds run()方法执行超时时间(Default: 1000) execution.isolation.semaphore.maxConcurrentRequests 信号量隔离最大并发数(Default:10) circuitBreaker.errorThresholdPercentage 熔断的错误百分比阀值(Default:50) circuitBreaker.requestVolumeThreshold 断路器生效必须满足的流量阀值(Default:20) circuitBreaker.sleepWindowInMilliseconds 熔断后重置断路器的时间间隔(Default:5000) circuitBreaker.forceOpen 设true表示强制熔断器进入打开状态(Default: false) circuitBreaker.forceClosed 设true表示强制熔断器进入关闭状态(Default: false) 配置项(前缀hystrix.threadpool.*.) 含义 coreSize 使用线程池时的最大并发请求(Default: 10) maxQueueSize 最大LinkedBlockingQueue大小,-1表示用SynchronousQueue(Default:-1) default.queueSizeRejectionThreshold 队列大小阀值,超过则拒绝(Default:5) Hystrix DashboardHystrix有一个不错的可选功能是能够在仪表板上监视其状态。 为了启用它,我们将spring-cloud-starter-hystrix-dashboard和spring-boot-starter-actuator放入项目的pom.xml中。 然后添加@EnableHystrixDashboard注解。 启动应用程序后,将浏览器指向http://localhost:8080/hystrix,输入“ hystrix.stream”的指标URL并开始监视。 参考: A Guide to Spring Cloud Netflix – Hystrix 从0开始用SpringCloud搭建微服务系统【一】 从0开始用SpringCloud搭建微服务系统【二】 从0开始用SpringCloud搭建微服务系统【三】 从0开始用SpringCloud搭建微服务系统【四】 从0开始用SpringCloud搭建微服务系统【五】]]></content>
<categories>
<category>Spring</category>
<category>Spring Cloud</category>
</categories>
<tags>
<tag>微服务</tag>
<tag>Spring Cloud</tag>
</tags>
</entry>
<entry>
<title><![CDATA[从0开始用SpringCloud搭建微服务系统【三】]]></title>
<url>%2F2020%2F04%2F25%2F%E4%BB%8E0%E5%BC%80%E5%A7%8B%E7%94%A8SpringCloud%E6%90%AD%E5%BB%BA%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%B3%BB%E7%BB%9F%E3%80%90%E4%B8%89%E3%80%91%2F</url>
<content type="text"><![CDATA[服务间通信微服务间可以使用 HTTP 协议,RESTful 规范进行通信。Spring Cloud 提供了 2 种 RESTful 调用方式:Ribbon 和 Feign 。 Ribbon客户端软负载组件,支持Eureka对接,支持多种可插拔LB策略。依赖 spring-cloud-starter-netflix-eureka-client 中已经默认加载了 Ribbon 的依赖。 Ribbon作为Spring Cloud的负载均衡机制的实现: Ribbon可以单独使用,作为一个独立的负载均衡组件。只是需要我们手动配置 服务地址列表。 Ribbon与Eureka配合使用时,Ribbon可自动从Eureka Server获取服务提供者地址列表(DiscoveryClient),并基于负载均衡算法,请求其中一个服务提供者实例。 Ribbon与OpenFeign和RestTemplate进行无缝对接,让二者具有负载均衡的能力。OpenFeign默认集成了ribbon。 Ribbon 的自定义配置以及一些高级使用可以参考官方文档:Client Side Load Balancer: Ribbon Ribbon组成官网首页:https://github.com/Netflix/ribbon ribbon-core: 核心的通用性代码。api一些配置。 ribbon-eureka:基于eureka封装的模块,能快速集成eureka。 ribbon-examples:学习示例。 ribbon-httpclient:基于apache httpClient封装的rest客户端,集成了负载均衡模块,可以直接在项目中使用。 ribbon-loadbalancer:负载均衡模块。 ribbon-transport:基于netty实现多协议的支持。比如http,tcp,udp等。 调用方式使用RestTemplate123456789101112131415161718192021@Configurationpublic class LoadBalancerClientConfig { /** * 负载均衡的RestTemplate。 */ @LoadBalanced @Bean(name = "loadBalanced") RestTemplate loadBalanced() { return new RestTemplate(); } /** * 常规的RestTemplate。 */ @Primary @Bean(name = "restTemplate") RestTemplate restTemplate() { return new RestTemplate(); }} 以上代码中使用 @LoadBalanced 注解,这样就可以让 RestTemplate 在请求时拥有客户端负载均衡的能力。 12345678@Autowired@Qualifier("loadBalanced")private RestTemplate loadBalanced;public String getTime() { return loadBalanced.getForEntity("http://service-producer/get_time", String.class).getBody();} 使用DiscoveryClient123456789101112@Autowiredprivate DiscoveryClient discoveryClient; public List<String> getAllInstance(@RequestParam("service_id") String serviceId) { List<ServiceInstance> instances = discoveryClient.getInstances(serviceId); return instances.stream() .map(instance -> String.format("http://%s:%s", instance.getHost(), instance.getPort())) .collect(Collectors.toList());} 负载均衡算法Ribbon 提供了很多负载均衡的策略。详情可见: 策略名称 策略描述 BestAvailableRule(最低并发策略) 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。逐个找服务,如果断路器打开,则忽略。 AvailabilityFilteringRule(可用过滤策略) 会先过滤掉多次访问故障而处于断路器跳闸状态的服务和过滤并发的连接数量超过阀值得服务,然后对剩余的服务列表安装轮询策略进行访问。 WeightedResponseTimeRule(响应时间加权策略) 据平均响应时间计算所有的服务的权重,响应时间越快服务权重越大,容易被选中的概率就越高。刚启动时,如果统计信息不中,则使用RoundRobinRule(轮询)策略,等统计的信息足够了会自动的切换到WeightedResponseTimeRule。响应时间长,权重低,被选择的概率低。反之,同样道理。此策略综合了各种因素(网络,磁盘,IO等),这些因素直接影响响应时间。 RetryRule(重试策略) 先按照RoundRobinRule(轮询)的策略获取服务,如果获取的服务失败则在指定的时间会进行重试,进行获取可用的服务。如多次获取某个服务失败,就不会再次获取该服务。主要是在一个时间段内,如果选择一个服务不成功,就继续找可用的服务,直到超时。 RoundRobinRule(轮询策略) 以简单轮询选择一个服务器。按顺序循环选择一个server。 RandomRule(随机策略) 随机选择一个服务器。 ZoneAvoidanceRule(区域权衡策略)【默认实现】 复合判断Server所在区域的性能和Server的可用性,轮询选择服务器。 切换负载均衡策略注解方式1234567@Configurationpublic class DefaultRibbonConfig { @Bean public IRule ribbonRule() { return new BestAvailableRule(); }} 如上,配置IRule的新值,直接可以切换负载均衡策略 配置文件方式给所有的服务指定负载均衡策略: 123ribbon: NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule 给特定的服务指定负载均衡策略: 1<服务名>.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule Ribbon拦截在服务调用的时候,我们可能不仅仅是简单地进行调用,会涉及到一些接口的校验、权限的校验等。要实现这些,可以实现ClientHttpRequestInterceptor接口。 123456789101112public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { System.out.println("拦截啦!!!"); System.out.println(httpRequest.getURI()); ClientHttpResponse response = clientHttpRequestExecution.execute(httpRequest, bytes); System.out.println(response.getHeaders()); return response; }} 添加到resttemplate中 12345678@LoadBalanced@Bean(name = "loadBalanced")@PrimaryRestTemplate loadBalanced() { RestTemplate restTemplate = new RestTemplate(); restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor()); return restTemplate;} 超时Ribbon的配置如下: 1234#连接超时时间(ms)ribbon.ConnectTimeout=1000#业务逻辑超时时间(ms)ribbon.ReadTimeout=6000 重试123456#同一台实例最大重试次数,不包括首次调用ribbon.MaxAutoRetries=1#重试负载均衡其他的实例最大重试次数,不包括首次调用ribbon.MaxAutoRetriesNextServer=1#是否所有操作都重试ribbon.OkToRetryOnAllOperations=false 使用ribbon重试机制,请求失败后,每个6秒会重新尝试 FeignFeign 是 Netflix 开发的声明式、模板化的 HTTP 客户端,Feign 的使用非常简单,创建一个接口,在接口上加入一些注解,这样就完成了代码开发。 Feign 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign 封装 了HTTP 调用流程,面向接口编程 Spring Cloud OpenFeignSpring Cloud OpenFeign 通过自动配置并绑定到Spring Environment和其他Spring编程模型习惯用法,为Spring Boot应用程序提供OpenFeign集成。 官方文档:Spring Cloud OpenFeign 服务调用使用 Feign 必须引入 spring-cloud-starter-openfeign。在启动类 Application 上 @EnableFeignClients 注解。 服务提供者12345678910111213@RequestMappingpublic interface HelloApi { @GetMapping(value = "/v0.1/greeting") String greeting();}@RestControllerpublic class HelloController implements HelloApi { @Override public String greeting(){ return "hello world"; }} 服务消费者:简单使用简单使用Feign不需要代码耦合,但是需要硬编码接口的信息。如下: 12345678910111213141516@FeignClient(value = "producer-service")public interface HelloClient{ @GetMapping(value = "/v0.1/greeting") String greeting();}@Servicepublic class TestService { @Autowired HelloClient helloClient; public String test(){ helloClient.greeting(); }} @FeignClient也可以脱离Eureka使用,如:@FeignClient(name = "xxx",url="") 这个url就是接口的地址。 服务消费者:接口继承方式此方法需要引入服务提供者提供的接口jar包。 123456789101112131415@FeignClient(value = "producer-service")public interface HelloService extends HelloApi {}@Servicepublic class TestService { @Autowired HelloService helloService; public String test(){ helloService.greeting(); }} 以上代码中,@FeignClient(value = "producer-service") 指定了使用哪一个服务。 高级用法自定义配置feign的默认配置类是:org.springframework.cloud.openfeign.FeignClientsConfiguration。默认定义了feign使用的编码器,解码器等。 允许使用@FeignClient的configuration的属性自定义Feign配置。自定义的配置优先级高于上面的FeignClientsConfiguration。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354@Configuration@Slf4j@EnableConfigurationProperties(FeignSecurityProperties.class)@ConditionalOnClass(value = {RequestInterceptor.class, Decoder.class, Encoder.class})public class FeignAutoConfig { @Autowired private FeignSecurityProperties feignSecurityProperties; /** * 内部微服务请求,加上timestamp,并且按字典序进行签名,附上sig * * @return RequestInterceptor 请求拦截器 */ @Bean @ConditionalOnMissingBean(RequestInterceptor.class) public RequestInterceptor requestTokenBearerInterceptor() { String apiSecretKey = feignSecurityProperties.getApiSignature().getInternalApiKey(); return requestTemplate -> ApiSigUtil.sig(apiSecretKey, requestTemplate); } @Bean public Decoder feignDecoder() { HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(customObjectMapper()); ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(jacksonConverter); return new ResponseEntityDecoder(new SpringDecoder(objectFactory)); } @Bean public Encoder feignEncoder() { HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(customObjectMapper()); ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(jacksonConverter); return new SpringEncoder(objectFactory); } public ObjectMapper customObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true); return objectMapper; } @Bean @ConditionalOnMissingBean(Retryer.class) public Retryer feignRetryer() { return Retryer.NEVER_RETRY; } @Bean @ConditionalOnMissingBean(Request.Options.class) Request.Options feignOptions() { return new Request.Options(30 * 1000, 30 * 1000); }} 如上面所示,在配置类上加上@Configuration注解,且该类在@ComponentScan所扫描的包中,那么该类中的配置信息就会被所有的@FeignClient共享。如果想要对某些的@FeignClient添加指定的配置,则:不指定@Configuration注解(或者指定configuration,用注解忽略),而是手动使用:@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class) 拦截器上面代码中: 123456@Bean @ConditionalOnMissingBean(RequestInterceptor.class) public RequestInterceptor requestTokenBearerInterceptor() { String apiSecretKey = feignSecurityProperties.getApiSignature().getInternalApiKey(); return requestTemplate -> ApiSigUtil.sig(apiSecretKey, requestTemplate); } RequestInterceptor是一个请求拦截器,我们可以继承它做很多事情,如: 12345678910import feign.RequestInterceptor;import feign.RequestTemplate;public class MyBasicAuthRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { template.header("Authorization", "Basic cm9vdDpyb290"); }} 如果是BasicAuth认证,可以重写BasicAuthRequestInterceptor。 然后,在配置文件中添加: 123456feign: client: config: service-valuation: request-interceptors: - cn.webfuse.passenger.feign.interceptor.MyBasicAuthRequestInterceptor 配置文件扩展指定服务名配置: 1234567feign: client: config: service-valuation: connect-timeout: 5000 read-timeout: 5000 logger-level: full 通用配置: 1234567feign: client: config: default: connect-timeout: 5000 read-timeout: 5000 logger-level: full 属性配置比Java代码优先级高。也可通过配置设置java代码优先级高: 123feign: client: default-to-properties: false 压缩1234567feign: compression: # 开启请求与响应的GZIP压缩 request: enabled: true min-request-size: 10000 # 单位是B response: enabled: true 超时Feign默认支持Ribbon;Ribbon的重试机制和Feign的重试机制有冲突,所以源码中默认关闭Feign的重试机制,使用Ribbon的重试机制。 保留原始异常信息当调用服务时,如果服务返回的状态码不是200,就会进入到Feign的ErrorDecoder中,因此如果我们要解析异常信息,就要重写ErrorDecoder 原理 主程序入口添加@EnableFeignClients注解开启对Feign Client扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。 当程序启动时,会进行包扫描,扫描所有@FeignClient注解的类,并将这些信息注入Spring IoC容器中。当定义的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名、请求方法等信息都在这个过程中确定。 然后由RequestTemplate生成Request,然后把这个Request交给client处理,这里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp。最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。 参考资源 Service Discovery: Eureka Clients Service Discovery: Eureka Server 深度剖析服务发现组件Netflix Eureka 深入理解Eureka之源码解析 深入理解Ribbon之源码解析 深入理解Feign之源码解析 从0开始用SpringCloud搭建微服务系统【一】 从0开始用SpringCloud搭建微服务系统【二】 从0开始用SpringCloud搭建微服务系统【三】 从0开始用SpringCloud搭建微服务系统【四】 从0开始用SpringCloud搭建微服务系统【五】]]></content>
<categories>
<category>Spring</category>
<category>Spring Cloud</category>
</categories>
<tags>
<tag>微服务</tag>
<tag>Spring Cloud</tag>
</tags>
</entry>
<entry>
<title><![CDATA[从0开始用SpringCloud搭建微服务系统【二】]]></title>
<url>%2F2020%2F04%2F22%2F%E4%BB%8E0%E5%BC%80%E5%A7%8B%E7%94%A8SpringCloud%E6%90%AD%E5%BB%BA%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%B3%BB%E7%BB%9F%E3%80%90%E4%BA%8C%E3%80%91%2F</url>
<content type="text"><![CDATA[服务注册与服务发现 背景:在传统应用中,组件之间的调用,通过有规范的约束的接口来实现,从而实现不同模块间良好的协作。但是被拆分成微服务后,每个微服务实例的网络地址都可能动态变化,数量也会变化,使得原来硬编码的地址失去了作用。需要一个中心化的组件来进行服务的登记和管理。 概念:实现服务治理,即管理所有的服务信息和状态。 注册中心好处:不用关心有多少提供方。 注册中心有哪些: Eureka,Nacos,Consul,Zookeeper等。 服务注册与发现包括两部分,一个是服务器端,另一个是客户端。 服务端(Server)是一个公共服务,为Client提供服务注册和发现的功能,维护注册到自身的Client的相关信息,同时提供接口给Client获取注册表中其他服务的信息,使得动态变化的Client能够进行服务间的相互调用。 客户端(Client) 将自己的服务信息通过一定的方式登记到 Server上,并在正常范围内维护自己信息一致性,方便其他服务发现自己,同时可以通过Server获取到自己依赖的其他服务信息,完成服务调用,还内置了负载均衡器,用来进行基本的负载均衡。 服务端和客户端功能server注册中心功能 服务注册表:记录各个微服务信息,例如服务名称,ip,端口等。注册表提供查询 API(查询可用的微服务实例)和管理 API(用于服务的注册和注销)。 服务注册与发现: 注册:将微服务信息注册到注册中心。 发现:查询可用微服务列表及其网络地址。 服务检查:定时检测已注册的服务,如发现某实例长时间无法访问,就从注册表中移除。 client功能 注册:每个微服务启动时,将自己的网络地址等信息注册到注册中心,注册中心会存储(内存中)这些信息。 获取服务注册表:服务消费者从注册中心,查询服务提供者的网络地址,并使用该地址调用服务提供者,为了避免每次都查注册表信息,所以 client 会定时去 serve r拉取注册表信息到缓存到 client 本地。 心跳:各个微服务与注册中心通过某种机制(心跳)通信,若注册中心长时间和服务间没有通信,就会注销该实例。 调用:实际的服务调用,通过注册表,解析服务名和具体地址的对应关系,找到具体服务的地址,进行实际调用。 Eureka Eureka 是 Netflix 开源的一个 Restful 服务,主要用于服务的注册发现。它由两个组件组成:Eureka 服务器 和Eureka 客户端。 Eureka 服务器用作服务注册服务器。各个节点启动后,会在 Eureka Server 中进行注册,这样 Eureka Server 中的服务注册表中将会存储所有可用服务节点的信息。 Eureka 客户端是一个 Java 客户端,用来简化与服务器的交互,作为轮询负载均衡器,发现相关的服务,并提供服务的故障切换。 Eureka 在设计时就优先保证可用性。即在 CAP 理论中,Eureka 满足 AP。Eureka 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。 Eureka 的几个时间点: 在应用启动后,将会向 Eureka Server 发送心跳,默认周期为 30 秒。 如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,Eureka Server 将会从服务注册表中把这个服务节点移除(默认90秒)。 Eureka Client 对已经获取到的注册信息做了 30s 缓存。即服务通过 Eureka 客户端第一次查询到可用服务地址后会将结果缓存,下次再调用时就不会真正向 Eureka 发起 HTTP 请求了。 负载均衡组件 Ribbon 也有 30s 缓存。Ribbon 会从上面提到的 Eureka Client 获取服务列表,然后将结果缓存 30s。 最大可能出现2分钟的延迟:注册延迟30s + Eureka服务器响应延迟30s + Eureka客户端更新延迟30s + Ribbon服务列表更新延迟30s。 Eureka 启动保护机制: 如果在 15 分钟内超过 85% 的节点都没有正常的心跳,那 么Eureka 就认为客户端与注册中心出现了网络故障。 自我保护机制的触发条件: 条件:当每分钟心跳次数( renewsLastMin ) 小于 numberOfRenewsPerMinThreshold 时,并且开启自动保护模式开关( eureka.server.enable-self-preservation = true ) 时,触发自我保护机制,不再自动过期续约。 其中: numberOfRenewsPerMinThreshold = expectedNumberOfRenewsPerMin * 续租百分比( eureka.server.renewalPercentThreshold, 默认0.85 ) expectedNumberOfRenewsPerMin = 当前注册的应用实例数 x 2 。为什么乘以 2: 默认情况下,注册的应用实例每半分钟续租一次,那么一分钟心跳两次,因此 x 2 。 解释:服务实例数10个,期望每分钟续约数10 2=20,期望阈值200.85=17,自我保护少于17时 触发。】 Eureka 启动保护机制会出现以下情况: Eureka 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务。 Eureka 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)。 当网络稳定时,当前实例新的注册信息会被同步到其它节点中。 架构原理图 Eureka Server引入依赖 spring-cloud-starter-netflix-eureka-server, 在启动类 Application 上,添加 @EnableEurekaServer 注解。 如果是单机服务,可以在 application.yml 中使用以下配置: 123456789101112131415161718192021eureka: environment: dev # 设置环境,可选 server: enable-self-preservation: false # 中小规模下,自我保护模式坑比好处多,所以关闭它 renewal-threshold-update-interval-ms: 120000 # 心跳阈值计算周期,如果开启自我保护模式,可以改一下这个配置 eviction-interval-timer-in-ms: 5000 # 主动失效检测间隔,配置成5秒 use-read-only-response-cache: false # 禁用readOnlyCacheMap wait-time-in-ms-when-sync-empty: 0 #在Eureka服务器获取不到集群里对等服务器上的实例时,需要等待的时间,单机模式设置为0 client: healthcheck: true service-url: defaultZone: http://${webfuse-security.user.name}:${webfuse-security.user.password}@localhost:2000/eureka/ registry-fetch-interval-seconds: 5 # 定时刷新本地缓存时间 register-with-eureka: false #表示是否将自己注册到Eureka Server,默认为true。由于当前这个应用就是Eureka Server,故而设为false。 fetch-registry: false #表示是否从Eureka Server获取注册信息,默认为true。因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,故而设为false。 instance: hostname: ${hostname:localhost} instance-id: ${spring.application.name}@${spring.cloud.client.ip-address}:${server.port} # 自定义实例ID prefer-ip-address: true lease-expiration-duration-in-seconds: 10 # 没有心跳的淘汰时间,10秒 lease-renewal-interval-in-seconds: 5 # 心跳间隔,5秒 启动服务,可在 http://localhost:2000 查看项目页面。 Eureka ClientEurekaClient 可以在客户端获取eureka服务器上的注册者信息。 引入依赖 spring-cloud-starter-netflix-eureka-client, 在启动类 Application 上,添加 @EnableDiscoveryClient 注解。 在 application.yml 中使用以下配置: 1234567891011121314eureka: environment: dev client: healthcheck: enabled: true service-url: defaultZone: http://user:[email protected]:8010/eureka/ registry-fetch-interval-seconds: 5 # 定时刷新本地缓存时间 instance: hostname: ${hostname:localhost} instance-id: ${spring.application.name}@${spring.cloud.client.ip-address}:${server.port} # 自定义实例ID prefer-ip-address: true lease-expiration-duration-in-seconds: 10 # 没有心跳的淘汰时间,10秒 lease-renewal-interval-in-seconds: 5 # 心跳间隔,5秒 启动项目即可。可在 http://localhost:2000 中看到注册的状态。 Eureka 高可用Eureka Server 之间是可以互相注册的。 举个例子,我们有 3 个 Eureka 注册中心,端口分别为 2001 、 2002 和 2003 。那么端口为 2001 的最基本的配置如下: 1234eureka: client: service-url: defaultZone: http://localhost:2002/eureka/,http://localhost:2003/eureka/ 端口 2002 和 2003 服务可以根据以上规则配置。 多网卡选择服务器有多个网卡,eh0,eh1,eh2,只有eh0可以让外部其他服务访问进来,而Eureka client将eh1和eh2注册到Eureka server上,这样其他服务就无法访问该微服务了。有两种方式: 指定IP注册 1234eureka: instance: prefer-ip-address: true ip-address: 实际能访问到的IP 使用spring.cloud.inetutils配置网卡选择 Eureka健康检查由于server和client通过心跳保持 服务状态,而只有状态为UP的服务才能被访问。看eureka界面中的status。 比如心跳一直正常,服务一直UP,但是此服务DB连不上了,无法正常提供服务。 此时,我们需要将 微服务的健康状态也同步到server。只需要启动eureka的健康检查就行。这样微服务就会将自己的健康状态同步到eureka。配置如下即可。 在client端加入Actuator,并配置eureka.client.healthcheck.enabled=true,将自己真正的健康状态传播到server。 通过代码来改动服务的状态: 123456789101112@Component@Datapublic class HealthStatusHandler implements HealthIndicator { private Boolean status = true; @Override public Health health() { if (status) { return new Health.Builder().up().build(); } return new Health.Builder().down().build(); }} 应用场景:比如说短信业务,欠费了等情况,可以暂时下线服务。 Eureka遇到的坑available-replicas为空的问题如果使用了eureka.instance.prefer-ip-address: true,然后eureka.client.service-url.defaultZone配置的IP与实例IP不一致,会出现available-replicas为空的问题。 解决方法:在eureka.instance.ip-address中强制设置IP,然后在eureka.client.service-url.defaultZone配置对应的IP。 Eureka 最佳实践本人所在的公司由于体量小,生产环境直接使用 Eureka 的默认配置进行高可用性运行,目前也没有出现太大的问题。 以下是一些实践参考文章(注意:文章中的版本号不是最新的,可能在配置上会有调整): Spring Cloud中,Eureka常见问题总结 Eureka Clustering documentation and best practices Documentation: changing Eureka renewal frequency WILL break the self-preservation feature of the server 从0开始用SpringCloud搭建微服务系统【一】 从0开始用SpringCloud搭建微服务系统【二】 从0开始用SpringCloud搭建微服务系统【三】 从0开始用SpringCloud搭建微服务系统【四】 从0开始用SpringCloud搭建微服务系统【五】]]></content>
<categories>
<category>Spring</category>
<category>Spring Cloud</category>
</categories>
<tags>
<tag>微服务</tag>
<tag>Spring Cloud</tag>
</tags>
</entry>
<entry>
<title><![CDATA[从0开始用SpringCloud搭建微服务系统【一】]]></title>
<url>%2F2020%2F04%2F22%2F%E4%BB%8E0%E5%BC%80%E5%A7%8B%E7%94%A8SpringCloud%E6%90%AD%E5%BB%BA%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%B3%BB%E7%BB%9F%E3%80%90%E4%B8%80%E3%80%91%2F</url>
<content type="text"><![CDATA[微服务与 Spring Cloud微服务从单体到微服务单体服务 概念:所有功能全部打包在一起,也就是全部的功能都在一个应用包中。应用大部分是一个 war 包或 jar 包。 优点:容易开发、测试、部署,适合项目初期试错。 缺点: 随着项目越来越复杂,团队不断扩大。坏处就显现出来了。 复杂性高:代码多,十万行,百万行级别。加一个小功能,会带来其他功能的隐患,因为它们在一起。 技术债务:人员流动,不坏不修,因为不敢修。 持续部署困难:由于是全量应用,改一个小功能,全部部署,会导致无关的功能暂停使用。编译部署上线耗时长,不敢随便部署,导致部署频率低,进而又导致两次部署之间 功能修改多,越不敢部署,恶性循环。 可靠性差:某个小问题,比如小功能出现 OOM,会导致整个应用崩溃。 扩展受限:只能整体扩展,无法按照需要进行扩展, 不能根据计算密集型(派单系统)和IO密集型(文件服务) 进行合适的区分。 阻碍创新:单体应用是以一种技术解决所有问题,不容易引入新技术。但在高速的互联网发展过程中,适应的潮流是:用合适的语言做合适的事情。比如在单体应用中,一个项目用 spring MVC,想换成s pring boot,切换成本很高,因为有可能10万,百万行代码都要改。 微服务 Martin Fowler 在 2014年提出 Microservices架构 微服务是一种架构风格,将单体应用划分为小型的服务单元。 微服务架构是一种使用一系列粒度较小的服务来开发单个应用的方式。 每个服务运行在自己的进程中 服务间采用轻量级的方式进行通信(通常是HTTP API) 这些服务是基于业务逻辑和范围,通过自动化部署的机制来独立部署的,并且服务的集中管理应该是最低限度的,即每个服务可以采用不同的编程语言编写,使用不同的数据存储技术。 优点: 独立部署。不依赖其他服务,耦合性低,不用管其他服务的部署对自己的影响。 易于开发和维护。关注特定业务,所以业务清晰,代码量少,模块变的易开发、易理解、易维护。 启动快。功能少,代码少,所以启动快,有需要停机维护的服务,不会长时间暂停服务。 局部修改容易。只需要部署 相应的服务即可,适合敏捷开发。 技术栈不受限。java,node.js,go 等 按需伸缩.某个服务受限,可以按需增加内存,cpu 等。 职责专一。专门团队负责专门业务,有利于团队分工。 代码复用。不需要重复写。底层实现通过接口方式提供。 便于团队协作:每个团队只需要提供 API 就行,定义好 API 后,可以并行开发。 缺点: 分布式固有的复杂性。容错,网络延时,调用关系、分布式事务等,都会带来复杂。 分布式事务的挑战。每个服务有自己的数据库,有点在于不同服务可以选择适合自身业务的数据库。订单用MySQL,评论用Mongodb等。目前最理想解决方案是:柔性事务的最终一致性。 接口调整成本高。改一个接口,调用方都要改。 测试难度提升。一个接口改变,所有调用方都得测。自动化测试就变得重要了,API文档的管理也尤为重要。 运维要求高。需要维护几十上百个服务,监控变的复杂,并且还要关注多个集群。 重复工作。比如 java 的工具类可以在共享 common.ja r中,但在多语言下行不通,C++ 无法直接用 java的 jar 包。 微服务组件基于微服务的特性,微服务的组件不局限于技术的实现。主要的组件有: 服务注册与发现:服务提供方将己方调用地址注册到服务注册中心,让服务调用方能够方便地找到自己;服务调用方从服务注册中心找到自己需要调用的服务的地址。 负载均衡:服务提供方一般以多实例的形式提供服务,负载均衡功能能够让服务调用方连接到合适的服务节点。并且,服务节点选择的过程对服务调用方来说是透明的。 服务网关:服务网关是服务调用的唯一入口,可以在这个组件中实现用户鉴权、动态路由、灰度发布、A/B测试、负载限流等功能。 配置中心:将本地化的配置信息(Properties、XML、YAML等形式)注册到配置中心,实现程序包在开发、测试、生产环境中的无差别性,方便程序包的迁移,也是无状态特性。 集成框架:微服务组件都以职责单一的程序包对外提供服务,集成框架以配置的形式将所有微服务组件(特别是管理端组件)集成到统一的界面框架下,让用户能够在统一的界面中使用系统。Spring Cloud 就是一个集成框架。 调用链监控:记录完成一次请求的先后衔接和调用关系,并将这种串行或并行的调用关系展示出来。在系统出错时,可以方便地找到出错点。 支撑平台:系统微服务化后,各个业务模块经过拆分变得更加细化,系统的部署、运维、监控等都比单体应用架构更加复杂,这就需要将大部分的工作自动化。现在,Docker 等工具可以给微服务架构的部署带来较多的便利,例如持续集成、蓝绿发布、健康检查、性能监控等等。如果没有合适的支撑平台或工具,微服务架构就无法发挥它最大的功效。 常见的架构图 Spring CloudSpring Cloud 是实现微服务架构的一系列框架的有机集合。是在 Spring Boot 基础上构建的,用于简化分布式系统构建的工具集。是拥有众多子项目的项目集合。利用 Spring Boot 的开发便利性,巧妙地简化了分布式系统基础设施(服务注册与发现、熔断机制、网关路由、配置中心、消息总线、负载均衡、链路追踪等)的开发。 Spring Cloud的基础组件 Eureka:服务注册与发现,用于服务管理。 Feign: web调用客户端,能够简化HTTP接口的调用。 Ribbon:基于客户端的负载均衡。 Hystrix:熔断降级,防止服务雪崩。 Zuul:网关路由,提供路由转发、请求过滤、限流降级等功能。 Config:配置中心,分布式配置管理。 Sleuth:服务链路追踪 Admin:健康管理 从0开始用SpringCloud搭建微服务系统【一】 从0开始用SpringCloud搭建微服务系统【二】 从0开始用SpringCloud搭建微服务系统【三】 从0开始用SpringCloud搭建微服务系统【四】 从0开始用SpringCloud搭建微服务系统【五】]]></content>
<categories>
<category>Spring</category>
<category>Spring Cloud</category>
</categories>
<tags>
<tag>微服务</tag>
<tag>Spring Cloud</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Golang简单入门笔记]]></title>
<url>%2F2020%2F04%2F06%2FGolang%E7%AE%80%E5%8D%95%E5%85%A5%E9%97%A8%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[基础关键字关键字有25个。关键字不能用于自定义名字,只能在特定语法结构中使用。 12345break default func interface selectcase defer go map structchan else goto package switchconst fallthrough if range typecontinue for import return var 预定义名字大约有30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。 12345678910内建常量: true false iota nil内建类型: int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex128 complex64 bool byte rune string error内建函数: make len cap new append copy close delete complex real imag panic recover 其中: byte 是 uint8 类型的别名,存储 raw data rune 是 int32 类型的别名,存储一个 Unicode code point 字符 这些内部预先定义的名字并不是关键字,你可以在定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。 命名规则Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。 如果一个名字是在函数内部定义,那么它的就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问。 命名的建议: 推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔。 包本身的名字一般总是用小写字母。 名字的长度没有逻辑限制,但是Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样。 如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。 声明和变量 var 用于声明变量 const 用于声明常量 type 用于声明一个新的类型 func 用于声明一个函数 1234567891011121314var foo int // 无初值的声明var foo int = 42 // 带初值的声明var foo, bar int = 42, 1302 // 一次性声明并初始化多个变量var foo = 42 // 类型推断,由使用的上下文决定foo := 42 // 简短模式,自动推断类型,并且必须在函数体内部const constant = "This is a constant" //声明一个常量type MyInt struct{ //声明一个类型 } func fooFuc(){ //声明一个函数 } var 形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。 简短模式有以下限制: 定义变量,同时显式初始化。 不能提供数据类型。 只能用在函数内部。 由于使用了:=,而不是赋值的=,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误。 在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错。 当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil 等。所有的内存在 Go 中都是经过初始化的。 匿名变量匿名变量的特点是一个下画线“”,“”本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。 12345678func GetData() (int, int) { return 100, 200}func main(){ a, _ := GetData() _, b := GetData() fmt.Println(a, b)} 变量的生命周期 对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。 局部变量的声明周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。 函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。 常量常量表达式的值在编译期计算,而不是在运行期。 iota 常量生成器常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。 1234567891011121314151617181920212223type Weekday intconst ( Sunday Weekday = iota Monday Tuesday Wednesday Thursday Friday Saturday)const ( _ = 1 << (10 * iota) KiB // 1024 MiB // 1048576 GiB // 1073741824 TiB // 1099511627776 (exceeds 1 << 32) PiB // 1125899906842624 EiB // 1152921504606846976 ZiB // 1180591620717411303424 (exceeds 1 << 64) YiB // 1208925819614629174706176) new 和 makenew 和 make 是两个内置函数,主要用来创建并分配类型的内存。new 只分配内存,而 make 只能用于 slice、map 和 channel 的初始化。 new 函数只接受一个参数,这个参数是一个类型,并且返回一个指向该类型内存地址的指针。同时 new 函数会把分配的内存置为零,也就是类型的零值。 12345678910111213var sum *intsum = new(int) //分配空间*sum = 98fmt.Println(*sum)type Student struct { name string age int}var s *Students = new(Student) //分配空间。如果我们不使用 new 函数为自定义类型分配空间,就会报错s.name ="dequan"fmt.Println(s) make 也是用于内存分配的,但是和 new 不同,它只用于 chan、map 以及 slice 的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。 运算符 运算符 描述 + 加 - 减 * 乘 / 除 % 取余 & 按位与 ` ` 按位或 ^ 按位异或 &^ 按位清除(AND NOT)&^ 即 AND NOT(x, y) = AND(x, NOT(Y)) << 左移 >> 右移 == 相等 != 不等 < 小于 <= 小于等于 > 大于 >= 大于等于 && 逻辑与 ` ` 逻辑或 ! 取反 & 寻址(生成指针) * 获取指针指向的数据 <- 向 channel 中发送 / 接收数据 包( package ) main package 才是可执行文件 package 名字与 import 路径的最后一个单词一致(如导入 math/rand 则 package 叫 rand) 写开头的标识符(变量名、函数名…),对其他 package 是可访问的 小写开头的标识符,对其他 package 是不可见的 包的初始化 包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化。 如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。 每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。 初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。 init初始化函数 对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数。 12345678910111213141516171819202122package popcount// pc[i] is the population count of i.var pc [256]bytefunc init() { for i := range pc { pc[i] = pc[i/2] + byte(i&1) }}// PopCount returns the population count (number of set bits) of x.func PopCount(x uint64) int { return int(pc[byte(x>>(0*8))] + pc[byte(x>>(1*8))] + pc[byte(x>>(2*8))] + pc[byte(x>>(3*8))] + pc[byte(x>>(4*8))] + pc[byte(x>>(5*8))] + pc[byte(x>>(6*8))] + pc[byte(x>>(7*8))])} 数据类型数据类型分为:基础类型、复合类型、引用类型和接口类型。 基础类型,包括:数字、字符串和布尔型等 复合数据类型:数组和结构体等 引用类型包括:指针、切片、字典、函数、通道等 基础类型 uint 32 位或 64 位 uint8 无符号 8 位整型 (0 到 255) uint16 无符号 16 位整型 (0 到 65535) uint32 无符号 32 位整型 (0 到 4294967295) uint64 无符号 64 位整型 (0 到 18446744073709551615) int 32 位或 64 位 int8 有符号 8 位整型 (-128 到 127) int16 有符号 16 位整型 (-32768 到 32767) int32 有符号 32 位整型 (-2147483648 到 2147483647) int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) byte uint8 的别名(type byte = uint8) rune int32 的别名(type rune = int32),表示一个 unicode 码 uintptr “无符号整型,用于存放一个指针是一种无符号的整数类型,没有指定具体的 bit 大小但是足以容纳指针。uintptr 类型只有在底层编程是才需要,特别是Go 语言和 C 语言函数库或操作系统接口相交互的地方。” float32 IEEE-754 32 位浮点型数 float64 IEEE-754 64 位浮点型数 complex64 32 位实数和虚数 complex128 64 位实数和虚数 string 字符串,默认值是空字符串,而非 NULL 复合数据类型复合数据类型类型包括数组、结构体、切片、字典等。 structGo 语言中没有 class 类的概念,取而代之的是 struct,struct 的方法对应到类的成员函数。 结构类型可以用来描述一组数据值,这组值的本质既可以是原始的,也可以是非原始的。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172// struct 是一种类型,也是字段成员的集合体// 声明 structtype Vertex struct { X, Y int}// 结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段。// 初始化 structvar v = Vertex{1, 2} // 字段名有序对应值var v = Vertex{X: 1, Y: 2} // 字段名对应值var v = []Vertex{{1,2},{5,2},{5,5}} // 初始化多个 struct 组成的 slice//用键值对填充结构体的例子type People struct { name string child *People}relation := &People{ name: "爷爷", child: &People{ name: "爸爸", child: &People{ name: "我", }, },}// 创建指针类型的结构体。实例化以后,实例的类型为 *T,属于指针。vertex := new(Vertex)// 取结构体的地址实例化。对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作。// 实例化以后,实例的类型为 *T,属于指针。v := &Vertex{}// 访问成员v.X = 4// 在 func 关键字和函数名之间,声明接收者是 struct// 在方法内部,struct 实例被复制,传值引用func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y)}// 调用方法(有接收者的函数)v.Abs()// 有的方法接收者是指向 struct 的指针// 此时在方法内调用实例,将是传址引用func (v *Vertex) add(n float64) { v.X += n v.Y += n}//取地址实例化是最广泛的一种结构体实例化方式,可以使用函数封装上面的初始化过程type Command struct { Name string // 指令名称 Var *int // 指令绑定的变量 Comment string // 指令的注释}func newCommand(name string, varref *int, comment string) *Command { return &Command{ Name: name, Var: varref, Comment: comment, }}cmd = newCommand( "version", &version, "show version",) 匿名结构体 使用 map[string]interface{} 开销更小且更为安全。 123point := struct { X, Y int}{1, 2} array12345678910111213var a [10]int // 声明长度为 10 的 int 型数组,注意数组类型 = (元素类型 int,元素个数 10)a[3] = 42 // 设置元素值i := a[3] // 读取元素值// 声明并初始化数组var a = [2]int{1, 2}a := [2]int{1, 2} // 简短声明a := [...]int{1, 2} // 数组长度使用 ... 代替,编译器会自动计算元素个数//遍历数组for k, v := range a { fmt.Println(k, v)} list在Go语言中,列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作。 初始化: 变量名 := list.New() var 变量名 list.List 1234567891011l := list.New()l.PushBack("fist") //在尾部插入l.PushFront(67) //在头部插入 l.Remove(element) // 移除 element 变量对应的元素。//遍历for i := l.Front(); i != nil; i = i.Next() { fmt.Println(i.Value)} slice切片是有三个字段的数据结构,指向底层数组的指针,切片访问元素的个数(长度),切片允许增长到的元素的个数(容量),这种数据结构便于使用和管理数据集。切片围绕动态数组的概念构建的,可以按需自动增长和缩小。切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法,切片的底层内存是在连续块中分配的。 语法: slice [开始位置 : 结束位置] slice:表示目标切片对象; 开始位置:对应目标切片对象的索引; 结束位置:对应目标切片的结束索引。 从数组或切片生成新的切片拥有如下特性: 取出的元素数量为:结束位置 - 开始位置; 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取; 当缺省开始位置时,表示从连续区域开头到结束位置; 当缺省结束位置时,表示从开始位置到整个连续区域末尾; 两者同时缺省时,与切片本身等效; 两者同时为 0 时,等效于空切片,一般用于切片复位。 根据索引位置取切片 slice 元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误,生成切片时,结束位置可以填写 len(slice) 但不会报错。 12345678910111213141516171819202122232425var a []int // 声明 slice,相当于声明未指定长度的数组var a = []int {1, 2, 3, 4} // 声明并初始化 slice (基于 {} 中给出的底层数组)a := []int{1, 2, 3, 4} // 简短声明chars := []string{0:"a", 2:"c", 1: "b"} // ["a", "b", "c"]var b = a[lo:hi] // 创建从 lo 到 hi-1 的 slice var b = a[1:4] // 创建从 1 到 3 的 slicevar b = a[:3] // 缺省 start index 则默认为 0 var b = a[3:] // 缺省 end index 则默认为 len(a)a = append(a,17,3) // 向 slice a 中追加 17 和 3c := append(a,b...) // 合并两个 slice// 使用 make 创建 slicea = make([]byte, 5, 5) // 第一个参数是长度,第二个参数是容量a = make([]byte, 5) // 容量参数是可选的// 从数组创建 slicex := [3]string{"Лайка", "Белка", "Стрелка"}s := x[:] // slice s 指向底层数组 x// 内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。slice1 := []int{1, 2, 3, 4, 5}slice2 := []int{5, 4, 3}copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置 mapmap (映射)是一种数据结构,用于存储一系列无序的键值对。 123456789101112131415161718192021222324252627var m map[string]intm = make(map[string]int)m["key"] = 42fmt.Println(m["key"])delete(m, "key")elem, ok := m["key"] // 检查 m 中是否键为 key 的元素,如果有 ok 才为 true// 使用键值对的形式来初始化 mapvar m = map[string]Vertex{ "Bell Labs": {40.68433, -74.39967}, "Google": {37.42202, -122.08408},}// 从映射获取值并判断键是否存在value, exits := colors["blue"]if exits { // blue fmt.Println(value)}// 使用range迭代映射for key, value := range colors { // key:red value:red fmt.Printf("key:%s value:%s\n", key, value)} Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。1.9 版本中提供了一种效率较高的并发安全的 sync.Map。 指针一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。 当一个指针被定义后没有分配到任何变量时,它的默认值为 nil。指针变量通常缩写为 ptr。 每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用在变量名前面添加 &操作符(前缀)来获取变量的内存地址(取地址操作),格式如下: ptr := &v // v 的类型为 T ==其中 v 代表被取地址的变量,变量 v 的地址使用变量 ptr 进行接收,ptr 的类型为*T,称做 T 的指针类型,*代表指针。== 变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。 从指针获取指针指向的值: 当使用&操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用*操作符,也就是指针取值。 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。 变量、指针地址、指针变量、取地址、取值的相互关系和特性如下: 对变量进行取地址操作使用&操作符,可以获得这个变量的指针变量。 指针变量的值是指针地址。 对指针变量进行取值操作使用*操作符,可以获得指针变量指向的原变量的值。 *操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值,当操作在左值时,就是将值设置给指向的变量。 new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。 通道如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。 通道的声明如下: 1ch := make(chan 类型) 如: ch := make(chan int) // ch has type 'chan int' 两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。 channel的发送和接受操作: 一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine 发送和接收两个操作都使用<-运算符 在发送语句中,<-运算符分割channel和要发送的值 在接收语句中,<-运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的 示例如下: 123ch <- x // a send statementx = <-ch // a receive expression in an assignment statement<-ch // a receive statement; result is discarded 不带缓存的channel 一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作。 如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。 无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生在唤醒发送者goroutine之前。 串联的Channels(Pipeline)Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。 示例: 12345678910111213141516171819202122232425func main() { naturals := make(chan int) squares := make(chan int) // Counter go func() { for x := 0; x < 100; x++ { naturals <- x } close(naturals) }() // Squarer go func() { for x := range naturals { squares <- x * x } close(squares) }() // Printer (in main goroutine) for x := range squares { fmt.Println(x) }} 单方向的ChannelGo语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。箭头<-和关键字chan的相对位置表明了channel的方向。 示例: 123456789101112131415161718192021222324252627func counter(out chan<- int) { for x := 0; x < 100; x++ { out <- x } close(out)}func squarer(out chan<- int, in <-chan int) { for v := range in { out <- v * v } close(out)}func printer(in <-chan int) { for v := range in { fmt.Println(v) }}func main() { naturals := make(chan int) squares := make(chan int) go counter(naturals) go squarer(squares, naturals) printer(squares)} 带缓存的Channels带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。 向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。 数据类型转换由于Go语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明: 123xvalueOfTypeB = typeB(valueOfTypeA)类型 B 的值 = 类型 B(类型 A 的值) 只有相同底层类型的变量之间可以进行相互转换(如将 int16 类型转换成 int32 类型),不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型)。 函数函数的基本语法为: 123func 函数名(形式参数列表)(返回值列表){ 函数体} 基本函数1234567891011121314151617181920212223242526272829303132333435363738394041424344// 最简单的函数func functionName() {}// 带参数的函数(注意类型也是放在标识符之后的)func functionName(param1 string, param2 int) {}// 类型相同的多个参数func functionName(param1, param2 int) {}// 声明返回值的类型func functionName() int { return 42}// 定义多个变量接受,多返回值func div(a, b int) (int, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil}// 一次返回多个值func returnMulti() (int, string) { return 42, "foobar"}var x, str = returnMulti()// 可以在函数体中直接对函数返回值进行赋值,在命名的返回值方式的函数体中,在函数结束前需要显式地使用 return 语句进行返回func returnMulti2() (n int, s string) { n = 42 s = "foobar" // n 和 s 会被返回 return}var x, str = returnMulti2()// 函数作为第一类型,可作为参数或返回值func test(x int) func() { // 匿名函数 return func() { fmt.Println(x) }} 匿名函数(函数作为值和回调使用)匿名函数语法为: 123func(参数列表)(返回参数列表){ 函数体} 一些示例: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364// 在定义时调用匿名函数func(data int) { fmt.Println("hello", data)}(100)func main() { // 将函数作为值,赋给变量 add := func(a, b int) int { return a + b } // 使用变量直接调用函数 fmt.Println(add(3, 4))}// 使用匿名函数实现操作封装var skillParam = flag.String("skill", "", "skill to perform")func main() { flag.Parse() var skill = map[string]func(){ "fire": func() { fmt.Println("chicken fire") }, "run": func() { fmt.Println("soldier run") }, "fly": func() { fmt.Println("angel fly") }, } if f, ok := skill[*skillParam]; ok { f() } else { fmt.Println("skill not found") }}// 回调函数作用域:在定义回调函数时能访问外部函数的值func scope() func() int{ outer_var := 2 foo := func() int { return outer_var} return foo}func another_scope() func() int{ // 编译错误,两个变量不在此函数作用域内 // undefined: outer_var outer_var = 444 return foo}// 回调函数不会修改外部作用域的数据func outer() (func() int, int) { outer_var := 2 inner := func() int { outer_var += 99 // 试着使用外部作用域的 outer_var 变量 return outer_var // 返回值是 101,但只在 inner() 内部有效 } return inner, outer_var // 返回值是 inner, 2 (outer_var 仍是 2)}inner, outer_var := outer(); // inner, 2inner(); // 返回 101inner(); // 返回 200 // 回调函数的特性 闭包函数 + 引用环境 = 闭包 12345678910111213141516171819202122package mainimport ( "fmt")// 创建一个玩家生成器, 输入名称, 输出生成器func playerGen(name string) func() (string, int) { // 血量一直为150 hp := 150 // 返回创建的闭包 return func() (string, int) { // 将变量引用到闭包中 return name, hp }}func main() { // 创建一个玩家生成器 generator := playerGen("high noon") // 返回玩家的名字和血量 name, hp := generator() // 打印值 fmt.Println(name, hp)} 可变参数函数1234567891011121314151617func main() { fmt.Println(adder(1, 2, 3)) // 6 fmt.Println(adder(9, 9)) // 18 nums := []int{10, 20, 30} fmt.Println(adder(nums...)) // 60}// 在函数的最后一个参数类型前,使用 ... 可表明函数还能接收 0 到多个此种类型的参数// 下边的函数在调用时传多少个参数都可以func adder(args ...int) int { total := 0 for _, v := range args { // 使用迭代器逐个访问参数 total += v } return total} range 函数123456789101112131415161718// 迭代数组或 slicefor i, e := range a { // i 是索引 // e 是元素值}// 如果你只要值,可用 _ 来丢弃返回的索引for _, e := range a {}// 如果你只要索引for i := range a {}// 在 Go 1.4 以前的版本,如果 i 和 e 你都不用,直接 range 编译器会报错for range time.Tick(time.Second) { // 每隔 1s 执行一次} 流程控制if…else…123456789101112131415//一般的条件判断x := 1if x > 0 { return x} else { return -x}// 在条件判断语句前可以有一条语句if a := b + c; a < 42 { return a} else { return a - 42} switch…case…case 语句自带 break,想执行所有 case 需要手动 fallthrough 123456789101112131415161718// switch 分支语句switch arg { case "string": fmt.Println("string") case 1 fmt.Println("number") case arg < 1: fmt.Println("运算") case case ' ', '?', '&', '=', '#', '+', '%': // 多个 case 可使用逗号分隔统一处理 fmt.Println("Should escape") default: fmt.Println("Other")}// 可在判断变量之前加入一条赋值语句switch os := runtime.GOOS; os { case "darwin": ...} forGo 语言中循环结构只有 for,没有 do、while、foreach 等 123456789101112131415for i := 1; i < 10; i++ {}// 等效于 while 循环for ; i < 10; {}// 只有一个判断条件时可省去分号for i < 10 {}// 无条件循环时,等效于 while(true)for { } 错误处理处理运行时错误Go 中没有异常处理机制,函数在调用时在有可能会产生错误,可返回一个 Error 类型的值,Error 接口: 123type error interface { Error() string} 自定义一个错误: 1234567891011121314var err = errors.New("this is an error")// 创建错误对象func New(text string) error { return &errorString{text}}// 错误字符串type errorString struct { s string}// 返回发生何种错误func (e *errorString) Error() string { return e.s} 一个可能产生错误的函数: 12345678910func doStuff() (int, error) {}func main() { result, err := doStuff() if err != nil { // 错误处理 } // 使用 result 处理正常逻辑} defer(延迟错误处理)Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。 关键字 defer 的用法类似于面向对象编程语言 Java 的 finally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。因为 defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。 宕机(panic)与宕机恢复(recover)Go语言的 panic 机制类似于其他语言的异常,但 panic 的适用场景有一些不同,由于 panic 会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致。任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,我们应该使用Go语言提供的错误机制,而不是 panic。 手动触发宕机: panic("crash") Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。 panic 和 recover 的关系panic 和 recover 的组合有如下特性: 有 panic 没 recover,程序宕机。 有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行。 提示 虽然 panic/recover 能模拟其他语言的异常机制,但并不建议在编写普通函数时也经常性使用这种特性。 在 panic 触发的 defer 函数内,可以继续调用 panic,进一步将错误外抛,直到程序整体崩溃。 如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。 OOP对象见 struct 继承Go语言中的继承是通过内嵌或组合来实现的 1) 内嵌的结构体可以直接访问其成员变量 2) 内嵌结构体的字段名是它的类型名 1234567891011121314151617181920212223242526272829303132package mainimport "fmt"// 可飞行的type Flying struct{}func (f *Flying) Fly() { fmt.Println("can fly")}// 可行走的type Walkable struct{}func (f *Walkable) Walk() { fmt.Println("can calk")}// 人类type Human struct { Walkable // 人类能行走}// 鸟类type Bird struct { Walkable // 鸟类能行走 Flying // 鸟类能飞行}func main() { // 实例化鸟类 b := new(Bird) fmt.Println("Bird: ") b.Fly() b.Walk() // 实例化人类 h := new(Human) fmt.Println("Human: ") h.Walk()} 构造方法Go语言的类型或结构体没有构造函数的功能,但是我们可以使用结构体初始化的过程来模拟实现构造函数。 123456789101112131415161718192021222324252627282930313233//多种方式创建和初始化结构体——模拟构造函数重载type Cat struct { Color string Name string}func NewCatByName(name string) *Cat { return &Cat{ //定义用名字构造猫结构的函数,返回 Cat 指针。 Name: name, }}func NewCatByColor(color string) *Cat { return &Cat{ //定义用颜色构造猫结构的函数,返回 Cat 指针。 Color: color, }}//带有父子关系的结构体的构造和初始化——模拟父级构造调用type BlackCat struct { Cat // 嵌入Cat, 类似于继承}// “构造基类”func NewCat(name string) *Cat { return &Cat{ Name: name, }}// “构造子类”func NewBlackCat(color string) *BlackCat { cat := &BlackCat{} cat.Color = color return cat} 方法Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。 接收器语法格式: 123func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) { 函数体} 对各部分的说明: 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名。例如,Socket 类型的接收器变量应该命名为 s,Connector 类型的接收器变量应该命名为 c 等。 接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。 方法名、参数列表、返回参数:格式与函数定义一致。 接收器根据接收器的类型可以分为指针接收器、非指针接收器,两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。 指针接收器指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。 由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的。 1234567891011121314151617181920212223package mainimport "fmt"// 定义属性结构type Property struct { value int // 属性值}// 设置属性值func (p *Property) SetValue(v int) { // 修改p的成员变量 p.value = v}// 取属性值func (p *Property) Value() int { return p.value}func main() { // 实例化属性 p := new(Property) // 设置值 p.SetValue(100) // 打印值 fmt.Println(p.Value())} 非指针接收器当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效。 1234567891011121314151617181920212223package mainimport ( "fmt")// 定义点结构type Point struct { X int Y int}// 非指针接收器的加方法func (p Point) Add(other Point) Point { // 成员值与参数相加后返回新的结构 return Point{p.X + other.X, p.Y + other.Y}}func main() { // 初始化点 p1 := Point{1, 1} p2 := Point{2, 2} // 与另外一个点相加 result := p1.Add(p2) // 输出结果 fmt.Println(result)} 指针和非指针接收器的使用 在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器,大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。 在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的内部,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。熟悉C或者C艹的人这里应该很快能明白。 接口接口是用来定义行为的类型,这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。Go 接口实现机制很简洁,只要目标类型方法包含接口声明的全部方法,就被视为实现了该接口,无需做显示声明。当然,目标类可以实现多个接口。如果接口没有任何方法声明,那么就是一个空接口(interface{}),它的用途类似 Object,可被赋值为任何类型的对象。 123456789101112// 声明接口type Awesomizer interface { Awesomize() string}// 无需手动声明 implement 接口type Foo struct {}// 自定义类型如果实现了接口的所有方法,那它就自动实现了该接口func (foo Foo) Awesomize() string { return "Awesome!"} 类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。 value, ok := x.(T) 其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。 该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型: 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。 并发goroutinegoroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。 Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。 使用普通函数创建 goroutine: Go 程序中使用 go 关键字为一个函数创建一个 goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。语法:go 函数名( 参数列表 )。使用 go 关键字创建 goroutine 时,被调用函数的返回值会被忽略。如果需要在 goroutine 中返回数据,请使用后面介绍的通道(channel)特性,通过通道把数据从 goroutine 中作为返回值传出。 也可以使用匿名函数创建: 123go func( 参数列表 ){ 函数体}( 调用参数列表 ) 示例: 12345678910111213141516171819202122package mainimport ( "fmt" "time")func running() { var times int // 构建一个无限循环 for { times++ fmt.Println("tick", times) // 延时1秒 time.Sleep(time.Second) }}func main() { // 并发执行程序 go running() // 接受命令行输入, 不做任何事情 var input string fmt.Scanln(&input)} 反射Go语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射。 反射是由 reflect 包提供的。 它定义了两个重要的类型, Type 和 Value。 一个 Type 表示一个Go类型。函数 reflect.TypeOf 接受任意的 interface{} 类型, 并以reflect.Type形式返回其动态类型 一个 reflect.Value 可以装载任意类型的值。函数 reflect.ValueOf 接受任意的 interface{} 类型, 并返回一个装载着其动态值的 reflect.Value。]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Golang之禅]]></title>
<url>%2F2020%2F02%2F28%2FGolang%E4%B9%8B%E7%A6%85%2F</url>
<content type="text"><![CDATA[文章转载自 OSCHINA 社区 [http://www.oschina.net] 标题:Golang 之禅 地址:https://www.oschina.net/news/113606/the-zen-of-go 在本月初的 GopherCon 上,知名 Go 语言贡献者与布道师 Dave Cheney 发表了名为《The Zen of Go》的演讲,之后他整理了演讲内容在博客中分享,由于内容过长,他又写了一个简洁版本: 完整版:https://dave.cheney.net/2020/02/23/the-zen-of-go简洁版:https://the-zen-of-go.netlify.com这里简单翻译一下简洁版本的内容: 编写简单、可读、可维护的 Go 代码的十个工程要点。 每个包实现单一目标 设计良好的 Go 软件包提供一个单一的思路,以及一系列相关的行为。一个好的 Go 软件包首先需要选择一个好名字,使用电梯法则(30 秒内向客户讲清楚一个方案),仅用一个词来思考你的软件包要提供什么功能。 明确处理错误 健壮的程序其实是由处理故障案例的片段组成的,并且需要在故障出现之前处理好。冗余的if err != nil { return err }比出了故障再一个个去处理更有价值。panic 和 recover 也一样。 尽早 return,不要深陷 每次缩进时都会在程序员的堆栈中添加另一个先决条件,这会占用他们短期内存中的 7±2 个片段。避免需要深层缩进的控制流。与其深入嵌套,不如使用守卫子句将成功路径保持在左侧。 并发权留给调用者 让调用者选择是否要异步运行你的库或函数,不要强制他们使用异步。 在启动 goroutine 之前,要知道它什么时候会停止 goroutines 拥有资源、锁、变量与内存等,释放这些资源的可靠方法是停止 goroutine。 避免包级别的状态 要完成明确和减少耦合的操作,需要通过提供类型需要的依赖项作为该类型上的字段,而不是使用包变量。 简单性很重要 简单性不是老练的代名词。简单并不意味着粗糙,它意味着可读性和可维护性。如果可以选择,请遵循较简单的解决方案。 编写测试以确认包 API 的行为 软件包的 API 是与使用者的一份合约,不管先后,不管多少,一定要进行测试。测试是确定合约的保证。要确保测试使用者可以观察和依赖的行为。 如果你认为速度缓慢,先通过基准测试进行验证 以性能之名会犯下许多危害可维护性的罪行。优化会破坏抽象、暴露内部和紧密耦合。如果要付出这样的代价,请确保有充分理由这样做。 节制是一种美德 适度使用 goroutine、通道、锁、接口与嵌套。]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Python的logging实践]]></title>
<url>%2F2019%2F11%2F13%2FPython%E7%9A%84logging%E5%AE%9E%E8%B7%B5%2F</url>
<content type="text"><![CDATA[前言在使用 Python 的时候,logging 一度让我头疼。因为,从 Java 转过来以后,总是想着 logback、log4j 那样的统一配置。在使用过程中折腾了些时候,算是勉强给出了自己比较满意的效果。 logging 使用日志的简单使用可以参考官方的《日志操作手册》 其他的学习资料有: Python 日志模块 python logging日志模块以及多进程日志 Python之日志处理(logging模块) 我的 logging 配置我的 logging 配置主要基于 Tornado 的日志模块进行了修改。代码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261# coding=utf-8"""基于Tornado Log的logger实现."""import loggingimport logging.handlersimport osimport sysfrom typing import Dict, Any, cast, Union, Optionalbytes_type = bytesunicode_type = strbasestring_type = strtry: import colorama # type: ignoreexcept ImportError: colorama = Nonetry: import cursesexcept ImportError: curses = None # type: ignore_TO_UNICODE_TYPES = (unicode_type, type(None))def _unicode(value: Union[None, str, bytes]) -> Optional[str]: # noqa: F811 """Converts a string argument to a unicode string. If the argument is already a unicode string or None, it is returned unchanged. Otherwise it must be a byte string and is decoded as utf8. """ if isinstance(value, _TO_UNICODE_TYPES): return value if not isinstance(value, bytes): raise TypeError("Expected bytes, unicode, or None; got %r" % type(value)) return value.decode("utf-8")def _stderr_supports_color() -> bool: try: if hasattr(sys.stderr, "isatty") and sys.stderr.isatty(): if curses: curses.setupterm() if curses.tigetnum("colors") > 0: return True elif colorama: if sys.stderr is getattr( colorama.initialise, "wrapped_stderr", object() ): return True except Exception: # Very broad exception handling because it's always better to # fall back to non-colored logs than to break at startup. pass return Falsedef _safe_unicode(s: Any) -> str: try: return _unicode(s) except UnicodeDecodeError: return repr(s)class LogFormatter(logging.Formatter): """ Log formatter used in Tornado. """ DEFAULT_FORMAT = "%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s" # noqa: E501 DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" DEFAULT_COLORS = { logging.DEBUG: 4, # Blue logging.INFO: 2, # Green logging.WARNING: 3, # Yellow logging.ERROR: 1, # Red } def __init__( self, fmt: str = DEFAULT_FORMAT, datefmt: str = DEFAULT_DATE_FORMAT, color: bool = True, colors: Dict[int, int] = DEFAULT_COLORS, ) -> None: r""" :arg bool color: Enables color support. :arg str fmt: Log message format. It will be applied to the attributes dict of log records. The text between ``%(color)s`` and ``%(end_color)s`` will be colored depending on the level if color support is on. :arg dict colors: color mappings from logging level to terminal color code :arg str datefmt: Datetime format. Used for formatting ``(asctime)`` placeholder in ``prefix_fmt``. """ logging.Formatter.__init__(self, datefmt=datefmt) self._fmt = fmt self._colors = {} # type: Dict[int, str] if color and _stderr_supports_color(): if curses is not None: fg_color = curses.tigetstr("setaf") or curses.tigetstr("setf") or b"" for levelno, code in colors.items(): # Convert the terminal control characters from # bytes to unicode strings for easier use with the # logging module. self._colors[levelno] = unicode_type( curses.tparm(fg_color, code), "ascii" ) self._normal = unicode_type(curses.tigetstr("sgr0"), "ascii") else: # If curses is not present (currently we'll only get here for # colorama on windows), assume hard-coded ANSI color codes. for levelno, code in colors.items(): self._colors[levelno] = "\033[2;3%dm" % code self._normal = "\033[0m" else: self._normal = "" def format(self, record: Any) -> str: try: message = record.getMessage() assert isinstance(message, basestring_type) # guaranteed by logging record.message = _safe_unicode(message) except Exception as e: record.message = "Bad message (%r): %r" % (e, record.__dict__) record.asctime = self.formatTime(record, cast(str, self.datefmt)) if record.levelno in self._colors: record.color = self._colors[record.levelno] record.end_color = self._normal else: record.color = record.end_color = "" formatted = self._fmt % record.__dict__ if record.exc_info: if not record.exc_text: record.exc_text = self.formatException(record.exc_info) if record.exc_text: lines = [formatted.rstrip()] lines.extend(_safe_unicode(ln) for ln in record.exc_text.split("\n")) formatted = "\n".join(lines) return formatted.replace("\n", "\n ")class NullHandler(logging.Handler): def emit(self, record): passclass ExactLogLevelFilter(logging.Filter): def __init__(self, level): self.__level = level def filter(self, log_record): return log_record.levelno == self.__leveldef _pretty_logging(options: Dict, logger: logging.Logger) -> None: # 如果没有设置日志级别 if options['logging_level'] is None or options['logging_level'].lower() == "none": return if options['log_file_path']: rotate_mode = options['log_rotate_mode'] if rotate_mode == "size": channel = logging.handlers.RotatingFileHandler( filename=options['log_file_path'], maxBytes=options['log_file_max_size'], backupCount=options['log_file_num_backups'], encoding="utf-8", ) # type: logging.Handler elif rotate_mode == "time": channel = logging.handlers.TimedRotatingFileHandler( filename=options['log_file_path'], when=options['log_rotate_when'], interval=options['log_rotate_interval'], backupCount=options['log_file_num_backups'], encoding="utf-8", ) else: error_message = ( "The value of log_rotate_mode option should be " + '"size" or "time", not "%s".' % rotate_mode ) raise ValueError(error_message) channel.setFormatter(LogFormatter(color=False)) # 添加通过级别过滤 channel.addFilter(ExactLogLevelFilter(logging.getLevelName(options['logging_level']))) logger.addHandler(channel) if options['log_to_stderr'] or (options['log_to_stderr'] is None and not logger.handlers): # Set up color if we are in a tty and curses is installed channel = logging.StreamHandler() channel.setFormatter(LogFormatter()) logger.addHandler(channel)def initialize_logging(logger: logging.Logger = None, options: Dict = None): """ :param logger: :param options: { 'log_file_path':'日志文件路径', 'logging_level':'日志级别(DEBUG/INFO/WARN/ERROR)', 'log_to_stderr':'将日志输出发送到stderr(如果可能的话,将其着色)。如果未设置--log_file_prefix并且未配置其他日志记录,则默认使用stderr。', 'log_file_max_size':'每个文件最大的大小,默认:100 * 1000 * 1000', 'log_file_num_backups':'要保留的日志文件数', 'log_rotate_when':'时间间隔的类型('S', 'M', 'H', 'D', 'W0'-'W6')', 'log_rotate_interval':'TimedRotatingFileHandler的interval值', 'log_rotate_mode':'类型(size/time)' } :return: """ if logger is None: logger = logging.getLogger() if options is None: options = {} default_options = { 'log_file_path': '', 'logging_level': 'INFO', 'log_to_stderr': None, 'log_file_max_size': 100 * 1000 * 1000, 'log_file_num_backups': 10, 'log_rotate_when': 'M', 'log_rotate_interval': 1, 'log_rotate_mode': 'time' } default_options.update(options) # 设置日志级别 logger.setLevel(getattr(logging, default_options['logging_level'].upper())) log_level_path = { 'DEBUG': os.path.join(default_options['log_file_path'], 'debug/debug.log'), 'INFO': os.path.join(default_options['log_file_path'], 'info/info.log'), 'WARNING': os.path.join(default_options['log_file_path'], 'warning/warning.log'), 'ERROR': os.path.join(default_options['log_file_path'], 'error/error.log') } log_levels = log_level_path.keys() for level in log_levels: log_path = os.path.abspath(log_level_path[level]) if not os.path.exists(os.path.dirname(log_path)): os.makedirs(os.path.dirname(log_path)) default_options.update({'log_file_path': log_path, 'logging_level': level}) _pretty_logging(options=default_options, logger=logger) 以上的代码没有考虑到多线程情况下的使用场景,且暂时考虑的是在应用程序中使用的场景。]]></content>
<categories>
<category>Python</category>
</categories>
<tags>
<tag>Python</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java中AES加解密]]></title>
<url>%2F2019%2F07%2F09%2FJava%E4%B8%ADAES%E5%8A%A0%E8%A7%A3%E5%AF%86%2F</url>
<content type="text"><![CDATA[Java SDK 对 AES 的实现AES 是 Advanced Encryption Standard 的缩写,也就是 高级加密标准 。具体可以见:高级加密标准。本文主要讨论使用 Java SDK中对 AES 的实现。 从 JDK 的 Cipher 文档 中,知道它支持 4 种 AES 加密模式: AES/CBC/NoPadding (128) AES/CBC/PKCS5Padding (128) AES/ECB/NoPadding (128) AES/ECB/PKCS5Padding (128) AES 是一种加解密算法,那么 CBC, ECB, NoPadding 和 PKCS5Padding 是什么呢? CBC, CBC 是分组密码工作模式,是对于按块处理密码的加密方式的一种扩充。NoPadding, PKCS5Padding 是填充(Padding),是对需要按块处理的数据,当数据长度不符合块处理需求时,按照一定方法填充满块长的一种规则。 关于分组密码工作模式,可以参考: 分组密码工作模式 和 AES五种加密模式(CBC、ECB、CTR、OCF、CFB)关于填充,可以参考:Padding (cryptography) 参考,JAVA AES算法 知道(以下部分的内容为该文章里的内容): (1)缺省模式和填充为“AES/ECB/PKCS5Padding”,Cipher.getInstance(“AES”)与Cipher.getInstance(“AES/ECB/PKCS5Padding”)等效。 (2)JDK的PKCS5Padding实际是上述的PKCS7的实现。 (3)由于AES是按照16Byte为块进行处理,对于NoPadding而言,如果需要加密的原文长度不是16Byte的倍数,将无法处理抛出异常,其实是由用户自己选择Padding的算法。密文则必然是16Byte的倍数,否则密文肯定异常。 (4)如果加密为PKCS5Padding,解密可以选择NoPadding,也能解密成功,内容为原文加上PKCS5Padding之后的结果。 (5)如果原文最后一个字符为>=0x00&&<=0x10的内容,PKCS5Padding的解密将会出现异常,要么是符合PKCS5Padding,最后的内容被删除,要么不符合,则解密失败抛出异常。对此有两种思路,一是原文通过Base64编码为可见字符,二是原文自带长度使用NoPadding解密。 代码实现1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980import javax.crypto.Cipher;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import java.security.SecureRandom;import java.util.Base64;public class AesUtil { /** * AES/CBC/PKCS5Padding加密,然后进行base64加密 * * @param plainText * @param key * @return * @throws Exception */ public static String encryptBase64AESCBC(String plainText, String key) throws Exception { byte[] clean = plainText.getBytes(); //Cipher Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // Generating IV. int ivSize = cipher.getBlockSize(); byte[] iv = new byte[ivSize]; SecureRandom random = new SecureRandom(); random.nextBytes(iv); IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); // Encrypt. SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES"); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); byte[] encrypted = cipher.doFinal(clean); // Combine IV and encrypted part. byte[] encryptedIVAndText = new byte[ivSize + encrypted.length]; System.arraycopy(iv, 0, encryptedIVAndText, 0, ivSize); System.arraycopy(encrypted, 0, encryptedIVAndText, ivSize, encrypted.length); return Base64.getEncoder().encodeToString(encryptedIVAndText); } /** * base64解密后再进行AES/CBC/PKCS5Padding解密 * * @param encryptedIvText * @param key * @return * @throws Exception */ public static String decryptBase64AESCBC(String encryptedIvText, String key) throws Exception { byte[] encryptedIvTextBytes = Base64.getDecoder().decode(encryptedIvText); //Cipher Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // Extract IV. int ivSize = cipher.getBlockSize(); byte[] iv = new byte[ivSize]; System.arraycopy(encryptedIvTextBytes, 0, iv, 0, iv.length); IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); // Extract encrypted part. int encryptedSize = encryptedIvTextBytes.length - ivSize; byte[] encryptedBytes = new byte[encryptedSize]; System.arraycopy(encryptedIvTextBytes, ivSize, encryptedBytes, 0, encryptedSize); // Decrypt. SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES"); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); byte[] original = cipher.doFinal(encryptedBytes); String originalString = new String(original); return originalString; }} 以上代码中 iv的长度为 cipher.getBlockSize(),然后将 iv 放在加密文本的前部分。解密的时候一样先获得 iv,再进行加密内容的解密。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SpringBoot中使用Redis]]></title>
<url>%2F2019%2F07%2F09%2FSpringBoot%E4%B8%AD%E4%BD%BF%E7%94%A8Redis%2F</url>
<content type="text"><![CDATA[本文介绍SpringBoot Redis的基本使用和多Redis数据源配置 基本使用SpringBoot 2.x项目中引入spring-boot-starter-data-redis: 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency> 查看引入包,发现SpringBoot 2.x 使用的是: lettuce 。 配置 application.yml 123456spring: redis: host: 127.0.0.1 port: 6379 password: database: 0 使用: 12345@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Autowiredprivate StringRedisTemplate stringRedisTemplate; SpringBoot Redis的自动化配置位于:RedisAutoConfiguration 多Redis数据源配置项目中常常用到不止一个Redis数据源,如果按照上面的简单配置,那么很难满足需要。需要我们自己来声明 ConnectionFactory 和 RedisTemplate 。 application.yml中配置如下: 1234567891011121314spring: redis: #redis配置 redis-a: host: 127.0.0.1 port: 6379 password: pwd@local ssl: true database: 0 redis-b: host: 127.0.0.1 port: 6479 password: pwd@local ssl: true database: 0 以上配置是在spring.redis下做区分两个数据源,这是为了能够方便地查找配置位置。 添加RedisConfig.java,如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465@Configurationpublic class RedisConfig { @Bean(name = "aRedisTemplate") public StringRedisTemplate aRedisTemplate( @Qualifier("aLettuceConnectionFactory") RedisConnectionFactory aLettuceConnectionFactory) { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(aLettuceConnectionFactory); return template; } @Bean(name = "bRedisTemplate") public StringRedisTemplate bRedisTemplate( @Qualifier("bConnectionFactory") RedisConnectionFactory bConnectionFactory) { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(bConnectionFactory); return template; } @Bean(name = "aLettuceConnectionFactory") @Primary public LettuceConnectionFactory aLettuceConnectionFactory(@Value("${spring.redis.redis-a.host}") String host, @Value("${spring.redis.redis-a.port}") int port, @Value("${spring.redis.redis-a.password}") String password, @Value("${spring.redis.redis-a.ssl}") boolean ssl, @Value("${spring.redis.redis-a.database}") int database) { LettuceClientConfiguration.LettuceClientConfigurationBuilder builder = LettuceClientConfiguration.builder(); if (ssl) { builder.useSsl(); } RedisStandaloneConfiguration standaloneConfiguration = getRedisStandaloneConfiguration(host, port, password, database); return new LettuceConnectionFactory(standaloneConfiguration, builder.build()); } @Bean(name = "bConnectionFactory") public LettuceConnectionFactory bConnectionFactory(@Value("${spring.redis.redis-b.host}") String host, @Value("${spring.redis.redis-b.port}") int port, @Value("${spring.redis.redis-b.password}") String password, @Value("${spring.redis.redis-b.ssl}") boolean ssl, @Value("${spring.redis.redis-b.database}") int database) { LettuceClientConfiguration.LettuceClientConfigurationBuilder builder = LettuceClientConfiguration.builder(); if (ssl) { builder.useSsl(); } RedisStandaloneConfiguration standaloneConfiguration = getRedisStandaloneConfiguration(host, port, password, database); return new LettuceConnectionFactory(standaloneConfiguration, builder.build()); } private RedisStandaloneConfiguration getRedisStandaloneConfiguration(String host, int port, String password, int database) { RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration(); standaloneConfiguration.setHostName(host); standaloneConfiguration.setPort(port); standaloneConfiguration.setPassword(password); standaloneConfiguration.setDatabase(database); return standaloneConfiguration; }} 使用: 123456789@Autowired@Qualifier("aRedisTemplate")private StringRedisTemplate aRedisTemplate;@Autowired@Qualifier("bRedisTemplate")private StringRedisTemplate bRedisTemplate; 如果想要忽略掉默认的Redis配置,那么可以将 RedisAutoConfiguration 和 RedisReactiveAutoConfiguration 加入忽略: 1@EnableAutoConfiguration(exclude = {RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class})]]></content>
<categories>
<category>Spring</category>
<category>Spring Boot</category>
</categories>
<tags>
<tag>实战</tag>
<tag>Spring Boot</tag>
</tags>
</entry>
<entry>
<title><![CDATA[PMP必须掌握的知识——PMI主义]]></title>
<url>%2F2019%2F07%2F04%2FPMP%E5%BF%85%E9%A1%BB%E6%8E%8C%E6%8F%A1%E7%9A%84%E7%9F%A5%E8%AF%86%E2%80%94%E2%80%94PMI%E4%B8%BB%E4%B9%89%2F</url>
<content type="text"><![CDATA[你是专业的项目经理,管理是你的核心职能 必须以专业的方法做项目,即遵循 PMBOK 的要求 强调事业环境因素和组织过程资产 强调历史信息,强调经验教训总结,强调记录 问题重在预防,而非解决 必须有明确的目标,必须有正式的计划,才可行动 利害关系者很重要,尽早识别全部并让其参与 项目经理必须被任命,PM 是管理工作的核心责任点 项目是系统工程,PM 是整合者, 三重约束牢记在心 项目管理以结果为导向,项目成功是 PM 最终责任 变更影响项目成功,PM 应影响变更变化,管理变更 整合通过沟通实现,PM 要花 75% ~ 90% 时间用于沟通 PM 应拒绝提供不重要的信息要求 工作必须被详细描述,责任必须明确 任何情况下,质量都要达到客户满意 消减费用的前提是消减项目范围 PM 必须遵守职业道德(个人、公司、社会、道德、法律) PM 必须主动,PM 是神器、伟大的,可以拯救世界 一切决策必须以事实为依据,以程序为准绳,正确的程序优先于正确的结果 防止范围潜变,杜绝质量镀金 项目必须收尾 公正、公平、公开,勇敢、诚实地面对现实 双赢理念 决策要遵守四大价值观:责任、尊重、公平、诚信]]></content>
<categories>
<category>PMP</category>
</categories>
<tags>
<tag>PMP</tag>
</tags>
</entry>
<entry>
<title><![CDATA[web3.js编译Solidity,发布,调用全部流程]]></title>
<url>%2F2019%2F04%2F11%2Fweb3-js%E7%BC%96%E8%AF%91Solidity%EF%BC%8C%E5%8F%91%E5%B8%83%EF%BC%8C%E8%B0%83%E7%94%A8%E5%85%A8%E9%83%A8%E6%B5%81%E7%A8%8B%2F</url>
<content type="text"><![CDATA[根据 web3.js编译Solidity,发布,调用全部流程(手把手教程)进行。因为这篇文章中的一些方法和最新版本的不一样了,中间遇到了一些坑,现把解决方法记录如下: 准备工作安装 Node.js 、安装以太节点以及合约代码等与web3.js编译Solidity,发布,调用全部流程(手把手教程)中的一致。 安装好的NodeJs版本为:v10.15.3 安装好的以太节点版本为:[email protected] 遇到的坑第一个坑:Solidity安装按照官方文档使用 npm install -g solc 安装。 安装后验证版本应该使用 solcjs --version ,而不是 solc --version 。而且这边安装的是最新版本,也为后面的步骤埋下了坑。 第二个坑:TypeError: Cannot read property ‘solidity’ of undefined使用以下代码: 12let source = "pragma solidity ^0.4.0;contract Calc{ /*区块链存储*/ uint count; /*执行会写入数据,所以需要`transaction`的方式执行。*/ function add(uint a, uint b) returns(uint){ count++; return a + b; } /*执行不会写入数据,所以允许`call`的方式执行。*/ function getCount() returns (uint){ return count; }}";let calc = web3.eth.compile.solidity(source); 运行的时候报: 12345678910111213let calc = web3.eth.compile.solidity(source); ^TypeError: Cannot read property 'solidity' of undefined at Object.<anonymous> (/data/eth-contract/src/index.js:15:29) at Module._compile (internal/modules/cjs/loader.js:701:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10) at Module.load (internal/modules/cjs/loader.js:600:32) at tryModuleLoad (internal/modules/cjs/loader.js:539:12) at Function.Module._load (internal/modules/cjs/loader.js:531:3) at Function.Module.runMain (internal/modules/cjs/loader.js:754:12) at startup (internal/bootstrap/node.js:283:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3) 这个是因为 Geth 在 1.5.9 版本以后就不支持此功能。见:Ethereum Dapp初心者之路(7): web3.eth.compile.solidity()替代方案。 所以参考 https://ethereum.stackexchange.com/questions/6346/solc-version-command-not-found 文章,将代码改为: 1234const fs = require("fs");const solc = require('solc');let source = fs.readFileSync('Calc.sol', 'UTF-8');let compilationData = solc.compile(source); 第三个坑:编译报错编译的时候,如果遇到以下报错: 1{"errors":[{"component":"general","formattedMessage":"* Line 1, Column 1\n Syntax error: value, object or array expected.\n* Line 1, Column 2\n Extra non-whitespace after JSON value.\n","message":"* Line 1, Column 1\n Syntax error: value, object or array expected.\n* Line 1, Column 2\n Extra non-whitespace after JSON value.\n","severity":"error","type":"JSONError"}]} 这个是因为 solc 的版本不对,我们的代码中的版本是 ^0.4.0 ,而安装的 solc 的版本为 ^0.5.7 。应该用一样的版本。 代码中的版本改为^0.5.7,即pragma solidity ^0.5.7;。运行后,报: 123456789101112131415161718assert.js:350 throw err; ^AssertionError [ERR_ASSERTION]: Invalid callback specified. at wrapCallback (/data/eth-contract/node_modules/solc/wrapper.js:16:5) at runWithReadCallback (/data/eth-contract/node_modules/solc/wrapper.js:42:26) at compileStandard (/data/eth-contract/node_modules/solc/wrapper.js:83:14) at Object.compileStandardWrapper (/data/eth-contract/node_modules/solc/wrapper.js:90:14) at Object.<anonymous> (/data/eth-contract/src/index.js:8:28) at Module._compile (internal/modules/cjs/loader.js:701:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10) at Module.load (internal/modules/cjs/loader.js:600:32) at tryModuleLoad (internal/modules/cjs/loader.js:539:12) at Function.Module._load (internal/modules/cjs/loader.js:531:3) at Function.Module.runMain (internal/modules/cjs/loader.js:754:12) at startup (internal/bootstrap/node.js:283:19) at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3) 参考 https://stackoverflow.com/questions/53353167/npm-solc-assertionerror-err-assertion-invalid-callback-specified 后,如下操作: 123npm uninstall -g solcnpm uninstall solcnpm install [email protected] 然后将代码中的版本号改为:pragma solidity ^0.4.25; 完整源码index.js: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354const Web3 = require('web3');const fs = require("fs");const solc = require('solc');let web3;if (typeof web3 !== 'undefined') { web3 = new Web3(web3.currentProvider);} else { // set the provider you want from Web3.providers web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));}//编译合约let source = fs.readFileSync('Calc.sol', 'UTF-8').toString();let calcCompiled = solc.compile(source);//得到合约对象let jsonInterface = calcCompiled['contracts'][':Calc']['interface'];//获得abilet abi = JSON.parse(jsonInterface);//获取合约的代码let bytecode = calcCompiled['contracts'][':Calc']['bytecode']//得到合约对象const calcContract = new web3.eth.Contract(abi,null,{ data: '0x'+bytecode, defaultGas:'4700000'});//部署者的地址,当前取默认账户的第一个地址let deployeAddr = web3.eth.accounts[0];//部署合约calcContract.deploy().send({ from: deployeAddr}).on('error', (error) => { console.error(error) }).on('transactionHash', (transactionHash) => { console.log("transactionHash :" + transactionHash) }).on('receipt', (receipt) => { console.log("receipt:") console.log(receipt) }).on('confirmation', (confirmationNumber, receipt) => { console.log("confirmationNumber:"+confirmationNumber) }).then((newContractInstance) => { console.log(newContractInstance) console.log(newContractInstance.options.address) // instance with the new contract address}); Calc.sol: 1234567891011121314151617pragma solidity ^0.4.25;contract Calc{ /*区块链存储*/ uint count; /*执行会写入数据,所以需要`transaction`的方式执行。*/ function add(uint a, uint b) returns(uint){ count++; return a + b; } /*执行不会写入数据,所以允许`call`的方式执行。*/ function getCount() constant returns (uint){ return count; }} package.json: 123456789101112131415{ "name": "eth-contract", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "solc": "^0.4.25", "web3": "^1.0.0-beta.52" }} 参考 web3.js编译Solidity,发布,调用全部流程(手把手教程) web3.js - Ethereum JavaScript API Solidity 开发指南 Remix - Solidity IDE]]></content>
<categories>
<category>区块链</category>
</categories>
<tags>
<tag>区块链</tag>
</tags>
</entry>
<entry>
<title><![CDATA[初识比特闪电网络]]></title>
<url>%2F2019%2F04%2F08%2F%E5%88%9D%E8%AF%86%E6%AF%94%E7%89%B9%E9%97%AA%E7%94%B5%E7%BD%91%E7%BB%9C%2F</url>
<content type="text"><![CDATA[闪电网络在今年的区块链世界是一个很热门的词。这个词乍听起来很酷炫,很深奥,它到底是什么呢?本文带你了解它。 闪电网络是什么官方网站:http://lightning.network/ 闪电网络(Lightning Network)是一个去中心化的系统。闪电网络的卓越之处在于,无需信任对方以及第三方即可实现实时的、海量的交易网络。这也就解决了比特币支付存在着拥堵的问题。 闪电网络是基于微支付通道演进而来,创造性的设计出了两种类型的交易合约:序列到期可撤销合约RSMC(Revocable Sequence Maturity Contract),哈希时间锁定合约HTLC(Hashed Timelock Contract)。 RSMC解决了通道中币单向流动问题,也就是链下交易的确认问题;HTLC解决了币跨节点传递的问题,也就是支付通道问题。这两个类型的交易组合构成了闪电网络。 1. 入门级文字: 用大白话解释:闪电网络是什么? 闪电网络详解 闪电网络入门:什么是闪电网络?它是如何运作的? 什么是闪电网络?干货都在这了! 区块链学堂——闪电网络是什么鬼(故事篇)? 区块链学堂——闪电网络是个什么鬼(技术篇)? 2. 如果不喜欢文字资料,还可以通过以下两个视频简单入门: 中心化魔鬼or 扩容灵药,6分钟带你看懂闪电网络! 闪电网络五大误区,你踩雷了吗? 3. 白皮书: 比特币闪电网络白皮书:可扩展的 off-chain 即时支付 The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments 4. 其他资料: BITCOIN LIGHTNING NETWORK - Resource and Information Guide 闪电网络的优缺点闪电网络的优点 可以即时支付 对节点的性能需求并不高 通过路由系统自动寻径(寻找花费最少的路径),网络中的节点充分竞争使得交易费用可以低 匿名性(每一个节点只记录该通道内的交易) 闪电网络的缺点 如果换一种代币就需要重新开启通道,节点和用户的成本都相当的高 没有足够的商家支持闪电网络,使得作为节点的收益并不高,由于没有足够的商家支持,用户使用闪电网络的需求并不高 闪电网络是小额支付,开启通道是需要使用公网的,使用公网意味着要发起一次交易,如果只转入几百块,一次交易就需要几十块,可能就得不偿失了 关闭通道时,也需要发起交易,将通道中的币分配给通道以及用户,这又需要发起一次交易,又需要发起一次交易 其次每个节点都需要抵押一定的比特币,才能开启节点,长时间暴露在公网中,可能会存在私钥泄露的问题 现阶段闪电网络并不完善处于一个初始版本 闪电网络的浏览器 1ML Lightning network explorer Lightblock Robtex Bitcoin Lightning Explorer Lightning Explorer (explore casa) lndexplorer Lightning Explorerc(chaintools) Bitcoin Visuals rompert.com Bitcoin Exchange Rate 其他: lngraph - Personal private Lightning Network explorer using Neo4j Browser A (mostly) visual collection of the Lightning Network 闪电网络的钱包闪电网络的钱包可以参考Lightning mobile wallets列表。 GitHub上开源的钱包项目: 桌面 lightning-app - NodeJs实现 lnd-gui - Swift实现 eclair-node-gui - Java实现 zap-desktop - Electron+React+Redux实现 Presto - C++实现 Lightning Peach Wallet - Electron+React+Redux实现 spark-wallet - NodeJs实现 Web Discovery wallet - NodeJs实现 fulmo - Python实现 kugelblitz - Go实现 lncli-web - NodeJS+Angular实现 Ride The Lightning (RTL) - NodeJS+Angular7实现 移动端 可以参考Lightning mobile wallets中Open source为yes的项目。 闪电网络协议的实现在闪电网络的实现方面,现在比较主流的有Lightning Network Daemon、eclair和c-lightning。这三个实现是兼容的。 Lightning Network Daemon (LND)LND是论文《The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments》中提出者的项目,使用Go语言编写,它比较适用于社区和开发者方面。 项目地址:https://github.com/lightningnetwork/lnd LND Developer Site: https://dev.lightning.community/ 开发支持: LightningJ - Lightning APIs for Java 风险:目前LND还不支持导出私钥https://github.com/lightningnetwork/lnd/issues/732 eclairEclair(法语闪电)是闪电网络的Scala实现。它可以运行GUI,也可以不运行GUI,还可以使用JSON-RPC API。因为是Scala开发的,所以适用于所有的java语言,适合商业场景。 项目地址:https://github.com/ACINQ/eclair c-lightning用C实现的一个规范兼容的闪电网络。 项目地址:https://github.com/ElementsProject/lightning 其他实现 Raiden Network - Python实现 lit - Go实现 lightning-onion - Go实现 ptarmigan - C实现 其他资料 Awesome-lightning-network]]></content>
<categories>
<category>区块链</category>
</categories>
<tags>
<tag>区块链</tag>
<tag>比特币</tag>
<tag>闪电网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用Docker容器构建Bitcoin]]></title>
<url>%2F2019%2F03%2F21%2F%E4%BD%BF%E7%94%A8Docker%E5%AE%B9%E5%99%A8%E6%9E%84%E5%BB%BABitcoin%2F</url>
<content type="text"><![CDATA[虽然说Bitcoin Core有编译好的客户端。但如果能自己编译下源码,感觉也还是不错的。所以就有了以下的图文: 前置条件 已经安装好Docker 能够科学上网 构建Bitcoin客户端从源码构建bitcoin客户端,我们分为以下几步: 选择一个Linux,并安装系统依赖库 从github下载代码库,并切换最新的版本 运行autogen.sh脚本 运行configure脚本 运行make和make install 验证bitcoin安装成功 所以,我们的Dockerfile如下: 1234567891011121314151617181920212223242526272829303132333435# 基于alpine linux构建FROM alpine# 安装安装系统依赖库RUN apk update && apk add git \ make \ file \ autoconf \ automake \ build-base \ libtool \ db-c++ \ db-dev \ boost-system \ boost-program_options \ boost-filesystem \ boost-dev \ libressl-dev \ libevent-dev# 下载源码,并切换到最新的分支RUN git clone https://github.com/bitcoin/bitcoin --branch v0.17.0.1 --single-branch# 运行autogen.sh脚本 以及 运行configure脚本 以及 运行make和make installRUN (cd bitcoin && ./autogen.sh && \ ./configure --disable-tests \ --disable-bench --disable-static \ --without-gui --disable-zmq \ --with-incompatible-bdb \ CFLAGS='-w' CXXFLAGS='-w' && \ make -j 4 && \ strip src/bitcoind && \ strip src/bitcoin-cli && \ strip src/bitcoin-tx && \ make install ) 运行docker build -f bitcoin.dockerfile -t bitcoin-alpine .构建一个docker镜像。 在成功构建镜像后,通过命令运行镜像docker run -it bitcoin-alpine-bin。 bitcoind 默认的安装位置是/usr/local/bin。你可以通过询问系统下面2个可执行文件的路径,来确认bitcoin是否安装成功。 12345/ # which bitcoind/usr/local/bin/bitcoind/ # which bitcoin-cli/usr/local/bin/bitcoin-cli 然后,在运行的容器中运行bitcoin的后台程序: bitcoind -server=1 -rest=1 -regtest -txindex=1 -daemon 通过运行bitcoin-cli -regtest getblockchaininfo,bitcoin-cli -regtest getnetworkinfo,bitcoin-cli -regtest getwalletinfo可以获得到相关的状态信息。 其中: 123- getblockchaininfo: blocks, difficulty, chain- getnetworkinfo: version, protocolversion, timeoffset, connections, proxy, relayfee, warnings- getwalletinfo: balance, keypoololdest, keypoolsize, paytxfee, unlocked_until, walletversion 参考: https://leftasexercise.com/2018/04/12/building-a-bitcoin-container-with-docker/ https://www.codeooze.com/blockchain/bitcoind-running-in-docker/ http://blog.fens.me/bitcoin-install/ https://github.com/alexfoster/bitcoin-dockerfile/blob/master/Dockerfile]]></content>
<categories>
<category>区块链</category>
</categories>
<tags>
<tag>Docker</tag>
<tag>区块链</tag>
<tag>比特币</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于Docker搭建比特币测试网络]]></title>
<url>%2F2019%2F03%2F21%2F%E5%9F%BA%E4%BA%8EDocker%E6%90%AD%E5%BB%BA%E6%AF%94%E7%89%B9%E5%B8%81%E6%B5%8B%E8%AF%95%E7%BD%91%E7%BB%9C%2F</url>
<content type="text"><![CDATA[搭建比特币测试网络下载比特币测试网络镜像1~ docker pull freewill/bitcoin-testnet-box 运行docker images查看: 12REPOSITORY TAG IMAGE ID CREATED SIZEfreewil/bitcoin-testnet-box latest 4dac47b76f09 4 months ago 262MB 运行Docker镜像1docker run -t -i -p 19001:19001 -p 19011:19011 freewil/bitcoin-testnet-box 上述命令中的19001 和 19011是配置给两个节点提供RPC服务的端口。 启动比特币测试网络运行make start命令启动。 12345tester@84fdf4019179 ~/bitcoin-testnet-box$ make startbitcoind -datadir=1 -daemonBitcoin server startingbitcoind -datadir=2 -daemonBitcoin server starting 查看节点信息使用make getinfo命令获得节点信息 12345678910111213141516171819202122232425262728293031323334353637tester@84fdf4019179 ~/bitcoin-testnet-box$ make getinfobitcoin-cli -datadir=1 -getinfo{ "version": 160200, "protocolversion": 70015, "walletversion": 159900, "balance": 0.00000000, "blocks": 0, "timeoffset": 0, "connections": 1, "proxy": "", "difficulty": 4.656542373906925e-10, "testnet": false, "keypoololdest": 1544519547, "keypoolsize": 1000, "paytxfee": 0.00000000, "relayfee": 0.00001000, "warnings": ""}bitcoin-cli -datadir=2 -getinfo{ "version": 160200, "protocolversion": 70015, "walletversion": 159900, "balance": 0.00000000, "blocks": 0, "timeoffset": 0, "connections": 1, "proxy": "", "difficulty": 4.656542373906925e-10, "testnet": false, "keypoololdest": 1544519547, "keypoolsize": 1000, "paytxfee": 0.00000000, "relayfee": 0.00001000, "warnings": ""} 初始化和测试区块链数据产生区块使用make generate命令产生一个区块。 12345tester@84fdf4019179 ~/bitcoin-testnet-box$ make generatebitcoin-cli -datadir=1 generate 1[ "793a86b807ac6595529bfcbd58118fe09706aa64e736fd9448f63cb7d38a9eb1"] 使用make generate BLOCKS=数量命令批量产生区块。 1234567891011121314tester@84fdf4019179 ~/bitcoin-testnet-box$ make generate BLOCKS=10bitcoin-cli -datadir=1 generate 10[ "3a9d94273198bee2c448fcdeb2925a8e2685adc8f239f77fc64ce8b6f637b9aa", "3bd20765fa0162e1a17b2051c5b5711bfb271b25c88238f42a4351301d9e5ce6", "308e612999026677733ffd2954833e58d456e663e982037880f455ee5932c59c", "0b62ecc7990ac6770848655f10d4277d89df4ef1092fb7e25c0b2aa87a2bcbec", "5de9fbe62f7dc499568b41300610e3aa8b489a61375b3fa26717f65eab85392a", "14a000cae88c1bd881c06ba24a7a31364ff369578912e5a5abcf4023a0a28557", "414b9361debb64366cdf61b1acf5211e17b5f4f978ae5bfcad1aac916503e8a9", "75d1176835a12cf09cd51663fd7bd40e8e4fef15b392bf7e6ff94188398da041", "3d71ee9885fc9586013ce1c2954e197d726b25981c6977195e184fa90cdfa71d", "149dfcdc4cdfcbeeaf432bab90deca9a7b2b3935d359366ce652b071d6b72bb4"] 参考https://blog.csdn.net/yzpbright/article/details/81004202 https://www.codeooze.com/blockchain/bitcoind-running-in-docker/]]></content>
<categories>
<category>区块链</category>
</categories>
<tags>
<tag>Docker</tag>
<tag>区块链</tag>
<tag>比特币</tag>
</tags>
</entry>
<entry>
<title><![CDATA[WebMvcConfigurationSupport和WebMvcConfigurer]]></title>
<url>%2F2019%2F01%2F20%2FWebMvcConfigurationSupport%E5%92%8CWebMvcConfigurer%2F</url>
<content type="text"><![CDATA[SpringBoot帮我们做了很多的事情,但是有的时候会有自定义的Handler,Interceptor,ViewResolver,MessageConverter等,该怎么配置呢?为什么继承了WebMvcConfigurationSupport后有些配置会不生效呢?WebMvcConfigurer又是什么呢? WebMvcConfigurationSupport我们继承WebMvcConfigurationSupport可以自定义SpringMvc的配置。 跟踪发现DelegatingWebMvcConfiguration类是WebMvcConfigurationSupport的一个实现类,DelegatingWebMvcConfiguration类的setConfigurers方法可以收集所有的WebMvcConfigurer实现类中的配置组合起来,组成一个超级配置(这些配置会覆盖掉默认的配置)。而@EnableWebMvc又引入了DelegatingWebMvcConfiguration。 所以,我们继承了WebMvcConfigurationSupport,而后使用@EnableWebMvc会覆盖掉原来的配置。 WebMvcConfigurerWebMvcConfigurer配置类其实是Spring内部的一种配置方式,采用JavaBean的形式来代替传统的xml配置文件形式进行针对框架个性化定制。 在官方文档中有这么一段话: > If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. 所以,如果我们想要在Auto-configuration的基础上配置自定义的interceptors, formatters, view controllers等功能话,我们可以实现WebMvcConfigurer,并用@Configuration注释。 WebMvcConfigurer的主要方法有: configurePathMatch:配置路由请求规则 configureContentNegotiation:内容协商配置 configureAsyncSupport configureDefaultServletHandling:默认静态资源处理器 addFormatters:注册自定义转化器 addInterceptors:拦截器配置 addResourceHandlers:资源处理 addCorsMappings:CORS配置 addViewControllers:视图跳转控制器 configureViewResolvers:配置视图解析 addArgumentResolvers:添加自定义方法参数处理器 addReturnValueHandlers:添加自定义返回结果处理器 configureMessageConverters:配置消息转换器。重载会覆盖默认注册的HttpMessageConverter extendMessageConverters:配置消息转换器。仅添加一个自定义的HttpMessageConverter. configureHandlerExceptionResolvers:配置异常转换器 extendHandlerExceptionResolvers:添加异常转化器 getValidator getMessageCodesResolver 使用方式 使用@EnableWebMvc注解 等于 扩展了WebMvcConfigurationSupport,但是没有重写任何方法 使用“extends WebMvcConfigurationSupport”方式(需要添加@EnableWebMvc),会屏蔽掉springBoot的@EnableAutoConfiguration中的设置 使用“implement WebMvcConfigurer”可以配置自定义的配置,同时也使用了@EnableAutoConfiguration中的设置 使用“implement WebMvcConfigurer + @EnableWebMvc”,会屏蔽掉springBoot的@EnableAutoConfiguration中的设置 这里的“@EnableAutoConfiguration中的设置”是指,读取 application.properties 或 application.yml 文件中的配置。 所以,如果需要使用springBoot的@EnableAutoConfiguration中的设置,那么就只需要“implement WebMvcConfigurer”即可。如果,需要自己扩展同时不使用@EnableAutoConfiguration中的设置,可以选择另外的方式。]]></content>
<categories>
<category>Spring</category>
<category>Spring Boot</category>
</categories>
<tags>
<tag>实战</tag>
<tag>Spring Boot</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于Docker搭建以太坊私有网络]]></title>
<url>%2F2018%2F12%2F18%2F%E5%9F%BA%E4%BA%8EDocker%E6%90%AD%E5%BB%BA%E4%BB%A5%E5%A4%AA%E5%9D%8A%E7%A7%81%E6%9C%89%E7%BD%91%E7%BB%9C%2F</url>
<content type="text"><![CDATA[Running in Docker参考官方的Running in Docker,具体的解释如下: 运行节点拉取镜像: docker pull ethereum/client-go 启动一个节点: docker run -it -p 30303:30303 ethereum/client-go 启动一个节点并在8545上运行JSON-RPC接口: docker run -it -p 8545:8545 -p 30303:30303 ethereum/client-go --rpc --rpcaddr "0.0.0.0" 注意:“0.0.0.0”参数会在8545接口上接收所有主机发送的请求,公共网络慎用! 使用javascript控制台进行交互操作,可运行下命令启动节点: docker run -it -p 30303:30303 ethereum/client-go console 指定区块链数据存储位置docker run -it -p 30303:30303 -v /path/on/host:/root/.ethereum ethereum/client-go -v参数指定了存储的路径。上面的命令会将/root/.ethereum挂载到本地路径/path/on/host下面。这样,容器启动以后,数据的实际存储路径就在/path/on/host下。 使用启动的节点运行上面的命令后,节点会自动去拉取主网的区块数据。我们可以连接到运行的容器中去执行各个操作。 运行docker ps -a查看容器实例。 123~ docker ps -aCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES3791b8c1df57 ethereum/client-go "geth" 19 minutes ago Exited (0) 6 minutes ago eloquent_wiles 执行命令docker exec -it eloquent_wiles /bin/sh,其中eloquent_wiles是容器的名字。这样,它就连接到了现在运行的容器了。 示例操作: 12345678910111213141516171819202122232425262728/ # geth attachWARN [12-18|07:49:15.868] Sanitizing cache to Go's GC limits provided=1024 updated=666Welcome to the Geth JavaScript console!instance: Geth/v1.8.19-unstable-9a000601/linux-amd64/go1.11.2 modules: admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0> eth.syncing{ currentBlock: 11504, highestBlock: 6907887, knownStates: 40307, pulledStates: 17241, startingBlock: 725}> eth.blockNumber0> net.peerCount2> eth.syncing{ currentBlock: 27043, highestBlock: 6907887, knownStates: 61031, pulledStates: 40281, startingBlock: 725}> exit 默认情况下,当节点启动时会在 datadir 指定的目录之下,生成一个名字为 geth.ipc 的文件,当程序关闭时此文件随之消失。可以使用–ipcpath参数修改掉路径。可以配合 attach 命令来进入与 geth 节点进行 js 交互的窗口。基本命令为:/geth attach rpc:/path/on/host/geth.ipc 关于eth.blockNumber返回0的原因可以查看: eth.blockNumber is 0 while syncing #14338 eth.getBlock(“latest”).number is always 0 #16147 个性化Docker启动命令先来一条执行的命令: 1docker run -it -d --name ethereum-node -p 8545:8545 -p 30303:30303 --network eth-network --ip 192.168.0.34 -v /path/on/host:/root/.ethereum ethereum/client-go --rinkeby --rpc --rpcaddr "0.0.0.0" --rpcapi "admin,debug,eth,miner,net,personal,shh,txpool,web3,db" --nodiscover --networkid 15 --fast --cache=512 --dev console 3>>eth.log" Docker参数-it:交互式运行模式,-i 标准输入给容器,-t 分配一个虚拟终端 -d:以守护进程方式运行(后台) -p:指定端口号 -P:随机分配端口号 –name:指定容器名称 –network:指定网络连接 –ip:分配ip地址 Ethereum参数–rinkeby:运行rinkeby网络,不设置默认为主网。 –rpc:启用HTTP-RPC服务 –rpcaddr:HTTP-RPC服务监听接口(默认:localhost) –rpcapi:HTTP-RPC接口提供的api(默认:eth、net、web3) –fast:快速同步模式启动Geth –cache=512:内存容量分配 –dev:开发模式 –nodiscover:关闭节点自动发现,允许手动连接 –networkid:设置隔离网络(主网络id为1) –verbosity 日志等级:0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=detail (default: 3) console:进入JavaScript控制台 更多的参数可以见下节。 geth命令geth的命令可以在客户端运行geth -help或者在Command Line Options中查看。 中文资料可以参考:以太坊客户端Geth命令用法-参数详解 实战经验 关于数据目录。启动时一定要指定一个足够大硬盘的路径,不建议使用默认路径,此路径在后面使用的过程中会频繁用到,如果私钥文件没有单独存储,那么也将存储在此目录下。 keystore 目录下的文件一定要慎重保管,一旦丢失将永远丢失对应地址上的资产,它们和创建账户时设置的密码一一匹配。 在真实生产环境中 rpcapi 的 personal、admin 等级别较高的操作权限慎重开启,一旦使用不当会导致系统安全问题。]]></content>
<categories>
<category>区块链</category>
</categories>
<tags>
<tag>Docker</tag>
<tag>区块链</tag>
<tag>以太坊</tag>
</tags>
</entry>
<entry>
<title><![CDATA[《精通比特币》第二版]]></title>
<url>%2F2018%2F12%2F14%2F%E3%80%8A%E7%B2%BE%E9%80%9A%E6%AF%94%E7%89%B9%E5%B8%81%E3%80%8B%E7%AC%AC%E4%BA%8C%E7%89%88%2F</url>
<content type="text"><![CDATA[为了方便查阅,将精通比特币整理到这边。 目录如下: Introduction 中文版序言 译者序 序言 第二版更新内容 词汇表 第一章介绍 第二章比特币原理 第三章比特币核心 第四章密钥和地址 第五章钱包 第六章交易 第七章高级交易和脚本 第八章比特币网络 第九章区块链 第十章挖矿和共识 第十一章比特币安全 第十二章比特币应用 附录1:比特币改进提案(BIP) 附录2:Bitcore 附录3:Bitcoin Explorer(bx)命令 附录4:pycoin库、实用密钥程序ku和交易程序tx 附录5 交易脚本的操作符、常量和符号 附录6 比特币白皮书 附录7 隔离见证 附录8 染色币]]></content>
<categories>
<category>区块链</category>
</categories>
<tags>
<tag>区块链</tag>
<tag>BTC</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Docker基本命令]]></title>
<url>%2F2018%2F11%2F14%2FDocker%E5%9F%BA%E6%9C%AC%E5%91%BD%E4%BB%A4%2F</url>
<content type="text"><![CDATA[Docker基本命令Docker命令分为两大类:客户端命令和服务端命令。前者主要是操作接口,后者用来启动Docker daemon。 客户端命令基本格式:docker [OPTIONS] COMMAND [arg...] 服务端命令基本格式:docker daemon [OPTIONS] Docker客户端命令帮助执行 docker --help命令可以列出Docker的使用帮助,具体如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576Usage: docker [OPTIONS] COMMANDA self-sufficient runtime for containersOptions: --config string 设置docker客户端的配置文件地址 -D, --debug 开启调试模式,true|false -H, --host list 需要连接的虚拟机的接口 -l, --log-level string 设置日志的等级 --tls 是否使用TLS协议(安全传输层协议),true | false --tlscacert string tls CA签名的可信证书文件路径 --tlscert string TLS可信证书文件路径 --tlskey string TLS秘钥文件路径 --tlsverify 是否启用TLS校验 true|false -v, --version 输出版本信息并退出管理命令: checkpoint 管理检查点 config 管理集群中的配置信息 container 管理容器 image 管理镜像 network 管理容器的网络,包括查看、创建、删除、挂载、卸载等 node 管理Docker的集群结点,包括查看、更新、删除、提升/取消管理节点等 plugin 管理插件 secret 管理Docker敏感数据 service 管理Docker服务,包括创建、更新、删除等 stack 管理Docker堆栈 swarm 管理docker集群,包括创建、加入、退出、更新等 system 管理Docker trust 管理Docker镜像的信任问题 volume 管理docker的数据容器,包括查看、创建、删除等命令: attach 添加到一个正在运行的容器 build 通过dockerfile创建一个镜像 commit 从容器的变更中生成一个新镜像 cp 在容器和宿主文件系统中复制文件和文件夹 create 创建一个新的容器(只创建不允许) deploy 部署新堆栈或更新现有堆栈 diff 在容器中进行文件对比 events 获取服务器的实时事件 exec 在容器中运行命令 export 将一个容器的文件以tar压缩包的形式导出 history 显示镜像的历史记录 images 显示镜像列表 import 从本地文件中导入镜像 info 显示系统层的信息 inspect 显示更底层的容器、镜像和任务信息 kill 杀掉一个或多个正在运行的容器 load 加载容器 login 登录到一个镜像仓库 logout 退出镜像仓库 logs 获取容器的日志 pause 停止容器的进程 port 显示出容器的所有端口 ps 显示容器列表 pull 从镜像仓库中拉取一个镜像 push 将一个镜像推送到一个镜像仓库 rename 重命名一个容器 restart 重新启动容器 rm 移除容器 rmi 移除镜像 run 在容器中运行命令 save 将容器保存为一个压缩包 search 在docker hub中搜索镜像 start 启动容器 stats 显示实时的容器状态 stop 停止容器 tag 给镜像加标签 top 显示容器正在运行的进程 unpause 恢复容器中的所有进程 update 更新容器中的配置 version 显示docker的版本信息 wait 阻塞进程一直到容器被停止Run 'docker COMMAND --help' for more information on a command. 参考: docker常用命令总结 Docker常用命令 Docker常用命令大全 Docker从入门到实践]]></content>
<categories>
<category>Docker</category>
</categories>
<tags>
<tag>Docker</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SpringFramework常用的注解说明]]></title>
<url>%2F2018%2F09%2F28%2FSpringFramework%E5%B8%B8%E7%94%A8%E7%9A%84%E6%B3%A8%E8%A7%A3%E8%AF%B4%E6%98%8E%2F</url>
<content type="text"><![CDATA[在进行SpringBoot开发的时候经常要用到注解,有的时候很容易忘记某个注解的意思,现将一些常用的注解说明归纳如下: 注解 解释 @Required @Required注解检查。但它只检查属性是否已经设置而不会测试属性是否非空。@Required只能设置在setter方法上 @Autowired Spring提供的注入工具【由Spring的依赖注入工具(BeanPostProcessor、BeanFactoryPostProcessor)自动注入】 @Qualifier 如果需要byName(byName就是通过id去标识)注入,增加@Qualifier注释。一般在候选Bean数目不为1时应该加@Qualifier注释。 @Configuration 在用于指定配置信息的类上加上 @Configuration 注解,以明确指出该类是 Bean 配置的信息源。 @ComponentScan @ComponentScan告诉Spring 哪个packages 的用注解标识的类 会被spring自动扫描并且装入bean容器 @Bean @Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中。 @Lazy 如果我们想要 Spring 在启动的时候延迟加载 bean,即在调用某个 bean 的时候再去初始化,那么就可以使用 @Lazy 注解。 @Value 使用@Value注解,可以直接将属性值注入到beans中。 @Resource @Resource用法与@Autowired 用法 用法相似,也是做依赖注入的,从容器中自动获取bean。 @Inject 这是jsr330中的规范,通过‘AutowiredAnnotationBeanPostProcessor’ 类实现的依赖注入。 @PropertySource 指定文件地址。提供了一种方便的、声明性的机制,用于向Spring的环境添加PropertySource。与@configuration类一起使用。 @PostConstruct 标注在方法上,该方法在构造函数执行完成之后执行。 @PreDestroy 标注在方法上,该方法在对象销毁之前执行。 @ActiveProfiles 用来声明活动的profile–@ActiveProfiles(“prod”(这个prod定义在配置类中)) @Profile 表示当一个或多个指定的文件是活动的时,一个组件是有资格注册的。使用@Profile注解类或者方法,达到在不同情况下选择实例化不同的Bean。@Profile(“dev”)表示为dev时实例化。 @Component 表示一个带注释的类是一个“组件”,成为Spring管理的Bean。当使用基于注解的配置和类路径扫描时,这些类被视为自动检测的候选对象。同时@Component还是一个元注解。 @Controller 组合注解(组合了@Component注解),应用在MVC层(控制层),DispatcherServlet会自动扫描注解了此注解的类,然后将web请求映射到注解了@RequestMapping的方法上。 @Service 组合注解(组合了@Component注解),应用在service层(业务逻辑层) @Reponsitory 组合注解(组合了@Component注解),应用在dao层(数据访问层) @RestController @RestController注解相当于@ResponseBody + @Controller合在一起的作用。 @ResponseBody 将返回值放在response体内。返回的是数据而不是页面 @RequestBody 允许request的参数在request体中,而不是在直接链接在地址的后面。此注解放置在参数前。 @PathVariable 放置在参数前,用来接受路径参数。 @ModelAttribute 将键值对添加到全局,所有注解了@RequestMapping的方法可获得次键值对(就是在请求到达之前,往model里addAttribute一对name-value而已)。 @RequestAttribute 注解@RequestAttribute可以被用于访问由过滤器或拦截器创建的、预先存在的请求属性 @RequestHeader 获得指定的请求中的Header信息 @RequestParam 请求参数绑定 @ResponseStatus 带有@ResponseStatus注解的异常类会被ResponseStatusExceptionResolver 解析。可以实现自定义的一些异常,同时在页面上进行显示。 @CookieValue 用来获取Cookie中的值 @CrossOrigin @CrossOrigin是用来处理跨域请求的注解 @RequestMapping 用来映射web请求(访问路径和参数),处理类和方法的。可以注解在类和方法上,注解在方法上的@RequestMapping路径会继承注解在类上的路径。同时支持Serlvet的request和response作为参数,也支持对request和response的媒体类型进行配置。其中有value(路径),produces(定义返回的媒体类型和字符集),method(指定请求方式)等属性。 @GetMapping GET请求 @PostMapping POST请求 @PutMapping PUT请求 @PatchMapping PATCH请求 @DeleteMapping DELETE请求 @ExceptionHandler 用在方法上定义全局处理,通过他的value属性可以过滤拦截的条件:@ExceptionHandler(value=Exception.class)–表示拦截所有的Exception。 @ControllerAdvice 用在类上,声明一个控制器建言,它也组合了@Component注解,会自动注册为Spring的Bean @InitBinder 通过@InitBinder注解定制WebDataBinder(用在方法上,方法有一个WebDataBinder作为参数,用WebDataBinder在方法内定制数据绑定,例如可以忽略request传过来的参数Id等)。 @SessionAttribute @SessionAttribute作用于处理器类上,用于在多个请求之间传递参数,类似于Session的Attribute,但不完全一样,一般来说@SessionAttribute设置的参数只用于暂时的传递,而不是长期的保存,长期保存的数据还是要放到Session中。 @SessionAttributes @sessionattributes注解应用到Controller上面,可以将Model中的属性同步到session当中。 @Aspect 声明一个切面(就是说这是一个额外功能) @After 后置建言(advice),在原方法前执行。 @Before 前置建言(advice),在原方法后执行。 @Around 环绕建言(advice),在原方法执行前执行,在原方法执行后再执行(@Around可以实现其他两种advice) @PointCut 声明切点,即定义拦截规则,确定有哪些方法会被切入 @EnableAspectJAutoProxy 开启Spring对AspectJ的支持 @SpingBootApplication SpringBoot的核心注解,主要目的是开启自动配置。它也是一个组合注解,主要组合了@Configurer,@EnableAutoConfiguration(核心)和@ComponentScan。可以通过@SpringBootApplication(exclude={想要关闭的自动配置的类名.class})来关闭特定的自动配置。 @Async 注解在方法上标示这是一个异步方法,在类上标示这个类所有的方法都是异步方法。 @EnableAsync 开启异步任务支持。注解在配置类上。 @Scheduled 注解在方法上,声明该方法是计划任务。支持多种类型的计划任务:cron,fixDelay,fixRate @EnableScheduling 注解在配置类上,开启对计划任务的支持。 @EnableAutoConfiguration 此注释自动载入应用程序所需的所有Bean——这依赖于Spring Boot在类路径中的查找。该注解组合了@Import注解,@Import注解导入了EnableAutoCofigurationImportSelector类,它使用SpringFactoriesLoader.loaderFactoryNames方法来扫描具有META-INF/spring.factories文件的jar包。而spring.factories里声明了有哪些自动配置。 @WebAppConfiguration 一般用在测试上,注解在类上,用来声明加载的ApplicationContext是一个WebApplicationContext。他的属性指定的是Web资源的位置,默认为src/main/webapp,我们可以修改为:@WebAppConfiguration(“src/main/resources”)。 @Cacheable 声明数据缓存 @EnableWebMvc 用在配置类上,开启SpringMvc的Mvc的一些默认配置:如ViewResolver,MessageConverter等。同时在自己定制SpringMvc的相关配置时需要做到两点:1.配置类继承WebMvcConfigurerAdapter类2.就是必须使用这个@EnableWebMvc注解。 @BeforeTransaction @BeforeTransaction在事务之前执行 @AfterTransaction @AfterTransaction在事务之后执行 @Transactional 声明事务(一般默认配置即可满足要求,当然也可以自定义) @ImportResource 虽然Spring提倡零配置,但是还是提供了对xml文件的支持,这个注解就是用来加载xml配置的。 @ConfigurationProperties 将properties属性与一个Bean及其属性相关联,从而实现类型安全的配置。 @Conditional 根据满足某一特定条件创建特定的Bean @ConditionalOnBean 条件注解。当容器里有指定Bean的条件下。 @ConditionalOnClass 条件注解。当类路径下有指定的类的条件下。 @ConditionalOnExpression 条件注解。基于SpEL表达式作为判断条件。 @ConditionalOnJava 条件注解。基于JVM版本作为判断条件。 @ConditionalOnJndi 条件注解。在JNDI存在的条件下查找指定的位置。 @ConditionalOnMissingBean 条件注解。当容器里没有指定Bean的情况下。 @ConditionalOnMissingClass 条件注解。当类路径下没有指定的类的情况下。 @ConditionalOnNotWebApplication 条件注解。当前项目不是web项目的条件下。 @ConditionalOnResource 条件注解。类路径是否有指定的值。 @ConditionalOnSingleCandidate 条件注解。当指定Bean在容器中只有一个,后者虽然有多个但是指定首选的Bean。 @ConditionalOnWebApplication 条件注解。当前项目是web项目的情况下。 @EnableConfigurationProperties 注解在类上,声明开启属性注入,使用@Autowired注入。 @AutoConfigureAfter 在指定的自动配置类之后再配置。 @AutoConfigureBefore 在指定的自动配置类之前进行配置。 @RunWith 这个是Junit的注解,springboot集成了junit。一般在测试类里使用。 @ContextConfiguration 用来加载配置ApplicationContext,其中classes属性用来加载配置 参考: Spring 注解大全与详解 Spring Boot学习笔记 SPRING FRAMEWORK ANNOTATIONS]]></content>
<categories>
<category>Spring</category>
<category>Spring Boot</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>Spring Boot</tag>
</tags>
</entry>
<entry>
<title><![CDATA[区块链技术概览]]></title>
<url>%2F2018%2F09%2F03%2F%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E6%A6%82%E8%A7%88%2F</url>
<content type="text"><![CDATA[本文摘自《白话区块链》 区块链技术理念区块链的本质就是一种记账方法,是通过“区块链客户端”来记账。运行中的客户端软件称为“节点”。 在区块链系统中,所有的节点都是可以互相通信的,这个功能称为“网络路由” 共识算法就是一种大家都遵守的筛选方案。在选出一个节点后,则一段时间内的账务数据都以这个节点记录为准,这个节点记录后会广播出去,告诉其他的节点,其他节点只需要通过网络来接收新的数据,接收后各自根据自己现有的账本验证一下能不能接得上,有没有不匹配和不规范的,如果都符合要求,就存储到自己的账本中。 有些区块链系统在记账环节会设计出一种带有竞争的机制,让各个节点去抢,谁抢到这个机会谁就能获得打包数据的权利并且同时获得这笔奖励。这个过程称为“挖矿”。 在区块链系统中,识别不同的使用者是通过一种密码算法来实现的。这个算法分为公钥和私钥,公钥可以给别人,私钥自己保管。公钥是用来做身份识别的,从公钥可以算出地址来代表一个用户。用公钥加密的数据必须用对应的私钥才能解密,用私钥加密(通常称为“签名”)的数据必须用对应的公钥才能解密。 区块链技术栈区块链是将产生的数据按照一定的时间间隔,分成一个个的数据块记录,然后再根据数据块大的先后关系串联起来。 区块链的基本技术:区块链账本、共识机制、密码算法、网络路由、脚本系统。 区块链账本:表示一种特有的数据记录格式。每一个数据块之间通过某个标志连接起来,从而形成一条链。区块数据在逻辑上分成了区块头和区块体。每个区块头中通过梅克尔根关联了区块体众多的交易事务,每个区块之间通过区块头哈希值串联起来。 共识算法:在区块链系统中,每个节点必须要做的事情就是让自己的账本跟其他节点的账本保持一致。共识算法就是一个规则,每个节点都按照这个规则去确认各自的数据。共识算法其实也是一种筛选方案,比如PoW(Proof of Work,工作量证明)、PoS(Proof of Stake,权益证明)、DPoS(Delegate Proof of Stake,委托权益证明)、PBFT(Practical Byzantine Fault Tolerance,实用拜占庭容错算法)等。 密码算法:主要用到,哈希算法、梅克尔树。 脚本系统:脚本系统可以使区块链中实现各种各样的业务功能。 网络路由:区块链系统是一个分布式系统,各个节点之间的通信靠的就是网络路由。 区块链分类和架构区块链系统实际上是一个维护公共数据账本的系统,一切技术单元的设计都是为了更好地维护这个公共数据账本。通过共识算法达成节点的账本数据一致;通过密码算法确保账本数据的不可篡改性以及数据发送的安全性;通过脚本系统扩展账本数据的表达范畴。区块链系统实际上就是一种特别设计的数据库系统或者说分布式数据库系统。 区块链1.0架构这个阶段的区块链系统主要是用来实现数字货币的。 矿工要对区块数据进行打包;矿工能获得系统的奖励。钱包工具提供给用户管理自己账户地址以及余额。浏览器用来查看当前区块链网络中发送的数据情况(比如,最新区块高度、内存池的交易数等)RPC客户端和命令行接口用来访问核心节点 区块链2.0架构区块链2.0的代表产品是以太坊。最大的区别是支持智能合约,用时拥有以太坊虚拟机 区块链3.0架构区块链3.0是将区块链技术作为一种泛解决方案,可以面向行业应用。 区块链3.0可以看做是一套框架,通过对框架的配置和二次开发可以适应各行业的需求。“可插拔共识”意思就是共识机制是不固定的,可配置的。 区块链分类根据网络范围分类: 公有链:完全对外开放的,任何人都可以使用,没有权限的设定,完全公开透明。没有第三方管理、依靠一组事先约定好的规则。这个规则确保每个参与者在不信任的网络环境中能够发起可靠的交易事务。 私有链:不对外开放,仅仅在组织内部使用。 联盟链:网络范围介于公有链和私有链之间。使用在多个成员角色的环境中,如银行之间的支付结算、企业之间的物流等。 根据部署环境分类: 主链:部署在生产环境的真正的区块链系统。 测试链:开发者为了方便大家学习使用而提供的测试用途的区块链网络。 根据对接类型分类: 单链:能够单独运行的区块链系统都称为“单链”。比如:比特币主链、测试链,以太坊主链、测试链。 侧链:属于一种区块链的跨链技术。 互联链:所有的区块链都互联起来(只能说是如果可以的话)]]></content>
<categories>
<category>区块链</category>
</categories>
<tags>
<tag>区块链</tag>
</tags>
</entry>
<entry>
<title><![CDATA[以太坊学习摘要]]></title>
<url>%2F2018%2F08%2F24%2F%E4%BB%A5%E5%A4%AA%E5%9D%8A%E5%AD%A6%E4%B9%A0%E6%91%98%E8%A6%81%2F</url>
<content type="text"><![CDATA[以太坊以太坊(Ethereum)是一个建立在区块链技术之上, 去中心化应用平台。它允许任何人在平台中建立和使用通过区块链技术运行的去中心化应用。与比特币相比,以太坊属于区块链2.0的范畴。 从平台角度来看,以太坊类似于苹果或者安卓的应用商店;从技术角度来说,以太坊类似于一个区块链操作系统。 下图是以太坊的组成: 所以,可以理解为:“以太坊=区块链+智能合约”。开发者在以太坊上可以开发任意的应用,实现任意的智能合约。以太坊的虚拟机和智能合约扩展了外部应用程序在区块链技术上的应用。另外,以太坊中的智能合约是运行在虚拟机(也就是EVM,Ethereum Virtual Machine,以太坊虚拟机)上的。 智能合约智能合约就是以太坊上的程序,它是代码和数据(状态)的集合。所以,不管什么样功能的合约,站在技术的角度上来讲,就是通过执行一组程序改变了一些值。 我们不但可以实现数字货币,还可以实现众筹合约、担保合约、融资租赁合约、期货合约以及各种金融与非金融的订单合约。 在以太坊中,每个合约都有一个唯一的地址来标识它自己(由创建者的哈希地址和曾经发送过的交易的数量推算出来)。客户端可以与这个地址进行交互,可以发送ether,调用函数,查询当前的状态等。 有三种常见的智能合约语言,这些语言可以被编译成智能合约运行在以太坊虚拟矿机上。它们是: Solidity:和Javascript语言类似。这是目前最受欢迎的和功能丰富的智能合约脚本语言。 Serpent:和Python语言类似,在以太坊历史的早期受欢迎。 LLL (Lisp Like Language):和Lisp类似,只有在早期使用。它大概是最难用的。 状态状态可以理解为以太坊中的某些内容发生变化。 对于区块链账本,这里的变化可以指一笔转账,也可以是合约的某个规则被激活等,总之就是数据动了,以太坊中将变化的过程称为状态转变函数 在以太坊系统中,状态是由被称为“账户”的对象和在两个账户之间转移价值和信息的状态转换构成的。 以太坊的每个区块头中都包含了指向三棵树(状态树、交易树、数据树)的指针。 账户以太坊具有账户的概念。在以太坊中有两类的账户:外部账户 和 合约账户。 外部账户外部账户(EOA,Externally Owned Account),它就是一个一般账户的概念。外部账户是由一对秘钥定义的,一个私钥一个公钥,公钥的后20位作为地址。 外部账户没有关联任何的代码。 合约账户智能合约的部署是指把合约字节码发布到区块链上,并使用一个特定的地址来标示这个合约,这个地址称为合约账户。合约账户是可编程的,可以执行图灵完备的计算任务,合约账户之间可以传递消息。 区别和联系合约账户的地址是通过合约创建者的地址和该地址发出过的交易数量计算得到的。 一个外部账户可以通过创建和用自己的私钥来对交易进行签名,来发送消息给另一个外部账户或合约账户。 在两个外部账户之间传送消息是价值转移的过程。但从外部账户到合约账户的消息会激活合约账户的代码,允许它执行各种动作(比如转移代币,写入内部存储,挖出一个新代币,执行一些运算,创建一个新的合约等等)。 只有当外部账户发出指令时,合同账户才会执行相应的操作。 账户的结构以太坊中的账户包含以下4个部分: 随机数(Nonce)。用于确定每笔交易智能被处理一次的计数器,也就是每个账户的交易计数,用于防止重放攻击。当某个账户发送一笔交易时,根据已生成的交易数来累加这个数字。 账户目前的以太币余额(Balance)。 账户的存储(Root),它是一个哈希值,指向的是一棵patricia trie(帕夏尔前缀树)。默认为空 账户的合约代码(CodeHash),只有合约账户才有,否则为空。 交易以太坊中的交易就是状态的转换过程。 交易在以太坊中是在签名的数据包,这个数据包中存储了从外部账户发送的消息。所谓的交易就是一个消息,这个消息被发送者签名了。 交易类型: 转账交易。也就是从一个账户往另一个账户转账发以太币。 合约创建交易。也就是创建一个合约,因为创建合约也要消耗以太坊。 合约执行交易。在以太坊中,执行合约也算一种交易。 数据结构 AccountNonce:表明交易的发送者已经发送过的交易数,与账户中定义的随机数对应。 Price和GasLimit:用来抵抗拒绝服务攻击。就是为了让交易的执行带上成本,每进行一次交易都要支付一定的手续费,GasLimit是交易执行所需的计算量,Price是单价,两者的乘积就是手续费。如果交易在执行的过程中实际所需的消耗超出了Gas限制就会出错回滚,如果存在多余的Gas就会退还多余部分。 Recipient:接收方的地址。 Amount:发送的以太坊金额,单位wei。 Payload:交易携带的数据,根据不同的交易类型有不同的用法。 V、R、S:交易的签名数字。 以太坊的区块头中有交易信息。 计量单位最小单位是wei。1 ether = 1000 000 000 000 000 000 wei。 kwei = 1000wei mwei = 1000kwei gwei = 1000mwei szabo = 1000gwei finney = 1000szabo ether = 1000finney 收据在以太坊中,收据是指每条交易执行所影响的数据条,在以太坊的区块头中存储了收据树的根哈希值。 收据实际上是一个数据的统计记录,记录了执行后的特征数据。 数据结构如下: PostState: 状态树的根哈希。通过这个字段使得收据可以直接访问都状态数据。 CumulativeGasUsed:累计的Gas消耗,包含关联的本条交易以及之前的交易所消耗的Gas之和,或者说是指所在区域的Gas消耗之和。 TxHash:交易事务的哈希值。 ContractAddress:合约地址,如果是普通的转账交易则为空。 GasUsed:本条交易消耗的Gas。 Gas在以太坊中Gas可以理解为在以太坊平台上执行程序需要付出的成本或者手续费。 Gas是通过以太坊中合约的执行计算量来决定的,这个计算量可以简单理解为是算力的消耗,执行一次SHA3哈希计算会消耗20个Gas,执行一次普通的转账交易就要消耗21000个Gas。 以太币总额 = 消耗的Gas ✖️ Gas单价。 以太坊客户端以太坊客户端,其实我们可以把它理解为一个开发者工具,它提供账户管理、挖矿、转账、智能合约的部署和执行等等功能。 go-ethereum是官方的Go语言客户端。可用于挖矿、组件私有链、管理账号、部署智能合约等。它提供了一个交互式命令控制台,通过命令控制台中包含了以太坊的各种功能(API)。 除go-ethereum外,官方还提供了cpp-ethereum,基于C++写的。 以太坊网络我们可以把以太网络分为3种:主网、测试网络和私有网络。 生产环境网络(主网): 以太坊的生产网络顾名思义,也就是产生真正有价值的 的以太币的网络。 测试网络: 以太坊的测试网络也是官方提供的,顾名思义就是专供用户来开发、调试和测试的。上面的合约执行不消耗真实的以太币。官方提供的为:Rinkeby 私有网络:以太坊的私有网络,顾名思义就是由用户自己创建的私有网络。 以太币以太币每年的产量则是固定的。 以太币的数量以这种形式存在:Pre-mine(矿前) + Block rewards(区块奖励) + Uncle rewards(叔块奖励) + Uncle referencing rewards(叔块引用奖励) 矿前:2014年7月/8月间,为众筹大约发行了7200万以太币。这些币有的时候被称之为“矿前”。众筹阶段之后,以太币每年的产量被限制在7200万以太币的25%(每年以太币的矿产量,不高于1800万,除了一次性为crowdsale而发行的7200万以太币) 区块奖励:每产生一个新区块就会产生5个新以太币。每年有225万个区块被挖出来,每个区块5个以太币,也就是每年会产出1130万个以太币。 叔块奖励:有些区块被挖得稍晚一些,因此不能称为主区块链的组成部分,以太币称它们为“ uncles”,并且在之后的区块中,可以引用它们。如果uncles在之后的区块链中作为叔块被引用,每个叔块会为挖矿者产出大约4.375个以太币(5个以太币奖励的8分之7).这被称之为叔块奖励。 叔块引用奖励:矿工每引用一个叔块,就得到了大约0.15个以太币(最多引用两个叔块)。 引用: 《白话区块链》 以太坊白皮书 一个基础的以太坊介绍 以太坊是什么 - 以太坊开发入门指南 以太坊开发入门,完整入门篇]]></content>
<categories>
<category>区块链</category>
</categories>
<tags>
<tag>区块链</tag>
<tag>以太坊</tag>
</tags>
</entry>
<entry>
<title><![CDATA[离职前夕的随想]]></title>
<url>%2F2018%2F07%2F25%2F%E7%A6%BB%E8%81%8C%E5%89%8D%E5%A4%95%E7%9A%84%E9%9A%8F%E6%83%B3%2F</url>
<content type="text"><![CDATA[去年,离开ND后的第一天,在去临海的动车上,写了《十年》一文。没想到的是今年,在离开德诺的前一天晚上,又写下了一篇类似的文章。 离开德诺不舍是真,但是浓浓的不甘也是真。故事终究是有聚有散,事情也无法分辨谁对谁错,无非就是那一时刻的选择而已。 最近在看《少有人走的路》,也就刚看了第一章,也许明天开始又会重新从头开始看,不为别的,只是觉得第一章就已经很和我口味了。一年来,特别是今年以来的一些事情让我很无语,其实,现在想来也是自己的问题罢了。“我的时间是我的责任,是我,只有我,能决定怎么安排和利用我的时间”,同理,他人的时间应该也是他人负责,旁人无法左右。 最后,引用“自律是解决人生问题最主要的工具,也是消除人生痛苦最重要的方法。”这句来做个结束吧!希望,新的开始是自己想要的开始,新的征程是充满激情的征程~]]></content>
<categories>
<category>未分类</category>
</categories>
<tags>
<tag>生活</tag>
<tag>杂念</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java面试手册]]></title>
<url>%2F2018%2F07%2F22%2FJava%E9%9D%A2%E8%AF%95%E6%89%8B%E5%86%8C%2F</url>
<content type="text"><![CDATA[Java面试手册《Java面试手册》整理了从业到现在看到的、经历过的一些Java面试题。 主要发布在我的GitHUb上,见:Java面试手册 这些面试题的主要来源是一些网站还有github上的内容,由于平常在收藏到“印象笔记”中的时候没有保留来源出处,如果有介意版权的可以联系我。 主要分为以下部分: Java基础 面向对象 基础 集合 多线程 JVM NIO 设计模式 数据结构与算法 算法 数据结构 JavaWeb HTTP基础 JavaWeb基础 Spring系列 MyBatis Hibernate Tomcat 数据库与缓存 数据库基本理论 缓存基本理论 数据库索引 分库分表 MySQL MongoDB Redis 消息队列 MQ基础 分布式 微服务 安全和性能 安全 性能 网络与服务器 计算机网络 Nginx 软件工程 UML 业务 主要参考: Java面试通关要点汇总集【终极版】 《后端架构师技术图谱》 https://github.com/hadyang/interview https://github.com/crossoverJie/Java-Interview 后台开发常问面试题集锦]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于Token的权限认证服务]]></title>
<url>%2F2018%2F03%2F11%2F%E5%9F%BA%E4%BA%8EToken%E7%9A%84%E6%9D%83%E9%99%90%E8%AE%A4%E8%AF%81%E6%9C%8D%E5%8A%A1%2F</url>
<content type="text"><![CDATA[在这两年接触的项目基本上是基于前后端分离、多种后端服务组合而成的,这有别于先前的“单体”的项目,而且我们还需要考虑外部应用接入的场景、用户-服务的鉴权、服务-服务的鉴权等多种鉴权场景。 本方案基于客户端Token与网关结合的方式。所有的客户端请求都经过网关,网关将校验客户端发送上来的Token等信息,如果通过则转发给对应的服务,如果不通过则直接返回40X给客户端。 架构 获得authorize,用户/设备从UAA Service中获得authorize信息 附加mac token,请求在访问服务时候附加上mac token Gateway进行认证,Gateway将收到的每个mac token发送给UAA Service进行校验 每个服务只有权限去操作自己负责的那部分功能(待规划) 流程 用户/客户端从UAA Service中获得Token 如果UAA Service中无此用户/客户端,则抛出AuthorizationException异常并返回401 如果UAA Service认证成功,返回对应的Token信息 用户的请求数据,请求头Authorization中带上Token信息 网关过滤所有的请求,携带Token、请求信息向UAA Service请求鉴权 如果UAA Service鉴权失败,抛出AuthenticationException异常并返回403 如果UAA Service鉴权成功,返回用户的权限等信息 网关向业务发起请求(此时,不携带授权信息。因为鉴权等都在网关处理了) 业务服务向微服务发起请求,同样也不携带授权信息 返回结果给用户 协议根据开发阶段的鉴权、用户-服务的鉴权、服务-服务的鉴权等不同的场景,定义了三种方式的协议。 Debug Token协议debug token适用于在开发测试环境调试API。 协议在Client发出api请求之前,必须将debug token的信息放在HTTP Header的Authorization里面。 1Authorization:DEBUG userid="123456",realm="" 上面的代码中: userid为调试的用户 realm为可选字段,预定为用户所在的领域(应用、行业或者机构) 示例(伪代码)1234GET /v0.1/resources HTTP/1.1Host: a.dynamax.ioAccept: application/jsonAuthorization: DEBUG userid='',realm='' Mac Token协议mac token适用于不安全网络下的API授权 协议通过“登录接口”可以获得Token。mac_token的数据结构如下: 12345678{ "user_id":"", //用户标识 "access_token":"", //token标识 "expires_at":"", //本token的过期时间 "refresh_token":"", //用以续期 "mac_key":"", //hmac的密钥 "mac_algorithm":"hmac-sha-256" //hmac算法的名称} hmac算法见:[[https://tools.ietf.org/html/rfc2104]] 在Client发出api请求之前,必须将mac token的信息放在HTTP Header的Authorization里面。1Authorization:MAC id="",nonce="",mac="" 上面的代码中: id为mac_token.access_token nonce为 时间戳:随机码(客户端生成),有效时间+-5分钟 mac为请求签名: mac=base64(hmac(mac_token.mac_key,mac_token.mac_algorithm,request_content)) request_content = nonce + \n + http-method + \n + request-url + \n + host + \n http-method,请求的方法,大写,如:GET request-url,请求的地址(包含参数的部分,不包含域名部分),区分大小写,如/v0.1/databases host,为HTTP Header中的host,区分大小写,如dynamax.io 示例(伪代码)获得token request12345678POST /v0.1/tokens HTTP/1.1Host: api.uaa.dynamax.ioAccept: application/jsonContent-Type: application/json{ "username":"330134", "password":"******"} reqponse123456789101112HTTP/1.1 201 CreatedContent-Type: application/json;charset=UTF-8Cache-Control: no-storePragma: no-Cache{ "user_id":"", "access_token":"", "expires_at":"", "refresh_token":"", "mac_key":"", "mac_algorithm":""} 访问资源1234GET /v0.1/resources HTTP/1.1Host: resources.dynamax.ioAccept: application/jsonAuthorization:MAC id="adFeww3Fw4VV09876",nonce="1234234345343:adfasd32",mac="SDFS8weadfa42234" Bearer Token协议bearer token适用于安全网络下的api授权。也就是说,bearer适用于微服务之间的相互调用的api授权。 协议可以通过“bearer_token”的接口获得相应的bearer_token,数据结构如下: 123456{"user_id":"", //用户标识"access_token":"" //token标识"expires_at":"" //本token的过期时间"refresh_token":"" //用以续期} 在Client发出api请求之前,必须将bearer token的信息放在HTTP Header的Authorization里面。 1Authorization:BEARER "XXX123XXXXX" user_id:"" 上面的代码中: “XXX123XXXXX”为bearer_token.access_token user_id可选,该值位获得bearer token对应的账号。该值用来传递用户信息到其他服务端,用来判断是否有权限。 待优化的点 怎么鉴定一个请求是否需要认证?白名单? 各个业务服务以及微服务是否需要再次认证?会不会出现绕过网关的情况? 所有的服务都通过网关,在UAA Service中进行鉴权,会不会造成UAA Service是个瓶颈点? 是否能够很方便地扩展到OAuth2?]]></content>
<categories>
<category>UAA</category>
</categories>
<tags>
<tag>实战</tag>
<tag>权限认证</tag>
</tags>
</entry>
<entry>
<title><![CDATA[RBAC实践]]></title>
<url>%2F2018%2F03%2F01%2FRBAC%E5%AE%9E%E8%B7%B5%2F</url>
<content type="text"><![CDATA[RBAC,即大家很熟悉的“基于角色的访问控制”。理论什么的这里就不讲了,重点介绍下一种基于RBAC的实践。 名词解释 领域:Realm,角色的定义范围。 角色:Role,一组权限的集合。角色必须定义在指定领域内 资源:Resource,主要应用于前端UI元素可见性方面的权限管控 操作:Action,主要应用于服务端api调用合法性方面的管控 权限:Permission,一组“资源+操作”的组合,并具备一个用户易于理解的名称 数据结构角色 ROLE_INFO{ "id":"", // 角色id "tenant":"", // 租户id "code":"", // 角色代码 "realm":"", // 角色领域,global:应用内全局角色;app:app端角色;admin:管理端角色;web: web前端角色; "realm_id":"", // 领域id,选填。 "name":"", // 角色名称 "status":0, // 状态,0 启用,1 禁用 "type":0, // 类型:0 正常角色,1 临时角色,2 默认角色 "valid":1, // 2:未生效,1:生效 "valid_time":"", // 生效时间 "invalid_time":"", // 失效时间 "remark":"管理员", // 角色备注 "permissions": // 分配的权限列表 [ PERMISSION_INFO ] } 资源 RESOURCE_INFO{ "id":"", // 资源id "client":"", // 客户端类型,取值:app,web,admin "code":"", // 资源标识符,客户端控制用,全局唯一 "tag":"", // 资源标签 "remark":"" // 资源名称,管理后台显示用 } 权限 PERMISSION_INFO{ "id":"", // 权限id "name":"", // 权限名称 "code":"", // 权限代码 "status":0, // 状态,0 启用,1 禁用 "remark":"", // 权限备注 "actions":[ // 权限对应的ACTION列表 ACTION_INFO, ACTION_INFO, ... ], "resources":[ // 权限对应的资源列表 RESOURCE_INFO, RESOURCE_INFO, ... ] } 操作 ACTION_INFO{ "id":"", // 操作id "code":"", // 全局唯一:包名.类名.方法 "biz_type":"", // 二级服务需要填写此项 "uri":"", // Restful api,管理后台显示用 "remark":"" // api名称,管理后台显示用 } 业务流程服务端权限校验流程 当前请求接口有@RequirePermission注解,进入鉴权流程。没有@RequirePermission注解,返回鉴权成功 【当前请求接口的@RequirePermission内的权限列表】与【用户的拥有的权限列表】有交集,返回鉴权成功。无交集返回鉴权失败。 RBAC服务器故障,导致角色数据无法获取时,返回鉴权失败,同时启动RBAC Server存活检测线程,存活检查按递增间隔进行检测。发现RBAC Server正常后,重新加载角色数据。 客户端权限校验流程 每个组件将自己的所有UI元素中涉及权限控制的资源做成【权限资源列表】,这些资源默认不可见。 从RBAC获取当前登录用户的可见资源列表。 当用户进入某个page时,获取【用户可见资源列表】与【权限资源列表】的交集,将交集的资源设置为可见。 如果RBAC服务端无法连接,则【用户可见资源列表】为空,这些受控资源不可见。 缓存更新流程 服务端的角色缓存数据通过MQ订阅通知实现实时更新。 客户端如果使用IM组件时,通过IM事件消息触发更新。如果未使用IM组件,采用定时到服务端检查版本变更的策略,如果数据有更新再去RBAC服务端拉取。]]></content>
<categories>
<category>UAA</category>
</categories>
<tags>
<tag>实战</tag>
<tag>RBAC</tag>
</tags>
</entry>
<entry>
<title><![CDATA[多租户设计]]></title>
<url>%2F2018%2F02%2F15%2F%E5%A4%9A%E7%A7%9F%E6%88%B7%E8%AE%BE%E8%AE%A1%2F</url>
<content type="text"><![CDATA[多租户(Multi Tenancy/Tenant)是一种软件架构,其定义是:在一台服务器上运行单个应用实例,它为多个租户提供服务。 多个租户在数据上既有共享又有隔离,常有的存储方式有以下三种: 方案一:独立数据库方案二:共享数据库,隔离数据架构方案三:共享数据库,共享数据架构 独立数据库独立数据库即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。 这种模式下,我们可以为不同的租户提供独立的数据库,针对不同用户的独特需求扩展方便,数据备份以及修复比较简单。但是这种方案增多了数据库的数量,维护成本和购置成本高。 共享数据库,隔离数据架构共享数据库,隔离数据架构即多个或所有租户共享Database,但是每个租户一个Schema。 该模式为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。 但是该模式下,如果数据库出现故障,数据恢复比较困难,因为数据库的恢复会牵扯到其他租户的数据;如果需要跨租户统计数据,存在一定困难。 共享数据库,共享数据架构第三种方案,即租户共享同一个Database、同一个Schema,但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。 这种方案的维护和购置成本最低,允许每个数据库支持的租户数量最多。 但是这种隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;同时数据备份和恢复最困难,需要逐表逐条备份和还原。 如果希望以最少的服务器为最多的租户提供服务,并且租户接受牺牲隔离级别换取降低成本,这种方案最适合。 如何选择我们要如何选择这三种模式呢?衡量三种模式主要考虑的因素是隔离还是共享,还有成本。 考虑到成本角度因素:隔离性越好,设计和实现的难度和成本越高,初始成本越高。共享性越好,同一运营成本下支持的用户越多,运营成本越低。 安全因素:要考虑业务和客户的安全方面的要求。安全性要求越高,越要倾向于隔离。 从租户数量上考虑:租户越多,应该越倾向于共享;如果每个租户存储的数据越多,应该越倾向于隔离;每个租户同时访问系统的用户数量越多,应该越倾向于隔离;如果针对每个用户都要提供一些附加服务,就应该越倾向于隔离。 信息监管因素:要考虑政府,机关,企业,公司的安全和信息监管相关的一些政策和规定。 技术储备:共享性越高,对技术的要求越高。 参考: 【架构】如何设计支持多租户的数据库? SaaS多租户数据隔离的三种方案 数据层的多租户浅谈]]></content>
<categories>
<category>UAA</category>
</categories>
<tags>
<tag>实战</tag>
<tag>权限认证</tag>
</tags>
</entry>
<entry>
<title><![CDATA[多种用户登录模式设计]]></title>
<url>%2F2018%2F02%2F14%2F%E5%A4%9A%E7%A7%8D%E7%94%A8%E6%88%B7%E7%99%BB%E5%BD%95%E6%A8%A1%E5%BC%8F%E8%AE%BE%E8%AE%A1%2F</url>
<content type="text"><![CDATA[在做Web开发的时候,用户登录时最基本的功能。通常情况下,我们直接使用“用户名+密码”的模式,直接在users表中建立用户名密码等字段来完成工作。但是,如果我们的要求是邮箱、手机号和接入第三方的登录都要支持呢?很明显,在这种情况下,基本的用户名密码登录是完成不了我们的要求的。 在这种情况下,我们可以采用“拆表”的方法。这里所谓的拆表就是将一个用户表拆分成为几个表:user_info,user_auth,user_extra等表。user_info表中保存用户的基本信息,如user_id,username,nickname等;user_auth表用来保存用户的认证信息,如user_id,identity_type,identifier和certificate等;user_extra表是用户信息的扩展表,用户的一些扩展信息可以保存在该表中。 表结果如下: 123456789101112131415161718192021222324252627282930313233343536373839-- 用户信息DROP TABLE IF EXISTS user_info;CREATE TABLE IF NOT EXISTS user_info ( id VARCHAR(36) NOT NULL , --ID username VARCHAR(45) NOT NULL, --用户名 nickname VARCHAR(45) NULL, --昵称 user_type INT NOT NULL, --1管理员用户 2普通用户 3虚拟用户 avatar VARCHAR(100) NULL, --头像 register_source VARCHAR(16) NOT NULL, --注册来源:USER_NAME/PHONE/EMAIL/QQ/WECHAT/SINA_WEIBO/DOUBAN/GOOGLE/GITHUB/LINKEDIN/TWITTER/FACEBOOK account_non_expired TINYINT NULL, --用户是否过期 account_non_locked TINYINT NULL, --用户是否被锁 credentials_non_expired TINYINT NULL, --证书是否存在 create_time DATETIME DEFAULT CURRENT_TIMESTAMP(), update_time DATETIME DEFAULT CURRENT_TIMESTAMP(), status VARCHAR(16) NOT NULL, --状态,ENABLED(启用),DISABLE(禁用) enabled TINYINT DEFAULT TRUE);CREATE PRIMARY KEY ON user_info (id);--用户扩展DROP TABLE IF EXISTS user_extra;CREATE TABLE IF NOT EXISTS user_extra ( id BIGINT AUTO_INCREMENT , user_id VARCHAR(36) NOT NULL);CREATE PRIMARY KEY ON user_extra (user_id);--用户认证DROP TABLE IF EXISTS user_auth;CREATE TABLE IF NOT EXISTS user_auth ( id BIGINT AUTO_INCREMENT , user_id VARCHAR(36) NOT NULL, identity_type VARCHAR(16) NOT NULL, --授权来源:USER_NAME/PHONE/EMAIL/QQ/WECHAT/SINA_WEIBO/DOUBAN/GOOGLE/GITHUB/LINKEDIN/TWITTER/FACEBOOK identifier VARCHAR(64) NOT NULL, --手机号/邮箱/用户名或第三方应用的唯一标识 certificate VARCHAR(64) NOT NULL, --密码凭证(站内的保存密码,站外的不保存或保存token) create_time DATETIME DEFAULT CURRENT_TIMESTAMP(), update_time DATETIME DEFAULT CURRENT_TIMESTAMP(), status VARCHAR(16) NOT NULL, --状态,ENABLED(启用),DISABLE(禁用)); 以上,如果我们需要为用户添加一种认证方式,可以在user_auth表中对应添加一条记录。比如,我们需要对接微博认证,那么就直接在user_auth表中添加一条identity_type=SINA_WEIBO的数据记录。我们在登录的时候,就可以直接使用user_auth表的identity_type,identifier和certificate进行认证,从而获得到user_id,然后可以拿user_id到user_info表和user_extra表中获得更多的用户数据。 当然,这种方式也是有缺点的。最主要的缺点就是密码修改,当我们进行修改密码的时候,我们必须对手机号、邮箱以及用户名的所有的密码进行修改。还有一个缺点就是在登录时候要在程序中进行正则匹配,确定是哪种认证方式,那么匹配的顺序就很关键了。 这个也只是一种参考的实现思路,具体的情况具体分析,我们可以根据实际情况进行必要的调整和修改。 参考: 用户系统设计与实现 浅谈数据库用户表结构设计,第三方登录]]></content>
<categories>
<category>UAA</category>
</categories>
<tags>
<tag>实战</tag>
<tag>权限认证</tag>
</tags>
</entry>
<entry>
<title><![CDATA[《写给大忙人看的JavaSE8》读书笔记]]></title>
<url>%2F2018%2F02%2F08%2F%E3%80%8A%E5%86%99%E7%BB%99%E5%A4%A7%E5%BF%99%E4%BA%BA%E7%9C%8B%E7%9A%84JavaSE8%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[重新看了一遍《写给大忙人看的Java SE 8》,简单地做了一下笔记。该书还有很多内容没有细细品味,可以在用到的时候时不时地再翻一番。 import java.io.IOException; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.time.*; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalAdjusters; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.LongAdder; import java.util.stream.Collectors; import java.util.stream.Stream; public class Java8Demo { /** * 带有参数变量的表达式都被称为lambda表达式 * lambda表达式:基本型为 (形参列表)->{方法体} * 方法引用: ::操作符将方法名和对象或类的名字分割开来 */ public void aboutLambda() { Comparator<String> comp = (str1, str2) -> 0; new Thread(() -> { for (int i = 0; i < 100; i++) System.out.println("Lambda Expression"); }).start(); //函数式接口:对于只包含一个抽象方法的接口,你可以通过lambda表达式来创建该接口的对象。 Arrays.sort(new String[]{"a", "b"}, comp); //方法引用等同于提供方法参数的lambda表达式 Arrays.asList("", "").stream().forEach(System.out::print);//情况1:对象::实例方法 Arrays.asList(1, 2).stream().map(Math::abs).collect(Collectors.toList());//情况2:类::静态方法 Arrays.sort(new String[]{"", ""}, Comparator.comparingInt(String::length)); //情况3:类::实例方法。第一个参数会成为执行方法的对象.String::length 等于 (x)->x.length() String[] arr = Arrays.asList("", "").stream().toArray(String[]::new); //情况4:类::new //所有的lambda表达式都是延迟执行的 } public void aboutStreamAPI() { List<String> words = new ArrayList<>(); long count = words.parallelStream().filter(w -> w.length() > 12).count(); //Stream遵循“做什么,而不是怎么去做”的原则。 //在使用Stream时候,你会通过三个阶段来建立一个操作流水线: //1-创建一个Stream //2-在一个或多个步骤中,指定将初始Stream转换为另一个Stream的中间操作 //3-使用一个终止操作来产生一个结果。该操作会强制它之前的延迟操作立即执行。在这之后,该Stream操作就不会再被使用了。 Stream<String> wordStream = Stream.of(new String[]{}); Stream<String> song = Stream.of("", "", ""); Stream<String> echos = Stream.generate(() -> "Echo"); //创建一个含有常亮值的Stream Stream<Double> randoms = Stream.generate(Math::random); //创建一个含有随机数的Stream //iterate方法接受一个“种子”的值和一个函数作为参数 Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE)); //流转换是指从一个流中读取数据,并将转换后的数据写入到另一个流中。 //filter方法的参数是一个Predicate<T>对象——即一个从T到boolean的函数。 //我们使用map方法对流中的值进行某种形式的转换,它会对每个元素应用一个函数,并将返回的值收集到一个新的流中。 //distinct方法会根据原始流中的元素返回一个具有相同顺序、抑制了重复元素的新流。 //聚合方法:如果你希望对元素求和,或者以其他方式将流中的元素组合为一个值 // - count方法:返回流中元素的总数 // - max方法:返回流中最大值 // - min方法:返回流中最小值 // - reduce方法: 提供聚合操作 //Optional<T>对象或者是对一个T类型对象的封装,或者表示不是任何对象。 // if(optionalValue.ifPresent()) optionalValue.get().someMethod(); // optionalValue.ifPresent(v->results::add); // Optional<Boolean> added = optionalValue.map(results::add); //返回一个值 // optionalValue.orElse(""); // optionalValue.orElseGet(()->System.out.print("....")); // optionalValue.orElseThrow(NoSuchElementException:new); // 可以使用Optional.of(result)或者Optional.empty()来创建一个Optional对象 // ofNullable方法中,如果obj不为null,那么Optional.ofNullable(obj)会返回Optional.of(obj),否则会返回Optional.empty()。 //使用flatMap来组合可选函数。Optional<U> u = s.f().flatMap(T::g); //以下3个是等效的 HashSet<String> res = wordStream.collect(HashSet::new, HashSet::add, HashSet::addAll); List<String> res1 = wordStream.collect(Collectors.toList()); Set<String> res2 = wordStream.collect(Collectors.toSet()); //将字符串连接起来 String str = wordStream.collect(Collectors.joining()); //将字符串连接起来,中间以,隔开 String str1 = wordStream.collect(Collectors.joining(", ")); //使用(Int|Double|Long)SummaryStatistics来获得一个流的总和、平均值、最大值或最小值 IntSummaryStatistics summary = wordStream.collect(Collectors.summarizingInt(String::length)); double averageWordLength = summary.getAverage(); double maxWordLength = summary.getMax(); //将一个Stream对象中的元素收集到一个map中 //Map<Integer,String> idToName = peopleStream.collect(Collectors.toMap(Person::getId,Person::getName)); //上面的转换成Map中,如果存在相同的键异常,我们可以重写方法搞定。 Stream<Locale> locales = Stream.of(Locale.getAvailableLocales()); Map<String, String> languageNames = locales.collect(Collectors.toMap( l -> l.getDisplayLanguage(), l -> l.getDisplayLanguage(l), (existingValue, newValue) -> existingValue )); //groupingBy方法,对具有相同特性的值进行分组 //当分类函数式一个predicate函数(即返回一个布尔值的函数)时,流元素会被分为两组列表:一组是函数会返回true的元素,另一组返回false的元素。 //在这种情况下,使用partitioningBy会比groupingBy更有效率。 } public void aboutTime() { Instant start = Instant.now(); Instant end = Instant.now(); Duration timeElapsed = Duration.between(start, end); timeElapsed.toMillis(); timeElapsed.toDays(); timeElapsed.plusDays(1).getSeconds(); start.plusMillis(1); // 以下是来自廖雪峰的博客 // Java 8新增了LocalDate和LocalTime接口,为什么要搞一套全新的处理日期和时间的API?因为旧的java.util.Date实在是太难用了。 // java.util.Date月份从0开始,一月是0,十二月是11,变态吧!java.time.LocalDate月份和星期都改成了enum,就不可能再用错了。 // java.util.Date和SimpleDateFormatter都不是线程安全的,而LocalDate和LocalTime和最基本的String一样,是不变类型,不但线程安全,而且不能修改。 // java.util.Date是一个“万能接口”,它包含日期、时间,还有毫秒数,如果你只想用java.util.Date存储日期,或者只存储时间,那么,只有你知道哪些部分的数据是有用的,哪些部分的数据是不能用的。在新的Java 8中,日期和时间被明确划分为LocalDate和LocalTime,LocalDate无法包含时间,LocalTime无法包含日期。当然,LocalDateTime才能同时包含日期和时间。 // 新接口更好用的原因是考虑到了日期时间的操作,经常发生往前推或往后推几天的情况。用java.util.Date配合Calendar要写好多代码,而且一般的开发人员还不一定能写对。 // 取当前日期: LocalDate today = LocalDate.now(); // -> 2014-12-24 // 根据年月日取日期,12月就是12: LocalDate crischristmas = LocalDate.of(2014, 12, 25); // -> 2014-12-25 // 根据字符串取: LocalDate endOfFeb = LocalDate.parse("2014-02-28"); // 严格按照ISO yyyy-MM-dd验证,02写成2都不行,当然也有一个重载方法允许自己定义格式 LocalDate.parse("2014-02-29"); // 无效日期无法通过:DateTimeParseException: Invalid date // LocalTime只包含时间,以前用java.util.Date怎么才能只表示时间呢?答案是,假装忽略日期。 // LocalTime包含毫秒: LocalTime now = LocalTime.now(); // 11:09:09.240 //你可能想清除毫秒数: LocalTime now1 = LocalTime.now().withNano(0); // 11:09:09 //构造时间也很简单: LocalTime zero = LocalTime.of(0, 0, 0); // 00:00:00 LocalTime mid = LocalTime.parse("12:00:00"); // 12:00:00 //日期校正器 //TemporalAdjusters类提供了很多静态方法来进行常用的校正。你可以将一个校正放的结果传递给with方法。 LocalDate firstTuesday = LocalDate.of(2018, 2, 1).with(TemporalAdjusters.nextOrSame(DayOfWeek.THURSDAY)); //计算2月的第一个星期二 //实现自己的校验器 TemporalAdjuster NEXT_WORKDAY = TemporalAdjusters.ofDateAdjuster(w -> { LocalDate result = w; do { result = result.plusDays(1); } while (result.getDayOfWeek().getValue() >= 6); return result; }); LocalDate backToWork = LocalDate.now().with(NEXT_WORKDAY); //格式化与解析 // String formatted = DateTimeFormatter.ISO_DATE_TIME.format(); } public void aboutConcurrency() { //原子性 lambda // ConcurrentHashMap ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap(); map.putIfAbsent("", new LongAdder()); map.get("").increment(); //批量数据操作有三类: //1)search会对每个键和(或)值应用一个函数,直到函数返回一个非null的结果。然后search会终止并返回该函数的结果。 //2)reduce会通过提供的累积函数,将所有的键和(或)值组合起来。 //3)forEach会对所有的键和(或)值应用一个函数。 //在使用这几种操作时,你需要指定一个并行阈值。如果映射包含的元素数量超过了这个阈值,批量操作就以并行方式执行。 //如果你希望批量数据操作在一个线程中运行,请使用Long.MAX_VALUE作为阈值。 //如果你希望批量数据操作尽可能使用更多的线程,则应该使用1作为阈值。 Set<String> words = map.keySet(); //并行数组操作 //静态方法Arrays.parallelSort可以对原始类型数组或者对象数组进行排序。 try { String contents = new String(Files.readAllBytes(Paths.get("")), StandardCharsets.UTF_8); String[] words1 = contents.split(""); Arrays.parallelSort(words1); } catch (IOException e) { e.printStackTrace(); } //可完成的Future //Future<T>接口用来表示一个在将来某个时间点可用的、类型为T的值。 } public void aboutOthers() { String joined = String.join("/", "usr", "local", "bin"); String ids = String.join(", ", ZoneId.getAvailableZoneIds()); Objects.isNull(""); Objects.nonNull(""); } }]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Ehcache缓存持久化]]></title>
<url>%2F2017%2F12%2F22%2FEhcache%E7%BC%93%E5%AD%98%E6%8C%81%E4%B9%85%E5%8C%96%2F</url>
<content type="text"><![CDATA[因为在项目中需要将Ehcache的缓存数据写入磁盘,搜索一些怎么样持久化Ehcache缓存的资料,根据获得的资料在开发过程中遇到蛮多的坑,逐一记录下来。 根据官网上的配置踩过的坑在Ehcache的官网上,有关于怎么配置持久化的说明。详情见: Configuring Restartability and Persistence 然后,我按照常规的逻辑添加了一下的配置: 1234567<cache name="indexCache" eternal="true" maxElementsInMemory="1" overflowToDisk="true" diskPersistent="true"> <persistence strategy="localRestartable"/></cache> 然后,报错: 1Caused by: org.xml.sax.SAXException: null:17: Could not finish element <persistence>. Message was: net.sf.ehcache.config.InvalidConfigurationException: Cannot use both <persistence ...> and diskPersistent in a single cache configuration. 说明diskPersistent和persistence不能共存。修改配置后: 123<cache name="indexCache"> <persistence strategy="localRestartable"/></cache> 启动,报错: 12Caused by: net.sf.ehcache.config.InvalidConfigurationException: There is one error in your configuration: * Cache 'indexCache' error: If your CacheManager has no maxBytesLocalHeap set, you need to either set maxEntriesLocalHeap or maxBytesLocalHeap at the Cache level 添加 maxEntriesLocalHeap 后如下: 123<cache name="indexCache" maxEntriesLocalHeap="1000"> <persistence strategy="localRestartable"/></cache> 还是报错:You must use an enterprise version of Ehcache to successfully enable enterprise persistence. 原来需要用到BigMemory,但是BigMemory又是收费的。至此,第一次尝试失败。 根据网上资料踩过的坑资料显示:我们要在每次使用cache.put()后再调用cache.flush(),这样就能够将索引写入到磁盘。同时在配置中开启eternal(永久有效),overflowToDisk(磁盘缓存), diskPersistent(持久化到磁盘)和预热机制。 然后还要在web.xml中加入ShutdownListener的监听,这样可以保证在正常关闭的时候缓存数据成功写入磁盘。 所以有了以下配置: 12345<cache name="indexCache" maxElementsInMemory="1" eternal="true" overflowToDisk="true" diskPersistent="true"> <bootstrapCacheLoaderFactory class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory" properties="bootstrapAsynchronously=true"/></cache> 1234@Beanpublic ServletListenerRegistrationBean<ShutdownListener> testListenerRegistration() { return new ServletListenerRegistrationBean<>(new ShutdownListener());} 启动试验,发现只生成.data文件并没有生成.index文件。 跳出坑,解决问题跟踪调试代码,发现cache.flush()调用的是CacheStore中的flush。 123456789101112131415@Overridepublic void flush() throws IOException { if (authoritativeTier instanceof DiskStore && cacheConfiguration != null && cacheConfiguration.isClearOnFlush()) { final Lock lock = daLock.writeLock(); lock.lock(); try { cachingTier.clear(); ((DiskStore)authoritativeTier).clearFaultedBit(); } finally { lock.unlock(); } } else { authoritativeTier.flush(); }} 程序总是进入if条件中,并没有调用到authoritativeTier.flush()方法。 修改配置: 12345<cache name="indexCache" maxElementsInMemory="1" eternal="true" overflowToDisk="true" diskPersistent="true" clearOnFlush="false"> <bootstrapCacheLoaderFactory class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory" properties="bootstrapAsynchronously=true"/></cache> 重新运行,OK。 以上基于spring boot 1.5.4.RELEASE,ehcache的版本为2.10.4]]></content>
<categories>
<category>缓存</category>
</categories>
<tags>
<tag>实战</tag>
<tag>Ehcache</tag>
</tags>
</entry>
<entry>
<title><![CDATA[在SpringCloud Zuul中使用WebSockets]]></title>
<url>%2F2017%2F11%2F15%2F%E5%9C%A8SpringCloud%20Zuul%E4%B8%AD%E4%BD%BF%E7%94%A8WebSockets%2F</url>
<content type="text"><![CDATA[近期的项目中需要用到WebSocket,因为使用的是微服务架构,所以又直接使用了Spring Cloud的Zuul。然而,Zuul对WebSocket的支持不是那么友好,具体可以参考:https://github.com/spring-cloud/spring-cloud-netflix/issues/163。 Spring已经给我们提供了一套WebSockets的解决方案。我们需要用到的有:Sock.js、STOMP、Spring Messaging以及Spring Integration。 Sock.js Sock.js是一个JavaScript代码库,提供WebSocket-like对象、跨浏览器的JavaScript的API。它在浏览器和web服务器之间创建了一个低延迟、全双工、跨域通信通道。在低版本的浏览器不支持WebSocket时,它可以使用其他协议来处理。 STOMP STOMP是一种简单的(或流媒体)的消息传递协议。在多种语言、平台和代理之间提供简单和广泛的消息互操作性。 Spring组件 我们在项目中大量使用Spring组件,在这里我们使用到了Spring Messaging和Spring Integration。 Spring Messaging和Spring Integration具体与SockJs、STOMP组合使用可以参考: https://spring.io/guides/gs/messaging-stomp-websocket/ http://assets.spring.io/wp/WebSocketBlogPost.html Zuul中的处理 在使用Zuul作为网关的时候,因为我们使用的是Sock.js,所以它可以算得上是支持了。但是为了能够更保险些,需要写一个Filter: 12345678910111213141516171819202122232425262728293031@Componentpublic class WebSocketFilter extends ZuulFilter { @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); String upgradeHeader = request.getHeader("Upgrade"); if (null == upgradeHeader) { upgradeHeader = request.getHeader("upgrade"); } if (null != upgradeHeader && "websocket".equalsIgnoreCase(upgradeHeader)) { context.addZuulRequestHeader("connection", "Upgrade"); } return null; }} Spring WebSockets默认的心跳时间是25s,为了能够不被认为是连接超时,我们需要在Zuul中设置比较长的超时时间。 1234567891011hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 60000 #设置API网关中路由转发请求的HystrixCommand执行超时时间ribbon: ConnectTimeout: 3000 #设置路由转发请求的时候,创建请求连接的超时时间 ReadTimeout: 60000 #用来设置路由转发请求的超时时间 以上,基本上是完成了Zuul与WebSockets之间的代理了。 参考: https://jmnarloch.wordpress.com/2015/11/11/spring-cloud-sock-js-stomp-zuul-no-websockets/]]></content>
<categories>
<category>Spring</category>
<category>Spring Cloud</category>
</categories>
<tags>
<tag>Spring Cloud</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring Data MongoDB中的自定义级联]]></title>
<url>%2F2017%2F11%2F10%2FSpring%20Data%20MongoDB%E4%B8%AD%E7%9A%84%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BA%A7%E8%81%94%2F</url>
<content type="text"><![CDATA[背景在使用Spring Data操作MongoDB中: 在保存一个实体的时候,如果被@DBRef标识的类只传入Id,保存后返回的结果并没有全部的引用类内容,只有Id。 保存实体,不能保存引用实体。 例如:我们有一个实体Person,有一个实体EmailAddress。 12345678910@Document(collection = "test_person")public class Person { private String name; @DBRef private EmailAddress emailAddress; ... getter setter 方法} 1234567891011@Document(collection = "test_email")public class EmailAddress { @Id private String id; private String value; ... getter setter 方法} 当我们调用保存方法的时候: 123456789101112public Person test() { Person person = new Person(); person.setName("test"); EmailAddress emailAddress = new EmailAddress(); emailAddress.setId("5a05108d4dcc5dece03c9e69"); person.setEmailAddress(emailAddress); testRepository.save(person); return person;} 上述的代码中,返回的person只有id,没有emailAddress的其他值。 123456789101112public Person test() { Person person = new Person(); person.setName("test"); EmailAddress emailAddress = new EmailAddress(); emailAddress.setName("afafa"); person.setEmailAddress(emailAddress); testRepository.save(person); return person;} 上述的代码中,emailAddress不能被保存。 解决生命周期事件Spring Data MongoDB中存在一些生命周期事件,如:onBeforeConvert, onBeforeSave, onAfterSave, onAfterLoad and onAfterConvert等。我们可以继承AbstractMappingEventListener,然后重写这些方法,即可以实现。 代码1234567891011121314151617181920212223/** * MongoDB级联控制 * Created by guanzhenxing on 2017/11/9. */public class CascadeControlMongoEventListener extends AbstractMongoEventListener<Object> { @Autowired private MongoOperations mongoOperations; @Override public void onAfterSave(AfterSaveEvent<Object> event) { super.onAfterSave(event); Object source = event.getSource(); ReflectionUtils.doWithFields(source.getClass(), new CascadeAfterSaveCallback(source, mongoOperations)); } @Override public void onBeforeConvert(BeforeConvertEvent<Object> event) { super.onBeforeConvert(event); Object source = event.getSource(); ReflectionUtils.doWithFields(source.getClass(), new CascadeBeforeConvertCallback(source, mongoOperations)); }} 1234567891011121314151617181920212223242526272829303132333435363738394041/** * 级联控制的回调 * Created by guanzhenxing on 2017/11/10. */public class CascadeAfterSaveCallback implements ReflectionUtils.FieldCallback { private Object source; private MongoOperations mongoOperations; public CascadeAfterSaveCallback(final Object source, final MongoOperations mongoOperations) { this.source = source; this.mongoOperations = mongoOperations; } @Override public void doWith(final Field field) throws IllegalArgumentException, IllegalAccessException { ReflectionUtils.makeAccessible(field); if (field.isAnnotationPresent(DBRef.class)) { final Object fieldValue = field.get(source); //获得值 if (fieldValue != null) { doCascadeLoad(field); } } } /** * 级联查询 * * @param field */ private void doCascadeLoad(Field field) throws IllegalAccessException { Object fieldValue = field.get(source); List<Field> idFields = ReflectionUtil.getAnnotationField(fieldValue, Id.class); //该方法是为了获得所有的被@Id注解的属性 if (idFields.size() == 1) { //只处理一个Id Object idValue = ReflectionUtil.getFieldValue(fieldValue, idFields.get(0).getName()); Object value = mongoOperations.findById(idValue, fieldValue.getClass()); //查询获得值 ReflectionUtil.setFieldValue(source, field.getName(), value); } }} 1234567891011121314151617181920212223242526272829303132333435363738394041public class CascadeBeforeConvertCallback implements ReflectionUtils.FieldCallback { private Object source; private MongoOperations mongoOperations; public CascadeBeforeConvertCallback(Object source, MongoOperations mongoOperations) { this.source = source; this.mongoOperations = mongoOperations; } @Override public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { ReflectionUtils.makeAccessible(field); if (field.isAnnotationPresent(DBRef.class)) { final Object fieldValue = field.get(source); //获得值 if (fieldValue != null) { doCascadeSave(field); } } } /** * 级联保存 * * @param field * @throws IllegalAccessException */ private void doCascadeSave(Field field) throws IllegalAccessException { if (field.isAnnotationPresent(CascadeSave.class)) { //如果有标识@CascadeSave注解 Object fieldValue = field.get(source); List<Field> idFields = ReflectionUtil.getAnnotationField(fieldValue, Id.class); if (idFields.size() == 1) { mongoOperations.save(fieldValue); } } }} 1234@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public @interface CascadeSave {} 12345678910@Configurationpublic class MongoConfig { @Bean public CascadeControlMongoEventListener userCascadingMongoEventListener() { return new CascadeControlMongoEventListener(); }} 以上是核心代码。至此,我们就可以解决上述的问题了。 参考:http://www.baeldung.com/cascading-with-dbref-and-lifecycle-events-in-spring-data-mongodb]]></content>
<categories>
<category>Spring</category>
</categories>
<tags>
<tag>实战</tag>
<tag>MongoDB</tag>
</tags>
</entry>
<entry>
<title><![CDATA[将一个JavaBean中的非空属性转到Map中]]></title>
<url>%2F2017%2F11%2F09%2F%E5%B0%86%E4%B8%80%E4%B8%AAJavaBean%E4%B8%AD%E7%9A%84%E9%9D%9E%E7%A9%BA%E5%B1%9E%E6%80%A7%E8%BD%AC%E5%88%B0Map%E4%B8%AD%2F</url>
<content type="text"><![CDATA[最近在写项目的时候遇到一个这样的一个需要:将JavaBean中非空属性转到一个Map中。stackoverflow出一个还算不错的回答,记录如下: stackoverflow地址:https://stackoverflow.com/questions/8524011/java-reflection-how-can-i-get-the-all-getter-methods-of-a-java-class-and-invoke 最佳答案(大概意思的翻译): 不要用正则表达式,使用Introspector: 1234567for(PropertyDescriptor propertyDescriptor : Introspector.getBeanInfo(yourClass).getPropertyDescriptors()){ // propertyEditor.getReadMethod() exposes the getter // btw, this may be null if you have a write-only property System.out.println(propertyDescriptor.getReadMethod());} 通常情况下,我们不需要Object.class的属性,所以我们可以使用以下方法: 123 Introspector.getBeanInfo(yourClass, stopClass)// usually with Object.class as 2nd param// the first class is inclusive, the second exclusive 我们还可以使用commons/beanutils的一些方法,如:Map<String, String> properties = BeanUtils.describe(yourObject);它会找到并执行所有的getter并将结果存储到Map中,而且BeanUtils.describe()在返回之前将所有的属性值转换为字符串。 以下是一个用Java 8写的方法: 12345678910111213141516171819202122232425public static Map<String, Object> beanProperties(Object bean) { try { return Arrays.asList( Introspector.getBeanInfo(bean.getClass(), Object.class) .getPropertyDescriptors() ) .stream() // filter out properties with setters only .filter(pd -> Objects.nonNull(pd.getReadMethod())) .collect(Collectors.toMap( // bean property name PropertyDescriptor::getName, pd -> { // invoke method to get value try { return pd.getReadMethod().invoke(bean); } catch (Exception e) { // replace this with better error handling return null; } })); } catch (IntrospectionException e) { // and this, too return Collections.emptyMap(); }} 以上方法有一个缺点:Collectors.toMap()对于null的值会报错。所以,有了以下版本: 123456789101112131415161718192021222324public static Map<String, Object> beanProperties(Object bean) { try { Map<String, Object> map = new HashMap<>(); Arrays.asList(Introspector.getBeanInfo(bean.getClass(), Object.class) .getPropertyDescriptors()) .stream() // filter out properties with setters only .filter(pd -> Objects.nonNull(pd.getReadMethod())) .forEach(pd -> { // invoke method to get value try { Object value = pd.getReadMethod().invoke(bean); if (value != null) { map.put(pd.getName(), value); } } catch (Exception e) { // add proper error handling here } }); return map; } catch (IntrospectionException e) { // and here, too return Collections.emptyMap(); }} Guava版本: 12345678910111213141516171819202122232425public static Map<String, Object> guavaBeanProperties(Object bean) { Object NULL = new Object(); try { return Maps.transformValues( Arrays.stream( Introspector.getBeanInfo(bean.getClass(), Object.class) .getPropertyDescriptors()) .filter(pd -> Objects.nonNull(pd.getReadMethod())) .collect(ImmutableMap::<String, Object>builder, (builder, pd) -> { try { Object result = pd.getReadMethod() .invoke(bean); builder.put(pd.getName(), firstNonNull(result, NULL)); } catch (Exception e) { throw propagate(e); } }, (left, right) -> left.putAll(right.build())) .build(), v -> v == NULL ? null : v); } catch (IntrospectionException e) { throw propagate(e); }} 另外,还有一个JavaSlang的版本。基本上很少用这个,就不收集了。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[在使用SpringBoot的时候遇到的一些小坑]]></title>
<url>%2F2017%2F10%2F28%2F%E5%9C%A8%E4%BD%BF%E7%94%A8SpringBoot%E7%9A%84%E6%97%B6%E5%80%99%E9%81%87%E5%88%B0%E7%9A%84%E4%B8%80%E4%BA%9B%E5%B0%8F%E5%9D%91%2F</url>
<content type="text"><![CDATA[引入了spring-boot-starter-jdbc但是没有配置数据源出现问题原因:在开发过程中把一些公关的东西封装成一个jar包,多个项目共同使用这个jar包(这个jar中包含数据处理的代码)。然而,有些项目没有用到数据处理,没有配置数据源。所以,在启动的时候会报错。 解决方案:引入@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) 使用RestTemplate访问restful服务的异常处理问题描述: 在使用RestTemplate访问restful接口的时候,如果接口抛出异常,默认情况下捕获的异常不符合自定义的异常规范。 解决: RestTemplate默认使用的是DefaultResponseErrorHandler处理异常。如果我们需要定制自己的一处处理,可以编写定制化的处理器: 如: public class CustomResponseErrorHandler implements ResponseErrorHandler { private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler(); @Override public boolean hasError(ClientHttpResponse response) throws IOException { return errorHandler.hasError(response); } @Override public void handleError(ClientHttpResponse response) throws IOException { String theString = IOUtils.toString(response.getBody()); Map<String, String> error = (Map<String, String>) JsonMapper.fromJsonString(theString, Map.class); String errorCode = error.get("errorCode"); String message = error.get("message"); throw new RemoteCallException(errorCode, message, null); } } 然后 RestTemplate restTemplate = new RestTemplate(); restTemplate.setErrorHandler(new CustomResponseErrorHandler());]]></content>
<categories>
<category>Spring</category>
<category>Spring Boot</category>
</categories>
<tags>
<tag>实战</tag>
<tag>Spring Boot</tag>
</tags>
</entry>
<entry>
<title><![CDATA[IntelliJ Idea取消Could not autowire. No beans of 'xxxx' type found的错误提示]]></title>
<url>%2F2017%2F10%2F28%2FIntelliJ%20Idea%E5%8F%96%E6%B6%88Could%20not%20autowire.%20No%20beans%20of%20'xxxx'%20type%20found%E7%9A%84%E9%94%99%E8%AF%AF%E6%8F%90%E7%A4%BA%2F</url>
<content type="text"><![CDATA[遇到的问题: 在IntelliJ Idea使用Spring的@Autowired,有的时候遇到“Could not autowire. No beans of ‘xxxx’ type found”的标红提示。但是在程序的编译和运行时候都没有出现这个问题。 解决: 在出现该问题的地方加上@SuppressWarnings("SpringJavaAutowiringInspection")注解]]></content>
<categories>
<category>开发工具</category>
</categories>
<tags>
<tag>实战</tag>
<tag>Spring</tag>
</tags>
</entry>
<entry>
<title><![CDATA[在SpringBoot中集成MQTT]]></title>
<url>%2F2017%2F08%2F17%2F%E5%9C%A8SpringBoot%E4%B8%AD%E9%9B%86%E6%88%90MQTT%2F</url>
<content type="text"><![CDATA[MQTT( Message Queuing Telemetry Transport)是一个物联网传输协议,它被设计用于轻量级的发布/订阅式消息传输,旨在为低带宽和不稳定的网络环境中的物联网设备提供可靠的网络服务。在实际的开发中,我们通常会用到Spring,这里简单描述一下在SpringBoot中如何集成MQTT。 在Spring的一系列文档中,已经有了对应的集成代码。见: spring-integration-samples Spring Integration MQTT Support 引入依赖123456789101112<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-integration</artifactId></dependency><dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-stream</artifactId></dependency><dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-mqtt</artifactId></dependency> 配置 mqttClientFactory1234567@Beanpublic MqttPahoClientFactory mqttClientFactory() { List<String> urls = mqttUrls().getUrls(); DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory(); factory.setServerURIs("tcp:\\localhost:1883"); return factory;} 配置消费者123456789101112131415@Beanpublic IntegrationFlow mqttInFlow() { return IntegrationFlows.from(mqttInbound()) .transform(p -> p) .handle(mqttService.handler()) .get();}private MessageProducerSupport mqttInbound() { MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("customer", mqttClientFactory(), "test-topic"); adapter.setConverter(new DefaultPahoMessageConverter()); adapter.setQos(1); return adapter;} 配置生产者12345678910111213141516171819@Beanpublic IntegrationFlow mqttOutFlow() { return IntegrationFlows.from(outChannel()) .handle(mqttOutbound()) .get();}@Beanpublic MessageChannel outChannel() { return new DirectChannel();}@Beanpublic MessageHandler mqttOutbound() { MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler("publisher", mqttClientFactory()); messageHandler.setAsync(true); messageHandler.setDefaultTopic("test-topic"); return messageHandler;} 1234@MessagingGateway(defaultRequestChannel = "outChannel")public interface MessageWriter { void write(String data);} 生产者的使用可以为: 123456@AutowiredMessageWriter messageWritervoid publish(String data){ messageWriter.write(data)} 遇到的坑以及未解决的问题 生产者和消费者的clientId一定一定不能一样 未解决:多个topic和data的动态生产]]></content>
<categories>
<category>Spring</category>
<category>Spring Boot</category>
</categories>
<tags>
<tag>实战</tag>
<tag>Spring</tag>
<tag>Spring Boot</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring @Value—从yml文件中引入list]]></title>
<url>%2F2017%2F08%2F15%2FSpring%20%40Value%E2%80%94%E4%BB%8Eyml%E6%96%87%E4%BB%B6%E4%B8%AD%E5%BC%95%E5%85%A5list%2F</url>
<content type="text"><![CDATA[在SpringBoot中要引入yml配置文件中的数组,用常用的@Value是不可取的,我们必须另寻途径。详见以下代码: 配置文件,其中mqtt是一个对象,urls是一个字符串数组,mappings是一个对象数组 123456789101112mqtt: urls: - tcp://120.24.75.149:1883 clientId: mqtt-gateway completionTimeout: 5000router: mappings: - topicFilter: msg service: sensors - topicFilter: people service: people 配置代码: 12345678910111213141516171819202122232425262728import cn.webfuse.valuedemo.configuration.properties.Mappings;import cn.webfuse.valuedemo.configuration.properties.MqttUrls;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class DefaultConfiguration { @Bean @ConfigurationProperties(prefix = "mqtt") MqttUrls mqttUrls() { return new MqttUrls(); } @Bean @ConfigurationProperties(prefix = "router") Mappings mappings() { return new Mappings(); } @Value("${mqtt.clientId}") String clientId;} 上述代码中用到的对象类: 123456789101112131415161718192021public class Mapping { private String topicFilter; private String service; public String getTopicFilter() { return topicFilter; } public void setTopicFilter(String topicFilter) { this.topicFilter = topicFilter; } public String getService() { return service; } public void setService(String service) { this.service = service; }} 12345678910111213public class Mappings { private List<Mapping> mappings = new ArrayList<>(); public List<Mapping> getMappings() { return mappings; } public void setMappings(List<Mapping> mappings) { this.mappings = mappings; }} 123456789101112public class MqttUrls { private List<String> urls = new ArrayList<>(); public List<String> getUrls() { return this.urls; } public void setUrls(List<String> urls) { this.urls = urls; }} 使用: 12345678910111213141516171819202122@RequestMapping("/")@RestControllerpublic class DefaultController { @Autowired MqttUrls mqttUrls; @Autowired Mappings mappings; @GetMapping(value = "/urls/show") public List<String> show() { return mqttUrls.getUrls(); } @GetMapping("/mappings/show") public List<Mapping> showMappings() { return mappings.getMappings(); }}]]></content>
<categories>
<category>Spring</category>
<category>Spring Boot</category>
</categories>
<tags>
<tag>实战</tag>
<tag>Spring</tag>
<tag>Spring Boot</tag>
</tags>
</entry>
<entry>
<title><![CDATA[利用Spring的AbstractRoutingDataSource解决多数据源的问题]]></title>
<url>%2F2017%2F08%2F10%2F%E5%88%A9%E7%94%A8Spring%E7%9A%84AbstractRoutingDataSource%E8%A7%A3%E5%86%B3%E5%A4%9A%E6%95%B0%E6%8D%AE%E6%BA%90%E7%9A%84%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[在互联网的服务端开发的时候,我们很经常要在一个项目中去调用不同的数据库。在这种情况下,必然要涉及到多数据源问题。那么,我们该如何解决多数据源问题呢?有没有一种方法来动态切换数据源呢? 答案是有的。万能的Spring已经给了我们解决方案——利用AbstractRoutingDataSource。在AbstractRoutingDataSource类中,发现getConnection()方法,从名字上可以知道它是获得connection连接的。跟踪getConnection()方法,determineTargetDataSource()就这么进入我们的视线,继续下去,我们发现了重点——determineCurrentLookupKey()。determineCurrentLookupKey方法是一个抽象方法,它的返回值就是我们要用到的数据源dataSource的key,然后根据这个key从resolvedDataSources这个map中取出dataSource,如果找不到就使用默认的dataSource。 知道了这些,我们就来用代码实现吧!以下的代码是基于Spring Boot,所以需要建立一个Spring Boot项目,然后引入spring-boot-starter-jdbc。项目中又有用到AOP,肯定的,我们必须引入spring-boot-starter-aop。 接下来,我们需要建一个DynamicDataSource。 12345678public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { String dataSourceName = DynamicDataSourceContextHolder.getDataSourceName(); return dataSourceName; }} DynamicDataSource继承了抽象类AbstractRoutingDataSource,实现了determineCurrentLookupKey()。在DynamicDataSourceContextHolder中,我们使用ThreadLocal维护dataSouceName这个变量。这样,每一个线程都可以独立改变自己的副本,而不会影响其他线程所对应的副本。 12345678910111213141516171819202122232425/** * 动态数据源上下文 */public class DynamicDataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static List<String> dataSourceNames = new ArrayList<>(); public static void setDataSourceName(String name) { contextHolder.set(name); } public static String getDataSourceName() { return contextHolder.get(); } public static void clearDataSourceName() { contextHolder.remove(); } public static boolean containsDataSource(String dataSourceName) { return dataSourceNames.contains(dataSourceName); }} OK,我们已经有了动态数据源DynamicDataSource,也有了DynamicDataSourceContextHolder,我们怎么使用呢?让我们看下DataSourceConfig。 123456789101112131415161718192021222324252627282930313233343536373839404142434445@Configurationpublic class DataSourceConfig { @Bean(name = "primaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.primary") @Primary public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "secondaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.secondary") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } @Autowired @Qualifier("primaryDataSource") private DataSource primaryDataSource; @Autowired @Qualifier("secondaryDataSource") private DataSource secondaryDataSource; @Bean public DynamicDataSource dataSource() { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("primaryDataSource", primaryDataSource); targetDataSources.put("secondaryDataSource", secondaryDataSource); DynamicDataSourceContextHolder.dataSourceNames.add("primaryDataSource"); DynamicDataSourceContextHolder.dataSourceNames.add("secondaryDataSource"); DynamicDataSource dataSource = new DynamicDataSource(); //设置数据源映射 dataSource.setTargetDataSources(targetDataSources); //设置默认数据源,当无法映射到数据源时会使用默认数据源 dataSource.setDefaultTargetDataSource(primaryDataSource); dataSource.afterPropertiesSet(); return dataSource; }} 当然,上面的DataSourceConfig还得搭配application.properties配置文件。 12345678910#spring.datasource.primary.jdbcUrl=jdbc:mysql://localhost/test1?useSSL=falsespring.datasource.primary.username=rootspring.datasource.primary.password=rootspring.datasource.primary.driver-class-name=com.mysql.jdbc.Driver#spring.datasource.secondary.jdbcUrl=jdbc:mysql://localhost/test2?useSSL=falsespring.datasource.secondary.username=rootspring.datasource.secondary.password=rootspring.datasource.secondary.driver-class-name=com.mysql.jdbc.Driver 到现在为止,我们已经配置了动态的数据源,将我们的多个数据源设置给了DynamicDataSource。这样,我们就可以在Service或者是Dao中使用DynamicDataSourceContextHolder.setDataSourceName("primaryDataSource")方法来动态切换数据源了。但是,这样的写法会不会感到很不爽呢?其实,我们可以在方法上使用注解来切换数据源。 自定义一个注解TargetDataSource: 123456789101112/** * 在方法上使用,用于指定使用哪个数据源 */@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface TargetDataSource { String value();} 这样,我们就可以在Service上如以下使用: 1234567891011@TargetDataSource(value = "primaryDataSource")public List<Customer> findAll() { //TODO return null;}@TargetDataSource(value = "secondaryDataSource")public void addCustomer(String name, String email) {} 很明显的,到目前为止还不能完成我们的切换需求。我们还需要一段代码,来实现动态调用DynamicDataSourceContextHolder.setDataSourceName("xxx"),比较好的实现就是AOP啦。 123456789101112131415161718192021222324@Aspect@Component@Order(-1)// 保证该AOP在@Transactional之前执行public class DynamicDataSourceChangeAspect { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceChangeAspect.class); @Before("@annotation(targetDataSource)") public void changeDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) { String dsName = targetDataSource.value(); if (!DynamicDataSourceContextHolder.containsDataSource(dsName)) { System.err.println("数据源[{}]不存在,使用默认数据源 > {}" + targetDataSource.value() + joinPoint.getSignature()); } else { DynamicDataSourceContextHolder.setDataSourceName(targetDataSource.value()); //设置到动态数据源上下文中 } } @After("@annotation(targetDataSource)") public void restoreDataSource(JoinPoint point, TargetDataSource targetDataSource) { //方法执行完毕之后,销毁当前数据源信息,进行垃圾回收。 DynamicDataSourceContextHolder.clearDataSourceName(); }} 到现在为止,我们就实现了动态切换数据源了。]]></content>
<categories>
<category>Spring</category>
</categories>
<tags>
<tag>实战</tag>
<tag>Spring</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring Boot中ComponentScan的exclude]]></title>
<url>%2F2017%2F08%2F05%2FSpring%20Boot%E4%B8%ADComponentScan%E7%9A%84exclude%2F</url>
<content type="text"><![CDATA[在SpringBoot的组件自动扫描加载中怎么样去exclude classes/packages呢?我们可以使用@ComponentScan的excludeFilters来实现。 实现方法一ExcludedService.java package cn.webfuse.service.exclude; import org.springframework.stereotype.Service; @Service public class ExcludedService { public ExcludedService() { System.out.println("Instantiating " + getClass().getSimpleName()); } } IncludedService.java package cn.webfuse.service.include; import org.springframework.stereotype.Service; @Service public class IncludedService { public IncludedService() { System.out.println("Instantiating " + getClass().getSimpleName()); } } Application.java @SpringBootApplication @ComponentScan( excludeFilters = @ComponentScan.Filter( type = FilterType.REGEX, pattern = "cn.webfuse.service.exclude.*")) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 运行只得到IncludedService的打印。 实现方法二IgnoreDuringScan.java package cn.webfuse.annotation; public @interface IgnoreDuringScan { } ExcludedConfig.java package cn.webfuse.config.exclude; import cn.webfuse.annotation.IgnoreDuringScan; import org.springframework.context.annotation.Configuration; @Configuration @IgnoreDuringScan public class ExcludedConfig { public ExcludedConfig() { System.out.println("Instantiating " + getClass().getSimpleName()); } } IncludedConfig.java package cn.webfuse.config.include; import org.springframework.context.annotation.Configuration; @Configuration public class IncludedConfig { public IncludedConfig() { System.out.println("Instantiating " + getClass().getSimpleName()); } } Application.java @SpringBootApplication @ComponentScan(excludeFilters = @ComponentScan.Filter(IgnoreDuringScan.class)) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }]]></content>
<categories>
<category>Spring</category>
<category>Spring Boot</category>
</categories>
<tags>
<tag>实战</tag>
<tag>Spring Boot</tag>
</tags>
</entry>
<entry>
<title><![CDATA[持续集成环境搭建]]></title>
<url>%2F2017%2F07%2F04%2F%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA%2F</url>
<content type="text"><![CDATA[关于持续集成是什么,可以参考阮一峰老师的持续集成是什么?以及网上的一些资料。 基本环境搭建从安装最Mini的CentOS开始,我们需要解决如下事情。 在CentOS中使用yum命令安装出现错误提示”could not retrieve mirrorlist http://mirrorlist.centos.org *” 这是因为DNS配置错误,我装的是Cent OS 6.4 Server,没有图形界面,这个版本默认安装后,配置文件中没有配置DNS。需要通过更改配置文件来解决。方法如下:在命令提示符中输入vi /etc/sysconfig/network-scripts/ifcfg-eth0用vi 打开这个文件后,接下来会出现截图的内容, 其中要注意两个配置(按下面的值去设置)。 ONBOOT=yes MM_CONTROLLED=no 安装一些常用的工具 yum install net-tools yum install wget yum install vim yum install git 搭建Java环境步骤1:验证是否安装了Java。java -version 步骤2:下载JDK。运行以下: wget --no-cookies --no-check-certificate --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" "http://download.oracle.com/otn-pub/java/jdk/8u131-b11/d54c1d3a095b4ff2b6607d096fa80163/jdk-8u131-linux-x64.rpm" 步骤3:安装下载好的JDK。rpm -ivh jdk-8u131-linux-x64.rpm 步骤4:设置环境变量。运行vim /etc/profile.d/java.sh,然后写入以下语句: #!/bin/bash JAVA_HOME=/usr/java/jdk1.8.0_131/ PATH=$JAVA_HOME/bin:$PATH export PATH JAVA_HOME export CLASSPATH=. 保存关闭,执行命令chmod +x /etc/profile.d/java.sh让它可以运行;最后执行命令source /etc/profile.d/java.sh让环境变量失效。 步骤5:验证。java -version,echo $JAVA_HOME Jenkins搭建步骤1:添加yum repos,运行以下命令, sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key 步骤2:安装。yum install jenkins。 步骤3:配置Jenkins文件。主要配置Jenkins的端口等,避免与其它软件冲突。主要方法如下: $ sudo vim /etc/sysconfig/jenkins # 修改运行端口为9999,默认为8080 JENKINS_PORT="8181" 重启Jenkins。sudo service jenkins start 步骤4:开启防火墙。 firewall-cmd --zone=public --add-port=8081/tcp --permanent firewall-cmd --zone=public --add-service=http --permanent firewall-cmd --reload 步骤5:安装插件以及配置全局参数。 GitLab搭建参考官方文档进行安装。 步骤1:安装必要的依赖 sudo yum install curl openssh-server openssh-clients postfix cronie sudo service postfix start sudo chkconfig postfix on sudo lokkit -s http -s ssh 步骤2:下载 curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash sudo yum install gitlab-ce 步骤3:配置与启动服务 sudo gitlab-ctl reconfigure 步骤4:修改端口以及地址。 1)修改Nginx端口以及地址监听的操作如下: 在文件/etc/gitlab/gitlab.rb中修改: nginx['listen_addresses'] = ["0.0.0.0", "[::]"] # listen on all IPv4 and IPv6 addresses nginx['listen_port'] = 8082 2)修改地址:在文件/etc/gitlab/gitlab.rb中修改,external_url '你想要的地址' 3)系统默认会用到8080端口作为启动时候的必须端口。 如果不想用8080端口的话,可以通过这样的操作:在/etc/gitlab/gitlab.rb中修改unicorn['port']=端口号 4)运行sudo gitlab-ctl reconfigure 步骤5:登录。第一次登录GitLab的用户名密码为root和5iveL!fe。首次登录后会强制用户修改密码。 常用命令: sudo gitlab-ctl start # 启动所有 gitlab 组件; sudo gitlab-ctl stop # 停止所有 gitlab 组件; sudo gitlab-ctl restart # 重启所有 gitlab 组件; sudo gitlab-ctl status # 查看服务状态; sudo gitlab-ctl reconfigure # 启动服务; sudo vim /etc/gitlab/gitlab.rb # 修改默认的配置文件; gitlab-rake gitlab:check SANITIZE=true --trace # 检查gitlab; sudo gitlab-ctl tail # 查看日志; LDAP搭建步骤1:安装 1)检查是否安装:rpm -qa | grep openldap 2)安装: yum install -y openldap openldap-clients openldap-servers migrationtools cp /usr/share/openldap-servers/DB_CONFIG.example /var/lib/ldap/DB_CONFIG chown ldap. /var/lib/ldap/DB_CONFIG systemctl start slapd systemctl enable slapd 3)验证端口:netstat -tlnp | grep slapd 步骤2:设置 OpenLDAP 的管理员密码 1)生成处理以后的管理员密码: [root@bogon ~]# slappasswd New password: Re-enter new password: {SSHA}xxxxxxxxxxxxxxxxxxxxxxxx 2)新建文件chrootpw.ldif。{SSHA}xxxxxxxxxxxxxxxxxxxxxxxx为你在第1小步的时候生成的密码。 dn: olcDatabase={0}config,cn=config changetype: modify add: olcRootPW olcRootPW: {SSHA}xxxxxxxxxxxxxxxxxxxxxxxx 3)执行ldapadd -Y EXTERNAL -H ldapi:/// -f chrootpw.ldif 步骤3:导入基本的Schema ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/cosine.ldif ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/nis.ldif ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/inetorgperson.ldif 步骤4:设置自己的Domain Name 1)首先要生成经处理后的目录管理者明文密码。 slappasswd New password: Re-enter new password: {SSHA}xxxxxxxxxxxxxxxxxxxxxxxx 2)新建文件。chdomain.ldif,并把以下内容写入文件。(需要把"dc=***,dc=***"替换成你自己的dc内容,需要把{SSHA}xxxxxxxxxxxxxxxxxxxxxxxx替换成刚才生成的密码) dn: olcDatabase={1}monitor,cn=config changetype: modify replace: olcAccess olcAccess: {0}to * by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read by dn.base="cn=Manager,dc=dynamax,dc=io" read by * none dn: olcDatabase={2}hdb,cn=config changetype: modify replace: olcSuffix olcSuffix: dc=dynamax,dc=io dn: olcDatabase={2}hdb,cn=config changetype: modify replace: olcRootDN olcRootDN: cn=Manager,dc=dynamax,dc=io dn: olcDatabase={2}hdb,cn=config changetype: modify add: olcRootPW olcRootPW: {SSHA}xxxxxxxxxxxxxxxxxxxxxxxx dn: olcDatabase={2}hdb,cn=config changetype: modify add: olcAccess olcAccess: {0}to attrs=userPassword,shadowLastChange by dn="cn=Manager,dc=dynamax,dc=io" write by anonymous auth by self write by * none olcAccess: {1}to dn.base="" by * read olcAccess: {2}to * by dn="cn=Manager,dc=dynamax,dc=io" write by * read 3)执行:ldapmodify -Y EXTERNAL -H ldapi:/// -f chdomain.ldif 4)再新建文件:basedomain.ldif,并将以下内容写入。并将一些dc替换成你自己的 dn: dc=dynamax,dc=io objectClass: top objectClass: dcObject objectclass: organization o: Dynamax dc: Dynamax dn: cn=Manager,dc=dynamax,dc=io objectClass: organizationalRole cn: Manager description: Directory Manager dn: ou=people,dc=dynamax,dc=io objectClass: organizationalUnit ou: people dn: ou=groups,dc=dynamax,dc=io objectClass: organizationalUnit ou: groups 5)执行语句:ldapadd -x -D cn=Manager,dc=dynamax,dc=io -W -f basedomain.ldif (注意dc之类的替换) 步骤5:允许防火墙访问 LDAP 服务 firewall-cmd --add-service=ldap --permanent firewall-cmd --reload 步骤6:查询。ldapsearch -x -b "dc=dynamax,dc=io" -H ldap://192.168.118.130 步骤7:安装客户端。选择有:phpLDAPadmin等。 Nexus搭建Nexus现在为Nexus Repository Manager OSS 3.x。 安装下载安装包:wget https://sonatype-download.global.ssl.fastly.net/nexus/3/nexus-3.4.0-02-unix.tar.gz 解压(我这边是解压到/usr/local中):sudo tar -zxvf nexus-3.4.0-02-unix.tar.gz 创建链接:sudo ln -s nexus-3.4.0-02 nexus 创建nexus用户:sudo useradd nexus 授权: sudo chown -R nexus:nexus /usr/local/nexus sudo chown -R nexus:nexus /usr/local/sonatype-work/ 修改运行用户: sudo vi /usr/local/nexus/bin/nexus.rc # 修改一下内容 run_as_user="nexus" 修改端口号: 修改文件/usr/local/nexus/etc/nexus-default.properties中的application-port为你想要的端口号。 启动服务:在/usr/local/nexus/bin的目录下运行./nexus run & 注册为服务: vi /etc/systemd/system/nexus.service 添加如下内容: [Unit] Description=nexus service After=network.target [Service] Type=forking ExecStart=/usr/local/nexus/bin/nexus start ExecStop=/usr/local/nexus/bin/nexus stop User=nexus Restart=on-abort [Install] WantedBy=multi-user.target 安装并启动服务: sudo systemctl daemon-reload sudo systemctl enable nexus.service sudo systemctl start nexus.service 查看服务: sudo systemctl status nexus.service 添加防火墙规则 Jenkins配置LDAP认证登录简单介绍在Jenkins中使用LDAP进行认证的话需要哪些配置。 配置LDAP在“系统管理 ”->“Configure Global Security”中,访问控制选择LDAP。为了预防能在万一配置不成功的情况下依然可以进行配置,可以先勾选上授权策略中的“ 任何用户可以做任何事(没有任何限制)”。 如下图配置LDAP: 配置详解: Server:配置LDAP的服务器地址 root DN:配置根DN User search base:配置用户开始查找的DN User search filter:用户搜索过滤器,其实就是用户的登录标识 Group search base:配置用户的组查找的DN Group search filter:组搜索过滤器,其实就是组名的标识 Manager DN:LDAP服务器的管理账号 Manager Password:LDAP服务器的管理密码 然后点击“Test LDAP settings”,然后输入用户名密码(注意,这边的用户名为uid的值)。查看是否LDAP的配置是正确的。 然后使用LDAP中的账号登录Jenkins。 授权策略在LDAP中,我们新建2个Group(dev1和dev2),dev1组包含用户user1,dev2组包含用户user2。也就是: dn: ou=Group,dc=dynamax,dc=io objectClass: organizationalUnit ou: Group dn: cn=dev1,ou=Group,dc=dynamax,dc=io objectClass: posixGroup objectClass: top cn: dev1 memberUid: user1 gidNumber: 15107 dn: cn=dev2,ou=Group,dc=dynamax,dc=io objectClass: posixGroup objectClass: top cn: dev2 memberUid: user2 gidNumber: 32679 在Jenkins中的配置如下: 注意:上图中红色框内的为选勾内容,蓝色框中的为必选内容。 项目授权策略使用admin登录系统。新建2个job(dev1_job和dev2_job)。 如上图所示,点击“配置 ”按钮,对项目(任务)进行设置。 如下图所示,dev1_job配置用户组为dev1,dev2_job配置用户组为dev2。 值得注意的是“ Block inheritance of global authorization matrix”一定要勾选。 然后,用user1和user2登录Jenkins验证是否完成配置。 GitLab配置LDAP认证登录关于GitLab中搭配LDAP权限的操作可以参考GitLab的文档。 下面的内容是一些简单的配置,为了就是省去看英文文档的麻烦。 在 /etc/gitlab/gitlab.rb 或者 /home/git/gitlab/config/gitlab.yml 中添加以下内容: gitlab_rails['ldap_enabled'] = true gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below main: # 'main' is the GitLab 'provider ID' of this LDAP server label: 'LDAP' host: '192.168.118.129' port: 389 uid: 'uid' method: 'plain' # "tls" or "ssl" or "plain" allow_username_or_email_login: true bind_dn: 'cn=Manager,dc=dynamax,dc=io' password: 'gzhx0211' active_directory: false base: 'ou=People,dc=dynamax,dc=io' user_filter: '' EOS 然后运行以下命令让配置生效。gitlab-ctl reconfigure OK,这样以后你就可以使用LDAP的用户登录了。接下来问题来了,怎么分配角色呢?貌似CE版本没有提供,不过我们可以使用程序进行开发。 Nexus配置LDAP认证登录在安装Nexus 3.3.2-02以后,默认的用户名为admin,密码为admin123。用admin登录Nexus。 在“Security”->”LDAP”中配置LDAP的信息。 配置连接 点击“ Verify connection”如果连接成功则在屏幕的右上角弹出一个绿色的提示。 配置用户如下图显示: 配置角色如下图所示: 至此,CI需要的基本上就搭建OK了。剩下的Sonar和Jira等下次再搭建。]]></content>
<categories>
<category>运维</category>
</categories>
<tags>
<tag>运维</tag>
<tag>持续集成</tag>
</tags>
</entry>
<entry>
<title><![CDATA[十年]]></title>
<url>%2F2017%2F06%2F23%2F%E5%8D%81%E5%B9%B4%2F</url>
<content type="text"><![CDATA[有感于最近发生的一些事情,于在临海的动车上写下此篇文字。 今天,24号。十年前的今天应该是我高考成绩出来的时候。很清晰地记得,那天很早的时候就去山上的果园修剪火龙果的枝桠,妹妹在家帮我用电话查询成绩以后告知了爸爸。那个成绩,我很意外,但又是在意料之中。随后的查卷啊之类的,现在想想也是蛮多余的。那个时候的我应该就是个鸵鸟吧,遇到危险就把头低低地埋着,自以为那是躲避危险的好办法。自那以后,一股不自信就在心中油然而生,直到现在还依旧存在!没有选择复读,直接去上了一所专科学校。所以,昨天在看到2017年的高考线的时候,我真的不知道该说什么。说自己早生了十年?说现在的孩子运气真好? 昨天,也就是23号。我从大公司离职了。很矛盾地走了,有一丝舍不得,有一丝庆幸,也有一丝迷茫!2015年入职网龙的时候,我抱着很大的希望,希望能接触到架构方面的东西。但现实给了我很大的打击,没有项目,一切的一切都是内部预演,技术就是在sdp平台上进行开发。而且,很长时间我还做做着前端的事情。不过,不能否认的是在网龙的2年让我的简历稍微好看些,而且前端技术也接触到了,工资也还可以,我不亏!事不紧张、钱还可以、厦门公司的时间还算自由、公司大、稳定,这些都是很多人眼中好工作的标签吧!这样的好工作就像冷水煮青蛙一样,等到一定的程度,青蛙也就无能力跳出那个足以毁掉它的容器。也许,我这样说真的有很大的夸张;也许,他们又会是在公司上市后的富翁;也许,青蛙也是蛮幸福的事情。谁知道呢?职业的危机感让我觉得不能再这么下去了,所以选择了离开。 那天和金龙说,我用了7年的时间达到了一些毕业生的薪资水平;金龙说,他们以前比我们努力!可是从懂事以来的日子,我自己不够努力么?我每天都在努力地学习着,努力地做事着。为什么还是这样呢?我想我错了。我的那个努力不叫努力,应该叫刷存在感!首先是专心问题。做作业的时候专心了吗?看书的时候专心了吗?敲代码的时候专心了吗?不专心,也就是没有效率;没有效率,就意味着花费比别人更多的时间,而且结果还没有他人好。然后就是坚持。做一件事我坚持了么?追一个女孩我坚持了么?健身运动我坚持了么?学习英语我坚持了么?没有!没有一件是持之以恒的坚持,是三天打鱼两天晒网的行动,是以一些失败就放弃的结果!第三是心态。从读书时代的似懂非懂就是懂的态度,到现在的实用就好何必原理态度;丰富的计划贫瘠的实施,过分地追求所谓的完美;浅尝辄止,不求甚解! 努力真的不是一个普通的词。我想很多人都知道自己的毛病吧!也有很多人想改正自己的毛病吧!突然想到一句话,能改的叫上进,不能改的叫个性。计划时间表,奖励制度,惩罚制度,这三个是我能想到的解决方法。因地制宜因时制宜的计划,完成以后给自己奖励,不完成给自己惩罚。这样也许能让自己看上去更有意思些!不知道,身边也没有人监督自己,真的得找一个女朋友了!变得更优秀吧!也许,一切真的没有那么糟… 列车从连江站开出了。调整心态,去迎接接下来的事情吧!]]></content>
<categories>
<category>未分类</category>
</categories>
<tags>
<tag>生活</tag>
<tag>杂念</tag>
</tags>
</entry>
<entry>
<title><![CDATA[理解SpringBoot自动配置实现]]></title>
<url>%2F2017%2F06%2F16%2F%E7%90%86%E8%A7%A3Spring%20Boot%E8%87%AA%E5%8A%A8%E9%85%8D%E7%BD%AE%E5%AE%9E%E7%8E%B0%2F</url>
<content type="text"><![CDATA[Spring Boot的主程序是一个标注了@SpringBootApplication的类,用了main方法执行了SpringApplication的run方法,这个简单的主程序将加载应用的所有的配置和资源,并且启动了一个实例。 主程序的运行过程方法run按顺序做了以下几件事: 开启一个SpringApplicationRunListeners监听器 创建一个应用上下文ConfigurableApplicationContext 通过上下文加载应用所需的所有的配置和各种环境配置 启动一个实例 createApplicationContext方法创建了一个上下文;prepareContext方法装载上下文,它调用的load方法将调用BeanDefinitionLoader的load方法来装载应用定义的和需要的类以及各种资源;refreshContext方法调用了ApplicationContext的refresh方法重新加载了上下文;然后在afterRefresh方法中启动实例; 自动配置原理程序的所有自动配置都是从@SpringBootApplication引入的,它主要包含了3个非常非常主要的注解,@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan。@EnableAutoConfiguration用来启动自动配置,并将导入一些自动配置的类定义;@ComponentScan将扫描和加载应用中的一些自定义的类;@SpringBootConfiguration表明一个这个类提供了Spring配置; 在@EnableAutoConfiguration类中引用了SpringFactoriesLoader,而SpringFactoriesLoader会查询META-INF/spring.factories文件中包含的JAR文件,当找到spring.factories文件后,SpringFactoriesLoader将查询配置文件命名的属性,然后进行逐个自动配置(那个文件里面各个都是配置类)。在装载一个类的配置时,首先读取项目中的配置,只有在项目中没有相关配置的时候才启用配置的默认值。]]></content>
<categories>
<category>Spring</category>
<category>Spring Boot</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>Spring Boot</tag>
</tags>
</entry>
<entry>
<title><![CDATA[对《深入理解Java虚拟机》的总结(一)]]></title>
<url>%2F2017%2F05%2F14%2F%E5%AF%B9%E3%80%8A%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E3%80%8B%E7%9A%84%E6%80%BB%E7%BB%93%EF%BC%88%E4%B8%80%EF%BC%89%2F</url>
<content type="text"><![CDATA[这是《深入理解Java虚拟机》第二章和第三章的读书笔记。 Java内存区域以下的这张图给出了JVM所管理的内存在运行时的数据区域: JVM栈:它的生命周期和线程相同。它描述的是Java方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 Java堆:Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的是存放对象的实例,几乎所有的对象实例都在这里进行分配内存。是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。可分为新生代和老年代。 方法区:和Java堆一样是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。也称为永久代。 运行时常量池:是方法区的一部分。用于存放编译期生成的各种字面量和符号引用。它具备动态性。 程序计数器:它的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令(分支、循环、跳转、异常处理等)。在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。 本地方法栈:与JVM栈相似。本地方法栈服务于虚拟机执行Native方法,JVM栈服务于执行Java方法。 对象访问在最简单的访问中也会涉及Java栈、Java堆、方法区这三个最重要的内存区域之间的关系。 比如在代码:Object obj = new Object();如果该语句出现在方法体中,那么Object obj这一部分的语义将会反映在本地变量表中,作为一个reference类型数据出现。new Object()这部分的语义将反映到Java堆中,形成一块存储了Object类型所有实例数据值的结构化内存,根据具体类型以及虚拟机实现的对象内存布局的不同,这块内存的长度是不固定的。另外在方法区中还存储有能找到次对象类型数据的地址信息。 主流访问对象的方式有两种:使用句柄和直接指针。 使用句柄的方式:Java堆中将会划出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。如下图: 直接指针的方式:Java堆对象的布局中必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址。如下图: 使用句柄访问方式的最大的好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中实例数据指针,而reference本身不需要被修改。使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多也是一项非常可观的执行成本。 判断对象是否还活着的算法引用计数算法给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数值就减1;任何时刻计数器都未0的对象就是不可能再被使用的。ps:JVM不是通过使用引用计数算法来判断对象是否存活的。 根搜索算法(GC Roots Tracing)Java虚拟机使用该算法判断对象是否存活的。 基本思路:通过一系列的名为“GC Roots”的对象作为起始点,从这些起始点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链想连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用对象。 在Java语言中,可以作为GC Roots的对象包括: 虚拟机栈(栈帧中的本地变量表)中的引用的对象。 方法区中的类静态属性引用的对象。 方法区中的常量引用的对象。 本地方法栈中JNI的引用的对象。 4种引用类型引用可以分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。 强引用就是指在程序代码中普遍存在的,类似Object obj = new Object()这类的引用。只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象。 软引用用老描述一些还有用的,但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。JDK提供SoftReference类来实现软引用。 弱引用也是用来描述非必须对象的,它比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。JDK提供WealReference类来实现弱引用。 虚引用是最弱的引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的是希望能在这个对象被收集器回收的时候收到一个系统通知。JDK中使用PhantomReference类实现。 判断一个对象是生存还是死亡的算法在根搜索算法中不可达的对象,并非是必须死亡的。要真正宣告一个对象的死亡,至少需要经历两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法以及被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”,那么这个对象就可以死亡了。如果这个对象被判断为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalize线程去执行(虚拟机执行这个方法,但是不保证运行结束)。finalize()方法是对象逃离死亡的最后一次机会,GC将对F-Queue的对象进行第二次标记,如果对象在finalize()中重新建立了引用,那么就不会死亡,否则将会死亡。 垃圾收集算法以下介绍了“标记-清除算法”、“复制算法”、“标记-整理算法”以及“分代收集算法”。 标记-清除算法标记-清除算法(Mark-Sweep)是最基本的算法,分为“标记”和“清除”两个阶段。首先标记处所有需要回收的对象,在标记完成之后就统一清除掉所有被标记的对象。主要缺点: 效率问题。标记和清除的过程效率都不高 空间问题。标记清除以后会产生大量不连续的空间碎片,空间碎片太多会导致程序以后的内存分配问题。 复制算法复制算法为了解决效率问题。它将可同内存按容量划分为大小相等的两块,每次只使用其中的一块。当一块的内存用完了,就将还存活的对象复制到另一块上,然后再把原先那块内存空间一次清理掉。这样,就每次只对一块内存进行分配,也不用考虑内存碎片问题。 主要缺点: 没存缩小为原来的一半,代价高。 在对象存活率较高的时候就要执行较多的复制操作,效率将会变低。 现在的商业虚拟机都采用这种算法回收新生代。在新生代中,有一块比较大的Eden和两块比较小的Survivor空间,每次使用Eden和其中的一块Survivor;回收的时候,将Eden和Survivor中还活着的对象一次性拷贝到另一块Survivor上,清除已被使用的Eden和Survivor。当Survivor不够用的时候,使用老年代进行分担。所以,默认的Eden和两块Survivor大小比为8:1:1。 标记-整理算法Mark-Compact算法的标记过程和“标记-清除算法”一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。 分代收集算法当前的商业虚拟机都采用“分代收集(Generation Collection)”算法。这种算法根据对象的存活周期的不同将内存划分为几块。一般把Java的堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当地收集算法。 在新生代中,每次垃圾收集时都有大量对象死去,只有少量存活,那就使用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。在老年代中,对象存活率高,没有额外空间对它进行分配担保,使用“标记-清除”算法或者“标记-整理”算法。 垃圾回收器收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。 Serial收集器这个收集器是个单线程收集器。它在工作的时候必须暂停其他所有的工作线程(Stop The World),直到它收集结束。这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户的正常工作的线程停止掉,然后进行垃圾收集。 它是虚拟机运行在Client模式下的默认新生代收集器。优于其他收集器的地方是:简单而高效。 ParNew收集器ParNew收集器是Serial收集器的多线程版本。在控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器一样。 它是虚拟机运行在Server模式下的默认新生代收集器。它能与CMS收集器配合工作。它默认开启的线程数和CPU的数量相同。 Parallel Scavenge收集器它也是一个新生代收集器,也是使用复制算法,是并行的多线程收集器。它的目标是达到一个可控制的吞吐量(Throughput = 运行用户代码的时间/(运行用户代码的时间+垃圾回收时间))。所以被称为“吞吐量优先”收集器。 主要适用于在后台运行而不需要太多交互的任务。 Serial Old收集器Serial Old收集器是Serial收集器的老年代版本。也是一个单线程收集器,使用“标记-整理”算法。 它主要是被在Client模式下虚拟机使用。如果在Server模式下,它有:1)在1.5及以前版本中与Parallel Scavenge收集器搭配使用;2)作为CMS收集器的后备预案,在收集器发生Concurrent Mode Failure的时候使用。 Parallel Old收集器Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。 CMS收集器CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它是基于“标记-清除”算法实现的。整个过程分为4个步骤: 初始标记(CMS initial mark) 并发标记(CMS concurrent mark) 重新标记(CMS remark) 并发清除(CMS concurrent sweep) 初始标记需要进行Stop The World,它仅仅是标记一下GC Roots能直接关联到的对象,速度很快;并发标记就是进行GC Roots Tracing的过程;这个阶段的耗时比较长;重新标记也需要进行Stop The World,该阶段是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间会比初始标记长些而远远短于并发标记;并发清除就是进行清除的过程;这个阶段的耗时比较长; CMS收集器的内存回收过程是与用户线程一起并发地执行的。 CMS收集器很符合现在互联网或者B/S系统服务器的需求——重视服务的响应速度、希望系统停顿时间短。 CMS收集器的显著缺点: CMS收集器对CPU资源非常敏感。CMS默认的回收线程数为:(CPU数量+3)/4。也就是当CPU在4个以上的时候,并发回收时垃圾收集器线程最多占用不超过25%的CPU资源;当CPU不足4个的时候,那么CMS对用户程序的影响就比较大。 CMS收集器无法处理浮动垃圾(Floating Garbage),可能会出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。 CMS收集器采用“标记-清除”算法,在收集结束的时候可能会产生大量的空间碎片。 G1收集器Garbage First收集器。基于“标记-整理”算法,可以非常显著的控制停顿。 特点: 并行与并发:和CMS类似。 分代收集:保留了新生代和来年代的概念,但新生代和老年代不再是物理隔离的了它们都是一部分Region(不需要连续)的集合。同时,为了避免全堆扫描,G1使用了Remembered Set来管理相关的对象引用信息。 空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。 步骤: 初始标记(Initial Making) 并发标记(Concurrent Marking) 最终标记(Final Marking) 筛选回收(Live Data Counting and Evacuation) 初始阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以用的Region中创建新对象,这个阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这一阶段耗时较长但能与用户线程并发运行。而最终标记阶段需要吧Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但可并行执行。最后筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这一过程同样是需要停顿线程的,但Sun公司透露这个阶段其实也可以做到并发,但考虑到停顿线程将大幅度提高收集效率,所以选择停顿。 内存配置与回收策略Java技术体系中的自动内存管理最终可以归纳为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。 对象优先在Eden中分配大多数情况下,对象在新生代Eden区中分配。档Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。 -Minor GC:新生代GC,指发生在新生代的垃圾收集动作,因为Java对象大多数都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也非常快。-Major GC/Full GC:老年代GC,指发生在老年代的GC,出现了Major GC经常就会至少有一次Minor GC。Major GC的速度比Minor GC慢10倍以上。 大对象直接进入老年代大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。 长期存活的对象将进入老年代虚拟机给每个对象定义一个年龄计数器,对象在Eden中经历第一次Minor GC仍然存活,就被移动到Survivor空间,设置年龄为1,在Survivor空间中没经历一次Minor GC,年龄加1,当达到默认的年龄15以后,就将被放到老年代。 动态对象年龄判定如果在Survivor空间中,相同年龄所有对象大小的总和大于Survivor空间的一般,那么年龄大于或等于该年龄的对象就可以直接进入老年代。 空间分配担保发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,则只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。 新生代Eden,Survivor A, Survivor B三块空间和老生代Old之间的流程关系: 参考: http://www.cnblogs.com/wcd144140/p/5624063.html https://www.diycode.cc/topics/597]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>JVM</tag>
</tags>
</entry>
<entry>
<title><![CDATA[webpack打包过后文件太大的那些事]]></title>
<url>%2F2017%2F04%2F21%2Fwebpack%E6%89%93%E5%8C%85%E8%BF%87%E5%90%8E%E6%96%87%E4%BB%B6%E5%A4%AA%E5%A4%A7%E7%9A%84%E9%82%A3%E4%BA%9B%E4%BA%8B%2F</url>
<content type="text"><![CDATA[问题用ReactJs+Redux+Webpack开发已经有一年多了,因为项目基本上是在内网访问的,所以也就没有怎么去注意打包后生成文件的大小了。现在,项目在外网的情况收到的反馈是加载速度慢,发现一个bundle已经有好几M了,这是不可忍受的~!也就有了去优化打包的想法。 优化合并公共代码将一些公共的代码和共同引用的第三方类库单独打包,同时将reactJs相关的类库单独打包。如: entry: { index: [ './src/index.js' ], antd: ['antd'], react: ['react', 'react-dom', 'react-redux', 'react-router', 'redux', 'redux-router', 'redux-thunk'], vendors: ['crypto-js', 'history', 'isomorphic-fetch', 'jquery', 'lodash'] }, 以上将antd、react以及一些工具类分别单独打包出来。具体的使用可以参考: webpack CommonsChunkPlugin详细教程 CommonsChunkPlugin devtool 中的 source-mapsource-map在调试的时候非常好用。但是在生产环境下应该关闭它,设置为:devtool: false。 这些source-map有哪些区别呢?可以参考:webpack sourcemap 选项多种模式的一些解释 使用UglifyJsPlugin压缩文件使用UglifyJsPlugin压缩文件,并且去掉注释。可以让打包后的文件大小少了很多。 new webpack.optimize.UglifyJsPlugin({ comments: false, //去掉注释 compress: { warnings: false //忽略警告,要不然会有一大堆的黄色字体出现…… } }), 分离CSS样式分离CSS样式原先就这么做了,这里还是再拿出来讲一下。 new ExtractTextPlugin('index_[contenthash].css', { allChunks: true }), 使用DedupePlugin使用DedupePlugin插件可以找到Js的依赖树,并且删除这些重复的依赖 开启gzip压缩经过以上的步骤以后打包出来的文件已经小了很多了,但还是不太满意。通过使用gzip压缩可以进一步减少文件的大小,就是有个缺点是打包的时候会慢一些。 new CompressionWebpackPlugin({ //gzip 压缩 asset: '[path].gz[query]', algorithm: 'gzip', test: new RegExp( '\\.(js|css)$' //压缩 js 与 css ), threshold: 10240, minRatio: 0.8 }) HtmlWebpackPlugin中的设置设置minify属性的值: new HtmlWebpackPlugin({ title: '项目官网 3.0', template: 'index-template.html', inject: 'body', minify: { removeComments: true, //去注释 collapseWhitespace: true, //压缩空格 removeAttributeQuotes: true //去除属性引用 } }), 还可以再次进行的优化完成以上的方法以后,打包文件已经小了很多了。但依旧还是存在优化的空间,比如说可以使用externals然后采用cdn加载react、antd等。 参考: https://segmentfault.com/a/1190000007892189 https://github.com/youngwind/blog/issues/65 http://www.alloyteam.com/2016/01/webpack-use-optimization/]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>实战</tag>
<tag>webpack</tag>
</tags>
</entry>
<entry>
<title><![CDATA[数据库事务小结]]></title>
<url>%2F2017%2F04%2F05%2F%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BA%8B%E5%8A%A1%E5%B0%8F%E7%BB%93%2F</url>
<content type="text"><![CDATA[面试的时候,总会被问到事务。很多时候知道是怎么回事,但是回答的时候又是那么吞吞吐吐的,反而表达不清楚。仔细想想,表达不清楚那是因为自己还不熟悉。所以将先前的一些资料重新整理了一下。 什么是数据库事务数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。事务是数据库运行中的逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。 原子性:表示组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有的原子操作执行成功,整个事务才提交;事务中任何一个数据库操作失败,已经执行的任何操作都必须撤销,让数据库返回到初始状态。 一致性:事务操作成功后,数据库所处的状态和它的业务规则是一致的,即数据不会被破坏。如从A账户转钱到B账户,不管成不成功,A账户与B账户的总额是不变的。 隔离性:在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对对方产生干扰。也就是说,一个事务直到它被成功提交之后,它的结果对于任何其他的事务才是可见的。 持久性:一个已提交事务的任何结果都必须是永久性的,即“在任何系统奔溃的情况下都能够保存下来”。 数据并发的问题在多个客户端对数据库进行并发操作的时候,会出现一些并发问题。这些问题可以归纳为5类:3类数据读问题(脏读、不可重复读和幻读)和2类数据更新问题(第一类丢失更新和第二类丢失更新)。 脏读(dirty read)A事务读取B事务尚未提交的更改数据,并在这个数据的基础上操作。 不可重复读(unrepeatable read)A事务读取了B事务已经提交的更改数据。 幻读(phantom read)A事务读取B事务提交的新增数据。这个时候A事务出现幻读问题。幻读一般发生在计算统计的事务当中。 幻读和不可重复读的区别:幻读是指读到了其他已经提交事务的新增数据;不可重复读是指读到了已经提交事务的更改数据(更改或删除)。防止读取到更改数据,只需要对操作的数据添加行级锁,阻止操作中的数据发生变化;防止读取到新增的数据,往往需要添加表级锁——将整个表锁定,防止新增数据。 第一类丢失更新A事务撤销时,把已经提交的B事务的更新数据覆盖了。 第二类丢失更新A事务覆盖B事务已经提交的数据,造成B事务所做的操作丢失。 数据库锁机制表锁定和行锁定按照锁定的对象的不同可以分为表锁定和行锁定。表锁定是对整个表进行锁定。行锁定对表中特定的行进行锁定。 共享锁定和独占锁定从并发事务锁定的关系上来说,可以分为共享锁定和独占锁定。共享锁定会防止独占锁定,但允许其他的共享锁定。独占锁定既防止其他的独占锁定也防止其他的共享锁定。 事务隔离级别 隔离级别 脏读 不可重复读 幻读 第一类丢失更新 第二类丢失更新 READ UNCOMMITTED(读未提交) 允许 允许 允许 不允许 允许 READ COMMITTED(读已提交) 不允许 允许 允许 不允许 允许 REPEATABLE READ(可重复读) 不允许 不允许 允许 不允许 不允许 SERIALIZABLE(串行化) 不允许 不允许 不允许 不允许 不允许 SERIALIZABLE(串行化):可避免脏读、不可重复读、幻读的发生。 REPEATABLE READ(可重复读):可避免脏读、不可重复读的发生。 READ COMMITTED(读已提交):可避免脏读的发生。 READ UNCOMMITTED((读未提交):最低级别,任何情况都无法保证。 事务的隔离级别和数据库的并发是向对立的。READ UNCOMMITTED的隔离级别低,但拥有最高的并发性和吞吐量。SERIALIZABLE的隔离级别高,但并发性低。 默认的隔离级别是REPEATABLE READ(可重复读)]]></content>
<categories>
<category>数据库</category>
</categories>
<tags>
<tag>数据库事务</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Docker简单入门]]></title>
<url>%2F2017%2F03%2F06%2FDocker%E7%AE%80%E5%8D%95%E5%85%A5%E9%97%A8%2F</url>
<content type="text"><![CDATA[容器容器是一个大包了应用和服务的环境,是一个轻量级的虚拟机。每一个容器都由一组特定的应用和必要的依赖库组成。 创建容器 docker create命令 docker run命令 docker create创建的容器处于停止状态,docker run不仅创建了容器而且启动了容器(相当于docker create & docker run)。 代码示例: docker ps -a docker run hello-world docker ps -a 以上代码中的docker ps -a表示查看所有的容器。 容器的类型: 交互型容器:运行在前台,通常会指定有交互的控制台,可以给容器输入,也可以得到容器的输出。创建该容器的终端被关闭,在容器内部使用exit命令或者调用了docker stop、docker kill命令后,容器会变成停止状态。 后台型容器:运行在后台,创建启动之后就与终端无关。即便终端关闭了,该后台也依然存在,只有调用了docker stop或者docker kill命令才能使容器编程停止状态。 创建交互型容器命令:docker run -i -t --name=XXX image名称 -i表示用于打开容器的标准输入,-t告诉Docker为容器建立一个命令行终端 创建后台型容器命令:docker run --name=XXX -d image名称 -d参数用于创建后台型容器 查看容器docker ps -a命令可以查看当前的所有的容器。 该命令运行后出现了以下各列: CONTAINER ID:唯一标识容器的ID IMAGE:创建容器时所使用的的镜像 COMMAND:容器最后运行的命令 CREATED:创建容器的时间 STATUS:容器的状态 PORTS:对外开放的端口 NAMES:容器名 启动容器使用docker start来启动之前已经停止了的容器。 –restart会让容器在退出后能够自动重启。–restart=always表示不管容器返回的是什么,都会重启容器;–restart=on-failure:5表示当容器返回非0时,Docker重启容器,最多尝试重启5次。 终止容器交互型容器可以输入exit或者ctrl+d组合键来使其退出。 docker stop命令的默认行为会导致容器退出。docker kill可以强制退出容器。 删除容器使用命令docker rm删除容器。 在删除容器的时候,必须先停止然后才能删除 其他常用命令 docker attach依附命令,通常用在start或restart启动的交互型容器中。 docker logs命令,查看容器日志 docker top命令,查看容器进程 docker inspect命令,用于查看容器的配置 docker exec命令,用于在容器中运行新的任务(后台型任务和交互型任务) docker export命令,导出容器 docker import命令,导入容器 镜像镜像是一个包含程序运行必要依赖环境和代码的只读文件,它采用分层的文件系统,将每一次改变以读写层的形式增加到原来的只读文件上。 镜像是容器的运行基础,容器是镜像运行后的形态。 镜像的系统结构 镜像的本质是磁盘上一系列文件的集合。创建新的镜像其实也就是对已有镜像文件进行增、删、改操作。 镜像的写时复制机制通过docker run命令运行镜像创建一个容器时,实际上是在该镜像之上创建一个空的可读写文件系统层级。可以将这个文件系统当成一个新的临时镜像,而命令里所指定的镜像称为父镜像。父镜像的内容都是以只读方式挂载进来的,容器会读取共享父镜像的内容。不过一旦需要修改父镜像文件,便会触发Docker从父镜像中复制这个文件到临时镜像中来,所有的修改均发生在你的文件系统中,而不会对父镜像造成任何影响,这就是Docker镜像的写时复制机制。 管理本地镜像查看列出本机上的所有镜像: docker images 查看镜像的详细信息:docker inspect 镜像名 下载可以使用docker run命令,安装的时候下载,也可以使用docker pull命令预先下载镜像。 删除使用命令docker rmi。 有的时候出现镜像删除不掉的情况,遇到这种情况的很大一部分原因是由于这个镜像被容器所依赖。可以使用-f参数进行强制删除;或者先将依赖它的镜像和容器移除,然后再删除。 创建本地镜像第一种方式:使用导入tar包的方式要求是该tar包是由镜像导出的。 第二种方式:使用commit命令创建本地镜像通过实例来说明怎么操作的: docker run -t -i ubuntu apt-get update apt-get install sqlite3 echo “test docker commit” >> hellodocker exit docker commit -m=”Message” –author=”HingKwan” xxxxxxxx(容器的ID) hingkwan/sqlite3:v1 docker images docker run -t -i hingkwan/sqlite3:v1 第三种方式:使用Dockerfile创建镜像这种方法是将需要对镜像进行的操作全部写到一个文件中,然后使用docker build命令从这个文件中创建镜像。 首先创建如下文件: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950# version:1.0.0# 指定待扩展的父级镜像FROM ubuntu:latest# 用来声明创建的镜像的作者信息MAINTAINER xxx "[email protected]"# 设置root用户为后续命令的执行者USER root# 执行操作RUN apt-get updateRUN ["apt-get","update"]RUN apt-get install -y nginx# 使用&&拼接命令RUN touch test.txt && echo "abc" >> abc.txt# 对外暴露接口EXPOSE 80 8080 1038# 添加文件ADD abc.txt /opt/# 添加文件夹ADD /webapp /opt/webapp# 添加网络文件ADD http://xxx.com/img/aaa.png /opt/# 设置环境变量ENV WEBAPP_PORT=9090# 设置工作目录WORKDIR /opt/# 设置启动目录ENTRYPOINT ["ls"]# 设置启动参数CMD ["-a","-l"]# 设置卷VOLUME["/data","/var/www"]# 设置子镜像的触发操作ONBUILD ADD . /app/srcONBUILD RUN echo "on build excuted" >> onbuild.txt 执行命令:docker build -t hingkwan/test:v1 . 或者把Dockerfile保存到git上,然后执行:docker build -t hingkwan/test:v1 git的地址 数据卷创建数据卷可以通过两种方法创建数据卷: 在Dockerfile中,使用VOLUME指令。 在命令行中使用docker run时,使用-v参数来创建数据卷并将其挂载到容器中。 挂载主机目录和文件作为数据卷我们可以指定宿主主机上的某个目录或者文件作为数据卷。 使用命令示例如: docker run -d -P –name webapp -v pwd :/webapp training/webapp python app.py Dockerfile并不支持挂载本地目录到数据卷,这主要是因为不同操作系统的目录格式不尽系统。为了保证Dockerfile的移植性,所以不支持挂载本地目录到数据卷。 数据卷容器数据卷容器是指一个专门用于挂载数据卷的容器,以供其他容器引用和使用。它主要用在多个容器需要从一处获得数据时。 使用示例: docker run -d -v /dbdata –name dbdata training/postgres # 这行代码的意思是建立一个数据容器,名为dbdata;并为该容器新建数据卷/dbdata docker run -d –volumns-from=dbdata –name db1 training/postgres # 创建容器db1,它引用dbdata的数据卷 数据卷一旦声明,它的生命周期和声明它的那个容器就没有关系了。声明它的容器停止了,数据卷依然存在。一个容器引用一个数据卷容器时,并不要求数据卷容器是运行的。 数据的北方与恢复利用数据卷容器,还可以进行数据的备份和恢复等。 备份利用数据卷容器,我们可以备份一个数据卷容器的数据。 docker run -d -v /dbdata –name dbdata training/postgres # 创建一个数据卷容器 docker run –volumns-from dbdata -v $(pwd):/backup ubuntu tar cvf # 将/dbdata中的数据备份到本地,备份后的数据为backup.tar 恢复 docker run -v /dbdata –name dbdata2 ubuntu /bin/bash # 声明一个需要恢复的数据容器。创建了数据卷/dbdata & 名为dbdata2的容器 docker run –volumns-from dbdata2 -v $(pwd):/backup busybox tar xvf # 利用另一个引用它的容器来关联到本地目录,并将本地的数据解压进数据卷中 容器连接容器连接包括源容器和目标容器:源容器是提供服务的一方,对外提供指定服务;目标容器连接到源容器后,就可以使用其所提供的服务。 连接的格式为--link name:alias。 docker run -d –name dbdata training/postgres docker run -d -P –name web –link dbdata:db training/webapp python app.py 使用代理连接,可以解耦两个原本直接相连的容器的耦合性。]]></content>
<categories>
<category>Docker</category>
</categories>
<tags>
<tag>Docker</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Web应用中的缓存]]></title>
<url>%2F2017%2F02%2F06%2FWeb%E5%BA%94%E7%94%A8%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%2F</url>
<content type="text"><![CDATA[缓存技术在Web应用中也是一项关键技术,在提升性能方面起了重要的作用。 缓存命中率本质上,缓存是否有效依赖于你能多少次重用一个缓存响应业务请求,这个度量指标就是缓存命中率。她是应用缓存技术时简单而重要的一项指标。 影响缓存命中率的因素:缓存键集合大小、内存空间和缓存寿命。 缓存中每个对象使用缓存键进行识别,定义一个对象的唯一方式就是对象缓存键执行精确匹配。键数量越少,缓存的效率越高。 内存空间的大小直接决定了缓存对象的平均大小和缓存对象数量。物理上能缓存的对象越多,缓存命中率就越高。 对象缓存的时间越长,缓存对象被重用的可能性就越高。 基于HTTP的缓存 通读缓存:一个缓存组件,它能给客户端返回缓存资源,或在请求未命中缓存时获取实际数据。 HTTP缓存头大部分HTTP头信息是可选的,被用以协调期望的行为。 Cache-Control Cache-Control允许你指定多个选项。具体的设置可以参考:Cache-Control Expries 它指定一个缓存对象失效的绝对时间点。 Web响应的过期时间可以由Cache-Control:max-age=600定义,也可以使用Expries头定义一个绝对的时间值。在响应中同时包含这两种头是冗余的,会导致混乱和潜在的不一致性。因此,推荐使用你想用的头并固定使用它们,而不是将所有可能的头都用在响应中。 Vary 这个头的目的是高速缓存你需要基于某些HTTP请求头,生成响应的多个变体。 其他的HTTP缓存可以参考:HTTP缓存 HTTP缓存技术类型目前主要有4种HTTP缓存:浏览器缓存、缓存代理、反向代理和CDN。 伸缩HTTP缓存我们在进行伸缩HTTP缓存的时候,不用去担心浏览器缓存、第三方缓存代理和CDN的伸缩性。应该重点关注反向代理。 要想有效地伸缩反向代理层,一定要被关注的是缓存命中率: 1)缓存键空间。描述了反向代理在一段时间内需要记录多少唯一URL的数量。 2)平均响应存活时间TTL。它描述了响应被缓存的时间。 3)缓存对象的平均大小。它影响了反向代理能够使用多大的内存或存储,来保存最常访问的对象。 缓存应用对象定制对象缓存。对象缓存的使用方式和HTTP缓存不同,它是旁路缓存而不是通读缓存。在旁路缓存中,应用需要意识到缓存对象的存在,旁路缓存主动存储和获取对象,缓存并不是透明地位于应用和数据之间。 旁路缓存被应用视为一个独立的键值对存储。应用代码通常会询问对象缓存需要的对象是否存在,如果存在,它会获取并使用缓存的对象。如果需要的对象不存在或已过期,应用会做必要的工作从头构建对象,并将其保存会对象缓存中以便将来使用。 对象缓存的一般类型客户端缓存可以通过JavaScript把对象缓存到客户端浏览器中(localStorage等,key-value的形式)。 本地缓存将对象直接缓存到服务器上。通常有以下几种实现方式: 对象直接缓存在应用内存 对象存储在共享内存,同一台机器的多个进程可以访问它们 缓存服务器作为独立应用部署在Web服务器上 本地缓存的好处:持久化和访问速度都很快;开发和部署简单;本地缓存的缺点:服务器间很多重复的数据;缓存无法保持一致; 实现的框架有:EhCache等 分布式对象缓存分布式缓存与本地缓存主要的差别是:与分布式缓存交互需要与缓存服务器进行网络通信。分布式缓存提供了比本地缓存更好的伸缩性。 实现的技术有:Redis、Memcached等 伸缩对象缓存当要进行对象缓存伸缩时: 选择的方案取决于缓存的位置和类型。也就是根据业务选择客户端缓存、本地缓存或者分布式缓存 使用数据复制,或者混合使用数据分区和数据复制。 缓存的经验法则缓存整个调用栈关于缓存,其中最重要的一点就是:你能缓存的调用栈越高,能节省的资源就越多。 相比于仅仅缓存用来构建页面的数据库查询结果,如果你缓存整个页面片段,显然可以节省更多时间及资源。缓存的终极目的是避免Web请求到达Web服务器,即便做不到,你仍需要尽量地缓存整个调用栈。 用户间缓存重用尽可能多地在不同请求或用户间重用相同的缓存对象。缓存对象如果不被再次请求,那么就是时间和资源的浪费。 关键点在于你需要最大化缓存命中率,要做到这一点,你可以通过增加缓存池,延长缓存对象TTL,减少可能的缓存键的数量来实现。 从哪儿开始使用缓存?如果你在优化一个没有充分使用缓存的Web应用,你需要问自己,从哪儿入手?哪些查询是重要且需要缓存的?哪些页面值得缓存?哪些服务需要的缓存最多?因为就优化而言,都需要基于一个严格而简单的度量标准进行优先级排列,而不是仅仅依靠直觉。要评估哪些地方需要缓存,可以用生成特定响应的聚合时间来度量。聚合时间可以采用以下方式计算:聚合时间=每个请求消耗的时间请求数量*。 缓存失效的困难缓存失效会很快使应用的处理变得困难起来。 缓存失效最好的替代方案就是在缓存对象上设置一个较短的TTL,这样数据就不会太陈旧。]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
<tag>架构</tag>
<tag>缓存</tag>
</tags>
</entry>
<entry>
<title><![CDATA[可伸缩Web应用中的前端架构]]></title>
<url>%2F2017%2F01%2F24%2F%E5%8F%AF%E4%BC%B8%E7%BC%A9Web%E5%BA%94%E7%94%A8%E4%B8%AD%E7%9A%84%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%2F</url>
<content type="text"><![CDATA[前端包括客户端(Web浏览器、移动端)、客户端和数据中心之间的网络,以及数据中心响应用户请求的部分。因为用户的每次交互、每个连接、每个响应都要穿越前端的各个组件,所以前端需要具备很大的吞吐能力、并发处理能力等。 状态管理状态管理是Web前端伸缩性最重要的方面。状态管理分为有状态服务和无状态服务。 有状态服务和无状态服务的关键区别是无状态服务的实例是完全可交换的,客户端可以访问任意实例而不用担心结果会有不同。有状态服务需要知道不同请求的一些上下文信息,对于某个请求,并不是每个服务都是可用的。任何一个服务端想要使用一个有状态的服务,都必须黏滞在这个服务器实例上面,以避免产生各种难以预期的后果。 管理HTTP会话会话是用cookie实现的。通过使用cookie,服务器可以识别出来自同一个会话的请求。 会话需要存储在Web服务器之外,才能被任何一个Web服务器使用到。有三种常用的方法解决这个问题: 将会话状态存储在cookie中 将会话数据存储在外部数据存储系统中 使用支持会话黏滞的负载均衡服务器 以上方法中:采用第一种可以让事情变得简单,麻烦就是会话存储的代价非常高昂,cookie加密后再进行Base64编码数据量会增大三分之一;第二种方式是将会话存储在外部的数据存储服务器中,可以使用Memcached、Redis、DynamoDB或者Cassandra,主要的要求是基于key的读写操作的低延迟,如果使用技术是基于JVM可以使用Teracotta这类对象集群技术实现会话存储;第三种是在负载均衡服务器上获得相应以后往请求中插入一个负载cookie,用来跟踪请求的客户端与相应的服务器,但是这样允许了Web服务器存储本地数据,服务器件存在差异,系统会失去弹性,不推荐使用。 管理文件Web应用前端第二种常见的状态类型是文件存储,有两类: 用户上传到服务器的文件 系统生成供用户下载的文件 可以考虑使用S3、七牛等第三方供应商服务进行分布式文件存储,不建议自己实现文件存储(特别对于创业公司来说)。如果非要自己做,可以使用MongoDB的GridFS、Netflix的Astyanax等。 管理其他类型的状态这里的其他类型的姿态包括:本地缓存、应用内存状态、资源锁等。针对不同的应用采用不同的本地缓存策略(比如,博客系统和竞拍系统)。使用本地锁同步共享资源的访问时没有用的,它只能再每台服务器上起作用,不可能在服务器之间同步。所以,不能再Web服务器上使用锁。 资源锁的解决方案:创建一个独立的锁服务,然后用这个锁服务weib所有的Web应用服务器提供锁服务。优点是,比较容易实现伸缩、还能把共享状态从其他系统中分离成一个抽象层;缺点是,会增加延迟(因为需要执行一个远程调用)而且会增加更多组件,管理起来比较麻烦。可以使用到的技术:Zookeeper、Netflix的Curator库、结合Memcached/Redis和MySQL/PostgreSQl实现。 要使具有良好的伸缩性,那么就要保证所有Web服务器都是无状态的,包括所有的Web前端服务器和Web服务服务器。同时,不允许服务器在本地存储数据,不允许在Web服务器上使用资源锁。 前端组件以及实现前端组件包括Web服务器、负载均衡器、域名系统(DNS)、反向代理、以及CDN等。 DNSDNS就是域名系统,用户在访问网站时第一个需要的就是它。可以使用阿里云、腾讯云、DNSPod等供应商提供的服务。 负载均衡器强烈建议使用负载均衡器作为数据中心的入口点,这样做一则进行伸缩扩容的时候更方便,二则改变底层基础设施架构的时候也不会影响到用户。 在实践中常用的使用负载均衡的方法是,在Web服务器和客户端之间配置一个负载均衡器。Web服务器和客户端之间的所有的流量都会经过负载均衡器。这样做的好处有: 隐藏服务器维护 无缝增加处理能力 高效的失效管理 自动化伸缩 高效资源管理 目前的系统主要使用的负载均衡类型有以下3种: 使用云主机的负载均衡(阿里云、亚马逊云等均有提供,同时还提供前端服务器和内部服务器之间的负载均衡) 软件实现的自管理负载均衡器(可以使用Nginx、HAProxy等) 硬件负载均衡 Web服务器前端服务器不应该包含太多业务逻辑,应该只是一个用来集成Web服务层结果的展示层,不能是系统的核心。 可选择的技术有:PHP、Python、Groovy、Ruby、NodeJs等。任何语言、任何Web服务器并不重要。只要前端服务器是无状态的,就可以简单的通过增加机器实现水平伸缩。 缓存前端常用的缓存有CDN等。好处是,CDN可以处理绝大部分的访问流量,减少服务器的负载压力并提高响应速度;坏处,不是所有的Web应用都能使用CDN有效缓存界面。可以使用反向代理服务器控制哪些需要被缓存以及缓存多久,也在SPA或者移动端中通过浏览器存储来实现缓存。 自动伸缩自动伸缩式一种自动化管理网站基础设施的技术,根据访问压力及服务器负载增加或者减少虚拟服务器的数量。伸缩性有两个方面,一方面是伸(需要扩容),一方面是缩(需要缩容,为了节约成本)。实现自动伸缩最简单的方式是使用主机供应商提供的自动伸缩工具。 本文为读《互联网创业核心技术——构建可伸缩的Web应用》读书笔记。]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
<tag>架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[软件设计中“简单”这个原则]]></title>
<url>%2F2017%2F01%2F20%2F%E8%BD%AF%E4%BB%B6%E8%AE%BE%E8%AE%A1%E4%B8%AD%E2%80%9C%E7%AE%80%E5%8D%95%E2%80%9D%E8%BF%99%E4%B8%AA%E5%8E%9F%E5%88%99%2F</url>
<content type="text"><![CDATA[今天在看《互联网创业核心技术——构建可伸缩的Web应用》时候,书中的“简单”设计原则让我想起最近的工作以及先前处理此类问题的太多。那么,把书中的内容记录于此,方便时时查看。 最重要的原则是使事物简单。简单没有一个衡量标准,判断一个东西是不是简单的时候,你要首先回答的是这个是对谁以及什么时候而言的。 简单不是走捷径,不是为手边的问题找一个最快的方案。 简单是让别的软件工程师以一种最容易的方式使用你的方案。 简单是当系统变得更庞大更复杂的时候依然能够被理解。 学习如何简单的最好的途径是经验。 简化你的产品可以从以下4个基本步骤开始。 隐藏复杂与构建抽象隐藏复杂和构建抽象是实现简化最好的方法之一。 如果我们的系统无法保持简单,那么我们能做的就是保持局部简单。主要方式是确保在设计和实现两个方面上,任何单个的类、模块、应用的设计目标及工作原理都能被快速理解。 避免过度设计实现简化的第二个实践是避免过度设计。 好的设计方法是可以在后期逐渐添加新的功能特性,而不是一开始就开发一个超级大的系统。早期先构建一个合理的抽象层次,然后迭代地增加新特性,要比一开始就设计好全部的功能为将来各种可能进行开发好得多。 尝试测试驱动开发好处: 没有单元测试用例就没有代码,所以也就没有无用的代码; 工程师可以更好地把握工作的重点; 无论是否使用TDD,都要从用户视角思考问题。假设公司新来的工程师需要使用你开发的接口,那么他想调用的方法是什么?传递的参数是什么?期待的返回值是什么?从这个角度去思考问题,就会更好地开发易于用户调用的代码。 从软件设计的简化范例中学习可以试着分析一些框架,总结出一些简化的范例。这些框架被书中推荐的有:Grails、Hadoop、Google Maps API。 没有简单性,工程师就没法理解代码,不能理解软件,系统就无法保证持续发展。对于伸缩性,最好是设计简单的系统然后让它好好运行,而不是设计复杂的系统然后突然崩溃了。]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
<tag>架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[可伸缩的Web应用碎碎念]]></title>
<url>%2F2017%2F01%2F18%2F%E5%8F%AF%E4%BC%B8%E7%BC%A9%E7%9A%84Web%E5%BA%94%E7%94%A8%E7%A2%8E%E7%A2%8E%E5%BF%B5%2F</url>
<content type="text"><![CDATA[概念什么是伸缩性伸缩性是指系统可以根据需求和成本调整自身处理能力的一种能力。伸缩性常常意味着系统可以改变自身的处理能力以满足更多用户访问、处理更多数据而不会对用户体验造成任何影响。主要从以下几个方面度量: 处理更多数据 处理更高的并发 处理更高频次的用户交互 性能更多的是衡量系统处理一个请求或者执行一个任务需要花费多长时间,而伸缩性则更关注系统是否能够随着请求数量的增加(或减少)而相应地拥有相适应的处理能力。 伸缩性方案有两种不同的伸缩性方案:垂直伸缩和水平伸缩。 垂直伸缩通过升级硬件和网络吞吐能力可以实现垂直伸缩。方案: 通过使用RAID(独立冗余磁盘阵列)增加I/O吞吐能力。 通过切换到SSD改善I/O访问速度。 通过增加内存减少I/O操作。 通过升级网络接口或者增加网络接口提高网络吞吐能力、 更新服务器获得更多处理器或者更多虚拟核。 优缺点: 实现简单。不需要重构任何东西。 成本制约。当达到某个点以后,成本会很高。 垂直伸缩是有极限的。 缓存和CDN 水平伸缩水平伸缩是指通过增加服务器提升计算能力的一类架构方法。水平伸缩可以克服垂直伸缩带来的地位计算成本随着计算能力增加而迅速飙升的问题。 在水平伸缩架构中,每一种服务器角色都可以通过增加服务器进行扩容伸缩。构建水平伸缩系统先从那些容易的地方做起,比如Web服务器、缓存;暂缓那些难以做到的地方,比如数据库和其他持久层。 边缘缓存(edge-cache):一种距离用户较近的HTTP缓存服务器,便于部分缓存用户的HTTP流量。 应用架构概述应用架构是关于业务模型的演化。 将领域模型放在应用架构的核心,我们确保各种组件围绕这个核心展开,服务于这个业务,而不是其他什么东西。 前端前端的主要职责是成为用户的接口,用户通过网页、移动APP或者Web服务器调用完成和应用的交互。无论实际交互方式是什么,前端应该是介于公开接口和内部服务调用之间的处理层,是系统向用户呈现的功能展示,因此不该是整个系统的核心或者重点。前端应该尽量地保持简单(业务逻辑简单)。 业务逻辑只存在于Web服务层,因此我们可以避免视图和业务强耦合带来的问题。 前端不应该关心数据库或第三方服务。但是,允许前端组件发送消息给消息中间件以及使用缓存等。 Web服务SOA:面向服务的体系架构。是一种以低耦合和高度自治的服务为中心的软件架构,主要目标是实现业务需求。SOA倾向于所有的服务都基于约定由清晰的定义,并且都是用相同的通信协议。不管用什么样的技术或者协议,只要你的服务是松耦合的并解决一组特定的细分领域的业务需求就可以了。 其他架构:分层架构、六角形架构和事件驱动架构等。 无论使用什么样的架构,目的都是讲系统切分成更小的独立的功能单位。这样做的目的是构建更高层次的抽象以实现隐藏复杂性、减少依赖,各部分独立伸缩,以及每个部分并行开发。 支撑技术支撑技术有:消息队列、应用缓存、主数据存储、搜索引擎等。这些支撑技术应该是一种即插即用的扩张组件,更换这些组件的连接就能够切换组件,保证整体架构不受影响。]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
<tag>架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[前后端分离中的请求跨域问题]]></title>
<url>%2F2017%2F01%2F02%2F%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E4%B8%AD%E7%9A%84%E8%AF%B7%E6%B1%82%E8%B7%A8%E5%9F%9F%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[现在的WEB应用很多情况下都是前后端分离的,使用RESTful架构很经常遇到的一个问题的就是跨域问题。我们主要在这里讨论的是,在前端请求服务端接口的时候遇到的跨域的问题,不讨论前端之间的跨域互调。 什么是跨域只要协议、域名、端口有任何一个不同,都被当作是不同的域。简单地理解就是因为JavaScript同源策略的限制,a.com 域名下的js无法操作b.com或是c.a.com域名下的对象。 URL 说明 是否允许通信 http://www.a.com/a.js http://www.a.com/b.js 在同一域名下 允许 http://www.a.com/lab/a.js http://www.a.com/script/b.js 同域名下不同文件夹 允许 http://www.a.com:8000/a.js http://www.a.com/b.js 同域名下不同端口 不允许 http://www.a.com/a.js https://www.a.com/b.js 同一域名,不同协议 不允许 http://www.a.com/a.js http://70.32.92.74/b.js 域名和域名对应的IP 不允许 http://www.a.com/a.js http://script.a.com/b.js 相同主域名,不同子域名 不允许 http://www.a.com/a.js http://a.com/b.js 同一域名,不同二级域名(同上) 不允许 http://www.cnblogs.com/a.js http://www.a.com/b.js 不同域名 不允许 同源策略同源策略(Same origin policy)是一种约定,指的是浏览器对不同源的脚本或者文本的访问方式进行的限制。同源策略限制的不同源之间的交互主要针对的是js中的XMLHttpRequest等请求。下面这些情况是完全不受同源策略限制的: 页面中的链接 重定向 表单提交 跨域资源嵌入(但是浏览器限制了Javascript不能读写加载的内容) 跨域资源共享 CORSCORS 是 Cross Origin Resource Sharing 的缩写,定义了浏览器和服务器间共享内容的新方式,通过它浏览器和服务器可以安全地进行跨域访问,它是 JSONP 的现代继任者。服务器上的 CORS 配置可以精细地指定允许跨域访问的条件:来源域、HTTP 方法、请求头、内容类型……等等。并且,CORS 让 XMLHttpRequest 也可以跨域,我们可以像往常一样编写 AJAX 调用代码。 跨源资源共享标准通过新增一系列 HTTP 头,让服务器能声明哪些来源可以通过浏览器访问该服务器上的资源。另外,对那些会对服务器数据造成破坏性影响的 HTTP 请求方法(特别是 GET 以外的 HTTP 方法,或者搭配某些MIME类型的POST请求),标准强烈要求浏览器必须先以 OPTIONS 请求方式发送一个预请求(preflight request),从而获知服务器端对跨源请求所支持 HTTP 方法。在确认服务器允许该跨源请求的情况下,以实际的 HTTP 请求方法发送那个真正的请求。服务器端也可以通知客户端,是不是需要随同请求一起发送信用信息(包括 Cookies 和 HTTP 认证相关数据)。 简单请求所谓的简单,是指: 只使用 GET, HEAD 或者 POST 请求方法。如果使用 POST 向服务器端传送数据,则数据类型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一种。 不会使用自定义请求头(类似于 X-Modified 这种)。 上面的请求头中,Origin字段用来说明本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。如果Origin指定的源不在允许的范围内(服务器配置),服务端就会返回一个正常的HTTP回应。接收到该回应的浏览器发现回应头没有包含Access-Control-Allow-Origin字段字段,就抛出一个异常。但是,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个以’Access-Control-开头’的头信息字段。 Access-Control-Allow-Origin:它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。 Access-Control-Allow-Credentials:表示Cookie是否要包含在请求中发送给服务器。 Access-Control-Expose-Headers:CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。也就是该字段是设置浏览器允许访问的服务器的头信息的白名单。 非简单请求不满足“简单请求”条件的请求为非简单请求。不同于简单请求,非简单请求的时候会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。 预检请求预请求”要求必须先发送一个 OPTIONS 请求给目的站点,来查明这个跨站请求对于目的站点是不是安全可接受的。这样做,是因为跨站请求可能会对目的站点的数据造成破坏。 当请求具备以下条件,就会被当成预请求处理: 请求以 GET, HEAD 或者 POST 以外的方法发起请求。或者,使用 POST,但请求数据为 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 以外的数据类型。比如说,用 POST 发送数据类型为 application/json,application/xml 或者 text/xml 的 XML 数据的请求。 使用自定义请求头(比如添加诸如 X-PINGOTHER) 也就是说,我们平常的时候的以json为请求体的post、put请求,以及delete方法的请求都会被进行预请求处理。“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。 服务器回应预检请求的字段除了与“简单请求”的一样外,还有: Access-Control-Allow-Methods:指明资源可以被请求的方式有哪些(一个或者多个)。这个响应头信息在客户端发出预检请求的时候会被返回。 Access-Control-Max-Age:这个头告诉我们这次预请求的结果的有效期是多久,单位是秒,在此期间,不用发出另一条预检请求。 Access-Control-Allow-Headers:指明在实际的请求中,可以使用哪些自定义HTTP请求头。 一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。 附带凭证信息的请求默认情况下,CORS是不发送Cookie和HTTP验证信息的。如果想要把这些信息发送给服务器,那么,服务器必须Access-Control-Allow-Credentials:true;同时,Ajax请求的时候也必须加上xhr.withCredentials = true;特别注意的是:如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。 一个例子服务端以Spring MVC为例,客户端以fetch为例。 服务端: public class WafCorsFilter extends OncePerRequestFilter { private final Logger logger = LoggerFactory.getLogger(this.getClass()); public static final String WAF_CORS_ALLOW_ORIGIN = "waf.cors.allow.origin"; public static final String WAF_CORS_ALLOW_METHODS = "waf.cors.allow.methods"; public static final String WAF_CORS_ALLOW_HEADERS = "waf.cors.allow.headers"; public static final String WAF_CORS_MAX_AGE = "waf.cors.max.age"; static{ Properties defaultProperties = WafProperties.getDefaultProperties(); defaultProperties.setProperty(WAF_CORS_ALLOW_ORIGIN, "*"); defaultProperties.setProperty(WAF_CORS_ALLOW_METHODS, "GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE, PATCH"); defaultProperties.setProperty(WAF_CORS_ALLOW_HEADERS, "Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, Authorization, Cache-control, Orgname, vorg"); defaultProperties.setProperty(WAF_CORS_MAX_AGE, "1800"); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { logger.debug("WAF CorsFilter doFilter start"); // 设定CORS的初始化参数 // cors.allowed.origins *:Any origin is allowed to access the resource response.addHeader("Access-Control-Allow-Origin", WafProperties.getProperty(WAF_CORS_ALLOW_ORIGIN)); // cors.allowed.methods Access-Control-Allow-Methods: A comma separated // list of HTTP methods that can be used to access the resource response.addHeader("Access-Control-Allow-Methods", WafProperties.getProperty(WAF_CORS_ALLOW_METHODS)); // cors.allowed.headers Access-Control-Allow-Headers: A comma separated // list of request headers that can be used when making an actual // request response.addHeader("Access-Control-Allow-Headers", WafProperties.getProperty(WAF_CORS_ALLOW_HEADERS)); // cors.preflight.maxage Access-Control-Max-Age The amount of seconds, // browser is allowed to cache the result of the pre-flight request response.addHeader("Access-Control-Max-Age", WafProperties.getProperty(WAF_CORS_MAX_AGE)); filterChain.doFilter(request, response); logger.debug("WAF CorsFilter doFilter end"); } } 客户端: /** * 请求处理 * @param url url地址 * @param body 请求数据 * @param method 方法 * @param withAuthToken 是否是有校验码 * @returns {Promise.<T>} */ request(url, body, method = "GET", withAuthToken = true) { const _method = method.toUpperCase(); let headers = { 'Accept': 'application/json', 'Content-Type': 'application/json; charset=UTF-8' } if (withAuthToken) { headers['Authorization'] = new AuthUtil().getAuthHeader(url, _method); } let settings = { method: _method, headers: headers } if (!['get', 'head'].includes(_method) && body) { settings['body'] = JSON.stringify(body); } return fetch(url, settings).then(response => response.json().then(json => ({json, response})) ).then(({json, response}) => { if (!response.ok) { return Promise.reject(json); } return json; }); }]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>跨域</tag>
</tags>
</entry>
<entry>
<title><![CDATA[代码之外的软技能]]></title>
<url>%2F2016%2F12%2F09%2F%E4%BB%A3%E7%A0%81%E4%B9%8B%E5%A4%96%E7%9A%84%E8%BD%AF%E6%8A%80%E8%83%BD%2F</url>
<content type="text"><![CDATA[这是一篇《软技能-代码之外的生存指南》的读书笔记。在书到手的两个礼拜中除了在给书写上自己的名字意外,没有翻开过一页。最近的一个多礼拜没有碰代码,没有读书,想着趁着这个周末回老家应该没有被其他事情打扰,好好地看看。然后,文档记之。 优秀的软件开发人员的定义并不是说要精于编码之道,善于解决缺陷,通晓单元测试。而是,那些能够把控自己的职业生涯、达成目标、享受生活的人。 我们入职以来可能犯的最大的错误我们入职以来犯的最大的错误:没有把自己的软件开发事业当作一桩生意来看待。而是习惯于领取固定的工资,然后觉得给别人打工。我们要做的第一要务:转变心态,从被一纸“卖身契”束缚的仆人转变为一名拥有自己生意的商人。我们能够提供的服务:创建软件。仅有服务或者产品是不够的。我们还应该做到:1)专注于你正在提供的怎么样的服务,以及如何营销这项服务;2)想方设法提升你的服务;3)思考你可以专注为哪一特定的客户或者行业提供特定的服务;4)集中精力成为一名专家,专门为某一特定类型的客户提供专业的整体服务。那么,我们可以试着回答以下问题: 有一家企业,拥有某个产品或服务。他们讲如何推广这一产品或服务从而可以做到卓尔不凡? 如果只能用一句话来描述你能够为潜在雇主或客户提供怎样的特定服务,这句话是什么? 你的目标是什么很多时候,我们都不知道自己的目标是什么。怎么做呢? 设置一个大目标。 建立能帮你达成这个大目标的小目标。 大目标不一定要非常具体,只要能够具体到给你提供清晰的方向即可,能够让你自己知道在向它前进还是离它越来越远。应该定期追踪并更新自己设定的目标,必要时套调整。我们可以定期核定自己的目标,这有助于在必要时进行调整,让你对自己负责。 可以在每周末为下一周设定目标之前检查上周设定的目标。这同样适用于每月、每季和每年。 人际交往能力在软件开发领域,我们大多时候是和人打交道而非与计算机打交道。我们所写的软件首先是给人用的,其次才是让计算机可以理解。 作为一名软件开发人员,你的工作就是与人打交道。 每个人都希望感到自己很重要:以自己为核心,每个人都希望自己很重要。这是人类最深邃、最致命的欲望之一,也是社会和生活中取得伟大成就的主要动机。如果你希望人们接受你的想法,并认可其中的价值,首先你最好先主动给他人相同的礼遇。如果你不能保全他人的自尊,那么你永远也不可能赢得他的心。永远不要批评:奖励积极行为要比惩罚消息行为有效得多。换位思考:停止用“我”和“我想要什么”来思考,你应当开始思考对他人而言什么才最重要,什么才是他们需要的。避免争吵:我们要不惜一切代价避免争吵。 破解面试之道与主流的观念相反,大多数面试官决定雇佣某个人其实是基于各种各样的非技术因素。“破解”面试的要诀就是在面试开始之前就思考应对面试的策略。 就业选择:列出你的选择选择1:雇员选择2:独立咨询师选择3:创业者。软件开发创业者使用自己的软件技能开发自己的产品、拓展自己的业务。 你是哪类软件开发人员用“Java开发人员”等分工太宽泛。我们必须专业化,专业化的规则是:专业化程度越深,潜在的机会就越少,但获得这些机会的可能性越大。专业领域:Web开发栈、嵌入式系统、特定的操作系统、移动开发、框架、软件系统 公司与公司是不一样的小公司和创业公司:它们都有着非常独特的“创业心态”,表现为:关注快速增长,竭尽所能让公司盈利,或者达成其他一些迫切目标。选择为小公司或者初创公司工作的一个更好的理由是,你喜欢那种快节奏的,令人兴奋的工作环境,也希望构建伟大的产品并见证它的成长。中等规模的公司:角色定义明确,缓慢而稳健的做事风格通常能占得先机。大公司:完备的流程和规范,培训机会多,大型有影响力的项目。 软件开发公司和拥有软件开发人员的公司是有区别的。与雇佣软件开发人员但核心业务并非软件的公司相比,软件开发公司会使用更前沿的技术和工具。雇佣软件开发人员的公司不太会给软件开发人员足够的尊重和发展空间。 攀升晋升阶梯承担责任:在任何公司中能让你脱颖而出的最重要法宝就是承担更多的责任。有时候你不得不去主动寻找机会,去负责一项任务,或者牵头一个项目。只要深入挖掘,你总能找到一些被忽略的业务领域去发挥自己的聪明才智。没有人愿意涉足的领域是寻找机会最好的地方。另一种间接承担责任的方式是成为团队中其他人的导师,自愿帮助新人加速成长,为任何有需要的人提供帮助。引人注目:每天记录自己的活动日志;提供演讲或培训;发表意见;保证“曝光度”。自学:不断增加自己的技能和只是;同时不要忘记分享。成为问题的解决者:你要成为那个永远能为各种问题找到解决方案的人,要成为勇于执行这些解决方案以获得成果的人。关于政治:你应该对所在组织的政治气候保持警觉。尽管不能完全避开政治,但至少应该知道会发生什么,哪种人需要避开,哪种人永远不要有交集。 成为专业人士专业人士会遵守自己的原则;专注于正确完成工作;不惧怕承认自己错了,不会文过饰非;持续稳定;勇于承担责任。 赢得自由——如何辞职在自己的积蓄不能支撑得起你的生活之前,还是老老实实当雇员。在真正去创业之前,先是以副业的方式开始你的创业之旅,小有成就后再全职投入。 成为自由职业者:开启自己的一片天地通用规则是,成为自由职业者之后,你的时薪水平应该是成为全职雇员时的2倍。 创建你的第一个产品没有要解决问题的产品毫无意义,毫无意义的产品自然也就不会有用户。 你打算开始创业么第一种创业公司:在成立的时候就试图以获得外部投资者的投资来刺激公司快速成长。第二种创业公司:自力更生创业。完全由其创始人提供资金支持。 远程工作的生产策略挑战1:时间管理。你必须要有一个缜密的时间管理。挑战2:自我激励。依靠缜密的时间管理做到自律。挑战3:孤独感。走出去,确保每周都能见到他人。 假装自己能成功当说“假装自己能成功”的时候,指的是:在做事情之前,可以暗示自己“我已经成功完成任务”。然后以这样的心态去做事。 如何修改简历你的简历应该展现你都做了哪些工作以及相应的结果。 请勿陷入对技术的狂热之中没有理由去强烈坚持自己选择的技术就是最好的,而轻视甚至无视其他技术。如果固执己见,最终受损失的是你自己。 针对“码农”的营销基础课营销的核心在于将一些人所需要的所期待的产品或者服务与产品或服务本身连接起来。所以“自我营销”也就是把希望得到你提供的产品或者服务的人和你自己连接起来。自我营销的正确方式就是为他人提供价值。 打造引入注目的品牌品牌即承诺:承诺按照你预期的方式交付你所预期的价值。品牌四个要素:品牌要传递的信息、品牌的视觉符号、品牌的一致性和品牌的曝光率。 创建大获成功的博客持之以恒地坚持写作,坚持不懈地产生高品质的内容。如果发现自己不知道写什么,没什么可写的怎么办呢?解决这个问题的最好方法是提前头脑风暴出各种不同的想法,随时更新可能的博客主题的清单,这样你总能保持一堆话题可供选择。 你的主要目标:为他人增加价值给人们想要的东西;把你工作成果的90%都做成免费的。 善于运用社交媒体怎么样通过社交媒体提供在业界的声望?那就是“提供价值”。应该持续不断地分享和提供具有价值的内容,不要发布不适宜的、攻击性的内容或者那一些只与自己相关的类似“早餐吃了什么样的鸡蛋”这样毫无营养的内容。 演讲、报告和培训:做“说话的极客”OK,可以想想怎么在公司开始自己的培训和分享了。 著书立说,吸引追随者从写博客开始,不断地写。也不一定非得是实体书,可以写某个主题的电子书。 百折不挠,越挫越勇万事开头难;被看做傻瓜又如何;小步快跑 学习怎样学习:如何自我教育学习的最好的方法就是把它付诸于实践。然后可以将自己所学的打包教给别人 “十步学习法”为了能够掌握一门技术,我需要了解以下三个要点: 如何开始——要想开始使用自己所学的,我需要掌握哪些基本知识。 学科范围——我现在学的东西有多宏达?我应该怎么做?在开始阶段,我不需要了解每个细节,但是如果我能够对该学科的轮廓有大致的了解,那么将来我就能发现更多细节。 基础知识——不止在开始阶段,我需要了解基本的用户案例和最常见的问题,也需要知道自己学的拿20%就能满足80%的日常应用。 “十步学习法”的基本思想是:要对自己要学的内容有个基本的了解——了解自己不知道什么足矣。然后,利用这些信息勾勒出学习的范围,即需要学哪些内容,以及学成之后又会获得什么。依靠这些知识,你可以找出各种资源(不局限于书)来帮助自己学习。最后,你可以创建自己的学习计划,列出要去学习哪些相关课程,筛选学习材料,只保留帮助自己达成目标的优秀内容。 学习-实践-掌握-教授(Learning,Doing,Learning and Teaching,LDLT)。 十步学习法:了解全局——确定范围——定义目标——寻找资源——创建学习计划——筛选资源——开始学习,浅尝即止——动手操作,边玩边学——全面掌握,学以致用——乐为人师,融会贯通 了解全局:了解自己将要学习的主题的全局。 确定范围:集中精力去明确自己到底要学什么。将一个大的主题分解成可控的子主题。 定义目标:确定自己的学习经历中获得什么决定了你的成功标准是什么。确保在借此在学习结束之后评估自己是否达成了目标。 寻找资源:尽可能多的去找与自己所选主题相关的资源。此时,无需考虑这些资源的质量。 创建学习计划:打造自己的学习计划,一个好方法就是观察别人是如何教你感兴趣的主题的。(比如,可以对比先前找资料时找到的书籍,对比目录,如果存在多位作者把内容分解为相同的模块和顺序,就按照那个进行学习。) 筛选资源:把收集到的资源浏览一遍,找出哪些内容能够覆盖你的学习计划。 开始学习,浅尝即止:在这一步中,你的目标是获得足够多的与所学主题相关的信息,从而能够让你开始学习,并在下一步中动手操作。这一步的关键在于过犹不及。 动手操作,边玩边学:你无须提前了解全部,你要做的首要的一件事情就是亲自操作和亲身体验,采用这种方法,你通过探索和时间进行学习。 全面掌握,学以致用:这一步的目标就是让你找回好奇心驱动学习。利用先前收集的资料,深度学习。 乐为人师,融会贯通:走出自己的舒服区,将自己学到的知识教给别人。要明确自己确实掌握了某些知识,这是唯一的方法。重点在于,你要花时间将自己学到的东西从大脑中提取出来,以别人能理解的方式组织出来。 以上10步中,1-6步只要做一次;7-10步需要循环往复。 寻找导师在寻找一位导师的时候,你必须抛开自己的判断和推理,只关注导师的成就。 成为导师帮助他人时的成就感;深入学习和领悟知识的途径;你的徒弟有朝一日会帮到你;自身的成长,帮助别人的过程也就是自己成长的过程。 传道授业在教授别人的过程中发现自己的不足。 需要一个学位么?学位并不是那么重要,但是有学位比没有学位更重要。 发现自己的知识短板在哪些工作上花费时间最多?可以改进的重复性劳动;自己没有完全理解的东西;你回答不出来的面试问题; 一切始于专注专注就是注意力分散的对立面。要进入专注模式,必须要克服将自己的思绪集中于单一任务时的那种痛感。 生产力提升计划把一周的时间分配给一个一个用时不超过两小时的小任务。使用白板来安排自己的一周活动。 季度计划:尽力列出本季度完成的每一个大项目,制定一些较小的目标。月计划:规划处每天要完成的工作。从季度计划中挑选任务,看看有哪些任务可以写入月历。周计划:作者推荐使用Trello或者kanbanflow。日计划与执行:详细今天要做的事情。与干扰作斗争! 番茄工作法关于番茄工作法的资料和书籍很多,可以参考那些资料和书籍详细了解~~~ 如何做到超额完成工作什么是定额工作法:明确一个目标,规定自己要在预先确定的时间段内需要取得多大的进展。定额工作法的原理:一旦你明确了自己要做什么、多久做一次,接下来的步骤就是要做出“承诺”。“承诺”是“定额工作法”的核心。除了想法设法完成自己的工作,不给自己留下任何其他的选择。不能再定额必须完成的有效时间段之内放弃。 定额工作法的规则: 挑选一项重复性的工作。 明确有效时限,在此期间该任务被重复执行。 明确在给定的有效时限内该任务应该完成的次数的定额。 给自己承诺:一定要达到定额。 调整。调高或者调低定额,但是不能再有效的时间段之内调整。 对自己负责抉择一下,我们想如何度过自己的一生。花点时间创建一些自己的规则,确保自己能够朝着正确的方向前进。创建自己的责任制度,帮助自己严格执行规则。 要不要多任务并行批量处理生产效率更高 什么才是真正的多任务并行:比如一边听歌一边敲代码;一边开车一边听书等 职业倦怠在大多数情况下,倦怠感完全是自然而然产生的,它并不是一个严重的问题。所以我们必须,想办法去穿过阻挡的“那面墙”。 时间是怎样被浪费掉的看电视(电视剧)、社交媒体、新闻网站、不必要的会议、看小说、玩电子游戏….. 形成惯例的重要性你每天做什么样的决定塑造着未来的你。确定一个大目标,然后围绕着这个大目标安排自己的行程。 刷新你的代码习惯主要由三个元素构成:暗示,惯例和奖励。暗示是导致习惯被触发的某样东西。它可能是某一天的某个特定时刻、某种形式的社交场合、某个特定的环境或者其他任何东西。惯例就是你做的事情,也就是习惯的本质。奖励就是让习惯真正保持下去的“锚”。 试着从小事做起。选一个你找出的坏习惯,不要试图马上改变它。相反,尽量找出这个习惯被什么触发,你这么做有什么表现,以及是什么奖励激励你产生冲动要这么做。 如何吃掉一头大象帮助你克服拖延的提高生产力的窍门:分解任务。通过将大任务分解为小任务。 首先要明确完成这项任务需要哪些步骤。 努力工作的价值,以及为什么你总是逃避努力工作我们认为艰苦的工作最有可能就是让我们获益良多的工作。我们只能坐在办公桌前,做我们应该做的事情。我们必须学会脚踏实地、埋头苦干。 任何行动逗比不采取行动好拒绝采取行动,无数机会就会被浪费,无数可能性就会被挥霍。很多人不采取行动的原因很简单:恐惧。恐惧出错,恐惧把事情搞砸,恐惧后果不可估量或失败,恐惧改变,恐惧做不一样的事情。在面临多种选择的时候,最好的做法就是立刻选择其中之一,即使它不是最好的一个。通过采取行动,如果这个是无效的,那么就更换另外一个。 “采取行动检查表” 怎样支配你的薪水赚钱多并不能让一个人在财务上更精明。资产:指实用价值高于维护成本的东西。负债:指成本高于带来的价值的东西。任何能把钱装在你的口袋里的东西才是资产,而任何需要你把钱从口袋里掏出来的东西都是负债。 怎样进行薪酬谈判薪酬谈判始于求职之前。记住:自我营销做得越好,声望越高,薪酬谈判就越容易。获得工作的方式至关重要。你的最佳状态是:一家公司知道你,然后无需任何面试就直接为你提供一个职位。先出价者输。先出价的人会处于明显劣势。如果再预审面试时被直接文集期望薪酬是多少,那就给出类似这样的回复:这就取决于公司的整体薪酬方案,包括福利。=> 更多地了解下工作内容。 =>对于薪酬,你们一定有个预算范围,然后让对方说出薪酬范围。=>无法给出一个确切的数字,完全依赖于薪酬体系。如果被问及当前薪酬:能不告诉对方尽量不告诉(委婉地规避这个问题),或者换一种算法(比如16薪等) 一定要清楚自己值什么价钱。 期权:所有乐趣之所在期权的基本知识:期权就是选择做什么或者不做什么。期权背后的基本思想就是允许某人为在未来的某个日期买入或卖出股票的权利付费。期权从根本上来说就是赋予你在未来某个日期之前以固定价格购买一定数量股票的选择权。允许在未来一段时间内以固定价格购买股票的期权被称为“看涨期权”,允许在未来一定时间内以固定价格出售股票的期权被称为“看跌期权”。 房地产投资这个说的是国外,国内的房价…伤不起 真的了解自己的退休计划吗?当你的“被动收入”达到每月所需的生活开销的时候,你就可以正式退休了。所谓被动收入,就是不用工作就获得的收入。你必须确保被动收入会随着通货膨胀而增加。 债务的危害真正获得财务成功的唯一方法就是用钱生钱。如果想获得财务自由,你就必须要能够让你的钱为你所用。如果说收益给我们自由,那么后面唯一要加上一句——债务会给我们套上枷锁。背负债务的底线就是确保在背上债务之前,这笔债务实际上是一笔投资,它将为你产生的回报高于你为这笔债务所支付的利息。 如何做到33岁退休这里的退休的意思是财务自由。投资房产??? 为神魔需要破解自己的健康密码增强自信心;让自己拥有更强的大脑力量; 设定你的健身标准挑选一个具体的目标,谨记每次不要挑选一个以上的目标。目标比如:减肥、增肌、增加力量、改善健康等等。创建一系列的里程碑,沿着里程碑前进你就一定会达到最终的目标。一定要确保里程碑式可以实现的。 热力学、热量和你计算自己消耗的卡路里,计算自己可能获得卡路里,消耗大于摄入。 动力:让你的屁股离开椅子制作进度图表并且不断提醒自己你已经走了多远也是有帮助的。当你的动力消失殆尽的时候,用原则来代替激励。 心灵影响身体信念决定思想,思想决定言语,言语决定行动,行动决定习惯,习惯决定价值,价值决定命运。 拥有正确的心态:重新启动如果想改变自己的态度,你就必须改变自己的想法。如果想改变自己的想法,你就必须转变自己的思维模式。你的思维模式是由你的习惯决定的,因此我们可以追溯到改变你生活中处理任何关键事情所采用的主要方式——养成一个习惯。 构建一个积极的自我形象自我形象是在甩掉别人对你的看法,摆脱所有用来自我安慰的谎言和欺骗以后,你看到的自己的样子。 爱情与恋爱:计算机无法牵着你的手受“永远孤独”的互联网文化基因影响。 爱情就是一场游戏,这是真的。不管怎么努力尝试,你都无法跳出这个规则。 获得爱情的解决方法是:在行为上体现出自信,用一种自然随和且充满自信的态度与别人交往。“我自己感觉到很好,我不需要你,但是我觉得你挺有意思的,所以我想更好地了解你。” 不要把喜欢的那个人放在“神龛”上时时供奉着。 不要害怕失败,哪怕失败很多次;不要害怕被拒绝,没什么大不了的。 积极面对失败为什么我们会害怕失败呢?这可能是基于保护脆弱的自尊的想法。 失败不同于被打败。失败是暂时的,被打败是永恒的。 学会拥抱失败、期待失败、接受失败,并准备直面失败。]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[MongoDB使用技巧Tips]]></title>
<url>%2F2016%2F11%2F07%2FMongoDB%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7Tips%2F</url>
<content type="text"><![CDATA[这是一篇日常使用MongoDB时候遇到的问题的解决技巧的文章。 查找数组字段不为空的记录查找数据中数组字段不为空的记录。 举个例子:有以下Mongo文档, { "id" : "581c060f2b436c05aafb1632", "commit_history" : [ "581c20d52b436c05aafb1633", "581c21c12b436c05aafb1634" ] }, { "id" : "581c060f2b436c05aafb1633", "commit_history" : [] } 想要查找commit_history不为空的记录,有以下方法: 方法一: db.collection.find({commit_history: {$not: {$size: 0}}}) 方法二: db.collection.find({'commit_history.0': {$exists: 1}}) MongoDB添加用户在MongoDB中为一个Collection添加用户,可以如下操作: use collection_name 切换到某个库 12345678db.createUser( { user: "collection_name", pwd: "password", roles: [ "readWrite", "dbAdmin" ] })]]></content>
<categories>
<category>MongoDB</category>
</categories>
<tags>
<tag>实战</tag>
<tag>MongoDB</tag>
</tags>
</entry>
<entry>
<title><![CDATA[在NodeJs中使用事件、监听器、定时器和回调]]></title>
<url>%2F2016%2F11%2F01%2F%E5%9C%A8NodeJs%E4%B8%AD%E4%BD%BF%E7%94%A8%E4%BA%8B%E4%BB%B6%E3%80%81%E7%9B%91%E5%90%AC%E5%99%A8%E3%80%81%E5%AE%9A%E6%97%B6%E5%99%A8%E5%92%8C%E5%9B%9E%E8%B0%83%2F</url>
<content type="text"><![CDATA[在阅读《Node.js+MongoDB+AngularJS Web开发》第四章的时候,发现很多概念以前没有去注意到,觉得还是有摘录下来的必要,方便以后查看。 Node.js的事件模型Node.js应用程序在一个单线程的事件驱动模型中运行。 传统的线程网络模型中,请求进入一个Web服务器,并分配给一个可用的线程。对于请求的处理工作继续在该线程上运行,直到请求完成并发出响应。以上使用线程模型在不同线程上处理两个请求。 Node.js不是在各个线程为每个请求执行所有的工作。它是把工作添加到一个事件队列中,然后有一个单独的线程运行一个事件循环把这个工作提取出来。事件循环抓取时间队列中最上面的条目,执行它,然后抓取下一个条目。当执行长期运行或者有阻塞I/O的代码时,它不是直接调用该函数,而是把函数随同一个要在此函数完成后执行的回调一起添加到时间队列。当Node.js事件队列中的所有事件都被执行完成时,Node.js应用程序终止。以上处理两个请求在一个事件驱动线程,使用Node.js的回调事件模型 当我们遇到阻塞I/O(读取文件、查询数据库、请求套接字、访问远程服务)的时候,阻塞I/O停止当前线程的执行并等待一个回应,直到收到回应才能继续。 Node.js使用事件回调来避免对阻塞I/O的等待。所以,阻塞I/O的任何请求都在后台的不同的线程中执行。Node.js在后台实现线程池。当该块的I/O从事件队列中检索一个事件时,Node.js从线程池中获取一个线程,并在那里执行功能,而不是主事件循环线程执行功能。这可以防止阻塞I/O阻碍事件队列的其余事件。在被阻塞的线程上执行的函数任然可以把事件添加到要处理的事件队列中。 事件循环要么在事件循环线程本身上执行功能,要么在一个单独的线程上执行功能,阻塞I/O采用后一种方式。在Node.js事件模型中,工作作为一个带有回调的函数被添加到事件队列中,然后在事件循环线程中被提取出。之后,在无阻塞的情况下,在事件循环线程上执行该函数;或在阻塞的情况下,在一个单独的线程上执行它。 传统的线程模型遇到的问题: 受到线程数量的限制。比如,你一次创建5个线程,当有6个访问同时到达时,只能让一个访问等待 受到CPU数量的限制。一个CPU处理器只能处理一个线程 将工作添加到事件队列在用Node.js进行开发的时候,要利用事件模型的可扩展性和性能,要确保把工作分解成可以作为一系列的回调来执行的块。 可以使用以下的方法之一传递回调函数来在事件队列中调度工作: 对内置的事件,如http.request或server.connection添加一个时间监听器 创建自己的事件发射器并对它们添加自定义的监听器 使用process.nextTick选项来调度在事件循环的下一次循环中被提取出的工作 使用定时器来调度在特定的时间数量或每隔一段时间后要做的工作 对阻塞I/O库调用之一做出调用,如写入文件或连接到一个数据库 定时器3种类型的定时器:超时定时器、时间间隔定时器、即时定时器 超时定时器:用于将工作延迟一个特定的时间数。当时间到了,执行回调函数,定时器消失。对于只执行一次的工作,可以使用超时定时器。 setTimeout方法可以实现。 时间间隔定时器:用于按定期的延迟时间间隔执行工作。当延迟时间结束时,回调函数被执行,然后再次重新调度为该延迟时间。对于必须定期进行的工作,应该使用它。 setInterval方法可以实现。 即时计时器:用来在I/O事件的回调函数开始执行后,但任何超时时间或时间间隔事件被执行之前,立刻执行工作。它们允许你把工作调度为在事件队列中的当前事件完成之后执行。使用即时计时器为其他回调产生长期运行的执行段,以防止I/O事件饥饿。setImmediate方法可以实现 取消定时器:在setTimeout和setInterval返回的对象中可用unref函数,它能够在这些事件是队列中仅有的事件是,通知事件循环不要继续。 myInterval = setInterval(myFunc) myInterval.unref() 使用nextTick在事件队列上调度工作的一个非常有用的方法是使用process.nextTick(callback)函数。此函数调度要在事件循环的下一次循环中运行的工作。nextTick()函数在I/O事件被触发之前执行。 var fs = require('fs') fs.stat('nexttick.js',function(err,stats){ if(stats){ console.log('nexttick.js exists') } }) setImmediate(function(){ console.log('Immediate Timer 1 Executed') }) setImmediate(function(){ console.log('Immediate Timer 2 Executed') }) process.nextTick(function(){ console.log('Next Tick 1 Executed') }) process.nextTick(function(){ console.log('Next Tick 2 Executed') }) 事件发射器和监听器下列代码演示了Node.js中实现监听器和自定义事件发生器。 var events = require('events') function Account(){ this.balance = 0 events.EventEmitter.call(this) this.deposit = function(amount){ this.balance += amount this.emit('balancechanged') //触发事件 } this.withdraw = function(amount){ this.balance -= amount this.emit('balancechanged') //触发事件 } } Account.prototype.__proto__ = events.EventEmitter.prototype function displayBalance(){ console.log('Account balance: $%d',this.balance) } function checkOverdraw(){ if(this.balance < 0){ console.log('Account overdrawn !!!') } } function checkGoal(acc,goal){ if(acc.balance > goal){ console.log('goal achieved') } } var account = new Account() account.on('balancechanged',displayBalance) //添加监听器 account.on('balancechanged',checkOverdraw) account.on('balancechanged',function(){ checkGoal(this,1000) }) account.deposit(220) account.deposit(320) account.deposit(600) account.withdraw(1200) 实现回调回调的3个具体实现:将参数传递给回调函数、在循环内处理回调函数参数、嵌套回调 向回调函数传递额外的参数使用回调时,常见的一个问题是如何从调用函数给回调函数传递额外的参数。做到这一点的方法是在一个匿名函数中实现该参数,然后用来自匿名函数的参数调用回调函数。 var events = require('events') function CarShow() { events.EventEmitter.call(this) this.seeCar = function(make){ this.emit('sawCar',make) } } CarShow.prototype.__proto__ = events.EventEmitter.prototype var show = new CarShow() function logCar(make){ console.log('saw a '+ make) } function logColorCar(make,color){ console.log('saw a %s %s',color,make) } show.on('sawCar',logCar) show.on('sawCar',function(make){ //定义一个匿名函数 var colors = ['red','blue','white'] var color = colors[Math.floor(Math.random()*3)] logColorCar(make,color) }) show.seeCar("Ferrari") show.seeCar('Porsche') show.seeCar('Bugatti') show.seeCar('Lamborghini') show.seeCar('Aston Martin') 在回调中使用闭包如果某个回调函数需要访问父函数的作用域的变量,就需要提供闭包,使这些值在回调函数从事件队列被提取出时可以得到。 function logCar(logMsg, callback) { process.nextTick(function () { callback(logMsg) }) } var cars = ['Ferrari', 'Porsche', 'Bugatti'] for (var idx in cars) { var message = 'saw a ' + cars[idx] logCar(message, function () { console.log('normal callback: ' + message) }) } for (var idx in cars) { var message = 'saw a ' + cars[idx]; (function (msg) { logCar(msg,function(){ console.log('closure callback: ' + msg) }) })(message); } 链式回调使用异步函数时,如果两个函数都在事件队列上,那么我们无法保证它们的运行顺序。解决这一问题的最佳方法是让来自异步函数的回调再次调用该函数,直到没有更多的工作要做,以执行链式回调。 function logCar(car,callback){ console.log('saw a %s',car) if(cars.length){ process.nextTick(function(){ callback() }) } } function logCars(cars){ var car = cars.pop() logCar(car,function(){ logCars(cars) }) } var cars = ['Ferrari', 'Porsche', 'Bugatti','Lamborghini','Aston Martin'] logCars(cars)]]></content>
<categories>
<category>NodeJs</category>
</categories>
<tags>
<tag>NodeJs</tag>
</tags>
</entry>
<entry>
<title><![CDATA[《轻量级微服务架构》读书笔记]]></title>
<url>%2F2016%2F10%2F24%2F%E3%80%8A%E8%BD%BB%E9%87%8F%E7%BA%A7%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[微服务是一种分布式系统架构,它建议我们将业务切分为更加细粒度的服务,并使每个服务的责任单一且可独立部署,服务内部高内聚,隐含内部细节,服务之间低耦合,彼此互相隔离。 传统应用架构遇到的问题: 系统资源浪费 部署效率太低 技术选型单一 微服务需要满足的要求: 根据业务模块划分服务种类 每个服务可独立部署且互相隔离 通过轻量级API调用服务 服务需要保证良好的高可用性 交付流程: 设计阶段:拆分服务,设计API接口,给出API文档 开发阶段:服务端实现API接口;前端模拟API开发Web 测试阶段:部署到测试环境后,QA测试;产品进行功能验收 部署阶段:运维部署到预生产环境,QA进行冒烟测试;确认后部署到正式环境 开发规范: Git Flow的使用 特点与挑战: 特点 微小度颗粒 责任单一性 运行隔离性 管理自动化 挑战 运维要求高 分布式复杂性 部署依赖较强 通信成本较高 微服务架构图 微服务技术选型 使用JenKins部署服务 使用Spring Boot开发服务 使用Docker封装服务 使用ZooKeeper注册服务 使用Node.js调用服务 其他技术选型: Netflix: http://netflix.github.io/ Spring Cloud: 在Spring Boot的基础上封装了Netflix相关组件 JBoss的WildFly Swarm: http://wildfly-swarm.io/ J2EE官方的KumuluzEE: http://ee.kumuluz.com/ Dropwizard: http://www.dropwizard.io/ 自动化发布平台 (1)开发人员将源码提交到代码仓库系统GitLab中。(2)持续集成系统Jenkins定期会从GitLab上拉取指定项目的源码。(也可以人工手动触发去拉取)(3)在Jenkins上进行自动构建,并生成相关的Docker容器,形成相应的测试环境。(4)测试人员在自己的测试环境下进行功能测试。]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>读书笔记</tag>
<tag>架构</tag>
<tag>微服务</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Ubuntu14下安装JDK8]]></title>
<url>%2F2016%2F10%2F16%2FUbuntu14%E4%B8%8B%E5%AE%89%E8%A3%85JDK8%2F</url>
<content type="text"><![CDATA[安装JDK对于一个从事Java开发的人员来说应该是必会的,但是呢,很多时候在真正去安装JDK的时候又会去google下步骤。所以,记录下安装过程,方便下次安装JDK的时候免于查资料。 步骤一:下载Linux版本的JDK在Oracle官网上下载对应的JDK版,或者通过命令行直接下载JDK。 wget -c http://211.162.39.56/files/103300000084E22C/download.oracle.com/otn-pub/java/jdk/8u101-b13/jdk-8u101-linux-x64.tar.gz 步骤二: 解压安装 到要安装的路径下(或新建路径)。 将安装包放在该路径下。 解压。 具体代码: cd usr/lib sudo mkdir java #sudo mv jdk-8u101-linux-x64.tar.gz /usr/lib/java sudo tar vxzf jdk-8u101-linux-x64.tar.gz sudo rm -r jdk-8u101-linux-x64.tar.gz 步骤三:设置环境变量主要是要设置PATH、CLASSPATH和JAVA_HOME。 运行vi ~/.bashrc。 在文件的最后添加如下代码: export JAVA_HOME=/usr/lib/java/jdk1.8.0_101 export JRE_HOME=${JAVA_HOME}/jre export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib export PATH=${JAVA_HOME}/bin:$PATH 保存退出。然后运行source ~/.bashrc,让脚本生效。 第四步:配置默认的JDK分别运行以下命令,配置默认的JDK sudo update-alternatives --install /usr/bin/java java /usr/lib/java/jdk1.8.0_101/bin/java 300 sudo update-alternatives --install /usr/bin/javac javac /usr/lib/java/jdk1.8.0_101/bin/javac 300 sudo update-alternatives --install /usr/bin/jar jar /usr/lib/java/jdk1.8.0_101/bin/jar 300 sudo update-alternatives --install /usr/bin/javah javah /usr/lib/java/jdk1.8.0_101/bin/javah 300 sudo update-alternatives --install /usr/bin/javap javap /usr/lib/java/jdk1.8.0_101/bin/javap 300 然后运行 sudo update-alternatives --config java 如果是首次安装,则会出现: There is only one alternative in link group java (providing /usr/bin/java): /usr/lib/java/jdk1.8.0_101/bin/javaNothing to configure. 第五步:检验安装结果运行java -version,出现我们预期的效果。 java version "1.8.0_101" Java(TM) SE Runtime Environment (build 1.8.0_101-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode) 参考: http://www.cnblogs.com/a2211009/p/4265225.html http://blog.csdn.net/liuxinghao/article/details/39380015]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[从零开始nginx配置]]></title>
<url>%2F2016%2F10%2F14%2F%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8Bnginx%E9%85%8D%E7%BD%AE%2F</url>
<content type="text"><![CDATA[在Ubuntu14中安装好nginx,版本为1.4.6。 要了解nginx的配置,我喜欢从默认的配置开始。默认安装好的nginx的配置位于/etc/nignx目录下。 从默认的nginx.conf开始nginx.conf的默认配置(去掉注解掉的部分)如下: user www-data; worker_processes 4; pid /run/nginx.pid; events { worker_connections 768; } http { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; gzip on; gzip_disable "msie6"; include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; } 上面默认的配置文件中,最吸引人的就是3个include了: include /etc/nginx/mime.types; include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; 而include /etc/nginx/mime.types;能猜到是配置mime的,不是我们要了解的重点。剩下的include /etc/nginx/conf.d/*.conf;和include /etc/nginx/sites-enabled/*;在sites-enabled 目录下的配置文件才能够真正被用户访问,可以将nginx配置放在此目录底下;而默认的只有一个default。conf.d中默认没有任何内容,如果你想要添加多个站点,也可以把配置放在该文件下。 以上的配置中包含了全局设置、events设置和http设置。除此之外,还会有server设置、location设置和upstream设置。 user指定了Nginx Worker进程运行用户以及用户组。 worker_processes设置nginx的进程数,最好和CPU的数量一致。 pid用来指定进程id的存储文件位置。 events设置了nginx的工作模式和工作模式及连接数上限。 http负责HTTP服务器相关属性的配置。 该文件可以先了解到这里,让我们先看下最为耀眼的include的内容。 sites-enabled中的default配置在sites-enabled中,现在暂时只有一个default文件。打开default,整理后如下代码: server { listen 80 default_server; listen [::]:80 default_server ipv6only=on; root /usr/share/nginx/html; index index.html index.htm; server_name localhost; location / { try_files $uri $uri/ =404; } } 这一段代码是配置一个虚拟主机的。一个大型的网站通常会有很多的站点,有各自的服务器提供相应的服务,在nginx中我们可以通过虚拟主机的概念来将这些不同的服务配置隔离,这就是上面配置中的server的含义。 listen用于指定虚拟主机的服务端口。default_server指的是如果有其他http请求的host在nginx中不存在设置的话那么就用这个server的配置来处理。比如我们去访问 127.0.0.1 那么也会落到这个server来处理。root表示在这整个server虚拟主机内,web的根目录。server_name用来指定IP地址或者域名,多个域名之间用空格分开。 这里的server_name是和客户端http请求中的host行进行匹配的。location用来匹配请求的路径。location是支持正则表达式的,这里的/匹配所有的请求,也就是说 /xxx和/yyy都会进行匹配到。try_files意思是nginx会按照接下来的顺序去访问文件,将第一个匹配的返回。 conf.d中的配置在该目录下也是可以进行虚拟主机的配置的,配置的方法同default中的配置。 参考: nginx服务器安装及配置文件详解 Nginx配置参数中文说明 nginx的配置、虚拟主机、负载均衡和反向代理(1)]]></content>
<categories>
<category>运维</category>
</categories>
<tags>
<tag>实战</tag>
<tag>nginx</tag>
<tag>运维</tag>
</tags>
</entry>
<entry>
<title><![CDATA[PouchDB简单入门]]></title>
<url>%2F2016%2F09%2F24%2FPouchDB%E7%AE%80%E5%8D%95%E5%85%A5%E9%97%A8%2F</url>
<content type="text"><![CDATA[PouchDB是一款数据库。它可以在浏览器里面存储你的本地信息,所以适合于基于浏览器的可离线存储方案。PouchDB实际上是一个JavaScript库。 快速入门可以参考Getting Started Guide入门PouchDB。 安装PouchDB与使用安装:`npm install pouchdb --save` 使用:`var PouchDB = require('pouchdb');` 简单操作新增与修改数据db.put({},function callback(err,result){})。新的数据中每一条都要有一个唯一的主键_id,在提交的数据里,如果存在与数据库中相同的_id,PouchDB就将更新该数据,否者就创建一条新数据。使用db.post可以提交没有_id的数据。回调函数callback中:如果err为非空,说明插入数据失败;如果err为空,则表示插入成功,result里返回成功后数据_id和_rev,_rev代表这条数据的版本号。 删除数据db.remove()。删除数据的时候,我们需要同时提供_id和_rev项。我们使用remove()的时候,并没有真正删除掉文档,它只是被添加了一个_deleted标志,值为true。以下3种写法都可以删除数据: db.get('mittens').then(function (doc) { return db.remove(doc); }); db.get('mydoc').then(function (doc) { return db.remove(doc._id, doc._rev); }); db.get('mydoc').then(function (doc) { doc._deleted = true; return db.put(doc); }); 查询数据db.get(_id)获得指定的数据 在浏览器中查看PouchDB通过PouchDB Inspector,它是用于在Chrome中查看PouchDB的插件。 通过浏览器中的IndexedDB/WebSQL查看。 debug模式开启debug模式:PouchDB.debug.enable('*')关闭debug模式:PouchDB.debug.disable() 删除本地数据库在Chrome中,通过使用ClearBrowserData extension插件删除 在Firefox中,通过使用Clear Recent History+ add-on插件删除 在Safari中,通过 Safari → Clear History and Website Data 删除 通过代码:db.destroy(),返回的是一个Promise 理解revisions(_rev)字段_rev是一个版本标志。它是在新增或者修改的时候生成的一个随机数。在PouchDB中,更新数据的时候必须要有_rev字段作为条件。如: var doc = { '_id': 'mittens', '_rev': '1-7df5244f3d6cf5d5b260b9c641f9c604', 'name': 'Mittens', 'occupation': 'kitten', 'age': 4, 'hobbies': [ 'playing with balls of yarn', 'chasing laser pointers', "lookin' hella cute" ] } 如果没有_rev字段,更新的时候就会报错。 { status: 409, name: 'conflict', message: 'Documentupdateconflict', error: true } 为什么我们一定需要_rev呢?因为使用_rev可以更好地进行同步工作 Asynchronous CodePouchDB提供了一个完全异步API。 callback形式: db.get('mittens', function (error, doc) { if (error) { // oh noes! we got an error } else { // okay, doc contains our document } }); Promise形式: db.get('mittens').then(function (doc) { // okay, doc contains our document }).catch(function (err) { // oh noes! we got an error }); 批量操作可以使用bulkDocs()和allDocs()进行批量操作 使用bulkDocs()一次性操作多个docdb.bulkDocs([ { _id: 'mittens', occupation: 'kitten', cuteness: 9.0 }, { _id: 'katie', occupation: 'kitten', cuteness: 7.0 }, { _id: 'felix', occupation: 'kitten', cuteness: 8.0 } ]) 使用bulkDocs()可以在一次请求中进行处理操作。我们也可以使用bulkDocs()批量修改或者批量删除,每个对象中都包含_rev和_deleted即可。 因为PouchDB并不支持事务。所以在bulkDocs()中,一个操作失败了,并不能确保所有的操作都失败。 使用allDocs()一次性读取多个doc我们使用allDocs()返回的数据是按照_id排序的。 db.allDocs({include_docs: true}).then(res => { console.log(JSON.stringify(res)) }) 如果我们没有传入include_docs: true只会返回基本的id、key和value.rev。 在allDocs()中,你可以进行排序、过滤等操作。可以通过阅读“Pagination strategies with PouchDB”获悉如何进行分页操作。 附件操作PouchDB支持附件操作 如何存储附件可以使用base64-encoded格式或者Blob进行存储附件。 存储一个附件: db.put({ _id: 'mydoc', _attachments: { 'myattachment.txt': { content_type: 'text/plain', data: 'aGVsbG8gd29ybGQ=' } } }) 以上代码中,myattachment.txt是文件名;content_type是格式;aGVsbG8gd29ybGQ=是base64编码后的数据。 我们如何读取附件呢? db.get('mydoc').then(data => { console.log(data) }) 使用以上代码时,PouchDB只会附件的基本信息,返回: { _attachments: { myattachment.txt: { content_type: "text/plain", digest: "md5-XrY7u+Ae7tCTyyK7j1rNww==", length: 11, revpos: 1, stub: true } }, _id: "mydoc", _rev: "1-a2308e4759440419de42048838ed86d3" } 要获得附件内容,我们必须得在查询中添加参数:attachments: true。 db.get('mydoc', {attachments: true}).then(data => { console.log(data) }) 图片附件db.put({ _id: 'meowth', _attachments: { 'meowth.png': { content_type: 'image/png', data: 'iVBORw0KGgoAAAANSUhEUgAAACgAAAAkCAIAAAB0Xu9BAAAABGdBTUEAALGPC/xhBQAAAuNJREFUWEetmD1WHDEQhDdxRMYlnBFyBIccgdQhKVcgJeQMpE5JSTd2uqnvIGpVUqmm9TPrffD0eLMzUn+qVnXPwiFd/PP6eLh47v7EaazbmxsOxjhTT88z9hV7GoNF1cUCvN7TTPv/gf/+uQPm862MWTL6fff4HfDx4S79/oVAlAUwqOmYR0rnazuFnhfOy/ErMKkcBFOr1vOjUi2MFn4nuMil6OPh5eGANLhW3y6u3aH7ijEDCxgCvzFmimvc95TekZLyMSeJC68Bkw0kqUy1K87FlpGZqsGFCyqEtQNDdFUtFctTiuhnPKNysid/WFEFLE2O102XJdEE+8IgeuGsjeJyGHm/xHvQ3JtKVsGGp85g9rK6xMHtvHO9+WACYjk5vkVM6XQ6OZubCJvTfPicYPeHO2AKFl5NuF5UK1VDUbeLxh2BcRGKTQE3irHm3+vPj6cfCod50Eqv5QxtwBQUGhZhbrGVuRia1B4MNp6edwBxld2sl1splfHCwfsvCZfrCQyWmX10djjOlWJSSy3VQlS6LmfrgNvaieRWx1LZ6s9co+P0DLsy3OdLU3lWRclQsVcHJBcUQ0k9/WVVrmpRzYQzpgAdQcAXxZzUnFX3proannrYH+Vq6KkLi+UkarH09mC8YPr2RMWOlEqFkQClsykGEv7CqCUbXcG8+SaGvJ4a8d4y6epND+pEhxoN0vWUu5ntXlFb5/JT7JfJJqoTdy9u9qc7ax3xJRHqJLADWEl23cFWl4K9fvoaCJ2BHpmJ3s3z+O0U/DmzdMjB9alWZtg4e3yxzPa7lUR7nkvxLHO9+tvJX3mtSDpwX8GajB283I8R8a7D2MhUZr1iNWdny256yYLd52DwRYBtRMvE7rsmtxIUE+zLKQCDO4jlxB6CZ8M17GhuY+XTE8vNhQiIiSE82ZsGwk1pht4ZSpT0YVpon6EvevOXXH8JxVR78QzNuamupW/7UB7wO/+7sG5V4ekXb4cL5Lyv+4IAAAAASUVORK5CYII=' } } }).then(function () { return db.getAttachment('meowth', 'meowth.png') }).then(function (blob) { var url = URL.createObjectURL(blob) var img = document.createElement('img') img.src = url document.body.appendChild(img) }).catch(function (err) { console.log(err) }) 上面代码中,我们使用URL.createObjectURL(),将Blob转换成URL。 上面代码更值得让我们注意的是,使用了db.getAttachment方法。该方法返回一个Blob,而不是返回base64编码的String。不过我们可以在base64和blob直接互转。 直接存储二进制数据可以直接使用putAttachment()方法,如果已经存在附件则该方法将覆盖已存在的附件,如果不存在附件则创建一个新的附件。 在NodeJS中,PouchDB返回的Blobs被Buffers代替。同样的,在PouchDB中,NodeJS的Buffers被Blobs替代。 附件上传可以使用H5的FileAPI上传附件,也可以使用<input type="file">进行附件上传,而且它们的元素已经是Blob。以下是一个简单的例子: <input type="file"> var input = document.querySelector('input'); input.addEventListener('change', function () { var file = input.files[0]; // file is a Blob db.put({ _id: 'mydoc', _attachments: { filename: { type: file.type, data: file } } }).catch(function (err) { console.log(err); }); }); Base64 vs Blobs/BuffersPouchDB将根据Base64格式的数据或者Blobs/Buffers数据选择最佳的存储结构。 根据官方例子的代码,我们可以在使用put()保存Blobs,也可以使用putAttachment()保存Base64。getAttachment()方法总是返回Blobs。在各个查询方法中,都可以传入选项{attachments: true},返回Base64;传入参数{binary: true}返回二进制。]]></content>
<categories>
<category>PouchDB</category>
</categories>
<tags>
<tag>PouchDB</tag>
</tags>
</entry>
<entry>
<title><![CDATA[MOP方法合成]]></title>
<url>%2F2016%2F09%2F12%2FMOP%E6%96%B9%E6%B3%95%E5%90%88%E6%88%90%2F</url>
<content type="text"><![CDATA[方法注入(method injection):编写代码时知道想要添加到一个或多个类中的方法的名字。利用方法注入,可以动态地向类中添加行为。也可以向任意数目的类中注入一组实现某一特定功能的可复用方法。可以通过使用分类,使用ExpandoMetaClass或Groovy的Mixin工具来注入方法。 方法合成(method synthesis):想在调用时动态地确定方法的行为。Groovy的invokeMethod()、methodMissing()和GroovyInterceptable对于方法合成非常有用。 合成的方法可能直到调用时才会作为独立的方法存在。 使用methodMissing合成方法class Person { def work() { "working..." } def plays = ['Tennis', 'VolleyBall', 'BasketBall'] def methodMissing(String name, args) { println "methodMissing called for $name" def methodInList = plays.find { it == name.split('play')[1] } if (methodInList) { def impl = { Object[] vargs -> "playing ${name.split('play')[1]}..." } Person instance = this instance.metaClass."$name" = impl impl(args) } else { throw new MissingMethodException(name, Person.class, args) } } } jack = new Person() println jack.work() println jack.playTennis() println jack.playVolleyBall() println jack.playBasketBall() println jack.playTennis() try { jack.playPolitics() } catch (Exception e) { println "Error: " + e } 对于实现了GroovyInterceptable的对象,调用该对象上的任何方法,都会调用到invokeMethod()。所以,又可以用以下方式: class Person implements GroovyInterceptable { def work() { "working..." } def plays = ['Tennis', 'VolleyBall', 'BasketBall'] def invokeMethod(String name, args) { println "intercepting call for $name" def method = metaClass.getMetaMethod(name, args) if (method) { method.invoke(this, args) } else { metaClass.invokeMethod(this, name, args) } } def methodMissing(String name, args) { println "methodMissing called for $name" def methodInList = plays.find { it == name.split('play')[1] } if (methodInList) { def impl = { Object[] vargs -> "playing ${name.split('play')[1]}..." } Person instance = this instance.metaClass."$name" = impl impl(args) } else { throw new MissingMethodException(name, Person.class, args) } } } 使用ExpandoMetaClass合成方法在我们无法对源码进行修改的时候,我们也就无法使用methodMissing进行合成方法。这时,我们可以使用ExpandoMetaClass合成方法。 class Person { def work() { "working..." } } Person.metaClass.methodMissing = { String name, args -> def plays = ['Tennis', 'VolleyBall', 'BasketBall'] println "methodMissing called for $name" def methodInList = plays.find { it == name.split('play')[1] } if (methodInList) { def impl = { Object[] vargs -> "playing ${name.split('play')[1]}..." } Person.metaClass."$name" = impl impl(args) } else { throw new MissingMethodException(name, Person.class, args) } } jack = new Person() println jack.work() println jack.playTennis() println jack.playTennis() try { jack.playPolitics() } catch (Exception e) { println "Error: " + e } 为代码添加上invokeMethod拦截方法: Person.metaClass.invokeMethod = { String name, args -> println "intercepting call for ${name}" def method = Person.metaClass.getMetaMethod(name,args) if(method){ method.invoke(delegate,args) }else{ Person.metaClass.invokeMissingMethod(delegate, name, args) } } 为具体的实例合成方法class Person{} def emc = new ExpandoMetaClass(Person) emc.methodMissing = {String name,args -> "I'm Jack of all trades... I can $name" } emc.initialize() def jack = new Person() def paul = new Person() jack.metaClass = emc println jack.sing() println jack.dance() println jack.juggle() try{ paul.sing() }catch (Exception e){ println e }]]></content>
<categories>
<category>Groovy</category>
</categories>
<tags>
<tag>Groovy</tag>
</tags>
</entry>
<entry>
<title><![CDATA[对Spring MVC做单元测试]]></title>
<url>%2F2016%2F09%2F02%2F%E5%AF%B9Spring%20MVC%E5%81%9A%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%2F</url>
<content type="text"><![CDATA[简单介绍在工作用到的对Controller进行单元测试。其实,在编写单元测试的时候还是遇到了一些问题没有解决(基于公司封装的框架,不能用最新的包 (⊙﹏⊙)b)。先记录下主要的代码,其他问题慢慢解决。 所需要基本的依赖包<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>1.10.19</version> <scope>test</scope> </dependency> <dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path-assert</artifactId> <version>0.9.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> <version>1.3</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.0.3.RELEASE</version> <scope>test</scope> </dependency> 当然还需要一些基本的Spring的包,就不列出了。 被测试的代码Controller类: @RestController @RequestMapping("/v0.1/statistics") public class StatisticsController{ @Autowired StatisticsService statisticsService; @RequestMapping(value = "/diaries", method = RequestMethod.GET) public Object getDiariesStatistics(@AuthenticationPrincipal UserInfo userInfo) { if (userInfo == null) { throw new BizException(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "缺少认证"); } UserStatistics userStatistics = statisticsService.getDiariesStatistics(userInfo.getUserId()); return entityToVO(userStatistics); } } Service类: @Service public class StatisticsService { @Autowired StatisticsRepository statisticsRepository; public UserStatistics getDiariesStatistics(String userId) { return statisticsRepository.findByUserId(userId); } } 因为只是要对Controller进行单元测试,就不列举StatisticsRepository的代码了;另因为演示,也就不列举UserStatistics代码,可以自己替换为相关的实体。 独立的Controller单元测试这种是单纯地对Controller进行单元测试。这里会对Service进行mock处理,流程不会走到Service层。 import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.mockito.Matchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; public class StatisticsControllerTestWithStandalone { private MockMvc mockMvc; @Mock private StatisticsService statisticsService; @InjectMocks private StatisticsController statisticsController; private UserStatistics userStatistics; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); this.mockMvc = MockMvcBuilders.standaloneSetup(statisticsController).build(); userStatistics = new UserStatistics(); userStatistics.setUserId("330134"); Statistics statistics = new Statistics(); statistics.setDiaryCount(10); userStatistics.setStatistics(statistics); } @Test public void testGetDiariesStatistics() throws Exception { when(statisticsService.getDiariesStatistics(any(String.class))).thenReturn(userStatistics); mockMvc.perform(get("/v0.1/statistics/diaries").header("Authorization", "debug userId=330134")) .andDo(print()).andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=UTF-8")) .andExpect(jsonPath("diaryCount").value(10)); verify(statisticsService).getDiariesStatistics(any(String.class)); } } 先来说明下代码: @Mock:mock出一个对象 @InjectMocks:使mock对象的使用类可以注入mock对象。比如上面的例子中,我们要把StatisticsService注入到StatisticsController中,那么我们就要对StatisticsController进行InjectMocks,对StatisticsService进行mock MockitoAnnotations.initMocks(this): 将打上Mockito标签的对象起作用,使得Mock的类被Mock,使用了Mock对象的类自动与Mock对象关联。 通过MockMvcBuilders.standaloneSetup模拟一个Mvc测试环境,通过build得到一个MockMvc MockMvc:测试时经常用到核心API,具体可以看官网文档 运行结果: MockHttpServletRequest: HTTP Method = GET Request URI = /v0.1/statistics/diaries Parameters = {} Headers = {Authorization=[debug userId=330134]} Handler: Type = nd.sdp.imdiary.statistics.controller.StatisticsController Method = public java.lang.Object nd.sdp.imdiary.statistics.controller.StatisticsController.getDiariesStatistics(com.nd.gaea.rest.security.authens.UserInfo) Async: Was async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[application/json;charset=UTF-8]} Content type = application/json;charset=UTF-8 Body = {"diaryCount":10,"firstDiaryDate":null,"lastDiaryDate":null,"unfinishedDate":null} Forwarded URL = null Redirected URL = null Cookies = [] 细心看以上代码会发现,我参数用到是any(String.class)。这是因为在独立测试该Controller的时候,@AuthenticationPrincipal UserInfo userInfo怎么也获得不到对应的值。据说这个在最新的Spring Security中有解决方案(用WithSecurityContextTestExcecutionListener),而项目用到是3.2.3版本。 集成到Web的单元测试有的时候我们需要对系统进行集成单元测试,那么我们就可以做如下操作: import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import javax.servlet.Filter; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ContextConfiguration(classes = {WebConfig.class, MongodbConfig.class}) @WebAppConfiguration @RunWith(SpringJUnit4ClassRunner.class) public class StatisticsControllerTest { protected MockMvc mockMvc; @Autowired private Filter springSecurityFilterChain; @Autowired private WebApplicationContext wac; @Before public void setUp() { MockitoAnnotations.initMocks(this); this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).addFilters(springSecurityFilterChain).build(); } @After public void teardown() throws Exception { SecurityContextHolder.clearContext(); } @Test public void testGetDiariesStatistics() throws Exception { mockMvc.perform(get("/v0.1/statistics/diaries").header("Authorization", "debug userId=330134")) .andDo(print()).andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)); } } 以上的代码和单独的单元测试代码还是有一些不一样的。首先是多个几个类上的注解,然后就是少了对Service的mock,最后就是生成mockmvc的方式不一样了。 @ContextConfiguration():指定Bean的配置文件信息。项目中用的是注解风格配置,WebConfig.class等。如果你用的是xml配置,可以把它替换为对应的xml配置 @WebAppConfiguration:在运行单元测试的时候会启动一个Web服务,所有的测试用例跑完以后停掉 @RunWith(SpringJUnit4ClassRunner.class):示使用Spring Test组件进行单元测试 @Autowired WebApplicationContext wac:注入web环境的ApplicationContext容器 MockMvcBuilders.webAppContextSetup(this.wac):模拟真实的Spring MVC环境 @Autowired Filter springSecurityFilterChain:获得SecurityContextPersistenceFilter 参考: http://www.cnblogs.com/wade-xu/p/4311657.html http://zhaozhiming.github.io/blog/2014/06/16/spring-mvc-unit-test-part-1]]></content>
<categories>
<category>Spring</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>SpringMVC</tag>
</tags>
</entry>
<entry>
<title><![CDATA[grails备忘录(二)]]></title>
<url>%2F2016%2F08%2F25%2Fgrails%E5%A4%87%E5%BF%98%E5%BD%95%EF%BC%88%E4%BA%8C%EF%BC%89%2F</url>
<content type="text"><![CDATA[Controllers创建一个Controllergrails create-controller org.bookstore.hello 渲染(render)把响应以不同的形式呈现。 render "some text" render(text: "<xml>some xml</xml>", contentType: "text/xml", encoding: "UTF-8") // render a template to the response for the specified model def theShining = new Book(title: 'The Shining', author: 'Stephen King') render(template: "book", model: [book: theShining]) render(view: "viewName", model: [book: theShining]) render(contentType: "application/json") { book(title: b.title, author: b.author) } 请求方法定义默认,所有的方法可以对应所有的Http请求的方法。 我们可以自己配置Http请求方法与action的对应: static allowedMethods = [action1:'POST', action3:['POST', 'DELETE']] def action1() { … } def action2() { … } def action3() { … } bindData语法:bindData(target, params, includesExcludes, prefix) 用法: // binds request parameters to a target object bindData(target, params) // exclude firstName and lastName bindData(target, params, [exclude: ['firstName', 'lastName']]) // only use parameters starting with "author." e.g. author.email bindData(target, params, "author") bindData(target, params, [exclude: ['firstName', 'lastName']], "author") // using inclusive map bindData(target, params, [include: ['firstName', 'lastName']], "author") Chain使用flash storage让从一个action跳转到另一个action时保持模型不变。 chain(action: "details", model: [book: shawshankRedemption]) 语法:chain(controller*, namespace*, action, id*, model, params*) 默认的action可以指定默认的action。static defaultAction = "list" errors对象和hasErrors()方法errors保存了该controller中的所有的错误。hasErrors()返回controller中是否有错误。 flash对象一个临时对象,保存并且只保存转到下一个action时候session中的对象,当跳转到下一个action后清除。 def index() { flash.message = "Welcome!" redirect(action: 'home') } def home() {} forward和redirectforward:服务端跳转redirect:浏览器跳转 grailsApplicationGrailsApplication的实例。 def bookClass = grailsApplication.classLoader.loadClass("Book") namespace不同package的controller可以定义在同一个namespace中;如果没有定义namespace,相同package的命名空间一样。 static namespace = 'reports' //定义命名空间为reports paramsHttp请求的参数。 def book = Book.get(params.id) request和responseHttpServletRequest和HttpServletResponse的实例对象。 respond根据Accept的指定,以最合适的格式输出。比如:JSON、XML等。 respond Book.get(1) respond Book.get(1), [formats:['xml', 'json']] 可以在responseFormats中指定,static responseFormats = ['xml', 'json'] scope修改controller的作用域 static scope = "session" servletContextservletContext是 ServletContext 的实例对象。 input = servletContext.getResourceAsStream("/WEB-INF/myscript.groovy") sessionHttpSession的实例对象。 def logout() { log.info "User agent: " + request.getHeader("User-Agent") session.invalidate() redirect(action: "login") } withForm 和 withFormatwithForm示例: <g:form useToken="true" ...> withForm { // good request }.invalidToken { // bad request } withFormat示例: def list() { def books = Book.list() withFormat { html bookList:books js { render "alert('hello')" } xml { render books as XML } } } withForm:用来处理表单提交 withFormat:根据请求的Accept,呈现不同的response Service创建一个Servicegrails create-service org.bookstore.Book 对应地grails生成一个BookService文件。 import grails.transaction.Transactional @Transactional class BookService { def serviceMethod() { } } 作用域static scope = "session" score的值有:prototype,request,flash,flow,conversation,session,singleton。其中singleton为默认。 transactional使用static transactional = true设置事务。 也可以使用@Transactional注解。 GORM创建一个领域对象grails create-domain-class helloworld.Person 基础的CRUD操作createdef p = new Person(name:"Fred",age:40,lastVisit: new Date()) p.save() readdef p = Person.get(1) def p = Person.read(1) def p = Person.load(1) read和load的区别:对于load方法认为该数据在数据库中一定存在,可以放心的使用代理来延迟加载,如果在使用过程中发现了问题,只能抛异常;而对于get方法,一定要获取到真实的数据,否则返回null。 updatedef p = Person.get(1) p.name = "Bob" p.save() deletedef p = Person.get(1) p.delete() GORM中的关联关系Many-to-one and one-to-onemany-to-one class Face{ Nose nose } class Nose{ } 以上代码我们建立了一个多对一的关系。 class Face{ Nose nose } class Nose{ static belongsTo = [face:Face] } 这段代码是在先去的基础上添加了belongsTo,从而建立了级联关系。 one-to-one class Face{ static hasOne = [nose:Nose] } class Nose{ Face face } 这段代码建立了Face和Nose的一对一关联关系。 class Face{ static hasOne = [nose:Nose] static constraints = { nose unique: true } } class Nose{ Face face } 添加nose unique: true后,表示face必须有一个nose。 class Person { String name Person parent static belongsTo = [ supervisor: Person ] static mappedBy = [ supervisor: "none", parent: "none" ] static constraints = { supervisor nullable: true } } 对象自身的关联关系 one-to-manyclass Author { static hasMany = [books: Book] String name } class Book { String title } 上述代码定义了一对多的关联关系。 如果要求他们之间建立级联关系,则为Book修改如下: class Book { static belongsTo = [author: Author] String title } 如果多的一方有两个或以上相同的类型,可以如下代码: class Airport { static hasMany = [outboundFlights: Flight, inboundFlights: Flight] static mappedBy = [outboundFlights: "departureAirport", inboundFlights: "destinationAirport"] } class Flight { Airport departureAirport Airport destinationAirport } many-to-manyclass Book { static belongsTo = Author static hasMany = [authors:Author] String title } class Author{ static hasMany = [books:Book] String name } GORM中的组合class Person { Address homeAddress Address workAddress static embedded = ['homeAddress', 'workAddress'] } class Address { String number String code } Sets, Lists and Maps上面的那些代码中定义了hasMany的属性其实就是一个Set。 当然也可以定义many为其他类型: class Author { // SortedSet books //定义为SortedSet // List books //定义为List // Map books //定义为Map static hasMany = [books: Book] } 保存和更新def p = Person.get(1) p.save() //没有立马保存到库 try { p.save(flush: true) //立马保存到库 } catch (org.springframework.dao.DataIntegrityViolationException e) { // deal with exception } try { p.save(failOnError: true) //校验失败时抛出异常 } catch (ValidationException e) { // deal with exception } 删除def p = Person.get(1) p.delete() try { p.delete(flush: true) } catch (org.springframework.dao.DataIntegrityViolationException e) { flash.message = "Could not delete person ${p.name}" redirect(action: "show", id: p.id) } 还可以用以下写法: Customer.executeUpdate("delete Customer c where c.name = :oldName", [oldName: "Fred"]) 级联更新和级联删除不管是一对一、一对多或者是多对多,只要定义了belongsTo,就等于定义了级联操作。 class Airport { String name static hasMany = [flights: Flight] } class Flight { String number static belongsTo = [airport: Airport] } new Airport(name: "Gatwick") .addToFlights(new Flight(number: "BA3430")) .addToFlights(new Flight(number: "EZ0938")) .save() def airport = Airport.findByName("Gatwick") airport.delete() 以上代码定义了Airport、Flight,并且定义了Airport和Flight之间的级联关系。在新建Airport对象时,添加了些Flight,也就新建增了些Flight记录。删除airport时,也就删除了与它关联的Flight记录。 用belongsTo定义双向的one-to-manyclass A { static hasMany = [bees: B] } class B { static belongsTo = [a: A] } 这样设置会让级联策略为:one的一方是ALL,many一方是NONE 单向的one-to-manyclass A { static hasMany = [bees: B] } class B { } 这样的设置会让级联策略为SAVE_UPDATE 不用belongTo定义双向的one-to-manyclass A { static hasMany = [bees: B] } class B { A a } 这样的设置会让级联策略为one的一方为SAVE-UPDATE,many的一方为NONE 用belongsTo的单向的one-to-oneclass A { } class B { static belongsTo = [a: A] } 级联策略为:拥有的一方为ALL,belongsTo的一方为NONE]]></content>
<categories>
<category>Groovy</category>
</categories>
<tags>
<tag>Groovy</tag>
<tag>Grails</tag>
</tags>
</entry>
<entry>
<title><![CDATA[grails备忘录(一)]]></title>
<url>%2F2016%2F08%2F22%2Fgrails%E5%A4%87%E5%BF%98%E5%BD%95%EF%BC%88%E4%B8%80%EF%BC%89%2F</url>
<content type="text"><![CDATA[Grails是一套用于快速Web应用开发的开源框架,它基于Groovy编程语言,并构建于Spring、Hibernate和其它标准Java框架之上,从而为大家带来一套能实现超高生产力的一站式框架。 Grails命令首先看一个例子,用命令创建项目: grails create-app helloworld cd helloworld grails //按Tab键可寻求帮助 run-app //运行程序,在浏览器http://localhost:8080打开 create-controller hello //创建一个Controller 启动时端口参数修改:grails -Dserver.port=8090 run-app 测试应用:grails test-app 部署应用:grails war 部署dev环境(默认部署的是production环境):grails dev war 部署应用时设置JVM参数:-server -Xmx768M -XX:MaxPermSize=256m 具体的命令可参考:Command Line Grails目录结构Grails遵循COC(Convention over Configuration,约定由于配置)原则。 ├─gradle │ └─wrapper ├─grails-app //Grails程序的核心目录 │ ├─assets //静态资源 │ │ ├─images │ │ │ └─skin │ │ ├─javascripts │ │ └─stylesheets │ ├─conf //配置文件 │ │ └─spring │ ├─controllers //Web Controller,MVC中的C │ │ └─helloworld │ ├─domain //领域对象 │ ├─i18n //国际化 │ ├─init //初始化代码 │ │ └─helloworld │ ├─services //Service层 │ ├─taglib //自定义标签库 │ ├─utils //工具库 │ └─views //视图,MVC中的V │ └─layouts └─src ├─integration-test //集成测试代码 │ └─groovy ├─main //扩展的代码 │ ├─groovy │ └─webapp └─test //单元测试 └─groovy 配置文件默认配置构建配置文件:build.gradle 运行时系统配置文件:grails-app/conf/application.yml 日志配置文件:grails-app/conf/logback.groovy groovy获得配置文件的属性值: //在Controller中 def recipient = grailsApplication.config.getProperty('foo.bar.hello') //在Service中 GrailsApplication grailsApplication //全局变量 def recipient = grailsApplication.config.getProperty('foo.bar.hello') //用Spring PropertySource 的方法 @Value('${foo.bar.hello}') String recipient 多数据源配置,配置名字不同的数据源。domain中可以在static mapping = { datasources(['lookup', 'DEFAULT']) }代码块中引用数据源。 自定义配置继承GrailsAutoConfiguration。 class Application extends GrailsAutoConfiguration { /** * 自动扫面,获得所有包的名称 * @return */ @Override Collection<String> packageNames() { super.packageNames() + ['my.additional.package'] } /** * 注册Bean * @return */ @Bean MyType myBean() { return new MyType() } /** * 在GrailsApplication的生命周期GrailsApplicationLifeCycle中执行 * doWithSpring, doWithApplicationContext等 * @return */ @Override Closure doWithSpring() { {-> mySpringBean(MyType) } } } 插件创建插件//此命令在含有plugin或者grails项目的目录底下执行不成功,必须在没有它们的目录下执行 grails create-plugin [PLUGIN NAME] grails create-plugin [PLUGIN NAME] --profile=plugin 在src/main/groovy目录底下,你将发现一个以GrailsPlugin结尾的类。在此类中,你可以为插件添加title、author、description等信息。 安装本地插件运行grails install,grails会列出本地Maven缓存中已有的插件。选择一个安装。 创建Groovy脚本: create-script [NAME]。Grails会在src/main/scripts底下创建一个脚本文件。 创建命令:grails create-command [Command Name]。Grails会在grails-app目录下创建一个commands文件夹,该文件夹下会有一个以命令名命名的文件夹,会有一个以Command结尾的Groovy类。 参考文档: OSChina-grails]]></content>
<categories>
<category>Groovy</category>
</categories>
<tags>
<tag>Groovy</tag>
<tag>Grails</tag>
</tags>
</entry>
<entry>
<title><![CDATA[MOP方法注入]]></title>
<url>%2F2016%2F08%2F17%2FMOP%E6%96%B9%E6%B3%95%E6%B3%A8%E5%85%A5%2F</url>
<content type="text"><![CDATA[在Groovy中,可以随时打开一个类。也就是说,可以动态地向类中添加方法,允许它们在运行时改变行为。能够修改类的行为,是元编程和Groovy元对象协议(MOP)的核心。 Groovy的MOP支持以下3种技术注入行为: 分类(Category) ExpandoMetaClass Mixin 使用分类Groovy的分类提供了一种可控的方法注入方式——方法注入的作用可以限定在一个代码块内。分类(category)是一种能够修改类的MetaClass的对象,而且这种修改仅在代码块的作用域和执行线程内有效,退出代码块时,一切恢复原状。分类可以嵌套,也可以在一个代码块内应用多个分类。 要使用为对象添加的新的方法,只需要调用一个特殊的方法——use()。use方法接受两个参数:一个分类,一个闭包代码块。注入的方法就在该代码块内生效。 class StringUtil { def static toSSN(self) { //如果只想限制为String类型,则可以定义为toSSN(String self) if (self.size() == 9) { "${self[0..2]}-${self[3..4]}-${self[5..8]}" } } } use(StringUtil){ println "123456789".toSSN() println new StringBuffer("987654321").toSSN() } try{ println "123456789".toSSN() }catch (Exception e){ println e } 其中,StringUtil就是分类。self参数会被指派为目标实例。注意toSSN方法的定义,它是静态的。Groovy的分类注入的方法是静态的,而且至少接受一个参数,第一个参数指向的是方法调用的目标。要注入的方法所需要的参数都放在后面。 @Category(String) class StringUtilAnnotated { def toSSN() { if (size() == 9) { "${this[0..2]}-${this[3..4]}-${this[5..8]}" } } } use(StringUtilAnnotated) { println "123456789".toSSN() } 另一种可供选择的Groovy分类语法。这样,Groovy编译器就可以帮我们去转换为最开始的那个代码定义了。然而,这样的写法会限定方法只能使用参数中指定的类型,除非我们用指定参数类型为Object。 //参数为闭包 class FindUtil{ def static extractOnly(String self,closure){ def result = '' self.each{ if(closure(it)){result += it} } result } def static toSSN(self){ if (self.size() == 9) { "${self[0..3]}-${self[4..5]}-${self[6..8]}" } } } use(FindUtil){ println "121254123".extractOnly{ it == '4' || it == '5' } } //use的参数可以为一个分类,或由一个分类组成的列表 use(StringUtil,FindUtil){ str = '123456789' println str.toSSN() //存在相同的命名,use参数列表中的最后一个分类优先级最高 println str.extractOnly{it == '8' || it=='1'} } //拦截已有的方法,并取代。(可以理解为重载) class Helper{ def static toString(String self){ def method = self.metaClass.methods.find{it.name == 'toString'} '!!' + method.invoke(self,null) + '!!' } } use(Helper){ println 'hello'.toString() } 分类的限制:其作用限定在use()块内,所以也就限定于执行线程。因此注入的方法也是有限制的。注入的方法只能在use块内调用。多次进入和退出这个块时有代价的。每次进入时,Groovy都必须检查静态方法,并将其加入到新作用域的一个方法列表中。在块的最后还要清理该作用域。 什么情况下使用:调用不太频繁,而且想要分类这种可控的方法注入所提供的隔离性。 使用ExpandoMetaClass通过向类MetaClass添加方法可以实现向类中注入方法。这种注入方法是全局可用的。 Integer.metaClass.daysFromNow = { -> Calendar today = Calendar.instance today.add(Calendar.DAY_OF_MONTH,delegate) today.time } println 5.daysFromNow() 以上代码用了一个闭包实现了daysFromNow(),然后将其引入到Integer的MetaClass中。如果想要引入的是一个属性XXX,那么我们需要一个getXXX()方法,如: Integer.metaClass.getDaysFromNow={ -> Calendar today = Calendar.instance today.add(Calendar.DAY_OF_MONTH,delegate) today.time } println 5.daysFromNow 如果我们需要将一个方法注入到多个类中呢? daysFromNow = { -> Calendar today = Calendar.instance today.add(Calendar.DAY_OF_MONTH, (int)delegate) today.time } Integer.metaClass.daysFromNow = daysFromNow Long.metaClass.daysFromNow = daysFromNow println 6.daysFromNow() println 6L.daysFromNow() 这样,我们就可以向多个类中注入同一个方法了。同时,我们还可以向基类中注入方法,那么它的子类就直接拥有了该方法: Number.metaClass.someMethod = {-> println "someMethod called" } 2.someMethod() 2L.someMethod() 如果向类中注入静态方法,只需要将其加入到MetaClass的static属性中: Integer.metaClass.'static'.isEven = {val -> val % 2 == 0} println 'Is 2 even? '+ Integer.isEven(2) println 'Is 3 even? '+ Integer.isEven(3) 通过定义一个名为constructor的特殊属性可以加入构造器。因为我们是要添加一个构造器,而不是替换一个现有的,所以使用了<<操作符。注意:使用<<来覆盖现有的构造器或方法,Groovy会报错。 Integer.metaClass.constructor << { Calendar calendar -> new Integer(calendar.get(Calendar.DAY_OF_YEAR)) } println new Integer(Calendar.instance) 如果向替换掉构造器,可以使用=。在被覆盖的构造器内仍然可以使用反射调用原来的实现。 Integer.metaClass.constructor = { int val -> println "Intercepting constructor call" constructor = Integer.class.getConstructor(Integer.TYPE) constructor.newInstance(val) } println new Integer(4) println new Integer(Calendar.instance) 如果想要添加一堆的方法,ClassName.metaClass.method={…}的语法设置会让我们写起来很费劲。Groovy提供了一种可以将这些方法分组的方式,组织成一种叫作ExpandoMetaClass DSL的方便语法。 Integer.metaClass { daysFromNow = { -> Calendar today = Calendar.instance today.add(Calendar.DAY_OF_MONTH, delegate) today.time } getDaysFromNow = { -> Calendar today = Calendar.instance today.add(Calendar.DAY_OF_MONTH, delegate) today.time } 'static' { isEven = { val -> val % 2 == 0 } } constructor = { Calendar calendar -> new Integer(calendar.get(Calendar.DAY_OF_YEAR)) } constructor = {int val -> println "Intercepting constructor call" constructor = Integer.class.getConstructor(Integer.TYPE) constructor.newInstance(val) } } println new Integer(4) println new Integer(Calendar.instance) println 'Is 2 even? '+ Integer.isEven(2) println 'Is 3 even? '+ Integer.isEven(3) println 5.daysFromNow println 5.daysFromNow() 注意这里的加入构造器使用= 使用ExpandoMetaClass注入的方法只能在Groovy代码内使用,不能从编译过的Java代码中调用,也不能从Java代码中通过方式来使用。 向具体的实例中注入方法如果我们只想为某个实例注入方法,可以采用以下的方式: class Person{ def play(){ println 'playing...' } } def emc = new ExpandoMetaClass(Person) emc.sing = {-> 'oh baby baby...' } emc.initialize() def jack = new Person() def paul = new Person() jack.metaClass = emc println jack.sing() try{ paul.sing() }catch (ex){ println ex } Groovy提供了一种方便的方式来从实例中去掉这些注入的方法——只需要将metaClass属性设置为null。 jack.metaClass = null try{ jack.sing() }catch (ex){ println ex } 一种简单的方式: class Person{ def play(){ println'playing' } } def jack = new Person() def paul = new Person() jack.metaClass.sing = {-> 'oh baby baby...' } println jack.sing() jack.metaClass = null try{ jack.sing() }catch (ex){ println ex } 再来一组语法糖: jack.metaClass{ sing = {-> 'oh baby baby...' } dance = {-> 'start the music...' } } 使用Mixin注入方法Groovy的Mixin是一种运行时的能力,可以将多个类中的实现引入进来或混入。 如果将一个类混入到另一个类中,Groovy会在内存中把这些类的类实例链接起来。当调用一个方法时,Groovy首先将调用路由到混入的类中,如果该方法存在于这个类中,则在此处理。否则由主类处理。可以将多个类混入到一个类中,最后加入的Mixin优先级最高。 class Friend{ def listen(){ "$name is listening as a friend" } } @Mixin(Friend) class Human{ String firstName String lastName String getName(){ "$firstName $lastName" } } john = new Human(firstName: "John",lastName: "Smith") println john.listen() @Mixinz注解会将作为参数提供的类中的方法添加到被注解的类中。注解本身限制了这种方式只能由类提供者本身使用。 向已有的类中提供注入的语法: class Dog{ String name } Dog.mixin(Friend) buddy = new Dog(name: "Buddy") println buddy.listen() //对某个实例注入 class Cat{ String name } socks = new Cat(name:"Socks") socks.metaClass.mixin(Friend) //对该实例mixin println socks.listen() 在类中使用多个Mixin当混入多个类时,所有这些类的方法在目标类中都是可用的。当作为Mixin的两个或多个类中存在名字相同、参数签名也相同的方法时,最后加入到Mixin中的方法会隐藏掉已经注入的方法。 abstract class Writer { abstract void write(String message) } class StringWriter extends Writer { def target = new StringBuffer() @Override void write(String message) { target.append(message) } @Override public String toString() { return target.toString(); } } def writeStuff(writer){ writer.write("This is stupid") println writer } def create(theWriter,Object[] filters =[]){ def instance = theWriter.newInstance() filters.each {filter -> instance.metaClass.mixin(filter)} instance } class UppercaseFilter{ void write(String message){ def allUpper = message.toUpperCase() invokeOnPreviousMixin(metaClass,"write",allUpper) } } Object.metaClass.invokeOnPreviousMixin = { MetaClass currentMixinMetaClass,String method,Object[] args -> def previousMixin = delegate.getClass() for(mixin in mixedIn.mixinClasses){ if(mixin.mixinClass.theClass == currentMixinMetaClass.delegate.theClass){ break; } previousMixin = mixin.mixinClass.theClass } mixedIn[previousMixin]."$method"(*args) } class ProfanityFilter{ void write(String message){ def filtered = message.replaceAll('stupid','s*****') invokeOnPreviousMixin(metaClass,"write",filtered) } } writeStuff(create(StringWriter,UppercaseFilter)) writeStuff(create(StringWriter,ProfanityFilter)) writeStuff(create(StringWriter,UppercaseFilter,ProfanityFilter)) writeStuff(create(StringWriter,ProfanityFilter,UppercaseFilter)) 混入的顺序相当重要。方法调用会向链条中的左侧传递。]]></content>
<categories>
<category>Groovy</category>
</categories>
<tags>
<tag>Groovy</tag>
</tags>
</entry>
<entry>
<title><![CDATA[在Node.js中使用jQuery]]></title>
<url>%2F2016%2F08%2F17%2F%E5%9C%A8Node-js%E4%B8%AD%E4%BD%BF%E7%94%A8jQuery%2F</url>
<content type="text"><![CDATA[想要在NodeJs中使用jQuery? 首先,我们得安装jquery,npm install jquery。安装后的版本是3.1.0 接着,第一感觉我们会使用var $ = require('jquery')。 将以下代码保存为app.js var $ = require('jquery') $("body").append("<div>TEST</div>"); console.log($("body").html()); 运行node app.js 。提示错误: Error: jQuery requires a window with a document 那么我们该怎么做呢? 在npm的jquery安装包首页,我们看到可以使用jsdom进行模拟一个document。 require("jsdom").env("", function(err, window) { if (err) { console.error(err); return; } var $ = require("jquery")(window); $("body").append("<div>TEST</div>"); console.log($("body").html()); }); 运行,结果OK。 上面的代码,有一个让我不太舒服的地方就是要在回调函数中进行操作。那么我们如何做才可以不在回调函数中进行引入jquery呢? var $ = require('jquery')(require("jsdom").jsdom().defaultView); $("body").append("<div>TEST</div>"); console.log($("body").html()); 一样运行OK。]]></content>
<categories>
<category>NodeJs</category>
</categories>
<tags>
<tag>NodeJs</tag>
<tag>jQuery</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Groovy中的拦截方法]]></title>
<url>%2F2016%2F08%2F15%2FGroovy%E4%B8%AD%E7%9A%84%E6%8B%A6%E6%88%AA%E6%96%B9%E6%B3%95%2F</url>
<content type="text"><![CDATA[在Groovy中可以很轻松地实现AOP。这里有两种方法可以实现AOP: 使用GroovyInterceptable拦截方法 使用MetaClass拦截方法 使用GroovyInterceptable拦截方法如果一个Groovy对象实现了GroovyInterceptable接口,那么当调用该对象上的任何一个方法时——不管是否存在该方法,被调用的都会是invokeMethod()。也就是,GroovyInterceptable上的invokeMethod方法总是会被调用的。 class Car implements GroovyInterceptable{ def check(){ System.out.println("check called...") } def start(){ System.out.println("start called...") } def drive(){ System.out.println("drive called...") } def invokeMethod(String name, Object args) { System.out.print("call to $name intercepted... ") if(name != 'check'){ System.out.print("running filter...") Car.metaClass.getMetaMethod('check').invoke(this,args) } def validMethod = Car.metaClass.getMetaMethod(name,args) if(validMethod != null){ validMethod.invoke(this,args) }else{ Car.metaClass.invokeMethod(this,name,args) } } } car = new Car() car.start() car.drive() car.check() try{ car.stop() }catch (Exception e){ println e } 使用MetaClass拦截方法如果我们用的是第三方的源码或者用的是Java类,那么我们就不能用实现GroovyInterceptable接口的方法进行拦截了。在这种情况下,我们可以在MetaClass上实现invokeMethod()方法,并以此来拦截。 class Auto { def check() { System.out.println("check called...") } def start() { System.out.println("start called...") } def drive() { System.out.println("drive called...") } } Auto.metaClass.invokeMethod = { String name, Object args -> System.out.print("Call to $name intercepted...") if (name != 'check') { System.out.print("running filter...") Auto.metaClass.getMetaMethod('check').invoke(delegate, null) } def validMethod = Auto.metaClass.getMetaMethod(name, args) if (validMethod != null) { validMethod.invoke(delegate, args) } else { Auto.metaClass.invokeMissingMethod(delegate, name, args) } } auto = new Auto() auto.start() auto.drive() auto.check() try { auto.stop() } catch (Exception e) { println e } 上面的代码中,以闭包的形式实现了invokeMethod()。同时使用了delegate,delegate指向的是要拦截其方法的目标对象。 如果我们要对POJO进行拦截的话,原理是一样的,看一段对Integer进行拦截的代码。 Integer.metaClass.invokeMethod = {String name,Object args -> System.out.println("Call to $name interccepted on $delegate...") def validMethod = Integer.metaClass.getMetaMethod(name,args) if(validMethod == null){ Integer.metaClass.invokeMissingMethod(delegate,name,args) }else{ System.out.println("running pre-filter... ") result = validMethod.invoke(delegate,args) System.out.println("running post-filter... ") result } } println 5.floatValue() println 5.intValue() try{ println 5.isEmpty() }catch (Exception e){ println e } ps:ExpandoMethodClass是MetaClass接口的一个实现,也是Groovy种负责实现多态行为的关键类之一。通过向该类添加方法可以实现向类中注入行为,甚至可以使用该类特化单个对象。默认情况下,Groovy目前并没有使用ExpandoMethodClass。当我们想metaClass中添加一个方法时,默认的metaClass会被用一个ExpandoMetaClass实例替换掉。(注意obj1,obj2的结果…) import groovy.transform.Immutable def printMetaClassInfo(instance){ print "MetaClass of ${instance} is ${instance.metaClass.class.simpleName}" println " with delegate ${instance.metaClass.delegate.class.simpleName}" } printMetaClassInfo(2) println "MetaClass of Integer is ${Integer.metaClass.class.simpleName}" println "Adding a method to Integer metaClass" Integer.metaClass.someNewMethod = {-> /* */} printMetaClassInfo(2) println "MetaClass of Integer is ${Integer.metaClass.class.simpleName}" @Immutable class MyClass{ String name } obj1 = new MyClass("obj1") printMetaClassInfo(obj1) println "Adding a method to MyClass metaClass" MyClass.metaClass.someNewMethod = {-> /* */} printMetaClassInfo(obj1) println "obj2 created later" obj2 = new MyClass("obj2") printMetaClassInfo(obj2)]]></content>
<categories>
<category>Groovy</category>
</categories>
<tags>
<tag>Groovy</tag>
</tags>
</entry>
<entry>
<title><![CDATA[简单使用babel]]></title>
<url>%2F2016%2F08%2F11%2F%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8babel%2F</url>
<content type="text"><![CDATA[这篇文章的前提是读者知道什么是ES6以及为什么我们需要用到babel。本文的行文思路是怎么搭建一个es6+babel的开发环境。 基本使用创建工程目录 创建一个文件夹,命名为use-babel 使用npm init初始化目录 创建babel的配置文件.babelrc,该文件用来设置转码规则和插件 安装babel在工程目录下执行: npm install --save-dev babel-cli -g npm install --save-dev babel-preset-es2015 npm install --save-dev babel-preset-stage-0 npm install --save-dev babel-preset-stage-1 npm install --save-dev babel-polyfill 然后.babelrc文件中输入: { "presets": [ "es2015", "stage-0", "stage-1" ], "plugins": [] } 使用babel改写package.json如下: "scripts": { "babel-node": "babel-node --presets es2015,stage-0,stage-1", "start": "npm run babel-node src/index.js" }, 工程目录下执行 npm start。 Babel的一些解释babel-clibabel-cli是babel提供的一个命令行转码工具。具体的使用可以参考官方文档 特别要提的是,babel-cli还提供了babel-node。它类似于node,可以在运行前预编译ES2015,你就可以直接使用babel-node运行你的ES2015程序了。 因为babel-node会去读取.babelrc文件中的配置,所以上面的package.json中的代码又可以改写为: "scripts": { "start": "babel-node src/index.js" }, babel 插件babel插件提供了转码规则。我们可以根据自己的需要安装不同的转码规则。具体插件列表可以自行查看官方文档 ES2015的转码规则:babel-preset-es2015 ES7不同阶段语法提案的转码规则(共有4个阶段):preset-stage-0,preset-stage-1,preset-stage-2,preset-stage-3 ReactJs的转码规则:babel-preset-react babel-registerbabel-register是在你的程序中直接使用的。可以认为它改写了require,在使用babel-register的程序中,会对require的js先进行转码处理。这样,你就不需要自己去转码了。 但是,babel-register只会对require命令加载的文件转码,而不会对当前文件转码。 由于它是实时转码,所以只适合在开发环境使用。 使用npm install babel-register --save-dev安装。 require("babel-register"); require('./config'); //这样,我们就可以在config.js中使用ES2015的语法了。 babel-core如果你很喜欢在代码中使用babel,那么你就可以使用babel-core。(ps:目前我还没用过transform等方法) 具体的使用可以参考官方文档。 babel-polyfillbabel虽然能转码ES6,但是还有很多新的API语法是转不了的,比如Promise、Object.assign、Generator等。 如果想使用这些新的API,我们必须在代码中用到babel-polyfill。 使用npm install --save babel-polyfill安装,并且在代码中require('babel-polyfill');或import 'babel-polyfill'; 如果我们要在多个地方使用到babel-polyfill的话,我们可以直接使用babel-runtime。 安装babel-plugin-transform-runtime和babel-runtime,并更新.babelrc。 $ npm install --save-dev babel-plugin-transform-runtime $ npm install --save babel-runtime 向plugins中添加”transform-runtime” { "plugins": [ "transform-runtime" ] } 在webpack中使用一个比较经典的配置为: module: { loaders: [ { test: /\.js/, loader: 'babel?presets[]=es2015,presets[]=react,plugins[]=transform-runtime' } ] } 其他的一些babel的资料:Babel用户手册 : https://github.com/thejameskyle/babel-handbook/blob/master/translations/zh-Hans/user-handbook.mdBabel官网:https://babeljs.io]]></content>
<categories>
<category>JavaScript</category>
</categories>
<tags>
<tag>ES6</tag>
<tag>NodeJs</tag>
<tag>babel</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Groovy中的MOP——Groovy对象]]></title>
<url>%2F2016%2F08%2F10%2FGroovy%E4%B8%AD%E7%9A%84MOP%E2%80%94%E2%80%94Groovy%E5%AF%B9%E8%B1%A1%2F</url>
<content type="text"><![CDATA[Groovy对象是带有附加功能的Java对象。 POJO:普通的Java对象 POGO:用Groovy编写的对象,扩展了java.lang.Object,同时实现了groovy.lang.GroovyObject。 Groovy拦截器:扩展了GroovyInterceptable的Groovy对象。 public interface GroovyObject { Object invokeMethod(String name, Object args); Object getProperty(String propertyName); void setProperty(String propertyName, Object newValue); MetaClass getMetaClass(); void setMetaClass(MetaClass metaClass); } invokeMethod()、getProperty()和setProperty()使Java对象具有了高度的多态性,可以使用它们来处理运行中的方法和属性。getMetaClass()和setMetaClass()使创建代理拦截POGO的方法调用、在POGO上注入方法变得非常容易。 public interface GroovyInterceptable extends GroovyObject { } GroovyInterceptable接口扩展了GroovyObject接口,对于实现该接口的对象,其上所有的方法调用,都会被它的invokeMethod()方法拦截。 Groovy支持对POJO和POGO进行编程。对于POJO,Groovy维护了MetaClass的一个MetaClassRegistry;POGO有一个到其MetaClass的直接引用。 当我们调用一个方法时,Groovy会检查目标对象是一个POJO还是一个POGO。不同的对象类型,Groovy处理的方式不一样。 package exploring class TestMethodInvocation extends GroovyTestCase{ void testInterceptedMethodCallOnPOJO(){ def val = new Integer(3) Integer.metaClass.toString = {->'intercepted'} assertEquals "intercepted",val.toString() } void testInterceptableCalled(){ def obj = new AnInterceptable() assertEquals "intercepted",obj.existingMethod() assertEquals "intercepted",obj.nonExistingMethod() } void testInterceptedExistingMethodCalled(){ AGroovyObject.metaClass.existingMethod2 = {-> 'intercepted'} def obj = new AGroovyObject() assertEquals "intercepted",obj.existingMethod2() } void testUnInterceptedExistingMethodCalled(){ def obj = new AGroovyObject() assertEquals "existingMethod",obj.existingMethod() } void testPropertyThatIsClosureCalled(){ def obj = new AGroovyObject() assertEquals 'closure called',obj.closureProp() } void testMethodMissingCalledOnlyForNonExistent(){ def obj = new ClassWithInvokeAndMissingMethod() assertEquals "existingMethod",obj.existingMethod() assertEquals "missing called",obj.nonExistingMethod() } def testInvokeMethodCalledForOnlyNonExistent(){ def obj = new ClassWithInvokeOnly() assertEquals "existingMethod",obj.existingMethod() assertEquals "invoke called",obj.nonExistingMethod() } def testMethodFailsONNonExistent(){ def obj = new TestMethodInvocation() shouldFail (MissingMethodException){ obj.nonExistingMethod() } } } class AnInterceptable implements GroovyInterceptable{ def existingMethod(){} def invokeMethod(String name,args){ 'intercepted' } } class AGroovyObject{ def existingMethod(){'existingMethod'} def existingMethod2(){'existingMethod2'} def closureProp = {'closure called'} } class ClassWithInvokeAndMissingMethod{ def existingMethod(){'existingMethod'} def invokeMethod(String name,args){'invoke called'} def methodMissing(String name,args){'missing called'} } class ClassWithInvokeOnly{ def existingMethod(){'existingMethod'} def invokeMethod(String name,args){'invoke called'} } 运行时,可以查询一个对象的方法和属性,以确定该对象是否支持某一特定行为。 可以使用MetaObjectProtocal的getMetaMethod()(MetaClass扩展了MetaObjectProtocal)来获得一个元方法。 如果要查找一个static方法,可以使用getStaticMetaMethod()。 要获得重载方法的列表,使用这些方法的复数形式——getMetaMethods()和getStaticMetaMethods()。 getProperty()和getStaticProperty()可以获得元属性。 使用respondsTo()检查方法,使用hasProperty()检查属性。 MetaMethod的使用 str = "hello" methodName = "toUpperCase" methodOfInterest = str.metaClass.getMetaMethod(methodName) println methodOfInterest.invoke(str) print "Does String respond to toUpperCase()? " println String.metaClass.respondsTo(str, 'toUpperCase') ? 'yes' : 'no' print "Does String respond to compareTo(String)? " println String.metaClass.respondsTo(str, "compareTo", 'test') ? 'yes' : 'no' print "Does String respond to toUpperCase(int)? " println String.metaClass.respondsTo(str, "toUpperCase", 5) ? 'yes' : 'no' 动态访问对象 def printInfo(obj){ usrRequestedProperty = 'bytes' usrRequestedMethod = 'toUpperCase' println obj[usrRequestedProperty] println obj."$usrRequestedProperty" println obj."$usrRequestedMethod"() println obj.invokeMethod(usrRequestedMethod,null) } printInfo("hello") 要调用一个属性,可以使用索引操作符[],或使用点记号后跟一个计算属性名的GString。 迭代一个对象的所有属性: println "Properties of 'hello are:" 'hello'.properties.each{ println it}]]></content>
<categories>
<category>Groovy</category>
</categories>
<tags>
<tag>Groovy</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Groovy入门]]></title>
<url>%2F2016%2F07%2F26%2FGroovy%E5%85%A5%E9%97%A8%2F</url>
<content type="text"><![CDATA[安装以及简单使用安装到Groovy官网下载安装包。(本人使用的是windows,故以windows为例) 按照安装程序指引操作。 设置GROOVY_HOME环境变量和Groovy路径。GROOVY_HOME设置为安装目录(C:\Groovy\Groovy-2.4.7),Path中添加%GROOVY_HOME%/bin。 在命令行中运行groovy -v,验证是否安装正确。 使用groovysh在命令行窗口输入groovysh,我们会看到一个shell。然后就可以输入一些groovy代码了。 groovy:000> Math.sqrt(16) ===> 4.0 groovy:000> 输入:exit退出shell。 使用groovyConsolewindows用户可以在安装目录的bin中找到groovyConsole.bat,双击,控制台GUI工具就会弹出。可以在GUI上进行编码操作。 命令行中运行Groovy文件 编写一个groovy文件。 运行groovy命令执行。 hello.groovy内容: println "Hello Groovy!" 运行命令: groovy hello 结果:Hello Groovy! Groovy基础Groovy支持Java语法,并且保留了Java语义,我们可以随心所欲地混用两种语言。 一些特性: return方法几乎总是可选的 可以不使用分号分隔语句 方法和类默认是public的 静态方法内可以使用this来引用Class对象。 Groovy的==等价于Java的equals()。如果实现了Comparable接口,那么==就相当于compareTo()方法 def、in、it不建议作为变量的命名。def用于定义方法、属性和局部变量;in用于在for循环中指定循环期间。 Groovy闭包用花括号定义,匿名类也用花括号定义 http://groovy-lang.org/differences.html列出来Groovy和Java的不同。 实现循环的方式for循环 & range语法for (int i = 0; i < 3; i++){ println("this is " + i); } for(i in 0..2){ println("this is " + i); } upto()0.upto(2){ println("this is $it"); } 0是Integer的一个实例。$it代表进行循环时的索引值。upto()方法接受一个闭包作为参数。 times()3.times { println("this is $it"); } 范围从0开始。 step()0.step(10,2){ println("this is $it"); } 表示,从0到10,每次加2。 安全导航操作符 - ?.安全导航(safe-navigation)操作符(?.)在安全的情况下才会去调用指定的方法或者属性。 def foo(str){ // if(str !=null ) str.reverse(); str?.reverse(); } println(foo('evil')) println(foo(null)) 异常处理对于那些我们不想处理的异常,或者不适合在代码的当前处理的异常,Groovy并不强制我们处理。我们不处理的任何异常都会被自动传递给更高一层。所以,我们可以在更高层次的任何地方处理我们想要处理的异常。 JavaBeanclass Car { def miles = 0; final year; Car(theYear){ year = theYear; } } Car car = new Car(2016); println "Year: $car.year"; println "Miles: $car.miles"; println "Setting miles: "; car.miles = 25; println "Miles: $car.miles"; def声明一个属性。 Groovy会为JavaBean创建一个访问器(getter)和一个更改器(setter)。 使用final来声明属性时,该属性为只读。修改final字段会导致异常。 Groovy的实现不区分public、private和protected。 要把变量设置为私有的,必须实现一个拒绝任何修改的更改器(setter)。 示例如下: class Car { private miles = 0; final year; Car(theYear){ year = theYear; } def getMiles(){ miles; } private void setMiles(miles){ throw new IllegalAccessException("you're not allowed to change miles") ; } } 初始化和具名参数class Robot{ def type,height,width def access(location,weight,fragile){ println "Received fragile? $fragile, weight: $weight, loc: $location" } } robot = new Robot(type:'arm',width: 0,height: 40) println "$robot.type, $robot.height, $robot.width" robot.access(x:30,y:20,z:10,50,true) robot.access(50,true,x:30,y:20,z:10) robot.access(50,x:30,y:20,z:10,true) 在构造对象时,可以简单得以逗号分隔的“名值对”来给出“属性值”。第一个形参必须定义为Map。要让这种操作正确执行,必须要有一个无参构造器(编译器会默认提供一个)。此操作会在无参构造方法之后执行。 在使用方法时,如果第一个是Map,则可以将这个映射中的键值对展开放在实参的列表中。如果实参的个数多于方法的形参的个数,而且多出来的实参是键值对,那么Groovy会假设方法的第一个形参是Map,然后将实参列表中的素有键值对组织到一起,作为第一个形参的值。之后,再将剩下的实参按照给出的顺序赋给其余的形参。 站在阅读代码的角度,真的很不建议上述的做法。我们可以明确将一个形参指定为Map。 def access(Map location,weight,fragile){ //TODO } 可选形参Groovy可以把构造方法和方法的形参设置为可选的。要定义可选形参,只需要在形参列表中给它赋一个值就可以了。Groovy还把最后一个为数组的形参视为可选的。 def log(x,base = 10){ Math.log(x) / Math.log(base) } println log(1024) println log(1024,2) def task(name,String[] details){ println "$name - $details" } task('Hing','150-0000-0000') task('Hing','150-0000-0000','158-0000-0000') task('Hing') 使用多赋值 要从方法返回多个结果,并将它们一次性赋给多个变量,我们可以返回一个数组,然后将多个变量以逗号分隔,放在圆括号中,置于赋值表达式左侧即可。 可以用此特性来交互变量,无需创建中间变量来保存被交换的值,只需将与交换的变量放在圆括号内,置于赋值表达式左侧,同时将它们以相反顺序放在方括号内,置于右侧即可。 如果有多余的变量,Groovy会将它们设置为null;多余的值则会被丢弃。 示例: //返回多个结果 def splitName(fullName){ fullName.split(' ') } def (fistName,lastName) = splitName('Hing Kwan') println "$fistName, $lastName" //交换变量 def name1 = "Thomson" def name2 = "Thompson" println "$name1 and $name2" (name1,name2) = [name2,name1] println "$name1 and $name2" //多的参数 def(String cat,String mouse) = ['Tom','Jerry','Spike','Tyke'] println "$cat and $mouse" //多的结果 def(first,second,third) = ['Tom','Jerry'] println "$first and $second and $third" 实现接口在Groovy中,可以把一个映射或一个代码块转化为接口,因此可以快速实现带有多个方法的接口。 Groovy没有强制实现接口中的所有方法:可以只定义自己关心的,而不考虑其他方法。如果剩下的方法从来不会被调用,那也就没有必要去实现这些方法了。 接口中的每个方法可能需要不同的实现,在Groovy中只需要创建一个映射,以每个方法的名字作为键,以对应的代码体作为值,同时使用简单的Groovy风格,用冒号(:)分隔方法名和代码块即可。 如果没有提供实现的方法被调用了,则会抛出NullPointerException。 布尔求值根据上下文,Groovy会自动把表达式计算为布尔值。 以下是为true的条件 Boolean:值为true Collection:集合不为空 Character:值不为0 CharSequence:长度大于0 Enumeration:hasMoreElements()为true Iterator:hasNext() 为true Number:Double值不为0 Map:该映射不为空 Matcher:至少有一个匹配 Object[]:长度大于0 其他任何类型:引用不为null 操作符重载Groovy支持操作符重载。因为在Groovy中,每个操作符都会映射到一个标准的方法。 for(ch = 'a';ch < 'd';ch++){ println(ch) } for(ch in 'a'..'c'){ println(ch) } enum示例一: enum CoffeeSize { SHORT, SMALL, MEDIUM, LARGE, MUG } def orderCoffee(size){ print "Coffee order received for size $size :" switch (size){ case [CoffeeSize.SHORT,CoffeeSize.SMALL]: println "you're health conscious" break; case CoffeeSize.MEDIUM..CoffeeSize.LARGE: println "you gotta be a programmer" break; case CoffeeSize.MUG: println "you should try Caffeine IV" break; } } orderCoffee(CoffeeSize.SMALL) orderCoffee(CoffeeSize.LARGE) orderCoffee(CoffeeSize.MUG) print 'Available sizes are:' for(size in CoffeeSize.values()){ print "$size"; } 示例二: enum Methodologies{ Evo(5), XP(21), Scrum(30); final int daysInIteration Methodologies(days){ daysInIteration = days } def iterationDetails(){ println "${this} recommends $daysInIteration days for iteration" } } for(methodology in Methodologies.values()){ methodology.iterationDetails() } 变长参数Groovy以两周方式支持变长参数。1)支持使用省略号标记形参;2)以数组作为末尾形参的方法。 def receiveVarArgs(int a,int ... b){ } def receiveArray(int a,int[] b){ } 静态导入import static Math.random as rand import groovy.lang.ExpandoMetaClass as EMC double val = rand(); def metaClass = new EMC(Integer); 一些特色的注解@Canonical可以条件地使用或者去掉字段。 import groovy.transform.Canonical @Canonical(excludes = "lastName,age") class Person{ String firstName; String lastName; int age; } def sara = new Person(firstName: "Sara",lastName: "Walker",age: 49) println sara @Delegate使用@Delegate可以很方便地使用委托。 class Worker{ def work(){println 'get work done'} def analyze(){println 'analyze...'} def writeReport(){println 'get report written'} } class Expert{ def analyze(){println 'expert analysis...'} } class Manager{ @Delegate Expert expert = new Expert(); @Delegate Worker worker = new Worker(); } def bernie = new Manager() bernie.analyze() bernie.work() bernie.writeReport() 上面代码中因为Expert先被引入,所以analyze方法也先被引入,后面的就不引入相同的方法。 @Immutable用@Immutable注解标记一个类,Groovy会将其字段标记为final,并且额外为我们创建一些便捷的方法。 使用@Immutable注解可以轻松创建轻量级不可变对象。不可变实例是作为消息传递的理想实例。 @Immutable class CreditCard{ String cardNumber; int creditLimit; } println new CreditCard("",1000) @Lazy使用@Lazy可以把耗时对象的构建推迟到真正需要时。可以自动将字段标记为volatile,并确保创建期间是线程安全的。 class Heavy{ def size = 10; Heavy(){println "Creating Heavy with $size"} } class AsNeeded{ def value @Lazy Heavy heavy1 = new Heavy() @Lazy Heavy heavy2 = {new Heavy(size: value)}() AsNeeded(){println "Created AsNeeded"} } def asNeeded = new AsNeeded(value: 100) println asNeeded.heavy1.size println asNeeded.heavy1.size println asNeeded.heavy2.size @Newify可以用来创建实例。在创建DSL时,@Newify注解非常有用,它可以使得实例创建更像一个隐式操作。 @Newify([Person,CreditCard]) def fluentCreate(){ println Person.new(firstName: "John",lastName: "Doe",age: 20) println Person(firstName: "John",lastName: "Doe",age: 20) println CreditCard("",1000) } fluentCreate(); @Singleton@Singleton(lazy = true,strict=false) class TheUnique{ TheUnique(){ println 'TheUnique created' } def hello(){ println 'hello' } } TheUnique.instance.hello() TheUnique.instance.hello() 鸭子类型def takeHelp(helper){ helper.helpMoveThings(); } class Man{ void helpMoveThings(){ println "Man's helping" } } class Woman{ void helpMoveThings(){ println "Woman's helping" } } class Elephant{ void helpMoveThings(){ println "Elephant's helping" } } takeHelp(new Man()) takeHelp(new Woman()) takeHelp(new Elephant()) 多方法如果一个类中有重载的方法,Groovy会聪明地选择正确的实现——不仅基于目标对象(调用方法的对象),还基于所提供的参数。因为方法分派基于多个实体——目标加参数,所以这被称为多分派或多方法。 ArrayList<String> lst = new ArrayList<>(); Collection<String> col = lst; lst.add("One"); lst.add("Two"); lst.add("Three"); col.remove(0); col.remove(0); println lst; println col; 静态类型检查可以使用@TypeChecked注解进行静态类型检查。这个注解可以用于类或者单个方法上。如果用于一个类,则类型检查会在该类中所有方法、闭包和内部类上执行。如果用于一个方法,则类型检查仅在目标方法的成员上执行。 要利用静态类型检查,必须要指明方法和闭包的形参类型。 闭包闭包基础先从传统的方式看起,也许在我们编码的过程中,会有以下代码: def sum(n){ total = 0 for(int i = 2; i <= n ;i+=2){ total += i } total } def product(n){ prod = 1 for(int i = 2 ; i <= n; i+=2){ prod *= i } total } sum(10); product(10); 用闭包实现为: def pickEven(n,block){ for(int i = 2; i <= n; i++){ block(i); } } pickEven(10,{println it}); 就像上面的调用一样,代码块({}内的代码)被传给形参block,也就是block代表了传入的代码块执行。 如果闭包是最后一个实参,调用可以用下面的优雅的写法: pickEven(10){println it} 所以,当闭包是方法调用的最后一个实参时,可以将闭包附到方法调用上。这种情况下,代码块看上去就像是附在方法调用上的寄生虫。Groovy的闭包不能单独存在,只能附到一个方法上,或是赋值给一个变量。 上面代码中的it代表的意思:如果只向代码中传递一个参数,那么可以使用it这个特殊的变量名来指代该参数。我们也可以使用其他名称来代替it,如: pickEven(10){evenNum -> println evenNum} 闭包有两个非常擅长的领域:1)辅助资源清理;2)辅助创建内部的领域特定语言 普通函数在实现某个目标明确的任务时优于闭包。重构的过程是引入闭包的好时机。 闭包的使用将闭包赋值给变量并复用def totalSelectValues(n,closure){ total = 0 for(i in 1..n){ if(closure(i)){ total += i } } total } println totalSelectValues(10){it % 2==0} def isOdd = {it % 2 != 0} println totalSelectValues(10,isOdd) class Equipment{ def calculator Equipment(calc){ calculator = calc } def simulate(){ println "Running simulation" calculator() } } def eq1 = new Equipment({ println "Calculator 1"}) def aCalculator = { println "Calculator 2"} def eq2 = new Equipment(aCalculator) def eq3 = new Equipment(aCalculator) eq1.simulate(); eq2.simulate(); eq3.simulate(); 向闭包传递多个参数def tellFortune(closure){ closure new Date("07/29/2016"),"Your day is filled with ceremony" } tellFortune(){ date,fortune -> println "Fortune for ${date} is '${fortune}'" } //以上的代码其实也就是下面的代码: def tellFortune1(closure){ closure(new Date("07/29/2016"),"Your day is filled with ceremony") } tellFortune1({date,fortune -> println "Fortune for ${date} is '${fortune}'"}) 符号->将闭包的参数声明与闭包主体分隔开来。 使用闭包进行资源清理def writer = new FileWriter("out.txt"); writer.write('..'); // 没有调用writer.close() //当从闭包返回时,withWriter()会自动刷新并关闭这个流 new FileWriter('out.txt').withWriter { writer -> writer.write('!') } Execute Around Method模式 编写一个Execute Around方法,它接受一个块作为参数。在这个方法中,把对该块的调用夹到对那对方法的调用之间。即先调用第一个方法,然后调用该块,最后调用第二个方法。方法的使用者不必担心这对动作,他们会自动被调用。我们甚至可以在Execute Around方法内处理异常。 class Resource { def open() { println "opened..." } def close() { println "closed..." } def read() { println "read..." } def write() { println "writed..." } def static use(closure) { def r = new Resource(); try { r.open() closure(r) } finally { r.close() } } } Resource.use { res -> res.read() res.write() } 闭包与协程def iterate(n,closure){ 1.upto(n){ println "In iterate with value ${it}" closure(it) } } println "Calling iterate" total = 0 iterate(4){ total += it println "In closure total so far is ${total}" } println "Done" 上面的代码中,每次调用闭包,我们会从上一次的调用中恢复total值。 科里化闭包带有预绑定形参的闭包叫科里化闭包。 当对一个闭包调用curry()时,就是要求先绑定某些参数。在预先绑定了一个形参之后,调用闭包时就不必重复为这个形参提供实参了。 可以使用curry()方法科里化任意多个形参,但这些形参必须是从前面开始的连续若干个。也就是说,如果有n个形参,我们可以任意科里化前k个,其中 0 <= k <= n。 如果想科里化后面的形参,可以使用rcurry()方法。如果想科里化位于形参列表中间的形参,可以使用ncurry()方法,传入要进行科里化的形参的位置,同时提供相应的值。 def tellFortunesCurry(closure){ Date date = new Date(); postForturne = closure.curry(date) postForturne "Your day is filled with ceremony" postForturne "They're features,not bugs" } tellFortunesCurry(){ date,fortune -> println "Fortune for ${date} is '${fortune}'" } 动态闭包在给闭包传递参数时,也有很多灵活性。可以动态地确定一个闭包期望的参数的数目和类型。 即便一个闭包没有使用任何参数,就像{}或{it}中这样,其实它也会接受一个参数(名字默认为it)。如果调用者没有向闭包提供任何值,则第一个形参(it)为null。如果希望闭包完全不接受任何参数,必须使用{->}这种语法:在->之前没有形参,说明该闭包不接受任何参数。 利用maximumNumberOfParameters和parameterTypes属性,我们可以动态地检查闭包,而且在实现某些逻辑时有了更大灵活性。 def completeOrder(amount,taxComputer){ tax = 0 if(taxComputer.maximumNumberOfParameters == 2){ tax = taxComputer(amount,6.05) }else{ tax = taxComputer(amount) } println "Sales tax is ${tax}" } completeOrder(100){it * 0.0825} completeOrder(100){amount,rate -> amount * (rate/100)} def examine(closure) { println "$closure.maximumNumberOfParameters parameter(s) given:" for (aParameter in closure.parameterTypes) { println aParameter.name } println "--" } examine() {} examine() { it } examine() { -> } examine() { val1 -> } examine() { Date val1 -> } examine() { Date val1, val2 -> } examine() { Date val1, String val2 -> } 闭包委托this、owner和delegate是闭包的三个属性,用于确定由哪个对象处理该闭包内的方法调用。一般而言,delegate会设置为owner,但是对其加以修改。 def examiningClosure(closure){ closure() } examiningClosure(){ println "In First Closure" println "class is "+ getClass().name println "this is"+this +", super is" + this.getClass().superclass.name println "owner is "+ owner+", super is "+owner.getClass().superclass.name println "delegate is "+delegate+ ", super is "+delegate.getClass().superclass.name examiningClosure(){ println "In First Closure" println "class is "+ getClass().name println "this is"+this +", super is" + this.getClass().superclass.name println "owner is "+ owner+", super is "+owner.getClass().superclass.name println "delegate is "+delegate+ ", super is "+delegate.getClass().superclass.name } } 闭包内的this指向的是该闭包所绑定的对象(正在执行的上下文)。在闭包内引用的变量和方法都会绑定到this,它负责处理任何方法调用,以及对任何属性或变量的访问。如果this无法处理,则转向owner,最后是delegate。 使用尾递归def factorial(BigInteger number){ if(number == 1){ 1 }else{ number * factorial(number - 1) } } try { println "factorial of 5 is ${factorial(5)} " println "factorial of 5000 is ${factorial(5000)}" }catch (Exception e){ println ${e.getClass().name} } 以上代码在5000的时候会报错,那么有什么解决方法呢? Groovy语言通过闭包上的一个特殊的trampoline()方法提供了解决方法。 def factorial factorial = { int number,BigInteger theFactorial -> number == 1 ? theFactorial : factorial.trampoline(number - 1, number * theFactorial) }.trampoline() println "factorial of 5 is ${factorial(5,1)}" println "factorial of 5000 is ${factorial(5000,1)}" 上面代码中,factorial变量本身被赋值的就是在闭包上调用trampoline()方法的结果。 这种递归叫尾递归,因为方法中最后的表达式是结束递归或者是调用自身。 上面的代码可以优化为: def factorial(int factorialFor){ def tailFactorial tailFactorial = { int number,BigInteger theFactorial = 1 -> number == 1 ? theFactorial : tailFactorial.trampoline(number - 1, number * theFactorial) }.trampoline() tailFactorial(factorialFor) } 使用记忆缓存改变性能上面的代码是一种可以使用递归调用更高效地使用内存的技巧。而它的一个变种是将问题分解为可以多次重复解决的若干部分。在执行期间,我们将子问题的结果保存下来,当调用到重复的计算时,只需要简单地使用保存下来的结果。这样就避免了重复运行,因此极大地减少了计算时间。记忆化可以将一些算法的计算时间复杂度从输入规模(n)的指数级(O(k^n))减低到只有线性级(O(n))。 def rodPrices = [0, 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] def desiredLength = 27 @Immutable class RevenueDetails { int revenue ArrayList splits } def cutRod(prices, length) { if (length == 0) { return new RevenueDetails(0, []) } else { def maxRevenueDetails = new RevenueDetails(Integer.MIN_VALUE, []) for (rodSize in 1..length) { def revenueFromSecondHalf = cutRod(prices, length - rodSize) def potentialRevenue = new RevenueDetails( prices[rodSize] + revenueFromSecondHalf.revenue, revenueFromSecondHalf.splits + rodSize ) if (potentialRevenue.revenue > maxRevenueDetails.revenue) { maxRevenueDetails = potentialRevenue } } maxRevenueDetails } } timeIt desiredLength, { length -> cutRod(rodPrices, length) } 以上代码将花费我们很长时间去计算,优化如下 def cutRod cutRod ={ prices, length -> if (length == 0) { return new RevenueDetails(0, []) } else { def maxRevenueDetails = new RevenueDetails(Integer.MIN_VALUE, []) for (rodSize in 1..length) { def revenueFromSecondHalf = cutRod(prices, length - rodSize) def potentialRevenue = new RevenueDetails( prices[rodSize] + revenueFromSecondHalf.revenue, revenueFromSecondHalf.splits + rodSize ) if (potentialRevenue.revenue > maxRevenueDetails.revenue) { maxRevenueDetails = potentialRevenue } } maxRevenueDetails } }.memoize() 在以上代码中,Groovy创建了Memoize类的一个专用实例。该实例中有一个指向所提供闭包的引用,还有一个结果的缓存。当我们调用该闭包时,该实例会在返回结果之前响应缓存下来。在随后的调用中,如果对应某个参数已经有了相应的缓存下来的结果值,则返回该值。 记忆化技术是以空间换取时间。如果大规模计算的话,内存的需求可能会急剧增长。Groovy给我们提供了一些其他的选择,如:采用LRU算法的memoizeAtMost(),可以设置缓存下限的memoizeAtLeast()、可以设置缓存上限以及下限的memoizeBetween() 字符串在Groovy中,”” 和 ‘’都表示字符串。如果需要显式地创建一个字符,只需要输入 ‘a’ as char。 Groovy的字符串也是不可变的。可以使用[]操作符读取一个字符;不过不能修改。 def printClassIfnfo(obj){ println "class: ${obj.getClass().name}" println "superclass: ${obj.getClass().superclass.name}" } val = 25; printClassIfnfo("The Stock closed at ${val}") printClassIfnfo(/The Stock closed at ${val}/) printClassIfnfo("This is a simple String") 看下GString的惰性求值问题: price = 684.71 company = "Google" quote = "Today $company stock closed as $price" println quote stocks = [Apple:663.01,Microsoft:30.95] stocks.each{ key,value -> company = key price = value println quote } 得到的结果是: Today Google stock closed as 684.71 Today Google stock closed as 684.71 Today Google stock closed as 684.71 这样的结果明显不是我们想要的。程序修改如下: companyClosure = { it.write(company) } priceClosure = { it.write("$price") } quote = "Today ${companyClosure} stock closed at ${priceClosure}" stocks = [Apple: 663.01, Microsoft: 30.95] stocks.each { key, value -> company = key price = value println quote } 重构改为: quote = “Today ${->company} stock closed at ${-> price}” stocks = [Apple: 663.01, Microsoft: 30.95] stocks.each { key, value -> company = key price = value println quote } 可以通过将字符串包含在3个单引号内(’’’ … ‘’’)来定义多行字面常量。 langs = ['C++':'Stroustrup','java':'Gosling','Lisp':'McCarthy'] content = '' langs.each {language,author -> fragment = ''' <language name="${language}"> <author>${author}</author> </language> ''' content += fragment } xml = "<languages>${content}</languages>" println xml 操作符“~”可以方便地创建RegEx模式。这个操作符映射到String类的negate()方法。 要定义一个RegEx,使用正斜杠,如:/[G|g]roovy/ 要确定是否存在匹配使用=~ 要精确匹配使用==~ 集合lst = [1,3,4,1,8,9,2,6] println lst println lst.getClass().name println lst[0] println lst[lst.size() - 1] println lst[-1] //最后一个 println lst[-2] println lst[2..5] //从位置2开始,连续4个 println lst[-6..-3] sublst = lst[2..5] println sublst.dump() //打印该实例的详细信息 sublst[0] = 55 println "After sublst[0]=55 lst = $lst" lst.each { println it} //集合的迭代 //对每个元素进行操作,并返回集合 println lst.collect({it * 2}) println lst.find( {it == 2}) //查找 println lst.findAll({it == 2}) //查找所有 lst = ['Programming','In','Groovy'] count = 0 lst.each {count += it.size()} println count println lst.collect {it.size()}.sum() //与上面的代码效果一样 //inject对集合中的每个元素进行闭包调用 println lst.inject(0) {carryOver,element -> carryOver+element.size()} println lst.join(' ') lst[0] = ['Be','Productive'] //替换 println lst lst = lst.flatten() println lst println lst - ['Productive','In'] //去除元素 println lst.reverse() println lst.size() println lst*.size() Maplangs = ['C++':'Stroustrup','Java':'Gosling','Lisp':'McCarthy'] println langs.getClass().name println langs['List'] println langs.Java println langs.'C++' langs.each {entry -> println "Language $entry.key was authored by $entry.value" } langs.each{language,author -> println "Language $language was authored by $author" } println langs.collect {language,author -> language.replaceAll("[+]","P") } entry = langs.find{language,author -> language.size() > 3 } println entry.value selected = langs.findAll {language,author -> language.size() > 3 } selected.each {key,value -> println key + ',' +value } println langs.any {language,author -> //只要有一个符合 language =~ "[^A-Za-z]" } println langs.every {language,author -> //每个都符合 language =~ "[^A-Za-z]" } authors = langs.groupBy {it.value.split(' ')[0]} authors.each {author,values -> println "$author : ${values.collect {key,value -> value}.join(', ')}" }]]></content>
<categories>
<category>Groovy</category>
</categories>
<tags>
<tag>Groovy</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Visual Studio Code常用快捷键]]></title>
<url>%2F2016%2F07%2F20%2FVisual%20Studio%20Code%E5%B8%B8%E7%94%A8%E5%BF%AB%E6%8D%B7%E9%94%AE%2F</url>
<content type="text"><![CDATA[命令模式(Command Palette)通过F1或者Ctrl+Shift+P打开主命令面板。在主命令面板中可以执行VSCode的任何一条命令。Command Palette是VSCode中最有用的模式,在这里你可以安装组件也可以查看快捷键等。 在主面板模式下按backspace可以进入Ctrl+P模式中。 本地文件导航模式(Ctrl+P)通过Ctrl+P进入“本地文件导航模式”,该模式默认列出了你打开过的文件。在输入框中可以输入你想要打开的文件。 就像上图的输入框中的提示一样,输入”?”可以获得一些帮助。 以上输入框中: 输入:跳转到行数,也可以用Ctrl+G 输入@跳转到symbol(搜索变量或者函数),也可以Ctrl+Shift+O直接进入 输入@:根据分类跳转symbol,查找属性或函数,也可以Ctrl+Shift+O后输入:进入 输入#根据名字查找symbol,也可以Ctrl+T 编辑器以及窗口 打开新窗口:Ctrl+Shift+N 关闭窗口:Ctrl+Shift+W 新建文件:Ctrl+N 打开文件:Ctrl+O 在历史打开的文件之间切换:Ctrl+Tab、Alt+Left、Alt+Right 切出一个新的编辑界面Ctrl+\ 在各个编辑界面之间切换Ctrl+1、Ctrl+2、Ctrl+3 关闭编辑器:Ctrl+F4 查找:Ctrl+F 查找替换:Ctrl+H 整个文件夹搜索:Ctrl+Shift+F 显示/隐藏侧边栏:Ctrl+B Show Explorer:Ctrl+Shift+E Show Search:Ctrl+Shift+F Show Git:Ctrl+Shift+G Show Debug:Ctrl+Shift+D Show Output:Ctrl+Shift+U 代码 代码缩进:Ctrl+[、Ctrl+] 折叠/打开代码:Ctrl+Shift+[、Ctrl+Shift+] 复制一行(在不选中的情况下):Ctrl+C 剪切一行(在不选中的情况下):Ctrl+X 上下移动一行:Alt+Up、Alt+Down 上下复制一行:Alt+Shift+Up、Alt+Shift+Down 跳转到定义处:F12 定义处缩略图(不跳转过去):Alt+F12;Esc退出 列出所有的引用:Shift+F12,Esc退出 同时修改文本中的匹配:Ctrl+F2 重命名:F2 + 输入新的命名 跳转到下一个Warn/Error:F8 显示Warn/Error:Ctrl+Shift+M 光标 光标移到行首:Home 光标移到行尾:End 光标移到到文件开头:Ctrl+Home 光标移到文件结尾:Ctrl+End 选中当前行:Ctrl+i 选择从行首到光标处Shift+Home 选择从光标到行尾Shift+End 删除光标右侧的所有字Ctrl+Delete 同时选中所有匹配的:Ctrl+Shift+L、Ctrl+F2 选中下一个匹配:Ctrl+D 退回到上一个光标操作:Ctrl+U]]></content>
<categories>
<category>开发工具</category>
</categories>
<tags>
<tag>实战</tag>
<tag>Visual Studio Code</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用json-server模拟REST API]]></title>
<url>%2F2016%2F07%2F07%2F%E4%BD%BF%E7%94%A8json-server%E6%A8%A1%E6%8B%9FREST%20API%2F</url>
<content type="text"><![CDATA[现在在开发一个应用(Web或者App)的时候,通常需要前后端进行配合。一般情况下,我们使用REST风格的JSON结构进行数据交互。 实际工作中,我们会遇到服务端API的开发速度不能够满足前端开发速度的情况。这个时候,前端可以通过创建一个静态的JSON文件来模拟数据。但是呢,这样的模拟数据仅仅可以显示,如果要做增、删、改操作,必然不是那么方便。 json-server,就是为了给我们解决模拟REST API的一个npm模块。我们不需要自己写任何的代码,只需要将自己需要的模拟数据写入一个JSON文件中,json-server就可以向外提供REST风格的接口,方便我们进行数据模拟。 接下来,让我们看看这个神奇的工具吧! 基本使用安装npm install -g json-server 模拟数据创建一个文件db.json,并填入以下数据 { "posts": [ { "id": 1, "title": "用json-server模拟rest api", "author": "zxguan" } ], "comments": [ { "id": 1, "body": "用json-server模拟rest api", "postId": 1 } ], "profile": { "name": "zxguan" } } 使用运行: json-server --watch db.json 在浏览器中输入http://localhost:3000 输入http://localhost:3000/posts [{ id: 1, title: "用json-server模拟rest api", author: "zxguan" }] 其他类似的,我们可以通过postman进行POST、PUT和DELETE操作。使用时指定 Content-Type: application/json 。 其他特性过滤查询GET /posts?title=test.........&author=zxguan GET /posts?id=1&id=2 GET /comments?author.name=zxguan 分页使用 _start 、 _end 和 _limit 进行分页。 GET /posts?_start=0&_end=10 GET /posts/1/comments?_start=10&_end=20 GET /posts/1/comments?_start=20&_limit=10 排序使用 _sort 和 _order进行排序 GET /posts?_sort=views&_order=DESC GET /posts/1/comments?_sort=votes&_order=ASC 运算 _gte : 大于等于 _lte : 小于等于 _ne : 不等于 _like : like操作 用法为: 字段_gte 、字段_lte 、 字段_ne 、 字段_like GET /posts?views_gte=10&views_lte=20 GET /posts?id_ne=1 GET /posts?title_like=server 全文检索参数为q, GET /posts?q=internet 关联关系通过_embed获得子资源;通过_expand获得父级资源,通过资源名称获得嵌套资源 如: GET /posts?_embed=comments GET /posts/1?_embed=comments GET /comments?_expand=post GET /comments/1?_expand=post GET /posts/1/comments POST /posts/1/comments 高级特性使用其他数据源使用js生成随机数将以下代码复制进index.js module.exports = function() { var data = { users: [] } // Create 1000 users for (var i = 0; i < 1000; i++) { data.users.push({ id: i, name: 'user' + i }) } return data } 运行:json-server index.js 使用外部数据链接json-server http://jsonplaceholder.typicode.com/db 自定义路由方式一:使用routes.json { "/api/": "/", "/v0.1/:resource/:id/show": "/:resource/:id", "/v0.1/:category": "/posts/:id?category=:category" } 运行: json-server db.json --routes routes.json 方式二:使用自定义服务 将以下代码复制如server.js var jsonServer = require('json-server') var server = jsonServer.create() var router = jsonServer.router('db.json') var middlewares = jsonServer.defaults() server.use(middlewares) server.get("/echo",function(req,res){ res.jsonp(req.query) }) server.use(jsonServer.bodyParser) server.use(function(req,res,next){ if(req.method === 'POST'){ req.body.createAt = Date.now() } next() }) server.use(router) server.listen(3000, function () { console.log('JSON Server is running') }) 然后运行 node server.js 当然,这种方式你需要使用到package.json,然后将npm install json-server --save 方式三: 使用rewriter 复制以下代码到server.js var jsonServer = require('json-server') var server = jsonServer.create() var router = jsonServer.router('db.json') var middlewares = jsonServer.defaults() server.use(middlewares) server.use(jsonServer.bodyParser) server.use('/v0.1', router) server.listen(3000, function () { console.log('JSON Server is running') }) 或者: var jsonServer = require('json-server') var server = jsonServer.create() var router = jsonServer.router('db.json') var middlewares = jsonServer.defaults() server.use(middlewares) server.use(jsonServer.bodyParser) server.use(jsonServer.rewriter({ '/api/': '/', '/blog/:resource/:id/show': '/:resource/:id' })) server.use(router) server.listen(3000, function () { console.log('JSON Server is running') }) 自定义输出复制以下代码到server.js var jsonServer = require('json-server') var server = jsonServer.create() var router = jsonServer.router('db.json') var middlewares = jsonServer.defaults() var _ = require('lodash'); server.use(middlewares) server.use(jsonServer.bodyParser) router.render = function (req, res) { var value = res.locals.data if (_.isArray(value)) { res.jsonp({ items: value }) }else{ res.jsonp(value) } } server.use(router) server.listen(3000, function () { console.log('JSON Server is running') })]]></content>
<categories>
<category>NodeJs</category>
</categories>
<tags>
<tag>NodeJs</tag>
<tag>RESTful</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SpringMVC异常处理]]></title>
<url>%2F2016%2F06%2F24%2FSpringMVC%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%2F</url>
<content type="text"><![CDATA[SpringMVC处理异常的方式有以下四种: 使用 @ResponseStatus 使用 @ExceptionHandler 使用 @ControllerAdvice 使用 HandlerExceptionResolver 使用 @ResponseStatus@ResponseStatus可以被用于指定的方法或者类上,可以返回指定的HTTP Response状态码和原因。 使用方式如下: NotFoundException.java @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "NOT FOUND.") public class NotFoundException extends RuntimeException { public NotFoundException() { super(); } } TestController.java @RequestMapping("/test") public String test() { boolean isException = true; //模拟了有异常的时候 if (isException) { throw new NotFoundException(); //抛出异常 } return "success"; } 使用 @ExceptionHandler可以在Controller类的一些方法上添加@ExceptionHandler注解,该方法就可以用来处理该Controller类抛出的特定异常。 TestController.java //一个预定义的异常转换为一个HTTP状态代码 @ResponseStatus(value=HttpStatus.CONFLICT, reason="Data already present") @ExceptionHandler(SQLException.class) public void dataConflict() { System.out.println("----Caught SQLException----"); } //通过ModelAndView处理 @ExceptionHandler(Exception.class) public ModelAndView myError(Exception exception) { System.out.println("----Caught Exception----"); ModelAndView mav = new ModelAndView(); mav.addObject("exc", exception); mav.setViewName("myerror"); return mav; } //直接返回到error页面 @ExceptionHandler({SQLException.class,DataAccessException.class}) public String handDatabaseError(){ return "error"; } 这样的异常处理方式比较简单,扩展性好。我们可以创建一个处理异常的BaseExController基类,所有的要处理异常的Controller类都继承于该基类。 使用 @ControllerAdvice在ControllerAdvice的javadoc上有一段话,大致意思是@ControllerAdvice通常用来定义@ExceptionHandler,@InitBinder,@ModelAttribute方法,适用于所有@RequestMapping方法。 GlobalExceptionHandler.java @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(IOException.class) public ModelAndView myError(Exception exception) { ModelAndView mav = new ModelAndView(); mav.addObject("exception", exception); mav.setViewName("globalerror"); return mav; } @ExceptionHandler(KeywordNotFoundException.class) public String notFound() { return "404"; } } 使用 HandlerExceptionResolverSpringMVC通过HandlerExceptionResolver处理程序的异常。SpringMVC中默认使用了: DefaultHandlerExceptionResolver:HandlerExceptionResolver的默认实现。 ResponseStatusExceptionResolver:处理使用了@ResponseStatus注解的方法。 ExceptionHandlerExceptionResolver:处理使用了@ExceptionHandler注解的方法(当然也包括了使用了@ControllerAdvice) 我们可以通过实现HandlerExceptionResolver接口来定义自己的异常处理器,达到统一处理异常的目的。 最简单的,我们可以使用SpringMVC默认的SimpleMappingExceptionResolver来进行简单的统一异常处理。使用方法如下(因为使用了Spring boot,所以直接使用Java Configuration): @Configuration @ComponentScan("cn.webfuse.exception.demos") @EnableWebMvc public class WebConfig extends WebMvcConfigurerAdapter { @Bean(name = "simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver(); Properties errorMaps = new Properties(); errorMaps.setProperty("ElectricityNotFoundException", "error"); errorMaps.setProperty("NullPointerException", "error"); errorMaps.setProperty("DatabaseException", "databaseError"); resolver.setExceptionMappings(errorMaps); resolver.setDefaultErrorView("error"); resolver.setExceptionAttribute("exc"); return resolver; } } 当然我们也可以直接继承SimpleMappingExceptionResolver来定义我们自己的简单的异常处理类。]]></content>
<categories>
<category>Spring</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>SpringMVC</tag>
</tags>
</entry>
<entry>
<title><![CDATA[体验Meteor]]></title>
<url>%2F2016%2F06%2F15%2F%E4%BD%93%E9%AA%8CMeteor%2F</url>
<content type="text"><![CDATA[Meteor是什么?在Meteor官网的教程上是这样对Meteor进行解释的: Meteor is a full-stack JavaScript platform for developing modern web and mobile applications. Meteor includes a key set of technologies for building connected-client reactive applications, a build tool, and a curated set of packages from the Node.js and general JavaScript community. Meteor是一个构建在Node.js之上的全栈开发平台,用来开发实时网页程序(数据更新,页面立马重新渲染)。 Meteor位于程序数据库和用户界面之间,保持二者之间的数据同步更新。 Meteor在客户端和服务器端都使用JavaScript作为开发语言。 Meteor程序的代码在前后两端共用。 Meteor的数据库默认使用MongoDB。 Meteor存在丰富的资源:atmospherejs Quick start本文使用的开发环境是Windows 10,使用ReactJs开发。 创建一个简单的应用安装Meteor从官网上下载Meteor的安装包,按照常规安装方法进行安装即可。 创建一个应用常用命令meteor help <command> :帮助 meteor create <app-name> : 创建项目 meteor create --list : 查看有哪些示例项目 meteor create --example <example-name> : 创建示例项目的工程。example-name为meteor create --list命令的结果。 meteor create --package <package-name> : 创建本地meteor包 meteor update :升级meteor及其组件到最新版本 meteor add <package-name> : 添加包 meteor remove <package-name> : 删除包 meteor : 使用Meteor运行当前应用 meteor mongo : 打开meteor的mongodb shell。 meteor reset : 重置项目为初始状态,移除本地所有数据。 meteor deploy : 部署程序。 meteor debug : 用debug模式运行程序。 示例步骤1:meteor create news-list 步骤2:cd news-list 步骤3:meteor npm install 步骤4:meteor 步骤5:运行浏览器,输入http://localhost:3000查看结果 官网提供了丰富的TodoList系列例子是一个非常值得学习的范例。 还有一些比较有意思的项目: Rocket.Chat、wekan、Telescope等,我们可以在这些项目的基础上进行开发。 代码风格以及目录结构代码风格 建议采用统一的代码规范,可以使用eslint工具。 建议使用ES2015/ES6 目录结构文件结构通过官方的一个例子来说明目录结构: imports/ startup/ client/ index.js # import client startup through a single index entry point routes.js # set up all routes in the app useraccounts-configuration.js # configure login templates server/ fixtures.js # fill the DB with example data on startup index.js # import server startup through a single index entry point api/ lists/ # a unit of domain logic server/ publications.js # all list-related publications publications.tests.js # tests for the list publications lists.js # definition of the Lists collection lists.tests.js # tests for the behavior of that collection methods.js # methods related to lists methods.tests.js # tests for those methods ui/ components/ # all reusable components in the application # can be split by domain if there are many layouts/ # wrapper components for behaviour and visuals pages/ # entry points for rendering used by the router client/ main.js # client entry point, imports all client code server/ main.js # server entry point, imports all server code 建议将代码放到imports文件夹中,在Meteor v1.3中就可以根据需要对引用的文件进行懒加载。 imports/startup:将系统启动是需要进行处理的代码放在该文件夹下。 imports/api:将业务逻辑代码放在该文件夹下。 imports/ui:将UI渲染的代码放在此处。 client:存放在客户端运行的代码。 server:存放在服务端运行的代码。 加载顺序 html模板文件将首先加载。 以main.开头的文件将最后加载。 在任何lib/文件夹下的文件将随后加载。 更深层的目录将随后加载。 整个路径中文件以字母顺序加载。 例子如下: nav.html main.html client/lib/methods.js client/lib/styles.js lib/feature/styles.js lib/collections.js client/feature-y.js feature-x.js client/main.js 上述的例子是以加载顺序列出的。因为html总是被优先加载,所以main.html在第二个被加载了。因为第4条,目录比较深的client/lib/styles.js和client/lib/methods.js等会优先加载。根据第5条,client/lib/styles.js比lib/feature/styles.js优先加载。 可以通过Meteor.startup控制那些可以运行在服务端也可以运行在客户端的代码。 一些特殊的文件夹以下是一些特殊的文件夹说明: imports 该文件夹下没有被import的文件不会被加载。 client 该文件夹下的代码只会被客户端运行。 server 该文件夹下的代码只会在服务端运行。 public 该文件夹下中的文件会原样发送到客户端。用来放置资源等。假设有张图片/public/background.png, 在HTML中用 <img src=’/background.png’/>引用或是在CSS中用 background-image: url(/background.png) 引用。 注意图片URL中不包含/public。 private 该文件夹中的文件只能由服务端代码通过Assets API 来获取,客户端无法获取。 client/compatibility 这个文件夹是兼容的JavaScript库。各文件中使用var定义的变量将成为全局变量使用。该文件夹下的各文件在其他的客户端文件执行前被执行。 tests 测试文件夹。]]></content>
<categories>
<category>NodeJs</category>
</categories>
<tags>
<tag>NodeJs</tag>
<tag>Meteor</tag>
</tags>
</entry>
<entry>
<title><![CDATA[GraphQL初体验]]></title>
<url>%2F2016%2F06%2F03%2FGraphQL%E5%88%9D%E4%BD%93%E9%AA%8C%2F</url>
<content type="text"><![CDATA[什么是GraphQLGraphQL是Facebook开发的一个应用层的数据查询语言。通过GraphQL,客户端可以从服务端的数据集中轻松获得一个自定义结构的数据。 按照官网上的例子,如果我们的查询语句如: { user(id: 3500401) { id, name, isViewerFriend, profilePicture(size: 50) { uri, width, height } } } 那么,GraphQL服务器将返回: { "user" : { "id": 3500401, "name": "Jing Chen", "isViewerFriend": true, "profilePicture": { "uri": "http://someurl.cdn/pic.jpg", "width": 50, "height": 50 } } } 需要说明的是,有很多语言已经实现了GraphQL。具体见:awesome-graphql Getting Started还是让我们先参照官网上的例子,使用Node.js实现GraphQL服务器。 Step 1 创建工程目录hello-graphql 初始化项目,以及引入所需的包 npm init -f npm install graphql express express-graphql --save npm install babel-cli babel-preset-es2015 --save-dev npm install nodemon --save-dev Step 2创建data目录,并在该目录下创建data.json文件,并输入以下内容。 { "1": { "id": "1", "name": "Dan" }, "2": { "id": "2", "name": "Marie" }, "3": { "id": "3", "name": "Jessie" } } Step 3创建server.js文件,并输入以下代码内容。 import express from 'express'; var http = require('http'); const app = express(); const port = 3000; app.get('/', (req, res) => { res.send('Hello!'); }); app.set('port', port); var server = http.createServer(app); server.listen(port); server.on('listening', onListening); function onListening() { var addr = server.address(); var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; console.log('Listening on ' + bind); } 命令行运行nodemon --exec babel-node --presets=es2015 -- server.js,在浏览器输入http://localhost:3000。显示Hello!,说明你的服务express服务已经OK了。 Step 4创建文件夹/src/schema,并创建文件index.js。将在index.js文件中编写GraphQL Schema。Schema是GraphQL请求的入口,用户请求的GraphQL将会对应到具体的Schema。 import { GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString, GraphQLInt, GraphQLFloat, GraphQLEnumType, GraphQLNonNull } from 'graphql'; const data = require('../../data/data.json'); //我们要用的模拟数据 const User = new GraphQLObjectType({ name: 'User', description: 'User对象', fields: { id: { type: GraphQLString }, name: { type: GraphQLString }, } }); const Query = new GraphQLObjectType({ name: 'Query', fields: { user: { type: User, args: { id: { type: GraphQLString } }, resolve: function (_, args) { return data[args.id]; } } } }); const Schema = new GraphQLSchema({ query: Query }); export default Schema; Step 5修改server.js文件,连接schema。将server.js文件修改为以下内容: import express from 'express'; var http = require('http'); var expressGraphql = require('express-graphql'); // var Schema = require('./src/schema'); import Schema from './src/schema'; const app = express(); const port = 3000; app.set('port', port); app.use('/', expressGraphql({ schema: Schema, graphiql: true })); var server = http.createServer(app); server.listen(port); server.on('listening', onListening); function onListening() { var addr = server.address(); var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; console.log('Listening on ' + bind); } Step 6在浏览器中可以查看到如下界面。 在左边的空白处输入: { user(id: "1") { name } } 点击“运行”按钮。右边会给出结果 { "data": { "user": { "name": "Dan" } } } 期间遇到的一个问题注意到“Step 5”的代码中有一行被注释掉的代码:// var Schema = require('./src/schema');。本来我是这样写的,结果在运行的时候报错了,提示: 然后在graphiql的issues中找到了解决的答案,也就有了import Schema from './src/schema'。 总结到现在为止,我们已经搭好了简单的GraphQL服务器。此次的初体验只是我们将要用到GraphQL的预研,如果在生产环境中使用的话,肯定还会遇到许许多多新的问题。不过它强大的类型系统,它提供的抽象层可以让我们灵活地提供不同的数据支持还是让我印象深刻。 示例代码已上传GitHub:hello-graphql 参考资料: learngraphql graphql官网文档 GraphQL]]></content>
<categories>
<category>NodeJs</category>
</categories>
<tags>
<tag>GraphQL</tag>
<tag>NodeJs</tag>
</tags>
</entry>
<entry>
<title><![CDATA[读《大型网站技术架构-核心原理与案例分析》笔记(二)]]></title>
<url>%2F2016%2F05%2F31%2F%E8%AF%BB%E3%80%8A%E5%A4%A7%E5%9E%8B%E7%BD%91%E7%AB%99%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84-%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86%E4%B8%8E%E6%A1%88%E4%BE%8B%E5%88%86%E6%9E%90%E3%80%8B%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89%2F</url>
<content type="text"><![CDATA[瞬时响应:网站的高性能架构网站的性能是客观的指标,可以具体到响应时间、吞吐量等技术指标;也可以是主观感受。 性能测试不同视角下的网站性能1. 用户视角的网站性能 网站性能就是用户在浏览器上直观感受到的网站响应速度是快还是慢。 主要优化手段:前端架构优化技术。如:优化HTML、浏览器缓存、使用CDN、反向代理技术、浏览器的并发和异步等等。 2. 开发人员视角的网站性能 开发人员关注的网站性能就是程序本身及其子系统的性能,包括响应延迟、系统吞吐量、并发处理能力、系统稳定性等技术指标。 主要优化手段:缓存加速数据读取,使用集群提高吞吐能力,使用异步消息加快请求响应及实现削峰,使用代码优化手段改善程序性能。 3. 运维人员视角的网站性能 运维人员更关注基础设施性能和资源利用率。如:网络运营商的带宽能力、服务器硬件配置、数据中心网络架构、服务器和网络带宽的资源利用率等。 主要优化手段:建设优化骨干网络、使用性价比定制服务器、利用虚拟化技术优化资源利用等。 性能测试指标网站性能测试的主要指标有响应时间、并发数、吞吐量、性能计数器等。 响应时间指应用执行一个操作需要的时间,包括从发出请求开始到收到最后响应数据所需要的时间。响应时间是系统最重要的性能指标,直接反映了系统的“快慢”。 一些性能指标可以参考:Latency Numbers Every Programmer Should Know 并发数指系统能够同时处理请求的数目,这个数字也反映了系统的负载特性。对于网站而言,并发数即网站并发用户数,指同时提交请求的用户数目。 网站系统用户数 >> 网站在线用户数 >> 网站并发用户数 测试程序通过多线程模式并发用户的办法来测试系统的并发处理能力,为了真实模拟用户行为,测试程序并不是启动多线程然后不停地发送请求,而是在两次请求之间加入一个随机等待时间,这个时间被称为思考时间。 吞吐量指单位时间内系统处理的请求数量,体现系统的整理处理能力。对于网站来说,可以用“请求数/秒”、“页面数/秒”、“访问人数/天”、“处理的业务数/小时”等来衡量。 TPS:每秒处理的事务数 HPS:每秒处理HTTP请求数 QPS:每秒处理查询数 ps:一个很生动的高速公路上汽车、收费站的例子说明了并发数、吞吐量和响应时间的关系。 性能计数器它是描述服务器或操作系统性能的一些数据指标。 System Load:即系统负载,指当前正在被CPU执行和等待被CPU执行的进程数目总和,是反映系统忙闲程度的重要指标。 性能测试性能测试是一个总称,具体可细分为性能测试、负载测试、压力测试、稳定性测试。 性能测试时一个不断对系统增加访问压力,以获得系统性能指标、最大负载能力、最大压力承受能力的过程。增加访问压力指,在系统测试环境中,就是不断增加测试程序的并发请求数。 如上图:横坐标表示消耗的系统资源,纵坐标表示系统处理能力(吞吐量)。 在开始阶段,随着并发请求数目的增加,系统使用较少的资源就达到较好的处理能力(a~b段),这一段是网站的日常运行区间,网站的绝大部分访问负载压力都集中在这一段区间,被称作性能测试,测试目标是评估系统性能是否符合需求及设计目标; 随着压力的持续增加,系统处理能力增加变缓,直到达到一个最大值(c点),这是系统的最大负载点,这一段被称作负载测试*。测试目标是评估当系统因为突发事件超出日常访问压力的情况下,保证系统正常运行情况下能够承受的最大访问负载压力;* 超过这个点后,再增加压力,系统的处理能力反而下降,而资源消耗却更多,直到资源消耗达到极限(d点),这个点可以看作是系统的崩溃点,超过这个点继续加大并发请求数目,系统不能再处理任何请求,这一段被称作压力测试**,测试目标是评估可能导致系统崩溃的最大访问负载压力。 性能测试反应的是系统在实际生产环境中使用时,随着用户并发访问数量的增加,系统的处理能力。与性能曲线相对应的是用户访问的等待时间(系统响应时间) 性能优化策略性能分析:检查请求处理的各个环节的日志,分析哪个环节响应时间不合理、超过预期;然后检查监控数据,分析影响性能的主要因素是内存、磁盘、网络、还是CPU,是代码问题还是架构设计不合理,或者系统资源确实不足。 性能优化:根据网站分层架构,可分为Web前端性能优化、应用服务器性能优化、存储服务器性能优化3大类。 Web前端性能优化主要手段有:优化浏览器访问、使用CDN加速、使用反向代理。 优化浏览器访问 减少HTTP请求。主要手段,合并CSS、合并JavaScript、合并图片、尽可能合并数据请求。 使用浏览器缓存。通过设置HTTP头中Cache-Control和Expires的属性。 启用压缩。压缩HTML、CSS、JavaScript。 CSS放在页面最上面、JavaScript放在页面最下面。 减少Cookie传输。 使用CDN加速CDN能够缓存对的一般是静态资源,如图片、文件、CSS、Script脚本、静态网页等。 使用反向代理 具有保护网络安全的作用。 可以配置缓存功能加速Web请求。 实现负载均衡的功能。 应用服务器性能优化主要优化手段:缓存、集群、异步等。 缓存优先考虑使用缓存优化性能 缓存主要是为了存放那些读写比很高、很少变化的数据。 缓存使用中遇到的问题: 对频繁修改的数据使用缓存。(这个是极不合理的) 没有热点的访问。(这是在浪费缓存资源) 数据不一致与脏读。(这个要根据具体的业务分析) 缓存雪崩。(缓存的可用性很重要,通过分布式缓存可以解决) 缓存预热。(缓存系统启动的时候就把热点数据加载好) 缓存穿透。(大量访问不存在数据,缓存中不会存在,访问数据库) 分布式缓存架构 以JBoss Cache为代表的需要更新同步的分布式缓存 以Memcached为代表的不互相通信的分布式缓存 异步操作可以使用消息队列进行异步操作 使用集群使用负载均衡技术为一个应用构建一个由多台服务器组成的服务器集群。 代码优化多线程 使用多线程的原因主要由两个:IO阻塞和多CPU。 一台服务器启动多少线程合适呢?一般公式如下: 启动线程数 = [任务执行时间/(任务执行时间 - IO等待时间)] * CPU内核数 最佳启动线程数和CPU内核数量成正比,和IO阻塞时间成反比。如果任务都是CPU计算型任务,那么多线程最多不超过CPU内核数;如果任务需要等待磁盘操作,网络响应,那么多启动线程有助于提高任务并发度,提高系统吞吐能力,改善系统性能。 资源复用 从编程角度,资源复用的方式主要有两种模式:单例和对象池。 数据结构 垃圾回收 存储服务器性能优化 使用固态硬盘 关系型数据库主要使用B+树;NoSQL主要使用LSM树 RAID(廉价磁盘冗余阵列);HDFS(Hadoop分布式文件系统) 万无一失:网站的高可用架构网站的可用性(Availability)描述网站可有效访问的特性。 网站的可用性的度量与考核网站可用性度量网站不可用也被称作网站故障,业界通常用多少个9来衡量网站的可用性。 网站不可用时间(故障时间) = 故障修复时间点 - 故障发现(报告)时间点 网站年度可用性指标 = (1 - 网站不可用时间/年度总时间) * 100% 网站可用性考核可用性指标是网站架构设计的重要指标,对外是服务承若,对内是考核指标。可以用故障分对工程师进行考核。 故障分是指对网站故障进行分类加权计算故障责任的方法。 故障分 = 故障时间(分钟) * 故障权重。 ps:不同的故障类别设置不同的权重值。 高可用的网站架构网站的高可用架构设计的主要目的就是保证服务器硬件故障时服务依然可用、数据依然保存并能够被访问。 主要手段是数据和服务的冗余备份及失效转移。 分层模:应用层、服务层、数据层。应用层主要负责具体业务逻辑处理;服务层负责提供可复用的服务;数据层负责数据的存储于访问。 ps:个人理解。这里的分层和代码中的分层有相似的地方,但又有不同的地方。在代码中应用层(Controller)只是作为跳转,很少去处理业务逻辑。很高兴的是,本书提供了一张图说明了此处分层的意思: 高可用的应用应用层主要处理网站应用的业务逻辑,因此有时也称为“业务逻辑层”,应用的一个显著特点是应用的无状态性。 无状态的应用是指应用服务器不保存业务的上下文信息,而仅根据每次请求提交的数据进行相应的业务逻辑处理,每个服务实例(服务器)之间完全对等,请求提交到任意服务器,处理结构都是完全一样的。 通过负载均衡进行无状态服务的失效转移应用服务器集群的Session管理 Session复制。在集群规模小的时候使用。 Session绑定。将同一个IP源的请求,发给一台服务器。 利用Cookie记录Session。 Session服务器。 高可用的服务可复用的服务模块为业务产品提供基础公共服务,大型网站中这些服务通常都独立分布式部署,被具体应用远程调用。 分级管理 超时设置 异步调用 服务降级:拒绝服务和关闭服务 幂等性设计 高可用的数据保证数据存储高可用的手段是数据备份和失效转移机制。 CAP原理高可用的数据有如下几个层面的含义: 数据持久性 数据可访问性 数据一致性 CAP原理认为,一个提供数据服务的存储系统无法同时满足数据一致性(Consistency)、数据可用性(Availibility)、分区耐受性(Partition Tolerance,系统具有跨网络分区的伸缩性)这三个条件。 在大型网站中,通常会选择强化分布式存储系统的可用性(A)和伸缩性(P),而在某种程度上放弃一致性(C)。 数据一致性分为:数据强一致性、数据用户一致、数据最终一致。 数据备份冷备份热备份 异步热备方式 应用程序将数据写入主存储服务器(Master),然后再通过异步现场将操作数据同步到从存储服务器(Slave)。通常使用读写分离的方法访问Slave和Master数据库,写操作只访问Master数据库,读操作只访问Slave数据库。 同步热备方式 应用程序同时将操作数据写入各个村粗服务器。没有主从之分。 失效转移如果数据服务器集群中任何一台服务器宕机,那么应用程序针对这台服务器的所有读写操作都需要重新路由到其他服务器,保证数据访问不会失败,这个过程叫做失效转移。 失效转移操作由三部分组成: 失效确认:心跳检测和应用程序访问失败报告 访问转移 数据恢复 高可用网站的软件质量保证网站发布 自动化测试预发布验证预发布服务器和生产服务器的唯一不同就是没有配置在负载均衡服务器上,外部用户不能访问。 预处理服务器和线上正式服务器都部署在相同的环境下,可能使用的数据库也是相同的。 ps:怎么处理数据问题?难道不是应该部署在不同的数据环境下么? 代码控制trunk分支可以用来发布预生产和生产 dev分支可以用来发布测试和压测等。 自动化发布灰度发布可立即回滚的发布。 网站运行监控不允许没有监控的系统上线。 监控数据采集用户行为日志收集包括用户操作系统与浏览器版本信息、IP地址、页面访问路径、页面停留时间等 服务器日志收集 客户端浏览器日志收集 服务器性能监控收集服务器性能指标,如系统Load、内存占用、磁盘IO、网络IO等。 运行数据报告如缓存命中率、平均响应延迟时间、待处理的任务数等 监控管理系统报警失效转移自动优雅降级永无止境:网站的伸缩性架构网站的伸缩性是指不需要改变网站的软硬件设计,仅仅通过改变部署的服务器数量就可以扩大或者缩小网站的服务处理能力。 网站架构的伸缩性设计网站架构的伸缩性设计可分为两类,一类是根据功能进行物理分离实现伸缩,一类是单一功能通过集群实现伸缩。前者是不同的服务器部署不同的服务,提供不同的功能;后者是集群内的多台服务器部署相同的服务,提供相同的功能。 不同功能进行物理分离实现伸缩 纵向分离(分层后分离):将业务处理流程上的不同部分分离部署,实现系统伸缩性。 横向分离(业务分割后分离):将不同的业务模块分离部署,实现系统伸缩性。 单一功能通过集群规模实现伸缩集群伸缩性又可分为应用服务器集群伸缩性和数据库服务器集群伸缩性。这两种集群由于对数据状态管理的不同,技术实现也有非常大的区别。而数据服务器集群也可分为缓存服务器集群和存储数据服务器集群,这两种集群的伸缩性设计也不大相同。 应用服务器集群的伸缩性设计负载均衡服务器:HTTP请求分发装置。 HTTP重定向负载均衡HTTP重定向服务器是一台普通的应用服务器,其唯一的功能就是根据用户的HTTP请求计算一台真实的Web服务器地址,并将该Web服务器地址写入HTTP重定向响应中(状态码302)返回给用户浏览器。 优点:比较简单。 缺点:浏览器需呀两次请求服务器才能完成一次访问,性能较差;重定向服务器可能成为集群的瓶颈;搜索引擎对302状态码不友好。 DNS域名解析负载均衡每次域名解析请求都会根据负载均衡算法计算一个不同的IP地址返回(DNS服务器中有对一个域名配置多个IP地址)。这样一个域名记录中配置的多个服务器就构成一个集群,并可以实现负载均衡。 优点:将负载均衡的工作交给DNS,省去了网站管理维护负载均衡服务器的麻烦;DNS还支持基于地理位置的域名解析,改善性能。 缺点:DNS的多级解析,可能会存在域名、IP缓存问题;DNS在供应商那边,无法做更多的改善和更强大的管理。 反向代理负载均衡反向代理服务器可以同时提供负载均衡的功能。 管理一组Web服务器,将请求根据负载均衡算法转发到不同Web服务器上。Web服务器处理完成的响应也需要通过代理服务器返回给用户。由于Web服务器不直接对外提供服务,因此Web服务器不需要使用外部IO地址,而反向代理服务器则需要配置双网卡和内部外部两套IP地址。 优点:和反向代理服务器功能集成在一起,部署简单; 缺点:反向代理服务器是所有请求和响应的中转站,其性能可能成为瓶颈。 IP负载均衡关键在于真实物理Web服务器响应数据包如何返回给负载均衡服务器。一种方案是负载均衡服务器在修改目的IP地址的同时修改元地址,将数据包源地址设为自身IP,即源地址转换(SNAT),这样Web服务器的响应会再回到负载均衡服务器;另一种方案是将负载均衡服务器作为真实物理服务器集群的网关服务器,这样所有的响应数据都会到达负载均衡服务器。 数据链路层负载均衡数据链路层负载均衡是指在通信协议的数据链路层修改mac地址进行负载均衡。 这是目前大型网站使用最广的一种负载均衡手段。在Linux平台上最好的负载均衡开源产品是LVS(Linux Virtual Server)。 负载均衡算法负载均衡服务器的实现可以分为两个部分: 1、根据负载均衡算法和Web服务器列表计算得到集群中一台Web服务器的地址。2、将请求数据发送到该地址对应的Web服务器上。 负载均衡算法: 轮询(Round Robin,RR)所有请求被依次分发到每台应用服务器上,即每台服务器需要处理的请求数目都相同,适合于所有服务器硬件都相同的场景。 加权轮询(Weighted Round Robin,WRR)根据应用服务器硬件性能的情况,在轮询的基础上,按照配置的权重将请求分发到每个服务器,高性能的服务器能分配更多请求。 随机(Random)请求被随机分配到各个应用服务器。因为随机算法本身很均衡,在服务器性能不同的情况下也可以加权,所以这种方案简单实用。 最少连接(Least Connections)记录每个应用服务器正在处理的连接数(请求数),将新到的请求分发到最少连接的服务器上。这是最符合负载均衡定义的算法。 源地址散列(Source Hashing)根据请求来源的IP地址进行Hash计算,得到应用服务器,这样来自同一个IP地址的请求总在同一个服务器上处理,该请求的上下文信息可以存储在这台服务器上,在一个会话周期内重复使用,从而实现会话黏滞。 分布式缓存服务器集群的伸缩性设计分布式缓存服务器集群中不同服务器中缓存的数据各不相同,缓存访问请求不可以在缓存服务器集群中的任意一台处理,必须先找到缓存有需要数据的服务器,然后才能访问。 新加入缓存服务器后应使整个缓存服务器集群中已经缓存的数据尽可能还被访问到,这是分布式缓存集群伸缩性设计的最主要目标。 Memcached分布式缓存集群的访问模型 其中路由算法负责根据应用程序输入的缓存数据KEY计算得到应该讲数据写入到Memcached的哪台服务器(写缓存)或应该从哪台服务器读取数据(读缓存)。 Memcached分布式缓存集群的伸缩性挑战在缓存服务器扩容的时候,缓存不能被命中的问题是个很棘手的挑战。 一种办法是在网站访问量最少的时候扩容缓存服务器集群,然后模拟请求的方法逐渐预热缓存。 另一种方法是一致性Hash算法。 分布式缓存的一致性Hash算法一次性Hash算法通过一个叫作一致性Hash环的数据结构实现KEY到缓存服务器的Hash映射。 数据存储服务器集群的伸缩性设计数据存储服务器集群的伸缩性对数据的持久性和可用性提出了更高的要求。 关系数据库集群的伸缩性设计 如上图,数据写操作都在主服务器上,由主服务器将数据同步到集群中其他从服务器,数据读操作以及数据分析等离线操作在从服务器上进行。 同时我们也可以根据业务进行分库,比如将数据量很大的库拆分成多个库,分别存储。 支持数据分片的分布式关系型数据库解决方案产品有Amoeba和Cobar。 由于各类分布式关系型数据库解决方案都是很简陋,限制了关系型数据库的一些功能(比如事务)。所以,我们必须在业务上回避分布式关系型数据库的各种特点:避免事务或利用事务补偿机制代替数据库事务;分解数据访问逻辑避免JOIN操作等。 NoSQL数据库集群的伸缩性设计NoSQL数据库产品都放弃了关系型数据库的两大重要基础:以关系代数为基础的结构化查询语句(SQL)和事务一致性保证(ACID)。而强化了高可用性和可伸缩性。 随需应变:网站的可扩展架构扩展性(Extensibility):对现有系统影响最小的情况下,系统功能可持续扩展或提升的能力。表现在系统基础设施稳定不需要经常变更,应用之间较少依赖和耦合,对需求变更可以敏捷响应。 伸缩性(Scalability):指系统能够通过增加(减少)自身资源规模的方式增强(减少)自己计算处理事务的能力。 构建可扩展的网站架构设计网站可扩展架构的核心思想是模块化,并在此基础上,降低模块间的耦合性,提高模块的复用性。 利用分布式消息队列降低系统耦合性如果模块之间不存在直接调用,那么新模块或者修改模块就对其他模块影响最小,这样系统的可扩展性无疑是更好一些的。 事件驱动架构事件驱动架构(Event Driven Architecture):通过在低耦合的模块之间传输事件消息,以保持模块的松散耦合,并借助事件消息的通信完成模块间合作,典型的EDA架构就是操作系统中常见的生产者消费者模式。具体的实现如:分布式消息队列等。 分布式消息队列分布式消息队列可以看做是将先进先出的数据结构部署到独立的服务器上,应用程序可以通过远程访问接口使用分布式消息队列,进行消息存取操作,进而实现分布式的异步调用。 消息生产者应用程序通过远程访问接口将消息推送给消息队列服务器,消息队列服务器将消息写入本地内存队列后立即返回成功响应给消息生产者。消息队列服务器根据消息订阅列表查找订阅消息的消息消费者应用程序,将消息队列中的消息按照先进先出的原则将消息通过远程通信接口发送给消息消费者程序。 利用分布式服务打造可复用的业务平台使用分布式服务是减低系统耦合性的另一个重要手段。 解决巨无霸系统的方案就是拆分。将模块独立部署,降低系统耦合性。拆分可分为纵向拆分和横向拆分两种。 纵向拆分:将一个大应用拆分为多个小应用。如果新增业务较为独立,那么就直接将其设计部署为一个独立的Web应用系统。 横向拆分:将复用的业务拆分出来,独立部署为分布式服务,新增业务只需要调用这些分布式服务,不需要依赖具体的模块代码,即可快速搭建一个应用系统,而模块内业务逻辑变化的时候,只要接口保持一致就不会影响业务程序和其他模块。 纵向拆分相对比较简单,通过梳理业务,将较少相关的业务剥离,使其成为独立的Web应用。而对横向拆分,不但需要识别可复用的业务,设计服务接口,规范服务依赖关系,还需要完善的分布式服务管理框架。 ps:横向拆分和现在流行的微服务架构很类似。 WebServer与企业级分布式服务服务提供者通过WSDL(Web Services Description language,Web服务描述语言)向注册中心(Service Broker)描述自身提供的服务接口属性,注册中心使用UDDI(Universal Description,Discovery,and Integration,统一描述、发现和集成)发布服务提供者提供的服务,服务请求者从注册中心检索到服务信息后,通过SOAP(Simple Object Access Protocol,简单对象访问协议)和服务提供者通信,使用相关服务。 大型网站分布式服务的需求与特点 负载均衡 失效转移 高效的远程通信 整合异构系统 对应用最少侵入 版本管理 实时监控 分布式服务框架设计可以参见Facebook的Thrift、淘宝的Dubbo等 可扩展的数据结构可以使用NoSQL,做到可扩展数据结构设计。 利用开放平台建设网站生态圈开放平台是网站内部和外部交互的接口,外部需要面对众多的第三方开发者,内部需要面对网站内诸多的业务服务。 API接口 协议转换 安全 审计 路由 流程 固若金汤:网站的安全架构网站应用攻击与防御常见的攻击有XSS攻击、SQL注入攻击、CSRF攻击、Session劫持等手段。 XSS攻击XSS攻击即跨站点脚本攻击(Cross Site Script),指黑客通过篡改网页,注入恶意HTML脚本,在用户浏览网页时,控制用户浏览器进行恶意操作的一种攻击方式。 常见的XSS攻击类型: 反射型,攻击者诱使用户点击一个嵌入恶意脚本的链接,达到攻击的目的。 持久型,黑客提交含有恶意脚本的请求,保存在被攻击的Web站点的数据库中,用户浏览网页时,恶意脚本被包含在正常页面中,达到攻击的目的。 防范手段: 过滤,转换输入 HttpOnly 注入攻击 SQL注入 OS注入 CSRF攻击CSRF(Cross Site Request Forgery,跨站点请求伪造),攻击者通过跨站请求,以合法用户的身份进行非法操作。CSRF的主要手段是利用跨站请求,在用户不知情的情况下,以用户的身份伪造请求。其核心是利用了浏览器Cookie或服务器Session策略,盗取用户身份。 其他攻击和漏洞 Error Code HTML注释 文件上传 路径遍历 Web应用防火墙ModSecurity是一个开源的Web应用防火墙,探测攻击并保护Web应用程序,既可以嵌入到Web应用服务器中,也可以作为一个独立的应用程序启动。 网站安全漏洞扫描信息加密技术及密钥安全管理信息加密技术可分为三种:单项散列加密、对称加密和非对称加密。 单项散列加密如:MD5、SHA等 对称加密如:DES算法、RC算法等 非对称加密如:RSA算法等 密钥安全管理 将密钥和算法放在一台单独的服务器上,其他系统通过调用该系统的服务进行加解密。 加密算法放在应用系统中,密钥则放在独立服务器上。 信息过滤与反垃圾常见的信息过滤与反垃圾手段有以下几种。 文本匹配文本匹配主要解决敏感词过滤的问题。 分类算法比较简单实用的分类算法有贝叶斯分类算法。 黑名单电子商务风险控制风险风控规则引擎统计模型]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[也谈《致加西亚的信》]]></title>
<url>%2F2016%2F05%2F31%2F%E4%B9%9F%E8%B0%88%E3%80%8A%E8%87%B4%E5%8A%A0%E8%A5%BF%E4%BA%9A%E7%9A%84%E4%BF%A1%E3%80%8B%2F</url>
<content type="text"><![CDATA[第一次知道《致加西亚的信》这本书还是在12年的时候,那时候还在南京Glaway工作。记得某次大型会议上,领导推荐了这本书,当然本着和领导“对着干”的精神,压根就没有去看。 而在去年的夏天,在厦大闲逛的时候又看到了它,本着价格便宜的原则,也就把它收入在书柜上,并用一个礼拜陆陆续续看完了。那为什么现在才开始,谈论这书呢?那是因为,15年的5月离开了同步,在反复考虑后选择了ND。说句实在话,在这边其实过得真的很一般,绩效长期是B。虽说有些是客观原因导致的,但自己主观上就没有问题么?最近同事离职了,也考虑过是否也离职,去新的环境学习学习新的东西。 书中所说:“不要为薪水而工作,因为薪水只是工作的一种报偿方式,虽然是最直接的一种,但也是最短视的……..面对微薄的薪水,你应当懂得,雇主支付给你的工作报酬固然是金钱,但你在工作中给予自己的报酬,乃是珍贵的经验,良好的训练,才能的表现和品格的建立.这些东西与金钱相比,其价值要高出千万倍.”这句话,真的是站在企业的老板的角度上看的,在我第一次看到的时候。现在我想的却是:你真正想要的是什么,这份工作能给你什么?如果低薄的薪水却能给你带来能力上的锻炼,而这个锻炼出来的能力可能会让你以后的工作生活受益无穷,那么你就去认真对待它;而如果这份工作已经不能给你什么了,或者说给你的不是你所期待的,那么就离开它。说句很自私的话,因为不是你想要的工作,你肯定不开心,所以你的工作效率也会很低,抱着抵触的情绪,你也学不了东西,企业还得为你的低效率买单。这样对大家都不好,还不如早日离开! “每天多做一点”。说句很实在的话,已经很久没有这种心态在工作了。从刚毕业时候的每天留下来加班到现在的准时下班,真的真的我已经感觉到自己在失去以前的我的。从认识XLM以来,她的负责任,她的努力一直吸引着我。从她身上看到了好久没有出现的自己,也感觉到自己的迷失。不得不说,最近几年自己做得还真的真的很不够。俗话说,知错能改是好孩子。真好,明天也是六一节了,那就让自己做个“人见人爱”的好孩子吧! 最后,再给自己些时间,好好准备。如果真的要离开,也要以一个全新的我离开!对于ND,对于团队,还是很感谢!明天开始,好好加油,努力工作吧~以一个新的心态去迎接一切!]]></content>
<categories>
<category>未分类</category>
</categories>
<tags>
<tag>生活</tag>
</tags>
</entry>
<entry>
<title><![CDATA[读《大型网站技术架构-核心原理与案例分析》笔记(一)]]></title>
<url>%2F2016%2F05%2F30%2F%E8%AF%BB%E3%80%8A%E5%A4%A7%E5%9E%8B%E7%BD%91%E7%AB%99%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84-%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86%E4%B8%8E%E6%A1%88%E4%BE%8B%E5%88%86%E6%9E%90%E3%80%8B%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%80%EF%BC%89%2F</url>
<content type="text"><![CDATA[大型网站架构演化大型网站软件系统的特点: 高并发,大流量 高可用 海量数据 用户分布广泛,网络情况复杂 安全环境恶劣 需求快速变更,发布频繁 渐进式发展 大型网站架构演化历程的思维导图: 大型网站架构演化的价值观: 大型网站结构技术的核心价值是随着网站所需灵活应对 驱动大型网站技术发展的主要力量是网站的业务发展 不要一味追随大公司的解决方案 不要为了技术而技术 不要企图用技术解决所有问题 大型网站架构模式分层分层是企业应用系统中最常用的一种架构模式,将系统在横向微度上切分成几个部分,每个部分负责一部分相对比较单一的职责,通过上层对下层的依赖和调用组成一个完整的系统。在大型网站中也采用分层结构,将网站软件系统分为应用层、服务层和数据层。这三层可以部署在同一台服务器上,也可以在业务发展后部署在不同的服务器上。 应用层:负责具体业务和视图展示 服务层:为应用层提供服务支持 数据层:提供数据存储访问服务 大的分层内部还可以继续分层,如应用层可以再细分为视图层和业务逻辑层;服务层可以细分为数据接口层(适配各种输入和输出的数据格式)和逻辑处理层。 切割切割是在纵向方面对软件进行切分。 将这些不同的功能和服务分割开来,包装成高内聚低耦合的模块单元,一方面有利于软件的开发和维护;另一方面,便于不同模块的分布式部署,提高网站的并发处理能力和功能扩展能力。 疑问:要先进行分层,再进行分割呢?还是先进行分割,再进行分层呢? 分布式分布式是将分层和分割后的模块独立部署。 好处: 可以使用更多的计算机资源 能够处理更多的并发和数据 … 问题: 分布式服务器见通信的网络开销 服务器宕机可能会影响网站可用性 数据一致性和分布式事务比较难处理 开发管理维护困难 常用的分布式方案: 分布式应用和服务 分布式静态资源 分布式数据和存储 分布式计算 分布式配置 分布式锁 分布式文件系统 集群多台服务器部署相同应用构成一个集群,通过负载均衡设备共同对外提供服务。 缓存缓存就是将数据存放在距离计算最近的位置以加快处理速度。 大型网站架构设计在很多方面都使用了缓存设计: CDN缓存 反向代理 本地缓存 分布式缓存 使用缓存的两个前提条件:①数据访问热点不均衡,某些数据会被更频繁地访问,这些数据应该存在缓存中。②数据在某个时间段内有效,不会很快过期,否则缓存就会因为时效而产生脏读,影响结果的正确性。 异步异步的目的之一也是为了解耦。业务之间的消息传递不是同步调用,而是将一个业务操作分布成多个阶段,每个阶段之间通过共享数据的方式异步执行进行协作。 异步架构的典型就是生产者消费者模式,两者之间不存在直接调用。 分布式异步消息队列的特性: 提供系统可用性 加快网站响应速度 消除并发访问高峰 冗余为了7*24小时的服务连续运行,数据不丢失,需要在一定程度的服务器冗余运行,数据冗余备份,这样当某台服务器宕机时,可以将服务和数据转移到其他机器上。 可以通过冷备份、热备份、灾备数据中心冗余数据库。 自动化大型网站的自动化架构设计主要集中在发布运维方面。自动化发布过程包括: 自动化代码管理:git或svn的分支管理、版本控制等 自动化测试:自动化测试用例测试 自动化安全监测:静态代码检查等 自动化部署:根据不同的环境部署代码 自动化监控:宕机、存储空间、程序bug、突然爆发的访问等 自动化报警:发现异常自动报警 自动化失效转移:将失效的服务从集群中移除 自动化失效恢复:故障消除后,重新启动服务,同步数据等 自动化降级:通过拒绝部分请求及关闭部分不重要的服务将系统负载减低至一个安全水平 自动化分配资源:将空闲资源分配给重要的服务 安全通过各种手段进行安全保障。 大型网站核心架构要素软件架构除了关注当前需求外,还要关注性能、可用性、伸缩性、扩展性和安全这5个架构要素。架构设计过程要平衡这5个要素之间的关系以实现需求和架构目标,也可以通过考察这些架构要素来衡量一个软件架构设计的优劣,判断其是否满足期望。 性能网站架构设计的一个主要方面,任何软件架构设计方案都必须考虑可能会带来的性能问题。(衡量性能的指标、提高性能的方案)。 可用性高可用的设计目标就是服务器宕机的时候,服务或者应用依然可用。主要的解决手段是冗余。 对于应用服务器:实现的难点在于session共享。本地服务器不保存session,专门的服务器保存session,所有服务器共享session。 对于存储服务器:实时备份数据。事务的一致性是难点。CAP原理:一个提高服务的存储系统无法同时满足以下三个条件,一致性(Consistency) 、数据可用性(Avalibility) 、分区耐受性(Patition Tolerance,系统具有跨网络分区的伸缩性),通常只能同时满足两个条件。 伸缩性通过不断向集群中加入服务器的手段来缓解不断上升的用户并发访问压力和不断增长的数据存储要求。衡量架构伸缩性的主要标准就是是否可以用多台服务器构建集群,是否容易向集群中添加新的服务器。 对于应用服务器集群:服务器对等设计,使用负载均衡设备。 对于缓存服务器集群:新加入服务器后,可能导致缓存路由失效。通过改进缓存路由算法,可实现加入新的缓存服务器后对原有缓存的影响尽量减少。 对于关系型数据库集群:在数据库之外实现,通过路由分区等手段将部署有多个数据库的服务器组成一个集群,也可以考虑分表、分库。 扩展性网站的扩展性架构直接关注网站的功能需求。衡量标准就是在网站添加新的业务产品时,是否可以实现对向右产品透明无影响,不需要任何改动或者很少改动既有业务功能就可以上线新产品。不同产品之间是否很少耦合,一个产品改动对其他产品无影响,其他产品和功能不需要牵连进行改动。 主要手段:事件驱动架构和分布式服务。 事件驱动架构:通常利用消息队列实现,将用户请求和其他业务事件构造成消息发布到消息队列,消息的处理者作为消费者从消息队列中获取消息进行处理。 分布式服务:将业务和可复用服务分离开来,通过分布式服务框架调用。 安全性衡量网站安全架构的标准就是针对现存和潜在的各种攻击与窃密手段,是否有可靠的一堆策略。]]></content>
<categories>
<category>读书笔记</category>
</categories>
<tags>
<tag>架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[MongoDB学习整理]]></title>
<url>%2F2016%2F05%2F26%2FMongoDB%E5%AD%A6%E4%B9%A0%E6%95%B4%E7%90%86%2F</url>
<content type="text"><![CDATA[MongoDB概念 database:数据库。一个MongoDB实例可以容纳多个数据库。 collection:集合。类似于关系型数据库中的表。 document:文档。类似于关系型数据库中的行。 field:域。类似于关系型数据库中的字段。 常用的命令 show dbs:显示所有的数据库 db:显示当前数据库对象 use XXX:连接到指定的数据库,XXX为数据库名 创建与删除使用 use DATABASE_NAME命令可以创建数据库。如果数据库不存在就创建,如果存在就切到该数据库。刚创建的数据库在用show dbs命令时是不显示出来的,需要插入一条数据后使用show dbs命令才会显示。 使用db.dropDatabase()命令删除当前数据库。 增删改查新增使用insert()或save()方法向集合中插入文档。语法:db.COLLECTION_NAME.insert(document) db.col.insert({ "name" : "ZZ", "name_cn" : "造字法", "order" : 0, "update_time" : 1463404597556, "delete_flag" : false }) 使用insert()方法后,MongoDB会自动给你添加一个_id字段,并自动给予一个值。使用save()方法,如果不指定_id字段save()方法就类似于insert()方法,如果指定_id字段,则会更新该_id的数据(其实就是修改)。 删除使用remove()方法删除集合中的文档。语法: db.COLLECTION_NAME.remove( < query语句 >, { justOne: < boolean >, writeConcern: < document > } ) query语句:删除文档的条件(可选)。 justOne : (可选)如果设为 true 或 1,则只删除一个文档。 writeConcern :(可选)抛出异常的级别。 示例: db.col.remove({"name":"ZZ"}) 修改使用update()或save()方法向集合中插入文档。update()语法: db.COLLECTION_NAME.update( < query查询条件 >, < update语句 >, { upsert: < boolean >, multi: < boolean >, writeConcern: < document > } ) query语句:更新的查询条件,类似于SQL语句的where。 update语句:要更新的对象以及更新操作符(如:$),类似于SQL语句的set。 upsert:可选,这个参数的意思是,如果不存在update的记录,是否插入新对象;true为插入,默认是false,不插入。 multi:可选,mongodb默认是false,只更新找到的第一条记录;如果这个参数为true,就把按条件查出来多条记录全部更新。 writeConcern:可选,抛出异常的级别。 示例: db.col.update({"name":"ZZ"},{$set:{"name_cn":"造字"}}) db.col.update({"name":"ZZ"},{$set:{"name_cn":"造字"}},{multi:true}) save()语法: db.COLLECTION_NAME.save( < document >, { writeConcern: < document > } ) document:文档数据。 writeConcern:可选,抛出异常的级别。 查询使用find()方法对集合进行查询。 语法: db.COLLECTION_NAME.find(query, projection) query:查询的条件语句 projection:映射关系语句 示例: db.dimensions.find() db.dimensions.find({"name":"ZZ"}) 条件比较操作符 操作符 语法格式 等于 { < field >: < value > } $eq(等于) { < field >: { $eq: < value > } } $gt(大于) { < field >: { $gt: < value > } } $gte(大于等于) { < field >: { $gte: < value > } } $lt(小于) { < field >: { $lt: < value > } } $lte(小于等于) { < field >: { $lte: < value > } } $ne(不等于) { < field >: { $ne: < value > } } $in { < field >: { $in: [< value1 >, < value2 >, … < valueN > ] } } $nin { < field >: { $nin: [< value1 >, < value2 >, … < valueN > ] } } 逻辑操作符 操作符 语法格式 AND {key1:value1, key2:value2} $and(AND操作) { $and: [ { < expression1 > }, { < expression2 > } , … , { < expressionN > } ] } $or(OR操作) { $or: [ { < expression1 > }, { < expression2 > } , … , { < expressionN > } ] } $not(NOT操作) { field: { $not: { < operator-expression > } } } $nor(NOT OR操作) { $nor: [ { < expression1 > }, { < expression2 > }, … { < expressionN > } ] } 示例代码: db.t_resources.find({"create_time":1463233651537,"update_time":1463416983501}) db.inventory.find( { $and: [ { price: { $ne: 1.99 } }, { price: { $exists: true } } ] } ) db.inventory.find( { $or: [ { quantity: { $lt: 20 } }, { price: 10 } ] } ) db.inventory.find( { price: { $not: { $gt: 1.99 } } } ) db.inventory.find( { $nor: [ { price: 1.99 }, { sale: true } ] } ) 其他常用的操作符 操作符 语法格式 说明 $exists { field: { $exists: < boolean > } } 是否存在 $type { field: { $type: < BSON type number > or < String alias > } } 基于BSON类型来检索集合中匹配的数据类型,并返回结果 $all { < field >: { $all: [ < value1 > , < value2 > … ] } } 数组中的值全部包含 示例代码: db.articles.find( { tags: { $all: [ [ "ssl", "security" ] ] } } ) db.addressBook.find( { "zipCode" : { $type : 2 } } ) db.records.find( { b: { $exists: false } } ) 排序使用sort()对数据进行排序。sort()方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而-1是用于降序排列。 语法:db.COLLECTION_NAME.find().sort({field: value}) 示例: db.dimensions.find().sort({'name_cn':1,'update_time':-1}) limit和skip方法如果要读取指定数量的数据记录,可以使用limit()方法。 语法:db.COLLECTION_NAME.find().limit(NUMBER) 可以使用skip()方法跳过一定数量来读取数据。 语法:db.COLLECTION_NAME.find().limit(NUMBER).skip(NUMBER) 所以,skip()方法和limit()方法结合可以用来分页。 示例: db.dimensions.find().skip(10).limit(10) 索引创建索引来提升查询性能是很常见的做法。 在MySQL中explain显示了MySQL如何使用索引来处理select语句以及连接表。同理,Mongo中的EXPLAIN用来描述查询路径,通过判断查询使用了哪个索引来帮助开发者诊断慢查询。如:db.numbers.find( {num: {"$gt": 199995 }} ).explain() 创建索引的示例: db.numbers.ensureIndex({num: 1}) 或者 db.numbers.createIndex({num: 1}) 在MongoDB v3.0.0以后,建议使用的是createIndex()方法,ensureIndex()方法是不赞成在v3.0.0以后的Mongo版本中使用的。 语法:db.collection.createIndex( <key and index type specification>, <options> ) 语法中 Key 值为你要创建的索引字段,1为指定按升序创建索引,如果你想按降序来创建索引指定为-1即可。 下表是一些比较常用的创建索引时候的附加参数: 操作符 类型 说明 background Boolean 建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加 “background” 可选参数。 “background” 默认值为false。 unique Boolean 建立的索引是否唯一。指定为true创建唯一索引。默认值为false. name string 索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。 sparse Boolean 对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 false. expireAfterSeconds integer 指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间. weights document 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。 示例代码: db.numbers.createIndex({num: 1}, {background: true}) 聚合MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。 Aggregation Pipeline语法:db.collection.aggregate(pipeline, options),其中pipeline是一个管道数组,options是选项。 我们以一张官方文档上的图来说明一下聚合操作: 上图中我们看到了\$match,\$group。这两个是什么呢? 答案就在以下常用的piple操作列表。 $project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。 $limit:用来限制MongoDB聚合管道返回的文档数。 $skip:在聚合管道中跳过指定数量的文档,并返回余下的文档。 $unwind:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。 $group:将集合中的文档分组,可用于统计结果(参数与group方法不同哦~)。 $sort:将输入文档排序后输出。 $geoNear:输出接近某一地理位置的有序文档。 $lookup:将Left Outer Join引入到MongoDB。 \$match:用于过滤数据,只输出符合条件的文档。$match使用MongoDB的标准查询操作。 以上各操作的用法详见:MongoDB文档 distinct方法MongoDB的distinct命令是获取特定字段中不同值列表的最简单工具。该命令既适用于单键,也适用于数组键。distinct默认覆盖整个集合,但也可以通过查询选择器进行约束。 语法:db.collection.distinct(field, query) 示例: db.products.distinct("tags") db.inventory.distinct( "item.sku", { dept: "A" } ) group方法gourp方法的作用与SQL中的 “SELECT <…> GROUP BY <…>”的作用一样。 语法:db.collection.group({ key, reduce, initial [, keyf] [, cond] [, finalize] }) 示例: results = db.reviews.group({ key:{ user_id:true }, initial:{ reviews:0, votes:0 }, reduce:function(doc,aggregator){ aggregator.reviews +=1; aggregator.votes += doc.votes; }, finalize:function(doc){ doc.average_votes = doc.votes / doc.reviews; } }) group方法的各个参数说明如下: key,描述分组字段的文档。如上例,我们根据user_id分组,所以设置user_id:true。 keyf,这是一个JavaScript函数,应用于文档之上,为该文档生成一个键,当用于分组的键需要计算时,这个函数非常有用。 initial,作为聚合结果初始值的文档。reduce函数第一次运行时,该初始文档会作为聚合器的第一个值,通常会包含所有要聚合的键。 reduce,用于执行聚合的JavaScript函数。该函数接受两个参数:正被迭代的当前文档和用于存储聚合结果的聚合器文档。聚合器的初始值就是初始文档。reduce函数并不返回任何内容,它只不过是修改聚合器对象。 cond,过滤要聚合文档的查询选择器。如果不希望分组操作处理整个集合,就必须提供一个查询选择器。 finalize,在返回结果集之前应用于每个结果文档的JavaScript函数。该函数支持对分组操作的结果进行后置处理。 Map-ReduceMap-Reduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE)。MongoDB提供的Map-Reduce非常灵活,对于大规模数据分析也相当实用。 语法: db.collection.mapReduce( <map>, <reduce>, { out: <collection>, query: <document>, sort: <document>, limit: <number>, finalize: <function>, scope: <document>, jsMode: <boolean>, verbose: <boolean>, bypassDocumentValidation: <boolean> } ) 同样的,来一张官方的图: 主要的参数说明如下; map,应用于每个文档之上的JavaScript函数。该函数必须调用emit()来选择要聚合的键和值。 reduce,一个JavaScript函数,接受一个键和一个值列表。该函数对返回值的结构有严格要求,必须总是与values数组所提供的结构一致。reduce函数通常会迭代一个值的列表,在此过程中对其进行聚合。 query,用于过滤映射处理的集合的查询选择器。该参数的作用与group的cond参数相同。 sort,对于查询的排序。 limit,一个整数,指定了查询和排序的条数。 out,该参数决定了如何返回输出内容。要将所有输出作为命令本身的结果,传入{inline: 1}。请注意,这仅适用于结果集符合16 MB返回限制的情况。 参考: http://www.runoob.com/mongodb/mongodb-tutorial.html 《MongoDB 实战》 《MongoDB官方文档》]]></content>
<categories>
<category>MongoDB</category>
</categories>
<tags>
<tag>MongoDB</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ES6入门-Map]]></title>
<url>%2F2016%2F05%2F25%2FES6%E5%85%A5%E9%97%A8-Map%2F</url>
<content type="text"><![CDATA[ES6为我们提供了Map数据结构。它是一个”value-value”的对应。如果需要“键值对”的数据结构,Map是一个很合适的数据结构。 Map用法简单用法var map = new Map(); map.set('name','zxguan'); map.set('age',27); map.get('name'); map.get('age'); 通过console.log(map);打印出的结果为:Map { 'name' => 'zxguan', 'age' => 27 } 数组作为构造参数Map接受一个数组作为构造函数的参数。该数组的成员是一个个表示键值对的数组。 var map = new Map([['name','zxguan'],['age','27']]); console.log(map); 结果一样也是:Map { 'name' => 'zxguan', 'age' => 27 } 以上的代码实际上等同于: var map = new Map(); [['name','zxguan'],['age','27']].forEach(([key,value]) => map.set(key,value)); 迭代有以下方法来遍历一个Map: keys()返回所有的键名 values()返回所有的值 entries()返回所有的成员实体 forEach()遍历Map所有的成员 代码: var map = new Map([['name','zxguan'],['age','27']]); var keys = map.keys(); //遍历Key for(let key of keys){ console.log(key); } var values = map.values(); //遍历Value for(let value of values){ console.log(value); } var entries = map.entries();//遍历实体 for(let [key,value] of entries){ console.log(key,value); } map.forEach((value,index)=>{//value为值,index实际上就是key console.log("key:"+index + " value:"+value); }); var reporter = { report: function(key, value) { console.log("Key: %s, Value: %s", key, value); } }; map.forEach(function(value, key, map) { //forEach的第二个方法用来绑定this. this.report(key, value); }, reporter); 常用的方法 map.clear():clear方法清除所有成员,没有返回值。 map.delete(key):删除键为key的成员。删除成功,返回true;删除失败,返回false。 map.entries():返回所有的成员实体。 forEach(callback):遍历Map所有的成员。 get(key):读取key对应的键值,如果找不到key,返回undefined。 has(key):返回一个布尔值,表示某个键是否在Map数据结构中。 keys():返回所有的键名。 set(key,value):设置key所对应的键值,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新建一个成员。 size属性:属性返回Map结构的成员总数。 values():返回所有的值。 值得注意的是,只有对同一个对象的引用,Map才会视为同一个key。如: var map = new Map(); map.set(['KEY'],5); var value = map.get(['KEY']); console.log(value); //undefined var key1 = ['KEY']; var key2 = ['KEY']; var map = new Map(); map.set(key1,'AAA'); map.set(key2,'BBB'); console.log(map.get(key1)); //AAA console.log(map.get(key2)); //BBB 如果要将Map转换为数组,可以使用扩展运算符(…) var myMap = new Map().set(true, 7).set({foo: 3}, ['abc']); console.log([...myMap]); 以下是一些常用的转换工具方法: //Map转Object对象 function map2Obj(map) { let obj = {}; for (let [key, value] of map) { obj[key] = value; } return obj; } //Object对象转Map function obj2Map(obj) { let map = new Map(); for (let key of Object.keys(obj)) { map.set(key, obj[key]); } return map; } //Map转为Json function map2Json(map) { let keys = map.keys(); let isAllKeyStr = true; //是否key全部为字符串 for(let key of keys){ if(!((typeof key=='string')&&(key.constructor==String))){ isAllKeyStr = false; break; } } if(isAllKeyStr){ return JSON.stringify(map2Obj(map)); }else{ return JSON.stringify([...map]); } } //Json转为Map function json2Map(json) { let _json = JSON.parse(json); if(Array.isArray(_json)){ return new Map(JSON.parse(json)); }else{ return obj2Map(JSON.parse(json)); } } WeakMapWeakMap与Map大体上是类似的,只有在以下方面有所区别: 只接受Object(对象)作为键名(null除外),不接受基本类型作为键值。 WeakMap没有keys()、values()、entries()方法,也没有size属性 WeakMap没有clear()方法]]></content>
<categories>
<category>JavaScript</category>
</categories>
<tags>
<tag>ES6</tag>
</tags>
</entry>
<entry>
<title><![CDATA[一个工作中用得到的RESTful API规范]]></title>
<url>%2F2016%2F05%2F17%2F%E4%B8%80%E4%B8%AA%E5%B7%A5%E4%BD%9C%E4%B8%AD%E7%94%A8%E5%BE%97%E5%88%B0%E7%9A%84RESTful%20API%E8%A7%84%E8%8C%83%2F</url>
<content type="text"><![CDATA[RESTful API已经非常成熟,也得到了大家的认可。本文主要讲的是在工作中遇到的一个比较被认同的“规范”,总结下自己的经验。 按照Richardson Maturity Mode对REST评价的模型,规范基于level2来设计。 资源路径路径,API的具体地址。在REST中,每个地址都代表一个具体的资源(Resource)。所以就有了以下的约定: 路径仅表示资源的路径(位置),以及一些特殊的actions操作。 以复数(名词)进行命名资源,不管返回单个或者多个资源。 使用小写字母、数字以及下划线(“_”)。(下划线是为了区分多个单词,如token_access)。 资源的路径从父到子依次如:/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}/{sub_resource_property}。 使用?来进行资源的过滤、搜索以及分页等。 使用版本号,且版本号在资源路径之前。 优先使用内容协商来区分表述格式,而不是使用后缀来区分表述格式。 应该放在一个专用的域名下,如:http://api.webfuse.cn。 建议使用SSL。 综上,一个API路径可能会是 https://api.webfuse.cn/v0.1/{resource}/{resource_id}/{sub_resource}/{sub_resource_id}/{sub_resource_property} https://api.webfuse.cn/v0.1/{resource}?limit=10&offset=10 https://api.webfuse.cn/v0.1/{resource}?page=1&page_size=10 https://api.webfuse.cn/v0.1/{resource}?sortby=name&order=asc 操作用HTTP动词(方法)表示对资源的具体操作。常用的HTTP动词有: GET:从服务器取出资源(一项或多项)。 POST:在服务器新建一个资源。 PUT:在服务器更新资源(客户端提供改变后的完整资源,全部更新)。 PATCH:在服务器更新资源(客户端提供改变的属性,部分更新)。 DELETE:从服务器删除资源。 HEAD:获取资源的元数据。 OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。 所以, GET https://api.webfuse.cn/v0.1/users 获得用户列表 GET https://api.webfuse.cn/v0.1/users/{id} 获得指定的用户 POST https://api.webfuse.cn/v0.1/users 创建一个用户 PUT https://api.webfuse.cn/v0.1/users/{id} 更新指定的用户(提供该用户的全部信息) PATCH https://api.webfuse.cn/v0.1/users/{id} 更新指定的用户(提供该用户的部分信息) DELETE https://api.webfuse.cn/v0.1/users/{id} 删除指定的用户 数据数据是对资源的具体描述,分为请求数据和返回数据。约定如下: 查询,过滤条件使用query string。 Content body 仅仅用来传输数据。 通过Content-Type指定请求与返回的数据格式。其中请求数据还要指定Accept。 数据应该拿来就能用,不应该还要进行转换操作。 使用ISO-8601格式表达时间字段,例如: 2014-04-05T14:30Z。 采用UTF-8编码。 返回的数据应该尽量简单,响应状态应该包含在响应头中。 使用小写字母、数字以及下划线(“_”)描述字段,不使用大写描述字段。 建议资源使用UUID最为唯一标识。同时建议命名为id。 属性和字符串值必须使用双引号””。 建议对每个字段设置默认值(数组型可设置为[],字符串型可设置为””,数值可设置为0,对象可设置为{}),这一条是为了方便前端/客户端进行判断字段存不存在操作。 POST操作应该返回新建的资源;PUT/PATCH操作返回更新后的完整的资源;DELETE返回一个空文档;GET返回资源数组或当个资源; 为了方便以后的扩展兼容,如果返回的是数组,强烈建议用一个包含如items属性的对象进行包裹。如:{"items":[{},{}]} 所以,一个请求以及返回的数据结构可能如: POST https://api.webfuse.cn/v0.1/users Request headers: Accept: application/json Content-Type: application/json;charset=UTF-8 body: { "user_name": "HingKwan", "id":"550e8400-e29b-41d4-a716-446655440000" } Response status: 201 Created headers: Content-Type: application/json;charset=UTF-8 body: { "user_name": "HingKwan", "id":"550e8400-e29b-41d4-a716-446655440000" } 安全调用限制为了避免请求泛滥,给API设置速度限制很重要。入速度设置之后,可以在HTTP返回头上对返回的信息进行说明,下面是几个必须的返回头(依照twitter的命名规则): X-Rate-Limit-Limit :当前时间段允许的并发请求数 X-Rate-Limit-Remaining:当前时间段保留的请求数。 X-Rate-Limit-Reset:当前时间段剩余秒数 授权校验RESTful API是无状态的也就是说用户请求的鉴权和cookie以及session无关,每一次请求都应该包含鉴权证明。 可以使用http请求头Authorization设置授权码; 必须使用User-Agent设置客户端信息, 无User-Agent请求头的请求应该被拒绝访问。具体的授权可以采用OAuth2,或者自己定义并实现相关的授权验证机制(基于token)。 常见的请求Header为: User-Agent: Data-Server-Client Authorzation: Bearer 383w9JKJLJFw4ewpie2wefmjdlJLDJF 或 User-Agent: Data-Server-Client Authorzation: MAC id="2YotenFZFEjrizCsicMWpBA",nonce="14187532423423:adfadsfa",mac="SfadfaFdafadfdasFFyu" 以上两个代码只是列出来可以采用的格式,具体的实现可以根据各项目具体规划。 错误当API返回非2XX的HTTP响应时,应该采用统一的响应信息,格式如: HTTP/1.1 400 Bad Request Content-Type: application/json;charset=UTF-8 { "code":"INVALID_ARGUMENT", "message":"{error message}", "request_id":"", "host_id":"{server identity}", "server_time":"2016-05-17T22:08:00Z" "document":"https://developer.webfuse.cn/v0.1/errors/400" } HTTP Header Code:符合HTTP响应的状态码。详细见以下的“状态码”节。 code:用来表示某类错误,比如缺少参数等。是对HTTP Header Code的补充,开发团队可以根据自己的需要自己定义。 message:错误信息的摘要,应该是对用户处理错误有用的信息。 request_id:请求的id,方便开发定位发生错误的请求(可选)。 host_id:服务器实例的ID,方便开发定位是哪台服务器实例发生了错误(可选)。 server_time:发生错误时候的服务器时间(可选)。 document:错误解决的文档(方便前端展示,可选)。 code的定义约定: 采用大写字母命名,字母与字母之间用下划线(”_”)隔开。 采用{biz_name}/{err_code}的命名结构。其中biz_name为业务名称的缩写。 code应该用来定义错误类别,而非定义具体的某个错误。 状态码为每个请求返回适当的状态码,状态码可以参考HTTP的状态码。 常用的状态码有: 200 ok - 成功返回状态,对应,GET,PUT,PATCH,DELETE。 201 created - 成功创建。 304 not modified - HTTP缓存有效。 400 bad request - 请求格式错误。 401 unauthorized - 未授权。 403 forbidden - 鉴权成功,但是该用户没有权限。 404 not found - 请求的资源不存在 405 method not allowed - 该http方法不被允许。 410 gone - 这个url对应的资源现在不可用。 415 unsupported media type - 请求类型错误。 422 unprocessable entity - 校验错误时用。 429 too many request - 请求过多。 500 internal server error - 通用错误响应。 503 service unavailable - 服务当前无法处理请求。 参考: 公司内制定的规范 http://restful-api-design.readthedocs.io/en/latest/ http://arccode.net/2015/02/26/RESTful%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/]]></content>
<categories>
<category>架构</category>
</categories>
<tags>
<tag>RESTful</tag>
<tag>规范</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Node.js初入门]]></title>
<url>%2F2016%2F05%2F10%2FNode.js%E5%88%9D%E5%85%A5%E9%97%A8%2F</url>
<content type="text"><![CDATA[Node.js是运行在服务端的JavaScript。它是基于Google的V8引擎。它是事件驱动的、非阻塞I/O模型的、轻量、高效的。 很概念是不是?那就用一句简单的话:它现在很流行! 安装配置Node.js 从Node.js的官网中下载对应的安装程序,按照提示安装。 配置环境变量PATH(最新版本已经帮你配置好了) 运行node –version 创建Node.js应用第一个Node.js程序第一个程序一般来说就是hello world了。nodejs的官网正好给我们提供了样例hello world,而且还是server类型的。 创建一个文件夹,如node-example之类的。 创建文件first-node.js 从nodejs.org上复制相关代码 const http = require('http'); const hostname = '127.0.0.1'; const port = 3000; const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World\n'); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); }); 粘贴到first-node.js中。 在命令行中运行node first-node.js。 在浏览器中输入http://localhost:3000/ 关于NPMNPM是随同NodeJS一起安装的包管理工具。你可以通过npm命令从NPM服务器上下载第三方包、程序到本地使用,也可以把自己写的包、程序上传到NPM服务器给他人使用。常用的命令: npm init 引导你创建一个package.json文件,包括名称、版本、作者这些信息等 npm install --help 帮助信息 npm install 安装package.json中的依赖包 npm install <name> 安装nodejs的依赖包 npm install <name> -g 将nodejs的依赖包安装到全局 npm install <name> --save 在安装nodejs依赖包的同时把信息写入到package.json的dependencies中 npm install <name> --save-dev 在安装nodejs依赖包的同时把信息写入到package.json的devDependencies中 npm remove <name> 移除 npm update <name>更新 package.json文件包含项目详细信息的描述。具体可以参考阮一峰的《package.json文件》一文 工程目录NodeJS并没有官方要求目录结构应该是怎么样的(我是没有找到,如果有找到的话烦请告知)。但很多第三方的包的程序目录至少看上去像下面这样的。 bin 存放启动项目的脚本文件,默认www node_modules 存放所有的项目依赖库 src 项目的代码 tests 测试代码 index.js 可以是项目的入口 package.json 了解APINodeJs给我们提供了丰富API。受《七天学会NodeJS》启发,把API分为以下几类,方便查看学习。 文件操作类 Buffer(块):用来创建一个专门存放二进制数据的缓存区 Stream(流):一个抽象接口,Node 中有很多对象实现了这个接口。存在4种流类型,分别是Readable(可读操作)、Writable(可写操作)、Duplex(可读可写操作)和Transform(操作被写入数据,然后读出结果)。同时,所有的Stream对象都是EventEmitter的实例。 File System(文件系统):该模块提供本地文件的读写能力,基本上是POSIX文件操作命令的简单包装。这个模块几乎对所有操作提供异步和同步两种操作方式。 Path(路径):该模块包含一套用于处理和转换文件路径的工具集。 zlib:该模块提供了数据压缩和解压的功能。 网络操作类 HTTP:提供HTTP服务器或客户端功能。 HTTPS:处理HTTPS协议相关。 URL:该模块用于生成和解析URL。 Query Strings:模块用于实现URL参数字符串与参数对象的互相转换。 Net:模块封装了异步网络功能,提供了一些方法来创建服务器和客户端(称之为流)。 DNS(域名服务):处理DNS相关。 TLS/SSL:模块使用 OpenSSL 来提供传输层安全协议(Transport Layer Security)和/或安全套接层(Secure Socket Layer):加密过的流通讯。 UDP/Datagram:数据报套接字。 进程管理类 Process(进程):process对象是Node的一个全局对象,提供当前Node进程的信息。它可以在脚本的任意位置使用,不必通过require命令加载。该对象部署了EventEmitter接口。 Child Process(子进程):该模块用于新建子进程。子进程的运行结果储存在系统缓存之中(最大200KB),等到子进程运行结束以后,主进程再用回调函数读取子进程的运行结果。 Cluster(集群):集群模块允许你方便地创建一个共享服务器端口的进程网络。 基本 Crypto(加密):提供一些加密的方法。 Errors:提供Error相关。 Events(事件):Events模块是Node对“发布/订阅”模式(publish/subscribe)的实现。一个对象通过这个模块,向另一个对象传递消息。该模块通过EventEmitter属性,提供了一个构造函数。 Globals(全局对象):它及其所有属性都可以在程序的任何地方访问,即全局变量。 Modules(模块):模块是Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个Node.js文件就是一个模块,这个文件可能是JavaScript代码、JSON或者编译过的C/C++扩展。 Utilities(实用工具): 是一个Node.js 核心模块,提供常用函数的集合,用于弥补核心JavaScript 的功能 过于精简的不足。 Punycode(编码系统):处理编码。 Timers(定时器):所有的定时器函数都是全局变量。 Domain(域):Domain提供了一种方式,即以一个单一的组的形式来处理多个不同的IO操作。如果任何一个注册到domain的事件触发器或回调触发了一个‘error’事件,或者抛出一个错误,那么domain对象将会被通知到。而不是直接让这个错误的上下文从`process.on(’uncaughtException’)’处理程序中丢失掉,也不会致使程序因为这个错误伴随着错误码立即退出。 Readline(逐行读取):Readline程序允许逐行读取一个流内容。 OS(系统):提供一些基本的操作系统相关函数。 String Decoder(字符串解码器):这个模块将一个 Buffer 解码成一个字符串。 其他 Assertion Testing(断言):用于编写程序的单元测试用例,通过require(‘assert’)调用。 C/C++ Addons(C/C++ 扩展):Addons插件就是动态连接库。它类似胶水,将c、c++和Node粘贴起来。 Command Line Options:命令行的选项。 Console(控制台):类似于大部分 Web 浏览器提供的 console 对象函数。 Debugger(调试器):以debug参数启动Node。通过脚本的源代码中放置 debugger; 语句,您便可启用一个断点。 REPL(命令行交互):一个 Read-Eval-Print-Loop(REPL,读取-执行-输出循环)既可用于独立程序也可很容易地被集成到其它程序中。REPL 提供了一种交互地执行 JavaScript 并查看输出的方式。它可以被用作调试、测试或仅仅尝试某些东西。 TTY(终端):模块提供了 tty.ReadStream 和 tty.WriteStream 类。在大部分情况下,您都不会需要直接使用此模块。 V8:V8相关。 VM(虚拟机):虚拟机相关。 参考: 七天学会NodeJS Node.js API中文版 JavaScript 标准参考教程(alpha)]]></content>
<categories>
<category>NodeJs</category>
</categories>
<tags>
<tag>NodeJs</tag>
</tags>
</entry>
<entry>
<title><![CDATA[被亚马逊AWS扣费了怎么办?]]></title>
<url>%2F2016%2F05%2F08%2F%E8%A2%AB%E4%BA%9A%E9%A9%AC%E9%80%8AAWS%E6%89%A3%E8%B4%B9%E4%BA%86%E6%80%8E%E4%B9%88%E5%8A%9E%EF%BC%9F%2F</url>
<content type="text"><![CDATA[这是一个追回被亚马逊AWS扣款的亲身经历。 几天之前的一个早上,醒来后发现有一条消费524.6美元的短信,而且还是凌晨3点多的。起初以为是信用卡被盗,经银行查询后是亚马逊消费的。瞬间一头雾水,最近没有在亚马逊消费啊,怎么会有这么多的消费,而且还是美元!在疑惑中想要打开亚马逊一看究竟,不经意见看见电脑右下角的邮件提醒,呃……坑爹的AWS,一封524.6美元的扣款邮件! 进入AWS,在“账单”中已经存在4月和5月的账单信息。 怎么办?我只是在上面建立了两个实例,并没有去用,这个钱花得很冤枉!!! 还是找亚马逊解决吧!可是亚马逊客服在哪呢?在Google与知乎后,终于了解了可以在Amazon support中向他们说明了情况自己的账单有误,AWS可能会把钱退换给你。 进入Amazon support,创建一个Case,并填写自己情况的说明。如下图。 并填上自己的电话号码 在提交以后,亚马逊会给你打电话确认。这个时候,可以选择在接到电话以后多等一会儿,这样亚马逊的客户会接通电话,你就可以直接和大洋彼岸的亚马逊客户愉快地对话了。当然,也可以选择挂断电话,这样你只能等着他们分配case然后进行处理了。(另外,我也给AWS中国发了一份邮件,说明了情况。AWS中国区的客服也打电话过来,并告诉我可以协助我解决这个问题) 在AWS了解情况后,会给你一个解决方法邮件。邮件大概意思是将钱退还给你并且补偿现有的消费。 到这里,也就放心了。至少,钱不会没了! 当然,在向AWS说明情况的同时,我把创建的所有的实例给删除了,也删除了一些其他的服务。同时,为了不再被扣款,我把关联的信用卡过期时间设置为最近一个月(删除不了关联的信用卡)。考虑到现阶段也没有什么需要AWS服务的,最终选择了关闭亚马逊AWS的账户。 等过了几天,被扣的钱也最终还回来了。在收到钱的时候,亚马逊Case又会向你发一条信息,询问情况。 至此,事情圆满解决! PS.友情链接: Amazon AWS —— 免费的午餐不好吃]]></content>
<categories>
<category>未分类</category>
</categories>
<tags>
<tag>AWS</tag>
</tags>
</entry>
<entry>
<title><![CDATA[五二夜跑]]></title>
<url>%2F2016%2F05%2F02%2F%E4%BA%94%E4%BA%8C%E5%A4%9C%E8%B7%91%2F</url>
<content type="text"><![CDATA[五一假期最后一天,陆陆续续一个多月的雨也终于是停了。于是开启了停止半年的跑步。 当然了,跑步的过程是无聊的,但也很容易在这个时候思考问题。入职新公司还差半个月一年了,这一年中自己收获了什么?又失去了什么呢? 愚人节的时候,突然一个消息来袭,部门老大离职。然后就是项目停掉了,接着就是部门内各自为战,有想法的就抢占业务的山头,没想法的就待命工作。这个时候,到底是要走还是要留又一次地盘绕在我的心头。说句时候,入职新公司的一年是不快乐的一年。从原先的服务端的开发到现在的前端的开发,虽然学习了些前端的东西,和他人谈论的时候也会厚着脸皮说自己是Full Stack Developer。But !!!真的是么? 最好还是决定留下来。因为感觉自己还有很多的东西还不知道,还有很多的事情要求完成,在这个困难的时候离开真的就合适么?既然都一年了,何不再待一年试试?结果如何?谁知道呢!]]></content>
<categories>
<category>未分类</category>
</categories>
<tags>
<tag>生活</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring Ioc容器的依赖注入]]></title>
<url>%2F2016%2F04%2F30%2FSpring%20Ioc%E5%AE%B9%E5%99%A8%E7%9A%84%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%2F</url>
<content type="text"><![CDATA[前面的《Spring IoC容器的初始化过程》介绍了IoC容器的初始化过程。 SpringIoC容器的初始化过程完成的主要工作是在IoC容器中建立BeanDefinition数据映射。IoC容器的依赖注入的过程是在用户第一次向IoC容器索要Bean时触发的。 当我们调用getBean()时,我们实际上调用的是AbstractBeanFactory中的getBean方法,跟踪代码我们发现,其实调用的是doGetBean。 protected <T> T doGetBean( final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException { final String beanName = transformedBeanName(name); Object bean; //先从缓存中取得Bean,处理那些已经被创建过的单例模式的Bean,对这些Bean的请求不需要重复创建 Object sharedInstance = getSingleton(beanName); if (sharedInstance != null && args == null) { if (logger.isDebugEnabled()) { if (isSingletonCurrentlyInCreation(beanName)) { logger.debug("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference"); } else { logger.debug("Returning cached instance of singleton bean '" + beanName + "'"); } } //完成FactoryBean的相关处理,以取得FactoryBean的生产结果 bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); } else { if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } //检查factory中是否存在BeanDefinition BeanFactory parentBeanFactory = getParentBeanFactory(); if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { //如果当前factory中找不到,到parent中去找 String nameToLookup = originalBeanName(name); if (args != null) { return (T) parentBeanFactory.getBean(nameToLookup, args); } else {. return parentBeanFactory.getBean(nameToLookup, requiredType); } } if (!typeCheckOnly) { markBeanAsCreated(beanName); } try { //根据Bean的名字获得BeanDefinition final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); checkMergedBeanDefinition(mbd, beanName, args); //获得当前Bean的所有依赖Bean,这样会触发getBean的递归调用,直到取到一个没有任何依赖的Bean方法 String[] dependsOn = mbd.getDependsOn(); if (dependsOn != null) { for (String dependsOnBean : dependsOn) { if (isDependent(beanName, dependsOnBean)) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dependsOnBean + "'"); } registerDependentBean(dependsOnBean, beanName); getBean(dependsOnBean); } } //创建Bean实例(单例) if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { destroySingleton(beanName); throw ex; } } }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } else if (mbd.isPrototype()) { //创建一个prototype bean Object prototypeInstance = null; try { beforePrototypeCreation(beanName); prototypeInstance = createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); } else { String scopeName = mbd.getScope(); final Scope scope = this.scopes.get(scopeName); if (scope == null) { throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); } try { Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { beforePrototypeCreation(beanName); try { return createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } } }); bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); } catch (IllegalStateException ex) { throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider " + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", ex); } } } catch (BeansException ex) { cleanupAfterBeanCreationFailure(beanName); throw ex; } } //对创建的Bean进行类型检查,如果没有发现问题,就返回这个创建的Bean。 if (requiredType != null && bean != null && !requiredType.isAssignableFrom(bean.getClass())) { try { return getTypeConverter().convertIfNecessary(bean, requiredType); } catch (TypeMismatchException ex) { if (logger.isDebugEnabled()) { logger.debug("Failed to convert bean '" + name + "' to required type [" + ClassUtils.getQualifiedName(requiredType) + "]", ex); } throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } } return (T) bean; } getBean是依赖注入的起点,那么createBean就是重点了。在AbstractAutowireCapableBeanFactory中会创建Bean实例对象。 protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException { if (logger.isDebugEnabled()) { logger.debug("Creating instance of bean '" + beanName + "'"); } RootBeanDefinition mbdToUse = mbd; //判断创建的Bean是否可以实例化,这个类是否可以通过类转载器来载入 Class<?> resolvedClass = resolveBeanClass(mbd, beanName); if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { mbdToUse = new RootBeanDefinition(mbd); mbdToUse.setBeanClass(resolvedClass); } //校验和准备Bean中的方法覆盖 try { mbdToUse.prepareMethodOverrides(); } catch (BeanDefinitionValidationException ex) { throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), beanName, "Validation of method overrides failed", ex); } try { //如果Bean配置了PostProcessor,那么返回一个proxy Object bean = resolveBeforeInstantiation(beanName, mbdToUse); if (bean != null) { return bean; } } catch (Throwable ex) { throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, "BeanPostProcessor before instantiation of bean failed", ex); } Object beanInstance = doCreateBean(beanName, mbdToUse, args); //创建Bean的入口 if (logger.isDebugEnabled()) { logger.debug("Finished creating instance of bean '" + beanName + "'"); } return beanInstance; } //真正创建Bean的方法 protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) { //这个BeanWrapper是用来持有创建出来的Bean对象的 BeanWrapper instanceWrapper = null; if (mbd.isSingleton()) { //如果是singleton,先把缓存中的同名Bean清除 instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); } //这里是创建Bean的地方,由createBeanInstance来完成 if (instanceWrapper == null) { instanceWrapper = createBeanInstance(beanName, mbd, args); } final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null); Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null); //调用PostProcessor后置处理器 synchronized (mbd.postProcessingLock) { if (!mbd.postProcessed) { applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); mbd.postProcessed = true; } } //向容器中缓存单态模式的Bean对象,以防循环引用 boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { if (logger.isDebugEnabled()) { logger.debug("Eagerly caching bean '" + beanName + "' to allow for resolving potential circular references"); } //这里是一个匿名内部类,为了防止循环引用,尽早持有对象的引用 addSingletonFactory(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { return getEarlyBeanReference(beanName, mbd, bean); } }); } //Bean对象的初始化,依赖注入在此触发。 //这个exposedObject在初始化完成之后返回作为依赖注入完成后的Bean Object exposedObject = bean; try { populateBean(beanName, mbd, instanceWrapper); if (exposedObject != null) { exposedObject = initializeBean(beanName, exposedObject, mbd); } } catch (Throwable ex) { if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) { throw (BeanCreationException) ex; } else { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex); } } if (earlySingletonExposure) { Object earlySingletonReference = getSingleton(beanName, false);//获取指定名称的已注册的单态模式Bean对象 if (earlySingletonReference != null) { //如果根据名称获取的已注册的Bean和正在实例化的Bean是同一个 if (exposedObject == bean) { exposedObject = earlySingletonReference; //当前实例化的Bean初始化完成 } //当前Bean依赖其他Bean,并且当发生循环引用时不允许新创建实例对象 else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { String[] dependentBeans = getDependentBeans(beanName); Set<String> actualDependentBeans = new LinkedHashSet<String>(dependentBeans.length); //获取当前Bean所依赖的其他Bean for (String dependentBean : dependentBeans) { //对依赖Bean进行类型检查 if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); } } if (!actualDependentBeans.isEmpty()) { throw new BeanCurrentlyInCreationException(beanName, "Bean with name '" + beanName + "' has been injected into other beans [" + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + "] in its raw version as part of a circular reference, but has eventually been " + "wrapped. This means that said other beans do not use the final version of the " + "bean. This is often the result of over-eager type matching - consider using " + "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example."); } } } } //注册完成依赖注入的Bean try { registerDisposableBeanIfNecessary(beanName, bean, mbd); } catch (BeanDefinitionValidationException ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex); } return exposedObject; } 所以上面创造Bean的createBeanInstance和触发依赖注入的populateBean是我们要分析的重点。 protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) { //确认需要创建的Bean实例的类可以实例化 Class<?> beanClass = resolveBeanClass(mbd, beanName); if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Bean class isn't public, and non-public access not allowed: " + beanClass.getName()); } //这里使用工厂方法对Bean进行实例化 if (mbd.getFactoryMethodName() != null) { return instantiateUsingFactoryMethod(beanName, mbd, args); } // Shortcut when re-creating the same bean... boolean resolved = false; boolean autowireNecessary = false; if (args == null) { synchronized (mbd.constructorArgumentLock) { if (mbd.resolvedConstructorOrFactoryMethod != null) { resolved = true; autowireNecessary = mbd.constructorArgumentsResolved; } } } if (resolved) { if (autowireNecessary) { return autowireConstructor(beanName, mbd, null, null); } else { return instantiateBean(beanName, mbd); } } //使用构造函数进行实例化 Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); if (ctors != null || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR || mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) { return autowireConstructor(beanName, mbd, ctors, args); } //使用默认的构造函数对Bean进行实例化 return instantiateBean(beanName, mbd); } //最常见的实例化过程instantiateBean protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) { //使用默认的实例化策略对Bean进行实例化,默认的实例化策略是CglibSubclassingInstantiationStrategy,也就是使用CGLIB来对Bean进行实例化 try { Object beanInstance; final BeanFactory parent = this; if (System.getSecurityManager() != null) { beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { return getInstantiationStrategy().instantiate(mbd, beanName, parent); } }, getAccessControlContext()); } else { beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent); } BeanWrapper bw = new BeanWrapperImpl(beanInstance); initBeanWrapper(bw); return bw; } catch (Throwable ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex); } } 以上的代码可以看出Spring IoC默认用的CGLIB来对Bean进行实例化。在SimpleInstantiationStrategy的instantiate方法中可以看到有两种实例化java对象的方法:一种是通过BeanUtils,它使用了JVM的反射功能,一种是通过CGLIB来进行生成的。 在Bean对象生成以后,我们再来看看怎样把这些Bean对象的依赖关系设置好,完成整个注入过程。 protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) { PropertyValues pvs = mbd.getPropertyValues(); //取得在BeanDefinition中设置的property值 if (bw == null) { if (!pvs.isEmpty()) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance"); } else { return; } } boolean continueWithPropertyPopulation = true; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof InstantiationAwareBeanPostProcessor) { InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) { continueWithPropertyPopulation = false; break; } } } } if (!continueWithPropertyPopulation) { return; } //开始进行依赖注入过程,先处理autowire的注入 if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { MutablePropertyValues newPvs = new MutablePropertyValues(pvs); // 对autowire注入处理,可以根据Bean的名字完成对Bean的autowire if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) { autowireByName(beanName, mbd, bw, newPvs); } // 对autowire注入处理,可以根据Bean的类型完成对Bean的autowire if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { autowireByType(beanName, mbd, bw, newPvs); } pvs = newPvs; } boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors(); boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE); if (hasInstAwareBpps || needsDepCheck) { PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching); if (hasInstAwareBpps) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof InstantiationAwareBeanPostProcessor) { InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName); if (pvs == null) { return; } } } } if (needsDepCheck) { checkDependencies(beanName, mbd, filteredPds, pvs); } } //对属性进行注入 applyPropertyValues(beanName, mbd, bw, pvs); } 在applyPropertyValues方法中BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter);代码以下部分是为BeanDefinition的解析值创建一个副本,副本的数据将会被注入到Bean中。而BeanDefinition的解析在BeanDefinitionValueResolver中完成。而Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);将解析BeanDefinition。 protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { if (pvs == null || pvs.isEmpty()) { return; } MutablePropertyValues mpvs = null; List<PropertyValue> original; if (System.getSecurityManager() != null) { if (bw instanceof BeanWrapperImpl) { ((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext()); } } if (pvs instanceof MutablePropertyValues) { mpvs = (MutablePropertyValues) pvs; if (mpvs.isConverted()) { // Shortcut: use the pre-converted values as-is. try { bw.setPropertyValues(mpvs); return; } catch (BeansException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Error setting property values", ex); } } original = mpvs.getPropertyValueList(); } else { original = Arrays.asList(pvs.getPropertyValues()); } TypeConverter converter = getCustomTypeConverter(); if (converter == null) { converter = bw; } //BeanDefinitionValueResolver对Beanfinition的解析是在BeanDefinitionValueResolver对象valueResolver完成的。 BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter); // Create a deep copy, resolving any references for values. List<PropertyValue> deepCopy = new ArrayList<PropertyValue>(original.size()); boolean resolveNecessary = false; for (PropertyValue pv : original) { if (pv.isConverted()) { deepCopy.add(pv); } else { String propertyName = pv.getName(); Object originalValue = pv.getValue(); Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue); Object convertedValue = resolvedValue; boolean convertible = bw.isWritableProperty(propertyName) && !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName); if (convertible) { convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter); } // Possibly store converted value in merged bean definition, // in order to avoid re-conversion for every created bean instance. if (resolvedValue == originalValue) { if (convertible) { pv.setConvertedValue(convertedValue); } deepCopy.add(pv); } else if (convertible && originalValue instanceof TypedStringValue && !((TypedStringValue) originalValue).isDynamic() && !(convertedValue instanceof Collection || ObjectUtils.isArray(convertedValue))) { pv.setConvertedValue(convertedValue); deepCopy.add(pv); } else { resolveNecessary = true; deepCopy.add(new PropertyValue(pv, convertedValue)); } } } if (mpvs != null && !resolveNecessary) { mpvs.setConverted(); } // Set our (possibly massaged) deep copy. try { bw.setPropertyValues(new MutablePropertyValues(deepCopy)); } catch (BeansException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Error setting property values", ex); } } private Object resolveReference(Object argName, RuntimeBeanReference ref) { try { //从RuntimeBeanReference取得reference的名字,这个RuntimeBeanReference是在载入BeanDefinition时根据配置生成的 String refName = ref.getBeanName(); refName = String.valueOf(doEvaluate(refName)); if (ref.isToParent()) { //如果ref是在双亲IoC容器中,那就在双亲IoC容器中去获取 if (this.beanFactory.getParentBeanFactory() == null) { throw new BeanCreationException( this.beanDefinition.getResourceDescription(), this.beanName, "Can't resolve reference to bean '" + refName + "' in parent factory: no parent factory available"); } return this.beanFactory.getParentBeanFactory().getBean(refName); } else { //在当前容器中获得Bean,这里会触发一个getBean的过程,如果依赖注入没有发生,这里会触发相应的依赖注入的发生 Object bean = this.beanFactory.getBean(refName); this.beanFactory.registerDependentBean(refName, this.beanName); return bean; } } catch (BeansException ex) { throw new BeanCreationException( this.beanDefinition.getResourceDescription(), this.beanName, "Cannot resolve reference to bean '" + ref.getBeanName() + "' while setting " + argName, ex); } } 在applyPropertyValues方法中,还调用了setPropertyValues方法。这是真正把Bean对象设置到它所依赖的另一个Bean的属性中去的。 在Bean的创建和对象依赖注入的过程中,需要根据BeanDefinition中的信息来递归地完成依赖注入。在对Bean的属性进行依赖注入时,解析的过程也是一个递归的过程。]]></content>
<categories>
<category>Spring</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>IoC</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring IoC容器的初始化过程]]></title>
<url>%2F2016%2F04%2F30%2FSpring%20IoC%E5%AE%B9%E5%99%A8%E7%9A%84%E5%88%9D%E5%A7%8B%E5%8C%96%E8%BF%87%E7%A8%8B%2F</url>
<content type="text"><![CDATA[Spring IoC容器的初始化包括BeanDefinition的Resource定位、载入和注册这三个基本的过程。IoC容器的初始化过程不包含Bean依赖注入的实现。Bean依赖的注入一般会发生在第一次通过getBean向容器索取Bean的时候。 先看以下代码: ApplicationContext context = new ClassPathXmlApplicationContext("ioc.xml"); Car car = (Car) context.getBean("car"); System.out.println(car.getBrand()); 以上是我们常用的加载IoC容器,并获得Bean的代码。直接进入ClassPathXmlApplicationContext的构造方法,它实际调用的构造方法为: public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) throws BeansException { super(parent); setConfigLocations(configLocations); if (refresh) { refresh(); //调用容器的refresh,载入BeanDefinitionde的入口。 } } 调用super(parent)方法为容器设置好Bean资源加载器,该方法最终会调用到AbstractApplicationContext的无参构造方法,这里会默认设置解析路径的模式为Ant-style。 setConfigLocations(configLocations)设置Bean定义资源文件的定位路径。AbstractRefreshableConfigApplicationContext中的setConfigLocation(String location)说明了多个资源文件路径之间可以是用” ,; /t/n”分隔。setConfigLocations(String… locations)说明了它还接受字符串数组。 接下来看下最重要的refresh()方法。 @Override public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { //调用容器准备刷新的方法,获取容器的当时时间,同时给容器设置同步标识 prepareRefresh(); // 通知子类启动refreshBeanFactory()的调用 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); //为BeanFactory配置容器特性,例如类加载器、事件处理器等 prepareBeanFactory(beanFactory); try { //为子类设置BeanFactory的后置处理器 postProcessBeanFactory(beanFactory); //调用BeanFactoryPostProcessor,这些后置处理器都是在Bean定义中向容器定义的 invokeBeanFactoryPostProcessors(beanFactory); // 注册Bean的后置处理器,在Bean创建过程中调用 registerBeanPostProcessors(beanFactory); // 对上下文的消息源进行初始化 initMessageSource(); // 初始化上下文的事件机制 initApplicationEventMulticaster(); // 初始化其他特殊的Bean onRefresh(); // 检查监听Bean,并且将这些Bean向容器中注册 registerListeners(); // 实例化所有的(non-lazy-init) 单例 finishBeanFactoryInitialization(beanFactory); // 最后一步:发布容器事件,结束refresh过程 finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } /销毁已经创建的单态Bean destroyBeans(); // 重置'active'状态 cancelRefresh(ex); // Propagate exception to caller. throw ex; } finally { // Reset common introspection caches in Spring's core, since we // might not ever need metadata for singleton beans anymore... resetCommonCaches(); } } } 我们重点看ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();。这句代码的作用是告诉子类启动refreshBeanFactory方法以及通过getBeanFactory获得beanFactory。 refreshBeanFactory方法在AbstractApplicationContext中被定义,且在其子类AbstractRefreshableApplicationContext中实现。代码如下: @Override protected final void refreshBeanFactory() throws BeansException { if (hasBeanFactory()) { //如果已经有BeanFactory,销毁bean,关闭BeanFactory destroyBeans(); closeBeanFactory(); } try { DefaultListableBeanFactory beanFactory = createBeanFactory(); //创建IoC容器 beanFactory.setSerializationId(getId()); customizeBeanFactory(beanFactory); loadBeanDefinitions(beanFactory); //启动对BeanDefinitions的载入 synchronized (this.beanFactoryMonitor) { this.beanFactory = beanFactory; } } catch (IOException ex) { throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); } } 通过createBeanFactory()构建了一个DefaultListableBeanFactory IoC容器提供给ApplicationContext使用。同时通过loadBeanDefinitions(beanFactory)载入Bean定义。 loadBeanDefinitions方法的具体实现是在AbstractXmlApplicationContext中。 @Override protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { //创建XmlBeanDefinitionReader,即创建Bean读取器,并通过回调设置到容器中去,容器使用该读取器读取Bean定义资源 XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); // Configure the bean definition reader with this context's resource loading environment. beanDefinitionReader.setEnvironment(this.getEnvironment()); beanDefinitionReader.setResourceLoader(this); //为Bean读取器设置Spring资源加载器 beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); //当Bean读取器读取Bean定义的Xml资源文件时,启用Xml的校验机制 initBeanDefinitionReader(beanDefinitionReader); //通过beanDefinitionReader加载BeanDefinitions loadBeanDefinitions(beanDefinitionReader); } 在loadBeanDefinitions中创建了XmlBeanDefinitionReader实例,然后在IoC容器中设置该实例,最后通过loadBeanDefinitions方法来完成Bean定义在IoC容器中的载入。接下来看下真正实现加载BeanDefinitions的loadBeanDefinitions(beanDefinitionReader)方法: protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { Resource[] configResources = getConfigResources(); //获得Bean配置文件的资源位置 if (configResources != null) { //读取AbstractBeanDefinitionReader中定位的资源 reader.loadBeanDefinitions(configResources); } //获取ClassPathXmlApplicationContext构造方法中setConfigLocations方法设置的资源 String[] configLocations = getConfigLocations(); if (configLocations != null) { //读取AbstractBeanDefinitionReader中定位的资源,最终还是以Resource的形式去加载资源。 reader.loadBeanDefinitions(configLocations); } } 在AbstractBeanDefinitionReader的loadBeanDefinitions中开始进行BeanDefinitions的载入。 @Override public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException { Assert.notNull(resources, "Resource array must not be null"); int counter = 0; for (Resource resource : resources) { counter += loadBeanDefinitions(resource); } return counter; } 以上代码中的loadBeanDefinitions在AbstractBeanDefinitionReader中并没有实现,它只是一个在BeanDefinitionReader中定义的接口方法,具体的实现在各个子类(如:XmlBeanDefinitionReader)中。 在XmlBeanDefinitionReader中实现的loadBeanDefinitions方法会得到一个XML文件的InputStream,然后会获得一个InputResource,调用doLoadBeanDefinitions(inputSource, encodedResource.getResource())返回。doLoadBeanDefinitions方法是去从XML文件中加载BeanDefinitions,具体的过程是在该方法调用了registerBeanDefinitions(doc, resource)。 public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { //这里得到BeanDefinitionDocumentReader来对XML的BeanDefinition进行解析 BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); int countBefore = getRegistry().getBeanDefinitionCount(); //具体的解析过程在registerBeanDefinitions中完成 documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); return getRegistry().getBeanDefinitionCount() - countBefore; } 通过上面的代码可以知道具体的解析XML并转换为容器内部结构的过程是在BeanDefinitionDocumentReader中完成的,registerBeanDefinitions还对载入的Bean数量进行了统计。这里使用的documentReader是通过createBeanDefinitionDocumentReader()方法创建的默认的DefaultBeanDefinitionDocumentReader。而DefaultBeanDefinitionDocumentReader中定义了Spring的Bean规则。 @Override public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { this.readerContext = readerContext; logger.debug("Loading bean definitions"); Element root = doc.getDocumentElement(); doRegisterBeanDefinitions(root); } 在doRegisterBeanDefinitions中,由BeanDefinitionParserDelegate实现了解析过程。 protected void doRegisterBeanDefinitions(Element root) { BeanDefinitionParserDelegate parent = this.delegate; this.delegate = createDelegate(getReaderContext(), root, parent); if (this.delegate.isDefaultNamespace(root)) { String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); if (StringUtils.hasText(profileSpec)) { String[] specifiedProfiles = StringUtils.tokenizeToStringArray( profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) { return; } } } preProcessXml(root); parseBeanDefinitions(root, this.delegate); postProcessXml(root); this.delegate = parent; } 通过一路跟进parseBeanDefinitions方法,可以找到以下代码: private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { importBeanDefinitionResource(ele); } else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { processAliasRegistration(ele); } else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { processBeanDefinition(ele, delegate); } else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { // recurse doRegisterBeanDefinitions(ele); } } //这里是处理BeanDefinition的地方,具体的处理工作交给了BeanDefinitionParserDelegate的parseBeanDefinitionElement。 protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); if (bdHolder != null) { bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); try { //向IoC容器注册解析到BeanDefinition BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); } catch (BeanDefinitionStoreException ex) { getReaderContext().error("Failed to register bean definition with name '" + bdHolder.getBeanName() + "'", ele, ex); } // 在BeanDefinition想IoC容器注册完以后,发送消息。 getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); } } 在processBeanDefinition方法中处理BeanDefinitions,具体的处理工作交给了BeanDefinitionParserDelegate的parseBeanDefinitionElement,并且得到结果BeanDefinitionHolder,然后向IoC容器注册解析到的BeanDefinition,注册完成之后发送消息。BeanDefinitionParserDelegate类包含了对各种Spring Bean定义规则的处理。BeanDefinitionHolder是BeanDefinition的封装类,封装了BeanDefinition,Bean的名字、别名。用它来完成想IoC容器注册。 public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) { //在这里取得<bean>中定义的id、name、aliase属性的值 String id = ele.getAttribute(ID_ATTRIBUTE); String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); List<String> aliases = new ArrayList<String>(); if (StringUtils.hasLength(nameAttr)) { String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS); aliases.addAll(Arrays.asList(nameArr)); } String beanName = id; if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) { beanName = aliases.remove(0); if (logger.isDebugEnabled()) { logger.debug("No XML 'id' specified - using '" + beanName + "' as bean name and " + aliases + " as aliases"); } } if (containingBean == null) { checkNameUniqueness(beanName, aliases, ele); } //这个方法会引发对Bean元素的详细解析 AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); if (beanDefinition != null) { if (!StringUtils.hasText(beanName)) { try { if (containingBean != null) { beanName = BeanDefinitionReaderUtils.generateBeanName( beanDefinition, this.readerContext.getRegistry(), true); } else { beanName = this.readerContext.generateBeanName(beanDefinition); String beanClassName = beanDefinition.getBeanClassName(); if (beanClassName != null && beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) { aliases.add(beanClassName); } } if (logger.isDebugEnabled()) { logger.debug("Neither XML 'id' nor 'name' specified - " + "using generated bean name [" + beanName + "]"); } } catch (Exception ex) { error(ex.getMessage(), ele); return null; } } String[] aliasesArray = StringUtils.toStringArray(aliases); return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); } return null; } 以下代码是对Bean元素的详细解析: public AbstractBeanDefinition parseBeanDefinitionElement( Element ele, String beanName, BeanDefinition containingBean) { this.parseState.push(new BeanEntry(beanName)); //这里只是读取定义的<bean>中设置的class名字,然后载入到BeanDefinition中去,只是做个记录,并不涉及对象的实例化过程,对象的实例化实际上是在依赖注入时完成的 String className = null; if (ele.hasAttribute(CLASS_ATTRIBUTE)) { className = ele.getAttribute(CLASS_ATTRIBUTE).trim(); } try { String parent = null; if (ele.hasAttribute(PARENT_ATTRIBUTE)) { parent = ele.getAttribute(PARENT_ATTRIBUTE); } //这里生成需要的BeanDefinition对象,为Bean定义信息的载入做准备 AbstractBeanDefinition bd = createBeanDefinition(className, parent); //这里对当前的Bean元素进行属性解析,并设置description parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); //对各种<bean>元素的信息进行解析 parseMetaElements(ele, bd); parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); //解析<bean>的构造函数设置 parseConstructorArgElements(ele, bd); //解析<bean>的property设置 parsePropertyElements(ele, bd); parseQualifierElements(ele, bd); bd.setResource(this.readerContext.getResource()); bd.setSource(extractSource(ele)); return bd; } //这里是异常处理。 catch (ClassNotFoundException ex) { error("Bean class [" + className + "] not found", ele, ex); } catch (NoClassDefFoundError err) { error("Class that bean class [" + className + "] depends on not found", ele, err); } catch (Throwable ex) { error("Unexpected failure during bean definition parsing", ele, ex); } finally { this.parseState.pop(); } return null; } 以上就是BeanDefinition在IoC容器中的载入和解析过程。 在上面的DefaultBeanDefinitionDocumentReader类的processBeanDefinition方法中我们看到有这么一行代码:BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());。这里的作用是想IoC容器注册解析后获得到的BeanDefinition。通过追踪代码,发现DefaultListableBeanFactory实现了在BeanDefinitionRegistry中定义的registerBeanDefinition方法。 @Override public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException { Assert.hasText(beanName, "Bean name must not be empty"); Assert.notNull(beanDefinition, "BeanDefinition must not be null"); if (beanDefinition instanceof AbstractBeanDefinition) { try { ((AbstractBeanDefinition) beanDefinition).validate(); } catch (BeanDefinitionValidationException ex) { throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Validation of bean definition failed", ex); } } BeanDefinition oldBeanDefinition; oldBeanDefinition = this.beanDefinitionMap.get(beanName); if (oldBeanDefinition != null) { //检查是不是有相同名字的Bean在IoC容器中存在了 if (!isAllowBeanDefinitionOverriding()) { //存在相同的名字的Bean,但又不允许覆盖,那么会抛出异常 throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName + "': There is already [" + oldBeanDefinition + "] bound."); } else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) { // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE if (this.logger.isWarnEnabled()) { this.logger.warn("Overriding user-defined bean definition for bean '" + beanName + "' with a framework-generated bean definition: replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]"); } } else if (!beanDefinition.equals(oldBeanDefinition)) { if (this.logger.isInfoEnabled()) { this.logger.info("Overriding bean definition for bean '" + beanName + "' with a different definition: replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]"); } } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Overriding bean definition for bean '" + beanName + "' with an equivalent definition: replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]"); } } this.beanDefinitionMap.put(beanName, beanDefinition); } else { //这边是正常注册BeanDefinition。 if (hasBeanCreationStarted()) { // Cannot modify startup-time collection elements anymore (for stable iteration) synchronized (this.beanDefinitionMap) { this.beanDefinitionMap.put(beanName, beanDefinition); List<String> updatedDefinitions = new ArrayList<String>(this.beanDefinitionNames.size() + 1); updatedDefinitions.addAll(this.beanDefinitionNames); updatedDefinitions.add(beanName); this.beanDefinitionNames = updatedDefinitions; if (this.manualSingletonNames.contains(beanName)) { Set<String> updatedSingletons = new LinkedHashSet<String>(this.manualSingletonNames); updatedSingletons.remove(beanName); this.manualSingletonNames = updatedSingletons; } } } else { // Still in startup registration phase this.beanDefinitionMap.put(beanName, beanDefinition); this.beanDefinitionNames.add(beanName); this.manualSingletonNames.remove(beanName); } this.frozenBeanDefinitionNames = null; } if (oldBeanDefinition != null || containsSingleton(beanName)) { resetBeanDefinition(beanName); } } 总结: IoC容器初始化的入口是在构造方法中调用refresh()开始的。 通过ResourceLoader来完成资源文件位置的定位,DefaultResourceLoader是默认的实现,同时上下文本身就给出了ResourceLoader的实现。 创建的IoC容器是DefaultListableBeanFactory。 IoC容器对Bean的管理和依赖注入功能的实现是通过对其持有的BeanDefinition进行相关操作来完成的。 通过BeanDefinitionReader来完成定义信息的解析和Bean信息的注册。 XmlBeanDefinitionReader是BeanDefinitionReader的实现类,通过它来解析XML配置中的bean定义。 实际的处理过程是委托给 BeanDefinitionParserDelegate来完成的。得到bean的定义信息,这些信息在Spring中使用BeanDefinition对象来表示。 BeanDefinition的注册是由BeanDefinitionRegistry实现的registerBeanDefinition方法进行的。内部使用ConcurrentHashMap来保存BeanDefinition。 本文根据《Spring技术内幕-深入解析Spring架构与设计原理》第2章整理]]></content>
<categories>
<category>Spring</category>
</categories>
<tags>
<tag>Spring</tag>