潘逸安的博客

潘逸安

goframe配置数据库驱动

Mysql

1
go get github.com/gogf/gf/contrib/drivers/mysql/v2@latest

main.go 中引入

1
2
3
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"


pgsql

1
go get github.com/gogf/gf/contrib/drivers/pgsql/v2@latest

all

1
2
3
4
5
6
7
8
9
10
11
12
13
# Easy for copying:
go get github.com/gogf/gf/contrib/drivers/clickhouse/v2@latest
go get github.com/gogf/gf/contrib/drivers/dm/v2@latest
go get github.com/gogf/gf/contrib/drivers/gaussdb/v2@latest
go get github.com/gogf/gf/contrib/drivers/mariadb/v2@latest
go get github.com/gogf/gf/contrib/drivers/mssql/v2@latest
go get github.com/gogf/gf/contrib/drivers/oceanbase/v2@latest
go get github.com/gogf/gf/contrib/drivers/oracle/v2@latest
go get github.com/gogf/gf/contrib/drivers/pgsql/v2@latest
go get github.com/gogf/gf/contrib/drivers/sqlite/v2@latest
go get github.com/gogf/gf/contrib/drivers/sqlitecgo/v2@latest
go get github.com/gogf/gf/contrib/drivers/tidb/v2@latest

golang请求http

我来详细介绍一下 Go 中的 HTTP 请求实现:

1. 基本 GET 请求

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

import (
"fmt"
"io"
"net/http"
"time"
)

func main() {
// 简单的 GET 请求
resp, err := http.Get("https://api.github.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
fmt.Printf("Status: %s\n", resp.Status)
fmt.Printf("Body: %s\n", body[:100])
}

2. 自定义客户端和请求

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

import (
"fmt"
"io"
"net/http"
"net/url"
"time"
"strings"
)

func main() {
// 创建自定义客户端
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: false,
},
}

// 构建 POST 请求
data := url.Values{}
data.Set("username", "admin")
data.Set("password", "secret")

req, err := http.NewRequest(
"POST",
"https://httpbin.org/post",
strings.NewReader(data.Encode()),
)
if err != nil {
panic(err)
}

// 设置请求头
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", "MyGoClient/1.0")

// 发送请求
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()

// 处理响应
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}

3. 处理 JSON 请求和响应

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

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)

// 定义结构体
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}

