派大星的博客

很多事情不是会了才能做,而是做了才能学会


  • 首页

  • 标签

  • 分类

  • 关于

  • 搜索

SpringBoot安全验证之Referer拦截器

发表于 2019-03-17 | 分类于 springboot

自定义Referer拦截器

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
public class RefererInterceptor extends HandlerInterceptorAdapter {
// URL匹配器
private AntPathMatcher matcher = new AntPathMatcher();
@Autowired
private RefererProperties properties;
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
String referer = req.getHeader("referer");
String host = req.getServerName();
// 只验证POST请求
if ("POST".equals(req.getMethod())) {
if (referer == null) {
// 状态置为404
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return false;
}
java.net.URL url = null;
try {
url = new java.net.URL(referer);
} catch (MalformedURLException e) {
// URL解析异常,也置为404
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return false;
}
// 首先判断请求域名和referer域名是否相同
if (!host.equals(url.getHost())) {
// 如果不等,判断是否在白名单中
if (properties.getRefererDomain() != null) {
for (String s : properties.getRefererDomain()) {
if (s.equals(url.getHost())) {
return true;
}
}
}
return false;
}
}
return true;
}
}

配置白名单Referer域名

1
2
3
4
5
6
7
@Component
@ConfigurationProperties(prefix = "referer")
public class RefererProperties {
// 白名单域名
private List<String> refererDomain;
//setter,getter方法
}

yml配置

1
2
3
4
5
referer:
refererDomain:
- baidu.com
- pibigstar.com
- mxspvip.cn

使用docker安装consul服务发现

发表于 2019-03-13 | 分类于 工具使用,docker

1. 拉取consul镜像

1
docker pull consul:latest

2. consul参数详解

  • –net=host docker参数, 使得docker容器越过了net namespace的隔离,免去手动指定端口映射的步骤
  • -server consul支持以server或client的模式运行, server是服务发现模块的核心, client主要用于转发请求
  • -advertise 将本机私有IP传递到consul
  • -retry-join 指定要加入的consul节点地址,失败后会重试, 可多次指定不同的地址
  • -client 指定consul绑定在哪个client地址上,这个地址可提供HTTP、DNS、RPC等服务,默认是>127.0.0.1
  • -bind 绑定服务器的ip地址;该地址用来在集群内部的通讯,集群内的所有节点到地址必须是可达的,>默认是0.0.0.0
    allow_stale 设置为true则表明可从consul集群的任一server节点获取dns信息, false则表明每次请求都会>经过consul的server leader
  • -bootstrap-expect 数据中心中预期的服务器数。指定后,Consul将等待指定数量的服务器可用,然后>启动群集。允许自动选举leader,但不能与传统-bootstrap标志一起使用, 需要在server模式下运行。
  • -data-dir 数据存放的位置,用于持久化保存集群状态
  • -node 群集中此节点的名称,这在群集中必须是唯一的,默认情况下是节点的主机名。
  • -config-dir 指定配置文件,当这个目录下有 .json 结尾的文件就会被加载,详细可参考https://www.consul.io/docs/agent/options.html#configuration_files
  • -enable-script-checks 检查服务是否处于活动状态,类似开启心跳
  • -datacenter 数据中心名称
  • -ui 开启ui界面
  • -join 指定ip, 加入到已有的集群中

3. 简单使用

启动第一个节点,叫 consul1

1
docker run --name consul1 -d -p 8500:8500 -p 8300:8300 -p 8301:8301 -p 8302:8302 -p 8600:8600 consul agent -server -bootstrap-expect 2 -ui -bind=0.0.0.0 -client=0 .0.0.0

端口详解

  • 8500 : http 端口,用于 http 接口和 web ui访问;
  • 8300 : server rpc 端口,同一数据中心 consul server 之间通过该端口通信;
  • 8301 : serf lan 端口,同一数据中心 consul client 通过该端口通信; 用于处理当前datacenter中LAN的gossip通信;
    • 8302 : serf wan 端口,不同数据中心 consul server 通过该端口通信; agent Server使用,处理与其他datacenter的gossip通信;
    • 8600 : dns 端口,用于已注册的服务发现;

启动第二个节点,加入到 consul1

查看consul1的ip地址

1
docker inspect --format='{{.NetworkSettings.IPAddress}}' consul1

开启第二个节点(端口8501),并加入到 consul1

1
docker run --name consul2 -d -p 8501:8500 consul agent -server -ui -bind=0.0.0.0 -client=0.0.0.0 -join 172.17.0.4

开启第三个节点(端口8502),并加入到consul1‘

1
docker run --name consul3 -d -p 8502:8500 consul agent -server -ui -bind=0.0.0.0 -client=0.0.0.0 -join 172.17.0.4

查看consul集群信息

1
docker exec -it consul1 consul members

我们看到集群里有三个节点

4. 查看集群信息

我们可以打开浏览器: http://localhost:8500 来查看整个集群的信息

Go语言入门(2)Go语言基础

发表于 2019-03-13 | 分类于 go

1. 基础数据类型

  • 数字类型: 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且原生支持复数
  • 布尔型: true、false
  • 派生类型:
    (a) 指针类型(Pointer)
    (b) 数组类型
    (c) 结构化类型(struct)
    (d) Channel 类型
    (e) 函数类型
    (f) 切片类型
    (g) 接口类型(interface)
    (h) Map 类型

2. 基础语法

2.1 循环

go语言里面没有while,所以实现循环都是使用for

无限循环

1
2
3
for {

}

条件循环

1
2
for i:=0; i <10 ; i++{
}

2.2 判断

跟其他语言一样,判断有大于(>) 、小于(<) 、等于(==)、不等于(!=)、大于等于(>=)、小于等于(<=)

1
2
if i>10 {
}

3.3 选择

1
2
3
4
5
6
7
8
switch a {

case 1: fmt.Println("a为1")
case 2: fmt.Println("a为2")
default:
fmt.Println("a不确定")

}

3. 基本语法

3.1 定义常量、全局变量、局部变量

定义常量使用 const 关键字

1
const a int = 1 //必须要赋初值

全局变量,全局变量是在函数外定义并且必须要有var关键字

1
var b int = 10 //可以不用赋初值

局部变量,也就是在函数中定义的变量,可以省略 var关键字

1
2
3
var c int = 1 //最保守的写法
var c = 1 // 省略类型,让编译器自己判断
c := 1 //省略var关键字和类型,比较方便

注意:在go中 _ 这个小下划线被称为废弃数,如果某个值赋值给它则此值就被废弃不再使用了

3.2 定义数组、切片、map

  • 定义数组,

    1
    2
    // 定义了一个大小为10的数组
    var ids [10]int // 初始全为 0
  • 定义切片

    1
    2
    3
    //切片与数组最大的不同就是,切片是变长的,它的容量可以自动扩充
    var splice = make([]int,3) // 指定切片大小
    var splice = make([]int,3,10) // 指定切片大小和容量

len 是这个切片初始化时的大小
cap 是这个切片的容量,当len大于cap时,此切片就会扩容,扩容为此时的cap的2倍

  • 定义 map
1
2
3
4
// 定义一个map,key为string类型,value为int类型
var m = make(map[string]int)
// 使用
m["age"] = 18

3.3 定义派生类型

  • 定义结构体,也就是java中的bean对象
1
2
3
4
5
type person struct{
id int
name string
age int
}
  • 定义接口
1
2
3
type Ising interface {
sing()
}
  • 实现接口

go语言中只要一个实体实现了接口中的所有方法,那么这个实体就是实现了这个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
type person struct {
string name
}

// 让person 实现 Ising 接口
func (p *person) sing(){
fmt.Println(p.name+"在唱歌")
}

// 使用
p := new(person)
p.name = "pibigstar"
p.sing()

4. 异常

4.1 异常处理

go语言中一般都是把错误当成返回值给调用的函数

1
2
3
4
5
6
7
func test(name string)(int,error){
if string=="" {
return 0,eroors.New("name cannot is null")
}else {
return m[name],nil
}
}

4.2 中断程序

当调用panic() 函数,该出错线程就会停止,而有的时候我们不想让它停止,我们可以 使用recover() 来恢复它

1
2
3
4
5
6
7
8
	// 不让线程终止
defer func() {
if r :=recover(); r!=nil{
log.Println("hava a error")
}
}()

panic(err)

4.3 defer 函数

这个函数有点类似于java中的finally,都是在函数执行返回值之前做一些操作,一般都是执行一些关闭流的操作

1
defer reader.close()

Shell脚本模板

发表于 2019-03-11 | 分类于 shell
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
# 打印帮助信息
usage() {
cat >&1 <<-EOF

请使用: $0 <option>

可使用的参数 <option> 包括:

install 安装
uninstall 卸载
update 检查更新
help 查看脚本使用说明
EOF

exit $
}
# 检查是否已安装
is_installed(){
echo "检查是否已安装..."
# return 0表示已安装,1表示未安装
return 0
}

# 安装检查
installed_check() {
if is_installed; then
cat >&1 <<-EOF
检测到你已安装......
EOF
while true
do
cat >&1 <<-'EOF'
请选择你希望的操作:
(1) 覆盖安装
(2) 重新配置
(3) 检查更新
(4) 查看配置
(5) 完全卸载
(6) 退出脚本
EOF
read -p "(默认: 1) 请选择 [1~6]: " sel
[ -z "$sel" ] && sel=1

case $sel in
1)
echo "开始覆盖..."
# return 0 之后会继续执行下面的函数
return 0
;;
2)
echo "重新配置..."
;;
3)
echo "检查更新..."
do_update
;;
4)
echo "查看配置.."
do_update
;;
5)
echo "完全卸载.."
do_uninstall
;;
6)
echo "退出脚本.."
;;
*)
echo "输入有误, 请输入有效数字 1~6!"
continue
;;
esac
exit 0
done
fi
}
# 开始安装
install_app(){
echo "开始安装....."
}

# 安装命令
do_install(){
# 检查root权限
check_root
# 检查是否已安装
installed_check
# 安装
install_app

cat >&1 <<-EOF
恭喜! 服务端安装成功。
更多使用说明: ${0} help

如果这个脚本帮到了你,你可以请作者喝瓶可乐:
https://blog.csdn.net/junmoxi/
EOF
}
do_uninstall(){
echo "开始卸载....."
}
do_update(){
echo "开始更新....."
}

# 程序进入后的执行函数
action=${1:-"install"}
case "$action" in
install|uninstall|update)
do_${action}
;;
help)
usage 0
;;
*)
usage 1
;;
esac

Fiddler高级使用——规则编写

发表于 2019-03-11 | 分类于 工具使用

打开规则脚本编写

在此函数下面编写