type Post struct {
ID int `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
UserID int `json:"userId"`
}

func main() {
// 发送 JSON 数据
user := User{
Name: "John Doe",
Email: "john@example.com",
}

jsonData, _ := json.Marshal(user)

req, err := http.NewRequest(
"POST",
"https://jsonplaceholder.typicode.com/users",
bytes.NewBuffer(jsonData),
)
if err != nil {
panic(err)
}

req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()

// 解析 JSON 响应
var result User
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}

fmt.Printf("Created user: %+v\n", result)
}

4. 处理不同类型的请求

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

import (
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
)

func main() {
// GET with query parameters
params := url.Values{}
params.Add("page", "1")
params.Add("limit", "10")

resp, _ := http.Get("https://httpbin.org/get?" + params.Encode())
defer resp.Body.Close()
fmt.Println("GET request completed")

// POST with form data
formData := url.Values{
"key1": {"value1"},
"key2": {"value2"},
}

resp, _ = http.PostForm("https://httpbin.org/post", formData)
defer resp.Body.Close()
fmt.Println("POST form request completed")

// 文件上传
uploadFile("test.txt", "https://httpbin.org/post")
}

func uploadFile(filename string, targetURL string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()

body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", filepath.Base(filename))
if err != nil {
return err
}

io.Copy(part, file)
writer.Close()

req, err := http.NewRequest("POST", targetURL, body)
if err != nil {
return err
}

req.Header.Set("Content-Type", writer.FormDataContentType())

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

fmt.Println("File upload completed")
return nil
}

5. 并发请求和错误处理

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

import (
"context"
"fmt"
"net/http"
"sync"
"time"
)

func main() {
urls := []string{
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
}

var wg sync.WaitGroup
results := make(chan string, len(urls))

for _, url := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
if result, err := fetchWithTimeout(url, 5*time.Second); err != nil {
results <- fmt.Sprintf("Error fetching %s: %v", url, err)
} else {
results <- fmt.Sprintf("Success: %s - %s", url, result)
}
}(url)
}

go func() {
wg.Wait()
close(results)
}()

for result := range results {
fmt.Println(result)
}
}

func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", err
}

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

return resp.Status, nil
}

6. 高级特性 - 中间件和重试

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

import (
"fmt"
"io"
"net/http"
"time"
)

// 重试中间件
func withRetry(maxRetries int, client *http.Client) *http.Client {
client.Transport = &retryTransport{
maxRetries: maxRetries,
transport: http.DefaultTransport,
}
return client
}

type retryTransport struct {
maxRetries int
transport http.RoundTripper
}

func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
for i := 0; i <= t.maxRetries; i++ {
resp, err := t.transport.RoundTrip(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}

if i < t.maxRetries {
time.Sleep(time.Duration(i*i) * 100 * time.Millisecond)
}
}
return nil, fmt.Errorf("max retries exceeded")
}

func main() {
// 使用重试机制的客户端
client := &http.Client{
Timeout: 30 * time.Second,
}
client = withRetry(3, client)

// 设置代理
// proxyURL, _ := url.Parse("http://proxy:8080")
// client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)}

resp, err := client.Get("https://httpbin.org/status/500")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer resp.Body.Close()

fmt.Printf("Final status: %s\n", resp.Status)
}
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
package main

import (
"fmt"
"net/http"
"net/http/cookiejar"
)

func main() {
// 创建 Cookie Jar
jar, _ := cookiejar.New(nil)

client := &http.Client{
Jar: jar,
}

// 第一次请求会设置 Cookie
resp, _ := client.Get("https://httpbin.org/cookies/set?name=value")
defer resp.Body.Close()

// 后续请求会自动携带 Cookie
resp, _ = client.Get("https://httpbin.org/cookies")
defer resp.Body.Close()

fmt.Println("Cookies handled automatically")
}

8. 性能优化建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"net/http"
"time"
)

func createOptimizedClient() *http.Client {
return &http.Client{
Timeout: time.Second * 30,
Transport: &http.Transport{
MaxIdleConns: 100, // 最大空闲连接数
MaxConnsPerHost: 10, // 每个主机最大连接数
MaxIdleConnsPerHost: 10, // 每个主机最大空闲连接数
IdleConnTimeout: 90 * time.Second, // 空闲连接超时时间
TLSHandshakeTimeout: 10 * time.Second, // TLS握手超时
ExpectContinueTimeout: 1 * time.Second,
},
}
}

最佳实践

  1. 总是关闭响应体:使用 defer resp.Body.Close()
  2. 设置超时:避免请求阻塞
  3. 使用 context:支持取消和超时
  4. 重用 Client:避免为每个请求创建新客户端
  5. 处理错误:检查所有可能返回错误的操作
  6. 限制并发:避免过多并发请求
  7. 启用连接池:提高性能

这些示例涵盖了 Go 中 HTTP 请求的常见用法,你可以根据具体需求进行调整和扩展。

golang时间

在 Go 中获取当前时间有以下几种常用方式:

1. 获取当前时间(包含时区信息)

1
2
3
4
5
6
7
// 获取当前时间(本地时区)
now := time.Now()
fmt.Println(now) // 2023-10-01 15:30:45.123456 +08:00

// 获取当前 UTC 时间
utcNow := time.Now().UTC()
fmt.Println(utcNow) // 2023-10-01 07:30:45.123456 +0000 UTC

2. 获取时间的不同组成部分

1
2
3
4
5
6
7
8
9
10
now := time.Now()

year := now.Year() // 2023
month := now.Month() // October 或 10
day := now.Day() // 1
hour := now.Hour() // 15
minute := now.Minute() // 30
second := now.Second() // 45
nanosecond := now.Nanosecond() // 123456
weekday := now.Weekday() // Sunday

3. 格式化时间

1
2
3
4
5
6
7
8
now := time.Now()

// 格式化为字符串
fmt.Println(now.Format("2006-01-02 15:04:05")) // 2023-10-01 15:30:45
fmt.Println(now.Format("2006/01/02")) // 2023/10/01
fmt.Println(now.Format("15:04:05")) // 15:30:45
fmt.Println(now.Format(time.RFC3339)) // 2023-10-01T15:30:45+08:00
fmt.Println(now.Format(time.RFC1123)) // Sun, 01 Oct 2023 15:30:45 CST

4. 获取时间戳

1
2
3
4
5
6
7
8
9
10
11
12
13
now := time.Now()

// 秒级时间戳
timestamp := now.Unix() // 1696159845

// 毫秒级时间戳
milliTimestamp := now.UnixMilli() // 1696159845123

// 微秒级时间戳
microTimestamp := now.UnixMicro() // 1696159845123456

// 纳秒级时间戳
nanoTimestamp := now.UnixNano() // 1696159845123456789

5. 常用操作示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取今天开始时间(00:00:00)
today := time.Now().Truncate(24 * time.Hour)
fmt.Println(today) // 2023-10-01 00:00:00 +0800 CST

// 获取本月第一天
year, month, _ := time.Now().Date()
firstDayOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, time.Now().Location())

// 计算时间差
start := time.Now()
// ... 执行一些操作
duration := time.Since(start)
fmt.Printf("耗时: %v\n", duration)

6. 完整示例

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

import (
"fmt"
"time"
)

func main() {
// 获取当前时间
now := time.Now()

// 显示各种格式
fmt.Printf("当前时间: %v\n", now)
fmt.Printf("格式化: %s\n", now.Format("2006-01-02 15:04:05"))
fmt.Printf("时间戳(秒): %d\n", now.Unix())
fmt.Printf("时间戳(毫秒): %d\n", now.UnixMilli())

// 获取各部分
fmt.Printf("日期: %d年%d月%d日\n", now.Year(), now.Month(), now.Day())
fmt.Printf("时间: %02d:%02d:%02d\n", now.Hour(), now.Minute(), now.Second())
}

重要提示

  • Go 使用特定的参考时间进行格式化:2006-01-02 15:04:05
  • time.Now() 返回的是本地时间
  • 如果需要 UTC 时间,使用 time.Now().UTC()
  • 时间戳都是从 Unix 纪元(1970-01-01 UTC)开始计算的秒数/毫秒数等
    he

Add Copy Button to Code Blocks in Hexo

Hexo 为代码块添加复制功能按钮
本文详细记录如何在 Hexo 中为代码块添加一个 “复制” 按钮,实现一键复制代码功能。

项目地址:https://github.com/EvannZhongg/Blog-Learning.git

结构要求与适配说明
本复制功能脚本适用于以下结构的代码块:

1
2
3
4
5
6
7
8
<figure class="highlight">
<table>
<tr>
<td class="gutter">...</td>
<td class="code"><pre><code>...</code></pre></td>
</tr>
</table>
</figure>

这是 Hexo 中多数主题(包括 Chic、NexT、Butterfly 等)默认的代码块渲染结构。

如何检查自己主题的结构是否符合?
启动本地博客:hexo s
在浏览器中打开博客页面
右键代码块 → 点击“检查”
查看代码块的外层 HTML 标签是否为 figure.highlight
或者直接在浏览器中点击 F12 ,在 Elements 中直接搜索是否含有 figure.highlight

  1. 创建 JavaScript 脚本文件
    在 Hexo 博客项目的根目录下创建 JS 脚本文件 code-copy.js ,如果没有js文件夹则自己创建:
1
source/js/code-copy.js

并填入以下完整内容:

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
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('figure.highlight').forEach((figure) => {
if (figure.querySelector('.copy-btn')) return;

const copyBtn = document.createElement('button');
copyBtn.className = 'copy-btn';
copyBtn.title = '复制';

// 缩小后的复制图标(14*15)
const copyIcon = `
<svg xmlns="http://www.w3.org/2000/svg" height="14" width="15" viewBox="0 0 24 24" fill="white">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 18H8V7h11v16z"/>
</svg>
`;

// 成功后显示的勾(14*15)
const checkIcon = `
<svg xmlns="http://www.w3.org/2000/svg" height="14" width="15" viewBox="0 0 24 24" fill="#00cc66">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
`;

copyBtn.innerHTML = copyIcon;

// 按钮样式(浅灰底、缩小)
Object.assign(copyBtn.style, {
position: 'absolute',
top: '8px',
right: '8px',
height: '28px',
width: '28px',
padding: '4px',
background: '#aaa',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
opacity: '0.85',
zIndex: 1000,
transition: 'opacity 0.2s ease',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.15)'
});

copyBtn.addEventListener('mouseover', () => copyBtn.style.opacity = '1');
copyBtn.addEventListener('mouseout', () => copyBtn.style.opacity = '0.85');

copyBtn.addEventListener('click', () => {
const code = figure.querySelector('td.code');
const text = code ? code.innerText : '';
navigator.clipboard.writeText(text).then(() => {
copyBtn.innerHTML = checkIcon;
setTimeout(() => {
copyBtn.innerHTML = copyIcon;
}, 1000);
});
});

figure.style.position = 'relative';
figure.appendChild(copyBtn);
});
});

  1. 在页面底部引入 JS 文件
    打开文件:
1
themes/hexo-theme-Chic/layout/_partial/footer.ejs

在 标签之后添加以下代码:

1
<script src="/js/code-copy.js"></script>

这样可以确保复制按钮脚本在页面加载完毕后自动运行。

  1. 生成并本地预览效果
    运行以下命令,重新生成并启动本地预览:
1
2
3
hexo clean
hexo g
hexo d

然后访问 http://localhost:4000 ,查看任意一段代码块,右上角应出现复制图标按钮。

修改后的相关完整代码可以在文章开头的项目地址中获取

该项目代码基于 Hexo 和 hexo-theme-Chic 。

所有软件中国镜像源

WEB

Npm

为中国内地的Node.js开发者准备的镜像配置,大大提高node模块安装速度。

1
npm i -g mirror-config-china --registry=https://registry.npm.taobao.org

前端构建注意事项

1, 修改 下载仓库为淘宝镜像

1
npm config set registry http://registry.npm.taobao.org/

2, 如果要发布自己的镜像需要修改回来

1
npm config set registry https://registry.npmjs.org/

3, 安装cnpm

1
2
3
npm install -g cnpm --registry=https://registry.npm.taobao.org

npm install chromedriver --chromedriver_cdnurl=http://cdn.npm.taobao.org/dist/chromedriver

前端如果装不上node-sass 适当的提高node-sass的版本,可能由于node 的版本过高,装不上node-sass

1
2
3
4
5
brew install yarn

yarn install

yarn add [package_name]

PHP COMPSOER

安装

下载安装脚本

1
php -r "copy('https://install.phpcomposer.com/installer', 'composer-setup.php');"

执行脚本

1
php composer-setup.php

删除脚本

1
php -r "unlink('composer-setup.php');"

复制到系统目录

1
sudo mv composer.phar /usr/local/bin/composer

全局配置(推荐)

  • 所有项目都会使用该镜像地址:
    1
    composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
  • 取消配置:
    1
    composer config -g --unset repos.packagist

GO

1
go env -w GOPROXY=https://goproxy.cn,direct

Python

pip

瓣镜像地址:https://pypi.douban.com/simple/

虽然用easy_install和pip来安装第三方库很方便
它们的原理其实就是从Python的官方源pypi.python.org/pypi 下载到本地,然后解包安装。
不过因为某些原因,访问官方的pypi不稳定,很慢甚至有些还时不时的访问不了。

跟ubuntu的apt和centos的yum有各个镜像源一样,pypi也有。

在国内的强烈推荐豆瓣的源
http://pypi.douban.com/simple/
注意后面要有/simple目录。

使用镜像源很简单,用-i指定就行了:

1
2
sudo easy_install -i http://pypi.douban.com/simple/ ipython
sudo pip install -i http://pypi.douban.com/simple/ --trusted-host=pypi.douban.com/simple ipython

每次都要这样写? no!,做个别名吧,额,类似于这样

1
pip  install  -i  https://pypi.doubanio.com/simple/  --trusted-host pypi.doubanio.com  django

命令设置默认源

1
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

好像还不太好,肿么办?写在配置文件里吧。

    1. linux/mac用户将它命名为pip.conf, windows用户将它命名为pip.ini. 文件中写如下内容:
1
2
3
[global]
timeout = 60
index-url = https://pypi.doubanio.com/simple

** 注意: **如果使用http链接,需要指定trusted-host参数

1
2
3
4
[global]
timeout = 60
index-url = http://pypi.douban.com/simple
trusted-host = pypi.douban.com
    1. 将该文件放置在指定位置.

linux下指定位置为

$HOME/.config/pip/pip.conf
或者
$HOME/.pip/pip.conf

mac下指定位置为

$HOME/Library/Application Support/pip/pip.conf
或者
$HOME/.pip/pip.conf

windows下指定位置为

%APPDATA%\pip\pip.ini
或者
%HOME%\pip\pip.ini

centos

centos7 修改yum源为阿里源
首先是到yum源设置文件夹里

  1. 查看yum源信息:
1
yum repolist
  1. 定位到base reop源位置
1
cd /etc/yum.repos.d
  1. 接着备份旧的配置文件
1
sudo mv CentOS-Base.repo CentOS-Base.repo.bak
  1. 下载阿里源的文件
1
sudo wget -O CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo

安装epel repo源:

1
wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo

5.清理缓存

1
yum clean all

6.重新生成缓存

1
yum makecache
  1. 再次查看yum源信息
  2. yum repolist

java maven

1
2
3
wget https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz --no-check-certificate
https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.9.11/binaries/apache-maven-3.9.11-bin.zip
https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.8.9/binaries/apache-maven-3.8.9-bin.zip

mvn

1
2
3
4
5
6
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>

Jackson使用

属性只读无法修改

如果你使用Jackson和requestbody,在参数类需要忽略的字段上加上注解 jsonproperty access readonly就可以。

1
2
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String number;

追加属性

单独配置
1
2
3
4
5
6
7
8
public class User {
private String number;

@JsonProperty("customNumber")
private String getCustomNumber(){
return "no"+number;
}
}

格式化的时候,会多出一个customNumber的属性

序列化器

根据当前字段名称,写入另外一个字段,一般用于字典的文本描述写入

1
2
3
4
5
6
7
8
9
public class DictSerializer extends JsonSerializer<String> {
@Override
public void serialize(String o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
// 当前字段名称
String currentName = serializerProvider.getGenerator().getOutputContext().getCurrentName();

jsonGenerator.writeStringField(currentName+"Ext", o+"扩展");
}
}

SpringBoot整合redis作为缓存

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
/**
* 8.整合 SpringCache简化缓存开发
* 1.引入依赖
* spring-boot-starter-cache
* spring-boot-starter-data-redis
* 2.写配置
* 1. 自动配置了哪些
* CacheAuthConfiguration 会导入 RedisCacheConfiguration;
* 自动配好了缓存管理器RedisCacheManager
* 2. 配置使用redis作为缓存
* spring.cache.type=redis
* 3. 测试使用缓存
* @Cacheable Triggers Cache population触发将数据保存到缓存的操作
* @CacheEvict 将数据从缓存删除的操作
* @CachePut 不影响方法执行更新操作
* @Caching 组合以上多个操作
* @CacheConfig 在类级别共享缓存的相
* 1.开启缓存功能 @EnableCaching
* 2.只需要使用注解就能完成缓存操作
* 4. 原理
* CacheAutoConfiguration => RedisCacheConfiguration
* 自动配置了 RedisCacheManager -> 初始化所有的缓存 -> 每个缓存决定使用什么配置
* 如果redisCacheConfiguration 有就用已有的,没有就用默认配置
* ->想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可
* ->就会应用到当前RedisCacheManager管理的所有缓存分区中
*/

配置文件类

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
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
@Configuration
public class MyCacheConfig {
/**
* 将配置作为参数传入,从容器中自动获取
*
* @return
*/
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 设置键的序列化方式
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
// 设置值的序列化方式,将类名也序列化
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 将配置文件中的配置生效

// 获取redis的配置
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
// 设置过期时间
if (redisProperties.getTimeToLive() != null) {
System.out.println(redisProperties.getTimeToLive());
config = config.entryTtl(redisProperties.getTimeToLive());
}
// 设置key前缀
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}

// if (!redisProperties.isCacheNullValues()) {
// config = config.disableCachingNullValues();
// }
//
// if (!redisProperties.isUseKeyPrefix()) {
// config = config.disableKeyPrefix();
// }
return config;
}
}

SpringBoot全局跨域配置

通过实现 WebMvcConfigurer ,推荐,不能配置多个,只能配置一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("*")
.allowedMethods("*");
}
}

通过过滤器配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class CorsConfig {
@Bean
CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许所有请求头
config.addAllowedHeader("*");
// 允许所有方式
config.addAllowedMethod("*");
// 允许所有来源
config.addAllowedOrigin("*");
// 允许附带cookie信息
config.setAllowCredentials(true);

// 适用于所有请求
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}

log4j2日志配置

application.yml中指定日志配置文件

1
2
logging:
config: classpath:log4j2.xml

log4j2.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
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">

<properties>
<!-- 日志打印级别 -->

<property name="LOG_LEVEL">INFO</property>
<!-- APP名称 -->
<property name="APP_NAME" value="framework-project"/>
<!-- 日志文件存储路径 -->
<property name="LOG_HOME">./logs/</property>
<!-- 存储天数 -->
<property name="LOG_MAX_HISTORY" value="60d"/>
<!-- 单个日志文件最大值, 单位 = KB, MB, GB -->
<property name="LOG_MAX_FILE_SIZE" value="10 MB"/>
<!-- 每天每个日志级别产生的文件最大数量 -->
<property name="LOG_TOTAL_NUMBER_DAILY" value="10"/>
<!-- 压缩文件的类型,支持zip和gz,建议Linux用gz,Windows用zip -->
<property name="ARCHIVE_FILE_SUFFIX" value="zip"/>
<!-- 日志文件名 -->
<property name="LOG_FILE_NAME" value="${LOG_HOME}"/>
<property name="FILE_NAME_PATTERN" value="${LOG_HOME}%d{yyyy-MM-dd}"/>

<!--
格式化输出:
%date{yyyy-MM-dd HH:mm:ss.SSS}: 简写为%d 日期 2023-08-12 15:04:30,123
%thread: %t 线程名, main
%-5level:%p 日志级别,从左往右至少显示5个字符宽度,不足补空格 INFO
%msg:%m 日志消息 info msg
%n: 换行符
{cyan}: 蓝绿色(青色)
%logger{36}: %c 表示 Logger 名字最长36个字符
%C: 类路径 com.qq.demolog4j2.TestLog4j2
%M: 方法名 main
%F: 类名 TestLog4j2.java
%L: 行号 12
%l: 日志位置, 相当于 %C.%M(%F.%L) com.qq.demolog4j2.TestLog4j2.main(TestLog4j2.java:16)
-->
<!-- %d: 日期
%-5level: 日志级别,显示时占5个字符不足
[%t]: 线程名
%c{1.}: 显示调用者,只显示包名最后一截及方法名,前面的只取首字母
.%M(代码行号%L):
%msg%n": 需要打印的日志信息,换行:INFO>[MsgToMP:99]
Bright: 加粗 -->
<!--日志输出格式-控制台彩色打印-->
<property name="ENCODER_PATTERN_CONSOLE">%blue{%d{yyyy-MM-dd HH:mm:ss.SSS}} | %highlight{%-5level}{ERROR=Bright RED, WARN=Bright Yellow, INFO=Bright Green, DEBUG=Bright Cyan, TRACE=Bright White} | %yellow{%t} | %cyan{%c{1.}} : %white{%msg%n}</property>
<!--日志输出格式-文件-->
<property name="ENCODER_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %5pid --- [%15.15t] %c{1.} [%L] : %m%n</property>
<!--日志输出格式-控制台彩色打印-->
<property name="DEFAULT_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level} %style{%5pid}{bright,magenta} --- [%15.15t] %cyan{%c{1.} [%L]} : %m%n</property>
</properties>

<Appenders>
<!-- 控制台的输出配置 -->
<Console name="Console" target="SYSTEM_OUT">
<!--输出日志的格式-->
<PatternLayout pattern="${DEFAULT_PATTERN}" />
</Console>
<!-- 打印出所有的info及以下级别的信息,每次大小超过size进行压缩,作为存档-->
<RollingFile name="RollingFileAll" fileName="${LOG_FILE_NAME}/${date:yyyy-MM-dd}/info.log" filePattern="${FILE_NAME_PATTERN}/info.log">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="${LOG_LEVEL}" onMatch="ACCEPT" onMismatch="DENY" />
<!--输出日志的格式-->
<PatternLayout pattern="${ENCODER_PATTERN}" />
<Policies>
<!-- 归档每天的文件 -->
<TimeBasedTriggeringPolicy />
<!-- 限制单个文件大小 -->
<SizeBasedTriggeringPolicy size="${LOG_MAX_FILE_SIZE}" />
</Policies>
<!-- 限制每天文件个数 -->
<DefaultRolloverStrategy compressionLevel="9" max="${LOG_TOTAL_NUMBER_DAILY}">
<Delete basePath="${LOG_HOME}" maxDepth="1">
<IfFileName glob=".info.*.log" />
<IfLastModified age="${LOG_MAX_HISTORY}" />
</Delete>
</DefaultRolloverStrategy>
</RollingFile>

<RollingFile name="RollingFileDebug"
fileName="${LOG_FILE_NAME}/${date:yyyy-MM-dd}/debug.log"
filePattern="${FILE_NAME_PATTERN}/debug.log">
<Filters>
<ThresholdFilter level="DEBUG" />
<ThresholdFilter level="INFO" onMatch="DENY"
onMismatch="NEUTRAL" />
</Filters>
<PatternLayout pattern="${ENCODER_PATTERN}" />
<Policies>
<!-- 归档每天的文件 -->
<TimeBasedTriggeringPolicy />
<!-- 限制单个文件大小 -->
<SizeBasedTriggeringPolicy size="${LOG_MAX_FILE_SIZE}" />
</Policies>
<!-- 限制每天文件个数 -->
<DefaultRolloverStrategy compressionLevel="9"
max="${LOG_TOTAL_NUMBER_DAILY}">
<Delete basePath="${LOG_HOME}" maxDepth="1">
<IfFileName glob="*.debug.*.log" />
<IfLastModified age="${LOG_MAX_HISTORY}" />
</Delete>
</DefaultRolloverStrategy>
</RollingFile>

<RollingFile name="RollingFileWarn" fileName="${LOG_FILE_NAME}/${date:yyyy-MM-dd}/warn.log"
filePattern="${FILE_NAME_PATTERN}.warn.log">
<Filters>
<ThresholdFilter level="WARN" />
<ThresholdFilter level="ERROR" onMatch="DENY"
onMismatch="NEUTRAL" />
</Filters>
<PatternLayout pattern="${ENCODER_PATTERN}" />
<Policies>
<!-- 归档每天的文件 -->
<TimeBasedTriggeringPolicy />
<!-- 限制单个文件大小 -->
<SizeBasedTriggeringPolicy size="${LOG_MAX_FILE_SIZE}" />
</Policies>
<!-- 限制每天文件个数 -->
<DefaultRolloverStrategy compressionLevel="9"
max="${LOG_TOTAL_NUMBER_DAILY}">
<Delete basePath="${LOG_HOME}" maxDepth="1">
<IfFileName glob="*.warn.*.log" />
<IfLastModified age="${LOG_MAX_HISTORY}" />
</Delete>
</DefaultRolloverStrategy>
</RollingFile>

<RollingFile name="RollingFileError"
fileName="${LOG_FILE_NAME}/${date:yyyy-MM-dd}/error.log"
filePattern="${FILE_NAME_PATTERN}.error.log">
<Filters>
<ThresholdFilter level="ERROR" />
</Filters>
<PatternLayout pattern="${ENCODER_PATTERN}" />
<Policies>
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="${LOG_MAX_FILE_SIZE}" />
</Policies>
<DefaultRolloverStrategy compressionLevel="9" max="${LOG_TOTAL_NUMBER_DAILY}">
<Delete basePath="${LOG_HOME}" maxDepth="1">
<IfFileName glob="*.error.*.log" />
<IfLastModified age="${LOG_MAX_HISTORY}" />
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</Appenders>

<!--只有定义了logger并引入以上Appenders,Appender才会生效-->

<Loggers>
<!-- 开发环境debug,不是开发环境,删除掉这个logger -->
<Logger name="com.example" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFileAll"/>
<AppenderRef ref="RollingFileDebug"/>
<AppenderRef ref="RollingFileWarn"/>
<AppenderRef ref="RollingFileError"/>
</Logger>
<root level="${LOG_LEVEL}">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileAll"/>
<appender-ref ref="RollingFileDebug"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
</root>
</Loggers>
</configuration>

aop切面日志

依赖添加

1
2
3
4
 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

切面日志类

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
package com.example.yunxiao_deploy_demo;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

@Slf4j
@Aspect
@Component
public class LogAspect {



/**
* 定义切入点
*/
@Pointcut("execution(* *(..)) && @within(org.springframework.web.bind.annotation.RestController)")
public void pointcut(){

}

/**
* @description 在连接点执行之前执行的通知
*/
@Before("pointcut()")
public void before(JoinPoint joinPoint){

}

/**
* @description 在连接点执行之后执行的通知(返回通知)
*/
@AfterReturning(pointcut = "pointcut()", returning = "object")
public void afterReturning(JoinPoint joinPoint, Object object){
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
log.info("请求地址 : " + request.getRequestURL().toString());
log.info("请求参数 : " + Arrays.toString(joinPoint.getArgs()));
ObjectMapper objectMapper = new ObjectMapper();
try {
log.debug("请求结果 : " + objectMapper.writeValueAsString(object));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

/**
* @description 在连接点执行之后执行的通知(异常通知)
*/
@AfterThrowing(pointcut = "pointcut()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex){
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
log.error("请求地址 : " + request.getRequestURL().toString());
log.error("请求参数 : " + Arrays.toString(joinPoint.getArgs()));
if(ex.getStackTrace().length > 0){
StackTraceElement stackTraceElement = ex.getStackTrace()[0];
log.error("异常位置 : " + ex.getMessage() + " :" +stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + "("+stackTraceElement.getLineNumber()+")", ex);
} else {
log.error("异常位置 : " + ex.getMessage());
}
}
}