1. 替换json里面部分参数,然后返回给客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
if (oSession.fullUrl.Contains("http://www.baidu.com"))
{
// 获取Response Body、Request Body中JSON字符串,转换为可编辑的JSONObject变量
var responseStringOriginal = oSession.GetResponseBodyAsString();
var responseJSON = Fiddler.WebFormats.JSON.JsonDecode(responseStringOriginal);
var requestStringOriginal=oSession.GetRequestBodyAsString();
var requestJSON = Fiddler.WebFormats.JSON.JsonDecode(requestStringOriginal);
//请求参数中,若type为1,对返回值做如下修改
responseJSON.JSONObject['付费'] = "true";
// 重新设置Response Body
var responseStringDestinal = Fiddler.WebFormats.JSON.JsonEncode(responseJSON.JSONObject);
oSession.utilSetResponseBody(responseStringDestinal);
}

2. 修改request的Body里面的部分参数

1
2
3
4
5
6
7
8
9
10
11
if(oSession.uriContains("http://www.baidu.com"))
{
// 获取Request 中的body字符串
var strBody=oSession.GetRequestBodyAsString();
// 用正则表达式或者replace方法去修改string,将false改为true
strBody=strBody.replace("false","true");
// 弹个对话框检查下修改后的body
FiddlerObject.alert(strBody);
// 将修改后的body,重新写回Request中
oSession.utilSetRequestBody(strBody);
}

3. 修改cookie

1
2
3
4
5
6
7
if(oSession.HostnameIs('www.baidu.com') && oSession.uriContains('pagewithCookie') && oSession.oRequest.headers.Contains("Cookie"))
{
var sCookie = oSession.oRequest["Cookie"];
// 用replace方法或者正则表达式的方法去操作cookie的string
sCookie = sCookie.Replace("付费=false", "付费=true");
oSession.oRequest["Cookie"] = sCookie;
}

4. 查看是否访问了某个网站

1
2
3
4
if(oSession.HostnameIs("www.baidu.com")) 
{
oSession["ui-color"] = "red";
}

5. 自动保存某个接口的数据到本地

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (oSession.fullUrl.Contains("www.baidu.com/playurl/v1/") )
{
//消除保存的请求可能存在乱码的情况
oSession.utilDecodeResponse();
var fso;
var file;
fso = new ActiveXObject("Scripting.FileSystemObject");
//文件保存路径,可自定义
file = fso.OpenTextFile("D:\\Sessions.txt",8 ,true, true);
//file.writeLine("Response code: " + oSession.responseCode);
file.writeLine("Response body: " + oSession.GetResponseBodyAsString());
file.writeLine("\n");
file.close();
}

Nsq搭建与使用

发表于 2019-03-05 | 分类于 go,消息队列

1. 下载

下载之后解压,并将其bin路径添加到环境变量当中

github地址: https://github.com/nsqio/nsq/releases

文档: https://nsq.io/overview/quick_start.html

2. 使用说明

2.1 启动nsqlookupd

1
nsqlookupd

它会监听两个端口: http: 4161 客户端用它来发现和管理。 tcp: 4160 nsqd 用它来广播

可选参数:

  • http-address="127.0.0.1:4161" : 监听 HTTP 客户端地址
  • inactive-producer-timeout=5m0s: 从上次 ping 之后,生产者驻留在活跃列表中的时长
  • tcp-address="0.0.0.0:4160": TCP 客户端监听的地址
  • broadcast-address: 这个 lookupd 节点的外部地址, (默认主机名)
  • tombstone-lifetime=45s: 生产者保持 tombstoned 的时长
  • verbose=false: 允许输出日志
  • version=false: 打印版本信息

2.2 启动nsqd

1
nsqd --lookupd-tcp-address=127.0.0.1:4160

它是一个守护进程,负责接收消息,传递消息给客户端,排队。 会监听两个端口: http: 4151, tcp: 4150

3.3 启动nsqadmin

1
nsqadmin --lookupd-http-address=127.0.0.1:4161

它是一个Web页面,负责管理我们的消息队列, 它后面的地址即是我们在 nsqlookupd 里面http-address参数配置的地址,nsqadmin的监听地址为4171,通过127.0.0.1:4171地址可打开NSQ的Web管理页面

3. 基本使用

Channel是消费者订阅特定Topic的一种抽象。对于发往Topic的消息,nsqd向该Topic下的所有Channel投递消息,而同一个Channel只投递一次,Channel下如果存在多个消费者,则随机选择一个消费者做投递。这种投递方式可以被用作消费者负载均衡。和Topic一样,Channel同样有永久和临时之分,永久的Channel只能通过显式删除销毁,临时的Channel在最后一个消费者断开连接的时候被销毁

  • Topic 就是一个通道,我们可以往这个Topic里面发送消息
  • Channel起到一个负载均衡的作用,我们可以在一个Topic中建立多个Channel来共同消费这个Topic里面的消息。


    我们建立了一个叫 test-dev的Topic,Channel为default

往通道里面发送消息

1
curl -d 'hello world' 'http://127.0.0.1:4151/pub?topic=test-dev'

从通道中消费消息,这里我们要指定从哪个Channel里消费,

1
nsq_to_file --topic=test-dev -channel=default --output-dir=log --lookupd-http-address=127.0.0.1:4161

此时就会在当前目前下生成一个 log 文件夹,里面存放的就是我们这个Channel里的消息

4. 封装代码

安装第三方库

1
go get -u github.com/youzan/go-nsq
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
package nsq

import (
"flag"
"fmt"
"sync"
"time"

"github.com/youzan/go-nsq"
)

func init() {
flag.StringVar(&env,"env","dev","the nsq environment")
}

var (
env string
pubMu = &sync.RWMutex{}
pubMgrs = make(map[string]*nsq.TopicProducerMgr)
consumers = &sync.Map{}
)

type Config struct {
config *nsq.Config
lookAddr string
}

var DefaultConfig = func() *Config {
cfg := nsq.NewConfig()
return &Config{
config: cfg,
lookAddr: "127.0.0.1:4161",
}
}()

func getPubMgr(topic string)(*nsq.TopicProducerMgr,error){
pubMu.RLock()
if pubMgr,ok := pubMgrs[topic];ok {
pubMu.RUnlock()
return pubMgr,nil
}
pubMu.RUnlock()

pubMu.Lock()
defer pubMu.Unlock()

pubMgr,err := nsq.NewTopicProducerMgr([]string{topic},DefaultConfig.config)
if err != nil {
return nil,err
}
err = pubMgr.ConnectToNSQLookupd(DefaultConfig.lookAddr)
if err != nil {
return nil,err
}
pubMgrs[topic] = pubMgr

return pubMgr,nil
}

func wrapTopic(topic string) string {
return fmt.Sprintf("%s-%s",topic,env)
}

func Publish(topic string, data []byte) error {
topic = wrapTopic(topic)
pubMgr, err := getPubMgr(topic)
if err != nil {
return err
}
return pubMgr.Publish(topic,data)
}

func Consume(topic,channel string, handlerFunc nsq.HandlerFunc,concurrency int) error {
topic = wrapTopic(topic)
consumer, err := nsq.NewConsumer(topic, channel, DefaultConfig.config)
if err != nil {
return err
}
consumer.AddConcurrentHandlers(handlerFunc,concurrency)
// set the consumer to map for close
key := topic+":"+channel
consumers.Store(key,consumer)

return consumer.ConnectToNSQLookupd(DefaultConfig.lookAddr)
}

func Close() {
// 记录关闭过得mgr,每个pubMgr仅可被关闭一次
closedPubMgrs := &sync.Map{}
pubMu.RLock()
for _,pubMgr := range pubMgrs{
if _,ok := closedPubMgrs.Load(pubMgr);ok {
continue
}
closedPubMgrs.Store(pubMgr, struct{}{})
pubMgr.Stop()
}
pubMu.RUnlock()

// close the consumer
consumers.Range(func(key, value interface{}) bool {
if consumer,ok := value.(*nsq.Consumer);ok{
consumer.Stop()
select {
case <-consumer.StopChan:
case <-time.After(time.Second * 60):
//等待一分钟让其关闭handler
}
}
consumers.Delete(key)
return true
})
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package nsq

import (
"github.com/youzan/go-nsq"
"testing"
)
func TestPublish(t *testing.T) {
err := Publish("test", []byte("Hello Pibigstar"))
if err != nil {
t.Fatal(err)
}
c := make(chan struct{})

Consume("test", "default", func(msg *nsq.Message) error {
t.Log(string(msg.Body))
select {
case <-c:
default:
close(c)
}
return nil
}, 5)
<-c
}

如果你不想使用有赞的第三方库,你可以使用下面这个:

1
go get -u github.com/nsqio/go-nsq

代码

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
package main

import (
"fmt"
"github.com/nsqio/go-nsq"
"time"
)

var (
addr = "127.0.0.1:4150"
defaultConfig = nsq.NewConfig()
)

func main() {
Producer("test-dev",[]byte("Hello Pibigstar"))
Consumer("test-dev","default",HandleMessage)
time.Sleep(time.Second * 3)
}

func HandleMessage(msg *nsq.Message) error {
fmt.Println(string(msg.Body))
return nil
}

// nsq发布消息
func Producer(topic string,data []byte) error {
// 新建生产者
p, err := nsq.NewProducer(addr, defaultConfig)
if err != nil {
panic(err)
}
// 发布消息
return p.Publish(topic, data)
}

// 消费消息
func Consumer(topic,channel string,handlerFunc nsq.HandlerFunc) error {
//新建一个消费者
c, err := nsq.NewConsumer(topic, channel, defaultConfig)
if err != nil {
panic(err)
}
//添加消息处理
c.AddHandler(handlerFunc)
//建立连接
return c.ConnectToNSQD(addr)
}

AOP使用总结

发表于 2019-03-05 | 分类于 Java

[TOC]

1. AOP简介

AOP是一种编程范式,它主要是为了将非功能模块与业务模块分离开来,让我们更好的管理这些非功能模块。

它的使用场景有:权限控制、日志打印、事务控制、缓存控制、异常处理

2. AOP使用

  1. 在类上注解 @Aspect
  2. 如果想在方法执行前做操作,那么注解@Before注解
  3. 如果想在方法执行后做操作,那么注解@After注解

五大Advice注解:

  • @Before 方法执行前执行该切面方法
  • @After 方法执行后执行该切面方法
  • @AfterThrowing 方法抛出异常后执行该切面方法
  • @AfterReturning 方法返回值后执行该切面方法
  • @Around 环绕注解,集合前面四大注解

2.1 @Aspect

注解在类上,表明此类是一个面向切面的类,同时也要记得在类上注解@Component或者@Service 将此类交给Spring管理

2.2 @Poincut

用来注解在方法上,表明此方法为切面方法

常用表达式有:

1
2
3
4
5
6
7
8
@Poincut("@annotation(myLogger)") //只拦截有注解为@myLogger的方法

@Poincut("within(com.pibigstar.service.*) ") //只拦截com.pibigstar.service包下的方法

//拦截public修饰符,返回值为任意类型,com.pibigstar.servce包下以Service结尾的类的所有方法,方法参数为任意
@Poincut("execution("public * com.pibigstar.service.*Service.*(..)")

@Poincut("bean(*service)") //只拦截所有bean中已service结尾的bean中的方法

Poincut表达式

designators(指示器)常用的表达式

2.3 @Before

在方法执行下执行该切面方法,其用法与@Poincut类似

1
2
3
4
5
6
//只拦截com.pibigstar包下,并且注解为myLogger的方法
@Before("within(com.pibigstar..*) && @annotation(myLogger)")
public void printBeforeLog(MyLogger myLogger) {
String host = IPUtil.getIPAddr(request);
log.info("====IP:"+host+":开始执行:"+myLogger.description()+"=======");
}

2.4 @After

在方法执行后执行该切面方法

1
2
3
4
5
//方法结束后
@After("within(com.pibigstar..*) && @annotation(myLogger)")
public void printAfterLog(MyLogger myLogger) {
log.info("=====结束执行:"+myLogger.description()+"=====");
}

2.5 @AfterThrowing

当方法抛出异常后执行该切面方法

1
2
3
4
@AfterThrowing(@annotation(myLogger)",throwing = "e")
public void printExceptionLog(MyLogger myLogger,Exception e) {
log.info("======执行:"+myLogger.description()+"异常:"+ e +"=====");
}

2.6 @AfterReturning

方法有返回值,打印出方法的返回值

1
2
3
4
5
//方法有返回值
@AfterReturning(value = "@annotation(myLogger)",returning="result")
public void printReturn(Object result) {
log.info("======方法的返回值:"+result+"========");
}

2.7 @Around

集合@Before,@After,@AfterReturning,@AfterThrowing 四大注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//集合前面四大注解
@Around("@annotation(myLogger)")
public Object printAround(ProceedingJoinPoint joinPoint,MyLogger myLogger) {
Object result = null;
try {
log.info("======开始执行:"+myLogger.description()+"=======");
result = joinPoint.proceed(joinPoint.getArgs());
log.info("======方法的返回值:"+result+"========");
} catch (Throwable e) {
e.printStackTrace();
log.info("======执行异常"+ e +"=======");
}finally {
log.info("======结束执行:"+myLogger.description()+"=======");
}
return result;
}

springboot使用定时任务

发表于 2019-03-05 | 分类于 springboot,SpringBoot技能大全

1、pom包配置

pom包里面只需要引入springboot starter包即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

2、启动类启用定时

在启动类上面加上@EnableScheduling即可开启定时

1
2
3
4
5
6
7
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

3、创建定时任务实现类

  • 定时任务1:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Component
    public class SchedulerTask {
    private int count=0;
    /**
    * 分 时 日 月 周
    * 每隔一分钟执行一次(1 * * * *)
    * 每星期五则每小时执行一次(* 1 * * 5)
    **/
    @Scheduled(cron="1 * * * * ?")
    private void process(){
    System.out.println("this is scheduler task runing "+(count++));
    }

    }
  • 定时任务2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component
    public class Scheduler2Task {
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    @Scheduled(fixedRate = 6000)
    public void reportCurrentTime() {
    System.out.println("现在时间:" + dateFormat.format(new Date()));
    }

    }

结果如下:

1
2
3
4
5
6
7
8
this is scheduler task runing  0
现在时间:09:44:17
this is scheduler task runing 1
现在时间:09:44:23
this is scheduler task runing 2
现在时间:09:44:29
this is scheduler task runing 3
现在时间:09:44:35

参数说明

@Scheduled参数可以接受两种定时的设置,一种是我们常用的cron=”*/6 * * * * ?”
,另一种是 fixedRate = 6000,两种都表示每隔六秒打印一下内容。

fixedRate 说明

@Scheduled(fixedRate = 6000) :上一次开始执行时间点之后6秒再执行
@Scheduled(fixedDelay = 6000) :上一次执行完毕时间点之后6秒再执行
@Scheduled(initialDelay=1000, fixedRate=6000) :第一次延迟1秒后执行,之后按fixedRate的规则每6秒执行一次

使用frp进行内网穿透

发表于 2019-02-28 | 分类于 工具使用,树莓派

环境:

  • 具有公网IP的服务器
  • Centos7
  • 需要内网穿透的客户端

文档

https://github.com/fatedier/frp/blob/master/README_zh.md

  • 将 frps、frps.ini 及 frps.service 放到具有公网 IP 的机器上。

  • 将 frpc 、frpc.ini 及 frpc.service 放到处于内网环境的机器上。

文件位置:

公网机器:

  • frps /usr/bin/frps
  • frps.ini /etc/frp/frps.ini
  • frps.service /lib/systemd/system/frps.service

    内网机器

  • frpc /usr/bin/frpc
  • frpc.ini /etc/frp/frpc.ini
  • frpc.service /lib/systemd/system/frpc.service

1. 服务端

下载

1
https://github.com/fatedier/frp/releases

解压

1
tar -zxvf frp_0.28.2_linux_amd64.tar.gz

移动位置

1
mv frps /usr/bin/frps

修改配置

  • frps.ini
    1
    2
    3
    4
    5
    [common]
    bind_addr = 0.0.0.0
    bind_port = 9800
    # 将8080端口作为内网访问端口
    vhost_http_port = 8080

移动位置

1
mkdir -p /etc/frp & mv frps.ini /etc/frp/frps.ini
  • systemd/frps.service
    主要修改ExecStart
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [Unit]
    Description=Frp Server Service
    After=network.target

    [Service]
    Type=simple
    User=nobody
    Restart=on-failure
    RestartSec=5s
    ExecStart=/usr/bin/frps -c /etc/frp/frps.ini

    [Install]
    WantedBy=multi-user.target

移动位置

1
mv frps.service /lib/systemd/system/frps.service

启动

1
2
3
4
5
6
7
8
# 注册系统服务
systemctl enable frps.service
# 开启
systemctl start frps.service
# 查看状态
systemctl status frps.service
# 关闭
systemctl stop frps.service

2. 客户端

下载

1
https://github.com/fatedier/frp/releases

移动文件

1
mv frpc /usr/bin/frpc

修改配置文件

  • frpc.ini
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [common]
    server_addr = 139.189.64.253 # 公网服务器地址
    server_port = 9800 # frps.ini 中定义的 bind_port

    # ssh连接
    [ssh]
    type = tcp
    local_ip = 127.0.0.1
    local_port = 22
    remote_port = 6000
    # 内网web服务暴露于
    [web]
    type = http
    local_port = 80
    custom_domains = blog.pibigstar.com

将 blog.pibigstar.com的域名 A 记录解析到 我们公网IP地址: x.x.x.x
通过浏览器访问 http://blog.pibigstar.com:8080 即可访问到处于内网机器上的 web 服务。

ssh连接
ssh -oPort=6000 root@139.189.65.203

移动位置

1
mkdir -p /etc/frp & mv frpc.ini /etc/frp/frpc.ini
  • frpc.service
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    [Unit]
    Description=Frp Client Service
    After=network.target

    [Service]
    Type=simple
    User=nobody
    Restart=on-failure
    RestartSec=5s
    ExecStart=/usr/bin/frpc -c /etc/frp/frpc.ini
    ExecReload=/usr/bin/frpc reload -c /etc/frp/frpc.ini

    [Install]
    WantedBy=multi-user.target

移动位置

1
mv frpc.service /lib/systemd/system/frpc.service

启动

1
2
3
4
5
6
7
8
# 注册系统服务
systemctl enable frpc.service
# 开启
systemctl start frpc.service
# 查看状态
systemctl status frpc.service
# 关闭
systemctl stop frpc.service

面试总结---编程语言部分(java)

发表于 2019-02-26 | 分类于 面试

[TOC]

1. java集合

1.1 常用集合的导图

java集合导图

1.2 Collection和Map

java集合类分为collection 和 map两类

Collection

  • List(有序、可重复)

    • ArrayList :线程不安全,底层用数组实现,按插入先后排序,随机访问快,插入与删除慢。
    • LinkedList :线程不安全的,底层基于链表的数据结构实现,访问慢但插入元素与删除元素比较快。LinkedList类实现了Queue接口,可当做队列使用
    • Vector :线程安全的, 实现了Cloneable接口,即实现clone()函数。它能被克隆。Vector 实现Serializable接口,支持序列化。
  • Set(无序、不可重复)

    • HashSet :线程不安全的,值可以为null,层使用数据结构的hash算法实现的,因此具有很好的存取,查找的性能。在add对一个对象的添加要重写equals和hashcode,保证对象不重复性。
    • TreeSet :线程不安全的, 底层使用数据结构红黑树算法进行维护的。可通过实现compareTo(Object obj) 来进行来进行排序
    • LinkedHashSet :线程不安全的, 底层使用链表来进行维护的。里面的顺序是值插入的顺序,插入的时候性能不如hashSet而查询的时候性能会比hashSet更好。
  • Queue(队列,先进先出)

    • PriorityQueue :保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序

    常用队列操作方法:

    1
    2
    3
    4
    5
    6
    void add(Object e);//将指定元素加入此队列的尾部。
    Object element();//获取队列头部的元素,但是不删除该元素。
    boolean offer(Object e);//将指定元素加入此队列的尾部。当使用有容量限制的队列时,此方法通常比add(Object e)方法更好。
    Object peek(); //获取队列头部的元素,但是不删除该元素,如果此队列为空,则返回null。
    Object poll();//获取队列头部的元素,并删除该元素,如果此队列为空,则返回null。
    Object remove();//获取队列头部的元素,并删除该元素。

Map

  • HashMap :线程不安全的,基于哈希表实现。Key值和value值都可以为null
  • HashTable :线程安全的,基于哈希表实现。Key值和value值不能为null
  • TreeMap :线程不安全的,保存元素key-value不能为null,允许key-value重复;遍历元素时随机排列

2. java并发

  • 并行:表示两个线程同时做事情。
  • 并发:表示一会做这个事情,一会做另一个事情,存在着调度。单核 CPU 不可能存在并行(微观上)。

java实现多线程的两种方式:继承 Thread 类和实现 Runnable 接口

2.1 java多线程中名词的概念

多个进程或线程同时(或着说在同一段时间内)访问同一资源会产生并发问题。
具体详细内容可点击这里查看

  • 临界区

    临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。

  • 阻塞与非阻塞

阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其它所有需要这个资源的线程就必须在这个临界区中进行等待,等待会导致线程挂起。这种情况就是阻塞。
此时,如果占用资源的线程一直不愿意释放资源,那么其它所有阻塞在这个临界区上的线程都不能工作。阻塞是指线程在操作系统层面被挂起。阻塞一般性能不好,需大约8万个时钟周期来做调度。
非阻塞则允许多个线程同时进入临界区。

  • 死锁

死锁是进程死锁的简称,是指多个进程循环等待他方占有的资源而无限的僵持下去的局面。

  • 活锁

假设有两个线程1、2,它们都需要资源 A/B,假设1号线程占有了 A 资源,2号线程占有了 B 资源;由于两个线程都需要同时拥有这两个资源才可以工作,为了避免死锁,1号线程释放了 A 资源占有锁,2号线程释放了 B 资源占有锁;此时 AB 空闲,两个线程又同时抢锁,再次出现上述情况,此时发生了活锁。

简单类比,电梯遇到人,一个进的一个出的,对面占路,两个人同时往一个方向让路,来回重复,还是堵着路。
如果线上应用遇到了活锁问题,恭喜你中奖了,这类问题比较难排查。

  • 饥饿

饥饿是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。

2.2 java实现并发的方式

一般都是通过加锁进行实现。

2.2.1 synchronize关键字

  • 用法1
1
2
3
4
5
	public class Test{
public synchronized void print(){
....;
}
}
  • 用法2
1
2
3
4
5
6
7
public class Test{
public void print(){
synchronized(this){//锁住本对象
...;
}
}
}
  • 用法3
1
2
3
4
5
6
7
public class Test{
private String a = "test";
public void print(){
synchronized(a){//锁住a对象
...;
}
}
  • 静态方法的锁
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test{
public synchronized static void execute(){
...;
}
}
//效果同
public class Test{
public static void execute(){
synchronized(Test.class){
...;
}
}
}

2.2.2 java.util.Lock的使用

Lock能实现synchronized完成的所以功能,不同点是:Lock有比synchronize更精确的线程语义和更好的性能,synchronize会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。

1
2
3
4
5
6
7
8
9
final Lock lock = new ReentrantLock();
while(true){
lock.lock();//加锁
try{
j--;
}finally{
lock.unlock();//释放锁
}
}

2.2.3 使用线程池

通过上面的介绍,完全可以开发一个多线程的程序,为什么还要引入线程池呢。主要是因为上述单线程方式存在以下几个问题:

  • 线程的工作周期:线程创建所需时间为 T1,线程执行任务所需时间为 T2,线程销毁所需时间为 T3,往往是 T1+T3 大于 T2,所有如果频繁创建线程会损耗过多额外的时间;
  • 如果有任务来了,再去创建线程的话效率比较低,如果从一个池子中可以直接获取可用的线程,那效率会有所提高。所以线程池省去了任务过来,要先创建线程再去执行的过程,节省了时间,提升了效率;
  • 线程池可以管理和控制线程,因为线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控;
  • 线程池提供队列,存放缓冲等待执行的任务。

四种线程池的使用

  • newCachedThreadPool :创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

    1
    2
    3
    4
    5
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  
    cachedThreadPool.execute(new Runnable() {
    public void run() {
    System.out.println(index);
    }
  • newFixedThreadPool :创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

1
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
  • newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行

    1
    2
    3
    4
    5
    6
    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);  
    scheduledThreadPool.schedule(new Runnable() {
    public void run() {
    System.out.println("delay 3 seconds");
    }
    }, 3, TimeUnit.SECONDS); //延迟3秒执行
  • newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

    1
    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

2.2.4 wait 和 sleep 理解

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object
类中的。 sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态
依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对
此对象调用notify()方法或notifyAll()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

3. java异常

3.1 异常类导图

异常导图

3.2 异常的使用

一般都是通过try···catch···finally 进行处理

3.2.1 try、catch、finally

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try{
//try块中放可能发生异常的代码。
//如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。
//如果发生异常,则尝试去匹配catch块。

}catch(SQLException SQLexception){
//每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。
//catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。
//在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。
//如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。
//如果try中没有发生异常,则所有的catch块将被忽略。
}catch(Exception exception){
//...
}finally{
//finally块通常是可选的。
//无论异常是否发生,异常是否匹配被处理,finally都会执行。
//一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。
//finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。
}

需要注意的是:

  1. try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用。
  2. 每一个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会得到执行。
  3. try{}里有一个return语句,那么紧跟在这个try后的finally{}里代码会在return之前执行,更准确的说,是在return中间执行,看下面例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public int test(){
    int x = 1;
    try{
    return x;
    }finally{
    System.out.print("执行了");
    ++x;
    }
    }
    //执行结果是 :执行了
    //但是 x = 1

3.2.2 抛出异常

  1. throws 抛到上一层(用在方法上)

    1
    2
    3
    4
    5
    public void foo() throws SQLException, IOException,ClassNotFloundException
    {
    //foo内部可以抛出 SQLException, IOException,ClassNotFloundException 类的异常
    //或者他们的子类的异常对象。
    }
  2. throw 抛出异常 (用在方法内部)

    1
    2
    3
    4
    public void save(User user){
    if(user == null)
    throw new IllegalArgumentException("User对象为空");
    }

4. java反射

4.1 反射的概述

反射是框架设计的灵魂

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.

以上的总结就是什么是反射
反射就是把java类中的各种成分映射成一个个的Java对象

例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。
(其实:一个类中这些成员方法、构造方法、在加入类中都有一个类来描述)
如图是类的正常加载过程:反射的原理在与class对象。
熟悉一下加载的时候:Class对象的由来是将class文件读入内存,并为之创建一个Class对象。
这里写图片描述

4.2 反射中常用的方法

4.2.1 获得Class对象

  1. Object ——> getClass();

    1
    2
    Student stu1 = new Student();//产生一个Student对象,一个Class对象。  
    Class stuClass1 = stu1.getClass();//获得Class对象
  2. 任何数据类型(包括基本数据类型)都有一个“静态”的class属性

1
Class stuClass2 = Student.class;
  1. 通过Class类的静态方法:forName(String className)(常用)
    1
    Class stuClass3 = Class.forName("fanshe.Student");

4.2.2 获得构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
//1.加载Class对象  
Class clazz = Class.forName("fanshe.Student");

//可传递参数,根据参数的不同,获得的构造方法对象也不同
//2.获取所有公有构造方法
Constructor[] conArray = clazz.getConstructors();
//获得所有构造方法,包括公有和私有
conArray = clazz.getDeclaredConstructors();
//Constructor con = clazz.getConstructors(char.Class);
//如果是私有构造,可通过下面设置,即可调用newInstance
//con.setAccessible(true);//暴力访问(忽略掉访问修饰符)
//调用构造方法
Object obj = con.newInstance();

4.2.3 获得成员属性并设值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1.获取Class对象  
Class stuClass = Class.forName("fanshe.field.Student");
//2.获取所有公有的属性
//Field[] fieldArray = stuClass.getFields();
//获取所有的属性(包括私有、受保护、默认的)
//Field[] allFieldArray = stuClass.getDeclaredFields();
//获取name属性
Field f = stuClass.getField("name");
//获取构造方法并调用产生一个对象
Object obj = stuClass.getConstructor().newInstance();
//为obj对象的name属性设置值
f.set(obj, "刘德华");
Student stu = (Student)obj;
System.out.println("验证姓名:" + stu.name);
//获取私有的属性
f = stuClass.getDeclaredField("phoneNum");
f.setAccessible(true);//暴力反射,解除私有限定
f.set(obj, "18888889999");
System.out.println("验证电话:" + stu.phoneNum);

4.2.4 获得成员方法并调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//获取Class对象  
Class stuClass = Class.forName("fanshe.method.Student");
//获取所有公有方法
//Method[] methodArray = stuClass.getMethods();
//获取所有的方法,包括私有的
//methodArray = stuClass.getDeclaredMethods();
//获取公有的show()方法,它有一个参数,为String类型的
Method m = stuClass.getMethod("show", String.class);
//实例化一个Student对象
Object obj = stuClass.getConstructor().newInstance();
//调用show 方法,并传递一个参数
m.invoke(obj, "刘德华");
//获取私有的show()方法,它有一个参数为int类型的
m = stuClass.getDeclaredMethod("show", int.class);
m.setAccessible(true);//解除私有限定
//调用并得到返回值
Object result = m.invoke(obj, 20);
System.out.println("返回值:" + result);

4.2.5 获得main 方法

1
2
3
4
5
6
//1.获取Student对象的字节码  
Class clazz = Class.forName("fanshe.main.Student");
//2.获取main方法
Method methodMain = clazz.getMethod("main", String[].class);
//3.调用main方法
methodMain.invoke(null, (Object)new String[]{});

5. java常用的设计模式

5.1 单例模式

单例模式可以保证系统中一个类只有一个实例。即一个类只有一个对象实例

  1. 饿汉模式
    1
    2
    3
    4
    5
    6
    7
    public class Singleton{
    private static Singleton instance = new Singleton(){}
    private Singleton(){}//将构造私有化
    public static Singleton getInstance(){
    return instance;
    }
    }

对于饿汉模式来说,这种写法已经很‘perfect’了,
唯一的缺点就是,由于instance的初始化是在类加载时进行的,
类加载是由ClassLoader来实现的,如果初始化太早,就会造成资源浪费

  1. 懒汉模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance(){
    if(instance==null){
    return new Singleton();
    }
    return instance;
    }
    }

这种写法在单线程的时候是没问题的。
但是,当有多个线程一起工作的时候,如果有两个线程同时运行到 if (instance == null),
都判断为null(第一个线程判断为空之后,并没有继续向下执行,当第二个线程判断的时候instance依然为空)
最终两个线程就各自会创建一个实例出来。这样就破环了单例模式 实例的唯一性,

  1. 双重检查模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Singleton{
    //volatile关键字的一个作用是禁止指令重排,
    //把instance声明为volatile之后,对它的写操作就会有一个内存屏障
    //这样,在它的赋值完成之前,就不用会调用读操作。
    private static volatile Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance(){
    if(instance==null){
    sysnchronized(Singleton.class){
    if(instance==null){
    return new Singleton();
    }
    }
    }
    return instance;
    }
    }
  2. 静态内部类实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Singleton {
    private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static final Singleton getInstance() {
    return SingletonHolder.INSTANCE;
    }
    }

它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现

5.2 工厂模式

5.3 代理模式

5.4 适配器模式

5.5 状态模式

5.6 观察者模式

具体内容请移步:

java23中模式详解

java23中模式例子

1…101112…14
派大星

派大星

137 日志
64 分类
230 标签
GitHub CSDN
© 2019 派大星