开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

【windows gdi+】GDI+ Image类加载图片时

发表于 2021-08-02

​

问题描述:

项目里一个控件,需要加载本地图片,单张第一次加载的时候可以的,但是重新选择其他图片,会出现图片显示异常的现象。

直接上图,图片部分显示不全了。

)​

问题分析:

一开始怀疑现象是有个图片重复选择后显示是好的,其他某几个图很容易复现,就怀疑是图片问题,查了半天jpg图片完整性,发现图片也没什么区别,后面又查分辨率,在DrawImage()里修改分辨率为偶数了也不行。。。最后只能看代码了,发现是代码里自己清理GLOBAL内存了。其实之前有项目我遇到清理GLOBAL内存导致图片显示不全或者异常的问题。时间太久忘记了。还是记录一下吧。

知识附录:

1, 是Bitmap(RT_BITMAP)类型的图片无法加载, RT_BITMAP是预定义类型, 资源里面没有bmp文件的头, SizeofResource 的返回值要比图片文件少几个字节,因为少了这几个字节, 所以GDI+会返回invalid parameter错误。

2, 从IStream里面创建出来的Image对象会引用到堆里面的GLOBAL内存, 如果GLOBAL内存hBuffer被释放了, 创建的Image的内容就会被破坏,有时只能画出一小部分图片, 有时整个图片有大片的乱码,好像是编码失败的样子, 视当时的内存状况而定。

另外调用Image的Clone也没用,深层次想Clone不是真正的深拷贝,还是依赖那个GLOBAL内存的,如果那个内存被销毁了,Clone的图片也会异常。

只有在销毁或者析构的时候才能清理那个GLOBAL内存,否则会导致显示异常(不是100%出现,而是间歇性出现,但是肯定会出现)。

这是看到的demo,拷贝过来的,可以看看,注意看注释。

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
ini复制代码​

CYourClass::~CYourClass()
{
    for(IMG_VECTOR::iterator it = m_arImage.begin(); it != m_arImage.end(); it++)
        delete *it;
    for(HGLB_VECTOR::iterator it = m_arGlobal.begin(); it != m_arGlobal.end(); it++)
    {
        ::GlobalUnlock(*it);
        ::GlobalFree(*it);
    }
}


void CYourClass::AddImage(HMODULE hInst, UINT nResourceID, LPCTSTR lpType)
{
    if(lpType == RT_BITMAP)
    {
        //GDI+ can not load RT_BITMAP resouce, 
        //because they are predefined resource, 
        //they don't contains the image file header.
        assert(FALSE);
        return;
    }

    HRSRC hResource = ::FindResource(hInst, MAKEINTRESOURCE(nResourceID), lpType);
    if (!hResource)
        return;

    DWORD imageSize = ::SizeofResource(hInst, hResource);
    if (!imageSize)
        return;

    const void* pResourceData = ::LockResource(::LoadResource(hInst, hResource));
    if (!pResourceData)
        return;

    HGLOBAL hBuffer = ::GlobalAlloc(GMEM_FIXED, imageSize);
    if (NULL == hBuffer)
        return;

    void* pBuffer = ::GlobalLock(hBuffer);
    if (pBuffer)
    {
        CopyMemory(pBuffer, pResourceData, imageSize);
        IStream* pStream = NULL;
        if (::CreateStreamOnHGlobal(hBuffer, FALSE, &pStream) == S_OK)
        {
            Gdiplus::Image * pImage = Gdiplus::Image::FromStream(pStream);
            pStream->Release();
            if (pImage)
            { 
                if (pImage->GetLastStatus() == Gdiplus::Ok &&
                    pImage->GetWidth() > 0)
                {
                    m_arImage.push_back(pImage);

                    //it seems the image will take usage of the global memory.
                    //so the global memory should be kept until the image destroy.
//GDI++此种打开本地文件的方式占用了这片内存,只有在你销毁图片时才能销毁这片
//内存,否则会破坏你的图片,导致显示异常
                    m_arGlobal.push_back(hBuffer);
                    return;
                }

                delete pImage;
            }
        }
        ::GlobalUnlock(hBuffer);
    }
    ::GlobalFree(hBuffer);
}

​

\

​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

什么是IP地址、子网掩码,你知道吗?

发表于 2021-08-02

IP地址

IP作用于OSI参考模型中的网络层,在终端通信中作为唯一标识,便于确定数据的传递目标。

IP地址分为:IPv4 、IPv6;大多数用户熟悉并且流行的IP地址是IPv4,其是用点分四组十进制的表示方法展示的,例如,==192.168.23.4==,==117.43.56.3==等。而IPv6的位数就比较大了,是128位的,长度是IPv4的4倍,并且其表示方式是分块的八组四个十六进制数,例如:5f05:2000:80ad:5800:0058:0800:2023:1d71,每个块儿之间都是用 : 隔开的。如果把IPv6和端口号一起使用的话,可能会出现这样的情况:http://2001:0db8:85a3:08d3:1319:8a2e:0370:7344:443/

这样就没法区分端口号和地址了,所以就用这样的方式来表示一个地址和端口号:http://[2001:0db8:85a3:08d3:1319:8a2e:0370:7344]:443/

ip寻址

在长度为32位的IPv4地址中,有一段连续位称为网络号,还有一段连续位称为主机号。

网络号就是用来表示主机所处的网络区域,而主机号是用来确认表示主机所处当前网络中的特定主机。这样在寻址的时候就可以识别一台主机所处网络以及在网络区域中的具体地址了!

ipv4分类

在IPV4中,网络地址可以分为5类:A类,B类,C类,D类,E类;

  • A类:网络号共8位,首位固定为 0,接下来连续的7位可以自由设定;主机号为24位;地址范围:0.0.0.0~127.255.255.255;因为B类最小值就是前两位为10,后面全为0的数值,也就是128.0.0.0;后面的地址范围依次类推;
  • B类:网络号共16位,前两位固定为 1 0,接下来连续的14位可以自由设定;主机号为16位;
  • C类:网络号共24位,前三位固定为 1 1 0,接下来连续的21位可以自由设定;主机号为8位;
  • D类:为组播地址,共32位,前四位固定为 1 1 1 0;
  • E类: 为保留地址,共32位,前四位固定为 1 1 1 1;

A 、B 、C类地址大多都是单播地址,因此它们可用于接口分配。

就拿A类地址来说,因为A类地址网络号占8位,而首位固定是0,那么剩下的7位都是可以自由分配的,而主机号位数是24位,那么就有2^24个主机数,意思就是A类地址可以分配2 ^7个网络区域,每个网络区域可以有2 ^24台主机。

既然ip地址划分为5类,那么他们地址范围是什么呢?

image-20210802174226354

子网寻址

在一些特殊的场景下,如果一个区域的主机数不能跟实际需求想符合,比如如果我们在C类网络下需要他所能承受范围内的主机数;那么就会造成ip不足;另一方面,如果我们在A类网络区域下,但是主机却只有几台,那么就会造成浪费,这实在是暴殄天物啊!于是我们就采用了子网寻址的方法解决这个问题。

所谓子网寻址就是将一个类别的子网继续划分成多个子网区域,每个区域各自有对应的子网号和主机号;例如,B类网络网络号有16位,主机号有16位,我们可以将16位主机号分为8位子网号和8位主机号,这样将B类网络划分为了很多个子网,更加适用于我们实际的生产环境。

image-20210802204536052

子网掩码

确定一个ip地址的网络/子网部分的结束和主机部分的开始;他的长度和ip地址长度相等(IPV4是32位,ipv6是128位),对于IPv4来说,子网掩码也是用点分四组十进制来表示的。当子网掩码用二进制表示时,结构非常简单且容易记忆,从左边开始由一段连续的1组成,紧接着又一段连续的0组成直到最后。其中连续的1的长度称为前缀长度,用 /长度 来表示

十进制表示 前缀长度 二进制表示
128.0.0.0 /1 10000000 00000000 00000000 00000000
255.0.0.0 /8 11111111 00000000 00000000 00000000
255.192.0.0 /10 11111111 11000000 00000000 00000000
255.255.0.0 /16 11111111 11111111 00000000 00000000
255.255.254.0 /23 11111111 11111111 11111110 00000000
255.255.255.224 /27 11111111 11111111 11111111 11100000
255.255.255.255 /32 11111111 11111111 11111111 11111111

​ 子网掩码二进制位1对应ip地址的网络/子网部分;0对应主机号部分,以子网寻址的例子为例,将B类网络的主机位划分为8位子网号和8位主机位号,那么就会提前设置好一个长度为16+8=24的子网掩码,即255.255.255.0,表示前24位是网络/子网部分,那么路由器在处理ip地址的时候会手先查看子网掩码,将ip地址与子网掩码进行与运算就可以得出该IP地址所属的子网区域。例如,校验128.32.1.14,这个ip就是8位子网号8位主机号,那么与子网掩码255.255.255.0进行与运算就得到128.32.1.0,可以发现该结果和128.32.1.14属于同一网段,也就是处于相同子网。

在实际使用中子网掩码的长度是可变的,我们可以根据实际所需支持的主机数来设置子网掩码的长度。

image-20210802213653725

广播地址

广播地址是专门用于同时向网络中所有工作站进行发送的一个地址。在使用TCP/IP 协议 协议/2116790)的网络中,主机标识段host ID 为全1 的IP 地址为广播地址,广播的分组传送给host ID段所涉及的所有计算机。简单的说该子网段中数值最大的那个数就是广播地址。那么如何取得这个地址呢,就是将子网掩码的所有位取反,然后与子网内的ip进行或运算得到的结果就是广播地址。

image-20210802213442233

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

记一次mysql迁移的方案与遇到的坑| 8月更文挑战 背景

发表于 2021-08-02

背景

由于历史业务数据采用mysql来存储的,其中有一张操作记录表video_log,每当用户创建、更新或者审核人员审核的时候,对应的video_log就会加一条日志,这个log表只有insert,可想而知,1个video对应多条log,一天10w video,平均统计一个video对应5条log,那么一天50w的log, 一个月50 * 30 = 1500w条记录, 一年就是1500 * 12 = 1.8亿。目前线上已经有2亿多的数据了,由于log本身不面向C端,用于查询问题的,所以可以忍受一点的延迟。 但是随着时间的积累,必然会越来越慢,影响效率,于是提出改造。

image.png

方案一:老数据备份

由于log本身不是最关键的数据,但是也要求实时性高(用于实时查询问题),所以一开始的想法是核心的基础存储还是保持不变,较老的数据迁移出去,毕竟突然去查询一年前的操作记录的概率很小,如果突然要查,可以走离线。设计的话,我们只需要一个定时脚本,每天在凌晨4点左右(业务低峰期)抽数据。抽出的数据可以上报到一些离线存储(一般公司都有基于hive的数仓之类的),这样就可以保持线上的video_log的数据不会一直增长。

image.png

方案二:分表

分表也是一种解决方案,相对方案一的好处就是,所有的数据都支持实时查,缺点是代码要改造了。

  1. 首先确认sharding key,因为video_log是和video绑定的,所以自然而然选择video_id作为我们的sharding key
  2. 按什么分表确定了,接下来确认下分多少张表。先定个小目标,支撑3年。每张表最大数据量为1个亿(由于我们的查询简单),按照上面的统计,我们3年大概:3*1.8=5.4亿,那么大概需要5.4/1≈6张表。

image.png
接下来就是改造代码了,得解决新老数据读写的问题。

  1. 新数据的插入直接插入新表
  2. 由于log表只有insert,所以不存在update、delete这些操作,不需要考虑这些场景。
  3. 分表后,一个video的log存在两张表(老表和新表),所以临时两张表都查,然后做个合并
  4. 同步老数据到新表中
  5. 下线读取老表的代码

image.png

方案三:迁移至tidb

方案二的缺点比较明显,3年后咋办,继续拆表?感觉始终有个历史债在那。于是我们的目光定位到了tidb,tidb是分布式的数据库,接入了tidb,我们就无需关心分表了,这些tidb都帮我们做了,它会自己做节点的扩容。由于是分布式的,所以tidb的主键是无序的,这点很重要。

整个流程大概分为以下4个步骤:

  1. 先双写(记录下刚开始双写时的mysql的id,在此id前的肯定都是老数据)
  2. 同步老数据(通过第一步记录的id来区分)
  3. 切读(老数据同步完了)
  4. 下双写

image.png

重点说下同步老数据遇到的坑

迁移至tidb,看似很简单,其实在job脚本这里隐藏着几个坑。

  1. 要考虑万一job中途断了,重新启动咋办,撇开重头跑数据的时间成本,已经同步的数据重新跑会重复,还要考虑重复数据的问题。解决重复数据的问题,可以对老表新加一个字段标识是否已同步,每次同步完,更新下字段。缺点:线上数据大,加个字段不太安全,可能造成线上阻塞。
  2. 既然加个字段不好,那就用现有的主键id做约束,把主键id也同步过去,这样就算脚本重启,从头开始跑的,也因为相同的主健已经插入过,那么就会报错跳过。看似很完美,然而tidb是分布式的,主键id不是连续的,那么可能出现这样一种情况。正常的业务数据插入tidb,tidb分配的主键id和mysql同步的主键id重复,那么不管是谁,最后插入的那一条肯定是失败的。

image.png

最终同步脚本方案

综合考虑数据的重复性,job重启效率性,和整个同步的效率性,我大概做出以下方案:

  1. 任务分批提升效率:首先根据处理能力和预期完成时间,先对老数据进行分批,大概分了10批,10个job去跑不同批次的数据,互不干扰,且每次批量更新100条。
  2. 记录状态,重启自动恢复到断点:每次同步数据后记录下当前同步的位置(redis记录下当前的id),就算重启也可以从redis里拿到之前的更新位置,接着更新。
  3. 避免主键冲突:同步除了主键之外的所有字段(不同步主键)

最终通过方案三的四个切换步骤+高效率的同步脚本平稳的完成了数据的迁移

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用Java导出压缩包文件

发表于 2021-08-02

一.使用场景

生成多个excel文件打包压缩后导出压缩包。

二.解决方法探索

在面向百度(谷歌)编程之后的过程中发现大多数的解决方式都是先在本地建一个临时的文件夹,将每个excel放到这个文件夹中,全部添加完成后打包这个文件夹,再删除这个文件夹。

看到这种解决方式,我觉得这种方式并不是很合适!我觉得会有更优雅的方式,在一番搜索之后,我发现了下面代码所展示的这种解决方式,并不需要创建中间过渡文件,直接打包压缩,非常感谢这位博主。

原博链接javaWeb导出POI创建的多个excel的压缩文件.

三.代码

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
ini复制代码@ResponseBody
@RequestMapping(value = "/xxx")
public BaseResp unDirectExport(Date date, HttpServletResponse response) throws Exception {
String fileName = xxx;
ResponseUtil.setMultipartHeader(response, fileName);
List<T> list = xxxService.query(xx);

//按某个条件分组
Map<String, List<T>> map = list.stream().collect(Collectors.groupingBy(T::getXXX));
exportExcel(response, map);
return BaseResp.succResp();
}

private void exportExcel(HttpServletResponse response, Map<String, List<T>> map) throws IOException {
//Excel sheet 标题
String[] headers = {""};
ServletOutputStream outputStream = response.getOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream);
try {
map.forEach((k, v) -> {
//新建一个Excel 并设置下sheet头
HSSFWorkbook workbook = createExcelAndSetHeaders(headers, k);
//向sheet中 继续填充对象的数据
setSheetCellValue(workbook.getSheet(k), v);
try {
//重点开始,创建压缩文件
ZipEntry zipEntry = new ZipEntry(k + ".xls");
zipOutputStream.putNextEntry(zipEntry);
} catch (IOException e) {
logger.error("向XXX压缩包中添加Excel失败");
throw new Exception("向XXX压缩包中添加Excel失败");
}
try {
//写入一个压缩文件
workbook.write(zipOutputStream);
} catch (IOException e) {
logger.error("向zipOutputStream中写入流数据失败");
throw new Exception("向zipOutputStream中写入流数据失败");
}
});
zipOutputStream.flush();
} catch (Exception e) {
//重复丢出异常,有点多余,但是为了套上try catch finally 关闭数据流
logger.error("导出XXX失败,原因" + e.getErrorCode());
throw new Exception("导出XXX失败,原因:" + e.getErrorCode());
} finally {
//关闭数据流,注意关闭的顺序
zipOutputStream.close();
outputStream.close();
}
}

private HSSFWorkbook createExcelAndSetHeaders(String[] headers, String sheetName) {
HSSFWorkbook hssfWorkbook = new HSSFWorkbook();
HSSFSheet hssfSheet = hssfWorkbook.createSheet(sheetName);
HSSFRow row0 = hssfSheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
HSSFCell cellHeader = row0.createCell(i);
cellHeader.setCellValue(headers[i]);
}
return hssfWorkbook;
}

private void setSheetCellValue(HSSFSheet hssfSheet, List<T> dtos) {
for (T dto : dtos) {
//从当前sheet页的最后一行后新增一行,开始填充数据
HSSFRow row = hssfSheet.createRow(hssfSheet.getLastRowNum() + 1);
int count = -1;
row.createCell(++count).setCellValue(dto.getXXX);
row.createCell(++count).setCellValue(dto.getXXX);
row.createCell(++count).setCellValue(dto.getXXX);
row.createCell(++count).setCellValue(dto.getXXX);
row.createCell(++count).setCellValuedto.getXXX);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

/**
* @author zhaojq
* @email zhaojq@tsintergy.com
* @date 2019 2019/10/14 17:11
*/
public class ResponseUtil {

private ResponseUtil() {
}
public static void setMultipartHeader(HttpServletResponse response, String fileName) {
try {
response.setCharacterEncoding("utf-8");
response.setContentType("multipart/form-data");
response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + URLEncoder.encode(fileName, "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new BusinessException("文件名编码异常");
}
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Caffeine 当下最优秀的内存缓存框架的使用与最佳实践+

发表于 2021-08-02

如图,Caffeine是当前最优秀的内存缓存框架,不论读还是写的效率都远高于其他缓存,而且在Spring5开始的默认缓存实现就将Caffeine代替原来的Google Guava

image.png

基础使用

1
2
3
4
5
6
java复制代码<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.0.3</version>
</dependency>

手动创建缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码        Cache<Object, Object> cache = Caffeine.newBuilder()
//初始数量
.initialCapacity(10)
//最大条数
.maximumSize(10)
//PS:expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准。
//最后一次写操作后经过指定时间过期
.expireAfterWrite(1, TimeUnit.SECONDS)
//最后一次读或写操作后经过指定时间过期
.expireAfterAccess(1, TimeUnit.SECONDS)
//监听缓存被移除
.removalListener((key, val, removalCause) -> { })
//记录命中
.recordStats()
.build();

cache.put("1","张三");
System.out.println(cache.getIfPresent("1"));
System.out.println(cache.get("2",o -> "默认值"));

自动创建缓存

1
2
3
4
5
6
7
8
java复制代码LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
//创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存;refreshAfterWrite仅支持LoadingCache
.refreshAfterWrite(10, TimeUnit.SECONDS)
.expireAfterWrite(10, TimeUnit.SECONDS)
.expireAfterAccess(10, TimeUnit.SECONDS)
.maximumSize(10)
//根据key查询数据库里面的值
.build(key -> new Date().toString());

异步获取缓存

关于JDK8 CompletableFuture 说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder()
//创建缓存或者最近一次更新缓存后经过指定时间间隔刷新缓存;仅支持LoadingCache
.refreshAfterWrite(1, TimeUnit.SECONDS)
.expireAfterWrite(1, TimeUnit.SECONDS)
.expireAfterAccess(1, TimeUnit.SECONDS)
.maximumSize(10)
//根据key查询数据库里面的值
.buildAsync(key -> {
Thread.sleep(1000);
return new Date().toString();
});

//异步缓存返回的是CompletableFuture
CompletableFuture<String> future = asyncLoadingCache.get("1");
future.thenAccept(System.out::println);

PS:可以使用.executor()自定义线程池

记录命中数据

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
java复制代码LoadingCache<String, String> cache = Caffeine.newBuilder()
//创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存;refreshAfterWrite仅支持LoadingCache
.refreshAfterWrite(1, TimeUnit.SECONDS)
.expireAfterWrite(1, TimeUnit.SECONDS)
.expireAfterAccess(1, TimeUnit.SECONDS)
.maximumSize(10)
//开启记录缓存命中率等信息
.recordStats()
//根据key查询数据库里面的值
.build(key -> {
Thread.sleep(1000);
return new Date().toString();
});


cache.put("1", "小明");
cache.get("1");

/*
* hitCount :命中的次数
* missCount:未命中次数
* requestCount:请求次数
* hitRate:命中率
* missRate:丢失率
* loadSuccessCount:成功加载新值的次数
* loadExceptionCount:失败加载新值的次数
* totalLoadCount:总条数
* loadExceptionRate:失败加载新值的比率
* totalLoadTime:全部加载时间
* evictionCount:丢失的条数
*/
System.out.println(cache.stats());

PS:会影响性能,生产环境下建议不开启

淘汰策略

先了解一下常见的淘汰策略

  • LRU 最近最少使用,淘汰最长时间没有被使用的页面。
  • LRU 最不经常使用,淘汰一段时间内,使用次数最少的页面
  • FIFO 先进先出

LRU的优点:LRU相比于 LFU 而言性能更好一些,因为它算法相对比较简单,不需要记录访问频次,可以更好的应对突发流量。

LRU的缺点:虽然性能好一些,但是它通过历史数据来预测未来是局限的,它会认为最后到来的数据是最可能被再次访问的,从而给与它最高的优先级。有些非热点数据被访问过后,占据了高优先级,它会在缓存中占据相当长的时间,从而造成空间浪费。

LFU的优点:LFU根据访问频次访问,在大部分情况下,热点数据的频次肯定高于非热点数据,所以它的命中率非常高。

LFU的缺点:LFU 算法相对比较复杂,性能比 LRU 差。有问题的是下面这种情况,比如前一段时间微博有个热点话题热度非常高,就比如那种可以让微博短时间停止服务的,于是赶紧缓存起来,LFU 算法记录了其中热点词的访问频率,可能高达十几亿,而过后很长一段时间,这个话题已经不是热点了,新的热点也来了,但是,新热点话题的热度没办法到达十几亿,也就是说访问频次没有之前的话题高,那之前的热点就会一直占据着缓存空间,长时间无法被剔除。

而Caffeine 采用W-TinyLFU淘汰算法,结合LRU与LFU达到更佳的命中率与性能,具体参考:
www.cnblogs.com/zhaoxinshan…

4种淘汰方式与例子

Caffeine有4种缓存淘汰设置

  1. 大小 (会使用上面说到的W-TinyLFU算法进行淘汰)
  2. 权重 (大小与权重 只能二选一)
  3. 时间
  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
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
java复制代码import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Scheduler;
import com.github.benmanes.caffeine.cache.Weigher;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.util.concurrent.TimeUnit;

/**
* @author yejunxi
* @date 2021/7/23
*/
@Slf4j
public class CacheTest {


/**
* 缓存大小淘汰
*/
@Test
public void maximumSizeTest() throws InterruptedException {
Cache<Integer, Integer> cache = Caffeine.newBuilder()
//超过10个后会使用W-TinyLFU算法进行淘汰
.maximumSize(10)
.evictionListener((key, val, removalCause) -> {
log.info("淘汰缓存:key:{} val:{}", key, val);
})
.build();

for (int i = 1; i < 20; i++) {
cache.put(i, i);
}
Thread.sleep(500);//缓存淘汰是异步的

// 打印还没被淘汰的缓存
System.out.println(cache.asMap());
}

/**
* 权重淘汰
*/
@Test
public void maximumWeightTest() throws InterruptedException {
Cache<Integer, Integer> cache = Caffeine.newBuilder()
//限制总权重,若所有缓存的权重加起来>总权重就会淘汰权重小的缓存
.maximumWeight(100)
.weigher((Weigher<Integer, Integer>) (key, value) -> key)
.evictionListener((key, val, removalCause) -> {
log.info("淘汰缓存:key:{} val:{}", key, val);
})
.build();

//总权重其实是=所有缓存的权重加起来
int maximumWeight = 0;
for (int i = 1; i < 20; i++) {
cache.put(i, i);
maximumWeight += i;
}
System.out.println("总权重=" + maximumWeight);
Thread.sleep(500);//缓存淘汰是异步的

// 打印还没被淘汰的缓存
System.out.println(cache.asMap());
}


/**
* 访问后到期(每次访问都会重置时间,也就是说如果一直被访问就不会被淘汰)
*/
@Test
public void expireAfterAccessTest() throws InterruptedException {
Cache<Integer, Integer> cache = Caffeine.newBuilder()
.expireAfterAccess(1, TimeUnit.SECONDS)
//可以指定调度程序来及时删除过期缓存项,而不是等待Caffeine触发定期维护
//若不设置scheduler,则缓存会在下一次调用get的时候才会被动删除
.scheduler(Scheduler.systemScheduler())
.evictionListener((key, val, removalCause) -> {
log.info("淘汰缓存:key:{} val:{}", key, val);

})
.build();
cache.put(1, 2);
System.out.println(cache.getIfPresent(1));
Thread.sleep(3000);
System.out.println(cache.getIfPresent(1));//null
}

/**
* 写入后到期
*/
@Test
public void expireAfterWriteTest() throws InterruptedException {
Cache<Integer, Integer> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
//可以指定调度程序来及时删除过期缓存项,而不是等待Caffeine触发定期维护
//若不设置scheduler,则缓存会在下一次调用get的时候才会被动删除
.scheduler(Scheduler.systemScheduler())
.evictionListener((key, val, removalCause) -> {
log.info("淘汰缓存:key:{} val:{}", key, val);
})
.build();
cache.put(1, 2);
Thread.sleep(3000);
System.out.println(cache.getIfPresent(1));//null
}
}

另外还有一个refreshAfterWrite()表示x秒后自动刷新缓存可以配合以上的策略使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码private static int NUM = 0;

@Test
public void refreshAfterWriteTest() throws InterruptedException {
LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.SECONDS)
//模拟获取数据,每次获取就自增1
.build(integer -> ++NUM);

//获取ID=1的值,由于缓存里还没有,所以会自动放入缓存
System.out.println(cache.get(1));// 1

// 延迟2秒后,理论上自动刷新缓存后取到的值是2
// 但其实不是,值还是1,因为refreshAfterWrite并不是设置了n秒后重新获取就会自动刷新
// 而是x秒后&&第二次调用getIfPresent的时候才会被动刷新
Thread.sleep(2000);
System.out.println(cache.getIfPresent(1));// 1

//此时才会刷新缓存,而第一次拿到的还是旧值
System.out.println(cache.getIfPresent(1));// 2
}

最佳实践

在实际开发中如何配置淘汰策略最优呢,根据我的经验常用的还是以大小淘汰为主

实践1

配置:设置 maxSize、refreshAfterWrite,不设置 expireAfterWrite/expireAfterAccess

优缺点:因为设置expireAfterWrite当缓存过期时会同步加锁获取缓存,所以设置expireAfterWrite时性能较好,但是某些时候会取旧数据,适合允许取到旧数据的场景

实践2
配置:设置 maxSize、expireAfterWrite/expireAfterAccess,不设置 refreshAfterWrite

优缺点:与上面相反,数据一致性好,不会获取到旧数据,但是性能没那么好(对比起来),适合获取数据时不耗时的场景

与Srping Boot集成

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<!--缓存-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.0.3</version>
</dependency>

配置方式有2种

  1. yml 不推荐,因为淘汰策略是公用的,不可以给每一个缓存配置不一样的淘汰策略,此处不演示
  2. 使用@Configuration 类

此处演示第二种配置方式

  1. 开启缓存 @EnableCaching
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Configuration
public class CaffeineConfig {

@Bean
public CacheManager caffeineCacheManager() {
List<CaffeineCache> caffeineCaches = new ArrayList<>();

//可自行在yml或使用枚举设置多个缓存,不同名字的缓存的不同配置
caffeineCaches.add(new CaffeineCache("cache1",
Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.build())
);

SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(caffeineCaches);
return cacheManager;
}
}

直接可以使用Spring 缓存注解,@Cacheable、@CacheEvict、@CachePut等,此处也不作详解

1
2
3
4
5
6
7
8
9
java复制代码@Service
@Slf4j
public class StudentService {
@Cacheable(value = "cache1")
public String getNameById(int id) {
log.info("从DB获取数据:id=" + id);
return new Date().toString();
}
}

配合Redis做二级缓存

缓存的解决方案一般有三种

  1. 本地内存缓存,如Caffeine、Ehcache; 适合单机系统,速度最快,但是容量有限,而且重启系统后缓存丢失
  2. 集中式缓存,如Redis、Memcached; 适合分布式系统,解决了容量、重启丢失缓存等问题,但是当访问量极大时,往往性能不是首要考虑的问题,而是带宽。现象就是 Redis 服务负载不高,但是由于机器网卡带宽跑满,导致数据读取非常慢
  3. 第三种方案就是结合以上2种方案的二级缓存应运而生,以内存缓存作为一级缓存、集中式缓存作为二级缓存

市面上已有成熟的框架,开源中国官方开源的工具:J2Cache

大致原理就是这样

未命名文件 (1).png

配合spring boot使用参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<!-- 可以不引入caffine,j2cache默认使用2.x版本 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.0.3</version>
</dependency>

<dependency>
<groupId>net.oschina.j2cache</groupId>
<artifactId>j2cache-spring-boot2-starter</artifactId>
<version>2.8.0-release</version>
</dependency>

<dependency>
<groupId>net.oschina.j2cache</groupId>
<artifactId>j2cache-core</artifactId>
<version>2.8.0-release</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>

bootstrap.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yml复制代码j2cache:
config-location: classpath:/j2cache-${spring.profiles.active}.properties
# 开启对spring cahce的支持
open-spring-cache: true
# jedis 或 lettuce 对应在j2cache.properties 配置
redis-client: lettuce
# 是否允许null值
allow-null-values: true
# 是否开始二级缓存
l2-cache-open: true
# 如下配置在application.properties,可以选择缓存清除的模式
# * 缓存清除模式
# * active:主动清除,二级缓存过期主动通知各节点清除,优点在于所有节点可以同时收到缓存清除
# * passive:被动清除,一级缓存过期进行通知各节点清除一二级缓存
# * blend:两种模式一起运作,对于各个节点缓存准确性以及及时性要求高的可以使用(推荐使用前面两种模式中一种)
cache-clean-mode: passive

新增2个properties配置文件(不支持yml):

j2cache-test.properties

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
ini复制代码#J2Cache configuration
# caffeine 本地缓存定义文件
caffeine.properties=/caffeine.properties
j2cache.broadcast=net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
j2cache.L1.provider_class=caffeine
j2cache.L2.provider_class=net.oschina.j2cache.cache.support.redis.SpringRedisProvider
j2cache.L2.config_section=redis
# 序列化方式
j2cache.serialization=json
#########################################
# Redis connection configuration
#########################################
#########################################
# Redis Cluster Mode
#
# single -> single redis server
# sentinel -> master-slaves servers
# cluster -> cluster servers (数据库配置无效,使用 database = 0)
# sharded -> sharded servers (密码、数据库必须在 hosts 中指定,且连接池配置无效 ;
# 例子:redis://user:password@127.0.0.1:6379/0)
# sharded需要指定cluster name :redis.cluster_name = mymaster
#########################################
redis.mode=single
# redis通知节点删除本地缓存通道名
redis.channel=j2cache
# redis缓存key前缀
redis.namespace=j2cache
## connection
#redis.hosts = 127.0.0.1:26378,127.0.0.1:26379,127.0.0.1:26380
redis.hosts=127.0.0.1:6379
redis.timeout=2000
redis.password=xfish311
redis.database=1

caffeine.properties:

1
2
3
4
5
6
7
ini复制代码#########################################
# Caffeine configuration 定义本地缓存
# [name] = size, xxxx[s|m|h|d] (过期时间)
#########################################

default = 1000, 30m
cache1 = 1000, 30m

完毕。以上配置是本人整理后的配置,具体使用还需要查阅官方文档。

另外对二级缓存原理有兴趣建议去看另一个项目,源码相比j2cache写得比较容易理解,属于阉割版的二级缓存:github.com/pig-mesh/mu…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

超详细!100个拿来就用的shell脚本实例分享,建议收藏

发表于 2021-08-02

不懂shell的程序员不是好程序员,学习shell是为了自动化,使用自动化可以非常有效的提高工作效率。没有一个大公司不要求linux的基本技能的,只是不同岗位要求掌握的程度不同。

今天给大家分享一份王晓春老师精心整理的 《Shell 脚本 100 例》 ,融汇了 Shell 脚本所有常用知识点,通过100个实战经典脚本实例,展示了shell脚本编程的实用技术和常见工具用法,还是PDF格式,可自由复制,不过还是建议大家自己多多上手练习。

【领取方式见文末!!】

超详细!100个拿来就用的shell脚本实例分享,PDF格式可自由复制

超详细!100个拿来就用的shell脚本实例分享,PDF格式可自由复制

超详细!100个拿来就用的shell脚本实例分享,PDF格式可自由复制

超详细!100个拿来就用的shell脚本实例分享,PDF格式可自由复制

超详细!100个拿来就用的shell脚本实例分享,PDF格式可自由复制

内容实在太多,不一一截图了,需要的朋友点击下方链接获取【网盘链接和提取码】

cauh.cn/sErQcE/c7V2… 【复制到VX打开更方便】

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python ospath模块 8月更文挑战

发表于 2021-08-02

玩转Python

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

复习回顾

我们上一期学习了os模块相关文件目录操作方法,有了基本掌握,接下来我们来进一步学习一下os模块的姊妹os.path模块。

通常,我们在学习时会把os模块和os.path模块两个一起对照学习。

在学习之前,我们先看看文件目录和路径的区别

  • 目录:终点是文件夹

(1)目录是无法直接找到目标文件

(2)目录下可能有N个文件或者文件夹

(3)例如F:\Juejin\file\juejin 目标的终点为juejin文件夹

  • 路径:终点是目标文件。

(1)根据路径是可以直接找到目标文件

(2)路径又分为绝对路径与相对路径

(3)路径的终点是文件,叫文件路径;路径的终点是目录,叫目录路径

(4)例如路径F:\Juejin\juejin.txt,指向txt文件

💡敲黑板:路径是指具体的位置,它可以是文件📃,也可以是文件夹📁;目录则只能指文件所在的文件夹📁

💡os模块:主要对文件及目录操作;os.path模块重点对路径操作

1. os.path模块概述

os.path模块提供了路径判断、路径切分、路径链接、文件夹遍历的操作
常用的方便有如下:

方法 描述
os.path.isabs(path) 判断path是否绝对路径,返回布尔类型
os.path.isdir(path) 判断path是否为目录,返回布尔类型
os.path.isfile(path) 判断path是否为文件,返回布尔类型
os.path.exists(path) 判断指定路径的文件是否存在,返回布尔类型
os.path.getsize(filename) 返回文件的大小
os.path.abspath(path) 返回绝对路径
os.path.dirname(path) 返回目录的路径
os.path.getatime(filename) 返回文件最后访问时间
os.path.getmtime(filename) 返回文件最后修改时间
os.path.walk(top,func,arg) 递归方式遍历目录
os.path.join(path,*paths) 连接多个path
os.path.split(path) 对路径进行分割,以列表形式返回
os.path.splitext(path) 从路径中分割文件的扩展名

我们在cmd命令窗口🪟进行基本操作,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码>>> import os
>>> import os.path
>>> os.path.isabs("F:\JueJin")
True
>>> os.path.isdir("F:\Juejin")
True
>>> os.path.isdir("F:\Juejin\a.txt")
False
>>> os.path.exists("F:\Juejin")
True
>>> os.getcwd()
'F:\\Juejin'
>>> os.path.exists("a.txt")
True
>>> os.path.getsize(r"F:\JueJin\a.txt")
0
>>> os.path.getatime(r"F:\Juejin\a.txt")
1627867023.051934
>>> os.path.getmtime(r"F:\JueJin\a.txt")
1627867023.051934
>>> os.path.getctime(r"F:\JueJin\a.txt")
1627867023.051934

2. 对比os模块与os.path模块

我们前面学习了so模块,os.path模块有些方法也是与os模块重合,哪为什么还会有os.path模块存在?

带着疑问,我们来看看os模块和os.path模块两者区别

1
2
lua复制代码import os 
import os.path
  • os模块

通过import os来加载Python的模块目录中找到os.py文件里面的方法

  • os.path模块

在Python的模块目录中是不存在path.py文件,那os.path模块怎么被加载?

1.查看os.py源代码查看加载模块片段代码

(1)如果操作系统linux(’posix’),则会通过import posixpath as path加载path模块

os代码片段1

(2)如果windows(’nt’),则会通过import ntpath as path加载path模块

os代码片段2

2.Python解释器加载os.path步骤

(1)Python解释器会先加载基础的模块os模块
(2)通过os模块条件判断操作系统类型window还是Linux系统
(3)加载真正的模块并通过as命名path
(4)sys.modules以字典类型来加载的当前模块,因此from os.path import来加载具体的方法

✨🌟os.path模块运行前,Python解释器已经加载来os模块。简单来讲,os是包,path是包里的一个模块。

3. 小试牛刀

场景一:生成新文件保存在指定文件夹里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码import os.path
import time

def Mylogger(path):

rq = time.strftime('%Y%m%d%H%M', time.localtime(time.time()))

filepath = os.path.dirname(path) #去掉文件名,返回目录,在文件目录下
print(filepath)

filename = rq+"logger"+".log" # 文件名

file = os.path.join(filepath,filename) #创建文件

print(file)

Mylogger(r"F:\JueJin\a.txt")

运行结果如下:

os.path运行结果

总结

本期我们对os.path模块提供可以操作目录的方法,这些函数可以操作系统的目录本身。该模块提供exists()函数判断该目录是否存在;也提供来getctime()、getmtime()、getatime()函数来获取该目录的创建时间、最后一次修改时间、最后一次访问时间;还提供了getsize()函数来指定文件的大小等方法~

os模块与os.path模块在日常工作中对文件目录及路径相关方法在日志保存文件操作会频繁使用的,希望能帮到大家。

💪我们后续会继续学习文件相关的模块shutil、fileinputd等模块

以上是本期内容,期待大佬们点赞评论指正~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

spring5新特性日志体系

发表于 2021-08-02

spring5新特性日志体系

主流的log技术名词
1.jul

java自带的一个日志记录的技术,直接使用
java.util.logging.Logger

2.log4j
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码//log4j依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

//log4配置文件
log4j.rootLogger=info, stdout
#mybatis的sql级别(结果的日志级别为TRACE,SQL 语句的日志级别为DEBUG)
log4j.logger.com.log.dao=TRACE
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n

//log4j测试类
public static void main(String[] args) {
Logger log4j = Logger.getLogger("log4j");
log4j.info("log4j");
}

//输出
2019-09-03 11:17:06,273 INFO [log4j] - log4j

log4j特点:
可以不需要依赖第三方的技术,直接记录日志。

3.jcl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码//jcl依赖
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>

//jcl测试类
public static void main(String[] args) {
Log jcl = LogFactory.getLog("jcl");
jcl.info("jcl");
}

//输出
(1):2019-09-03 11:23:54,470 INFO [jcl] - jcl

(2):九月 03, 2019 11:24:43 上午 jcl main
信息: jcl

这个jcl什么情况下会有不同的输出?
1.当项目有logj4的依赖的时候,就会输出(1)信息。
2.当项目没有log4j依赖的时候,就会使用java自带的日志技术jul输出(2)信息。

  • jcl源码分析:
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
ini复制代码#LogFactory
public static Log getLog(String name) throws LogConfigurationException {
//通过Factory获取instance实例
return getFactory().getInstance(name);
}

#LogFactoryImpl
public Log getInstance(String name) throws LogConfigurationException {
//一开始没有缓存,所以为null
Log instance = (Log) instances.get(name);
if (instance == null) {
//这里是重点,创建logger实例
instance = newInstance(name);
//放入到缓存中
instances.put(name, instance);
}
return instance;
}

protected Log newInstance(String name) throws LogConfigurationException {
Log instance;
try {
if (logConstructor == null) {
//重要代码,发现log实现类
instance = discoverLogImplementation(name);
}
else {
Object params[] = { name };
instance = (Log) logConstructor.newInstance(params);
}
if (logMethod != null) {
Object params[] = { this };
logMethod.invoke(instance, params);
}
return instance;
}
}

private Log discoverLogImplementation(String logCategory)
throws LogConfigurationException {
if (isDiagnosticsEnabled()) {
logDiagnostic("Discovering a Log implementation...");
}
initConfiguration();

//需要返回的对象
Log result = null;

//查看用户是否指定要使用的日志实现
String specifiedLogClassName = findUserSpecifiedLogClassName();
if (specifiedLogClassName != null) {
if (isDiagnosticsEnabled()) {
logDiagnostic("");
}
result = createLogFromClass(specifiedLogClassName, logCategory, true);
if (result == null) {
StringBuffer messageBuffer = new StringBuffer("User-specified log class '");
messageBuffer.append(specifiedLogClassName);
messageBuffer.append("' cannot be found or is not useable.");

// Mistyping or misspelling names is a common fault.
// Construct a good error message, if we can
informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_LOG4J_LOGGER);
informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_JDK14_LOGGER);
informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_LUMBERJACK_LOGGER);
informUponSimilarName(messageBuffer, specifiedLogClassName, LOGGING_IMPL_SIMPLE_LOGGER);
throw new LogConfigurationException(messageBuffer.toString());
}
return result;
}
if (isDiagnosticsEnabled()) {
logDiagnostic("");
}
//如果用户没有指定日志实现类,jcl使用默认的实现类(4个),然后遍历依次创建对应的log实现类。
for(int i=0; i<classesToDiscover.length && result == null; ++i) {
result = createLogFromClass(classesToDiscover[i], logCategory, true);
}
if (result == null) {
throw new LogConfigurationException("");
}
return result;
}

//jcl内部默认的4个log实现类。
private static final String[] classesToDiscover = {
"org.apache.commons.logging.impl.Log4JLogger",
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
"org.apache.commons.logging.impl.SimpleLog"
};

//根据class创建log对象
private Log createLogFromClass(String logAdapterClassName,
String logCategory,
boolean affectState)
throws LogConfigurationException {

Object[] params = { logCategory };
Log logAdapter = null;
Constructor constructor = null;

Class logAdapterClass = null;
ClassLoader currentCL = getBaseClassLoader();

for(;;) {
logDiagnostic("Trying to load '" + logAdapterClassName + "' from classloader " + objectId(currentCL));
try {
Class c;
try {
//通过class.forName加载log类
c = Class.forName(logAdapterClassName, true, currentCL);
} catch (ClassNotFoundException originalClassNotFoundException) {
//表示找不到类(因为用户可能指定类log的实现类)
try {
c = Class.forName(logAdapterClassName);
} catch (ClassNotFoundException secondaryClassNotFoundException) {
break;
}
}
//拿到c的构造方法对象。然后通过构造对象来创建对象。(有参构造方法)
constructor = c.getConstructor(logConstructorSignature);
Object o = constructor.newInstance(params);

if (o instanceof Log) {
logAdapterClass = c;
logAdapter = (Log) o;
break;
}
handleFlawedHierarchy(currentCL, c);
} catch (NoClassDefFoundError e) {

String msg = e.getMessage();
logDiagnostic("");
break;
} catch (ExceptionInInitializerError e) {
String msg = e.getMessage();
logDiagnostic("");
break;
} catch (LogConfigurationException e) {
throw e;
} catch (Throwable t) {
handleThrowable(t);
handleFlawedDiscovery(logAdapterClassName, currentCL, t);
}
if (currentCL == null) {
break;
}
currentCL = getParentClassLoader(currentCL);
}
//返回创建的对象
return logAdapter;
}

通过分析jcl的代码可以得到:
jcl本身不实现日志记录,但是提供了记录日志的抽象方法即接口(info,debug,error…….)
底层通过一个数组存放具体的日志框架的类名,然后循环数组依次去匹配这些类名是否在项目中被依赖了,如果找到被依赖的则直接使用,所以他有先后顺序。
下图为jcl中存放日志技术类名的数组,默认有四个,后面两个可以忽略。
在这里插入图片描述
上面的代码81行就是通过一个类名去load一个class,如果load成功则直接new出来并且返回使用。如果没有load到class这循环第二个,直到找到为止。
可以看到这里的循环条件必须满足result不为空,也就是如果没有找到具体的日志依赖则继续循环,如果找到则条件不成立,不进行循环了。
总结:顺序log4j>jul

  • 虽然Log4JLogger是jcl的jar包中的类,但是该Log4JLogger类,依赖了log4j的类,当你没有引入log4j的依赖的时候,在创建Log4JLogger类,就会失败。
  • 下图是在没有引入log4j依赖的情况下,Log4JLogger类的情况图:
  • 在这里插入图片描述
  • 因为jul是java自带的日志类,所以在java环境下,jcl就算在创建log4j失败的情况下,也会去创建Jdk14Logger。可以看到Jdk14Logger是依赖了jul的。下图是Jdk14Logger的类图:
  • 在这里插入图片描述

jcl特点:
他不直接记录日志,他是通过第三方记录日志(jul)。
jcl是一个接口,默认有4个log实现类。

4.slf4j

slf4j他也不记录日志,通过绑定器绑定一个具体的日志记录来完成日志记录
官网:www.slf4j.org/

1
2
3
4
5
6
xml复制代码//slf4j依赖
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
  • 在只添加了slf4j依赖,而没有添加任意一个绑定器,日志是不会打印的。控制台会输出warn信息,如下图:
  • 在这里插入图片描述
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
xml复制代码//slf4j依赖
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>

<!--slf4jbind绑定器,将slf4j绑定到jul-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.25</version>
</dependency>

<!--slf4jbind绑定器,将slf4j绑定到log4j-->
<!--<dependency>-->
<!-- <groupId>org.slf4j</groupId>-->
<!-- <artifactId>slf4j-log4j12</artif
<!-- <version>1.7.5</version>-->
<!--</dependency>-->

<!--slf4jbind绑定器,将slf4j绑定到jcl-->
<!--<dependency>-->
<!-- <groupId>org.slf4j</groupId>-->
<!-- <artifactId>slf4j-jcl</artifactI
<!-- <version>1.7.25</version>-->
<!--</dependency>-->
  • slf4j提供了很多的绑定器,有log4j,jul,jcl。
  • 当引入了jul绑定器之后,slf4j就能打印日志了,如下图:
  • 在这里插入图片描述

总结:

  1. slf4j需要打印日志,就一定需要引入绑定器。slf4j提供了很多的绑定器,有jul,jcl,log4j等。
  2. slf4j如果引入了jcl绑定器,因为jcl也是一个接口,jcl会加载log4j,jul。
  1. 如果你想使用log4j,也需要引入log4j的依赖,log4j的配置文件
  2. 如果你不引入log4j的依赖,就默认使用jul
  1. slf4j如果引入了log4j绑定器,需要log4j的配置文件(这个时候不用引入log4j的依赖了,因为该绑定器已经帮我们引入了)
  • 问题:有这样的一个业务系统:
  • 项目A,是使用了slf4j打印日志,然后通过slf4j绑定器,绑定到jul,然后使用jul打印日志。
  • 项目A使用了spring框架,但是spring框架使用jcl打印日志,spring引入了log4j依赖,使得spring框架是用log4j打印日志。
  • 这样的话,项目A就出现了多个日志框架打印日志了,那就很混乱,现在要求项目A只能使用一种日志框架技术,这个要怎么处理?
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
xml复制代码//项目A的依赖
//log4j
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

//jcl
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>

//slf4j
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>

<!--slf4jbind绑定器,将slf4j绑定到jul-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.25</version>
</dependency>
  • 就引入上面的依赖jar包,项目就会有两种日志输出,如下图所示:
  • 在这里插入图片描述

解决方案:

  1. 可以通过修改slf4j的绑定器,直接改用slf4j的log4j绑定器。

因为使用slf4j绑定到log4j,只需要简单的引入一个依赖即可。

  1. 使用slf4j的桥接器,将spring使用jcl打印日志这步切断,将jclj桥接到slf4j,然后再走项目A的日志打印。

因为使用jcl桥接到slf4j,只需要简单的引入一个依赖即可。

1
2
3
4
5
6
7
xml复制代码//增加下面这个依赖即可:
<!--slf4j桥接器,将jcl桥接到slf4j-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.25</version>
</dependency>
  • 当使用了jcl-over-slf4j桥接器之后,可以使得jcl桥接到slf4j,然后再走slf4j这边的日志输出。从而实现将spring框架的日志输出,改为了slf4j–>jul
  • 在这里插入图片描述
  • 改进后的日志打印流程图如下:
  • 在这里插入图片描述

各种日志技术的关系和作用

  • 在这里插入图片描述

spring5新特性日志体系

Spring4当中依赖jcl,即Spring4当中采用的日志技术是jcl:commons-logging,即默认使用jul;加入log4j依赖和配置,即可切换为log4j
Spring5当中也是使用了jcl:spring-jcl,是重写为了jul框架。spring5使用的spring的jcl(spring改了jcl的代码)来记录日志的,但是jcl不能直接记录日志,采用循环优先的原则。

spring-jcl源码解析
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
java复制代码#AbstractApplicationContext
protected final Log logger = LogFactory.getLog(getClass());

#LogFactory(spring-jcl包下)
public static Log getLog(Class<?> clazz) {
return getLog(clazz.getName());
}

public static Log getLog(String name) {
switch (logApi) {
//log4j2
case LOG4J:
return Log4jDelegate.createLog(name);
//slf4j
case SLF4J_LAL:
return Slf4jDelegate.createLocationAwareLog(name);
case SLF4J:
return Slf4jDelegate.createLog(name);
default:
//默认是jul
return JavaUtilDelegate.createLog(name);
}
}

//默认是jul
private static LogApi logApi = LogApi.JUL;
//静态代码块,在类初始化的时候执行
static {
ClassLoader cl = LogFactory.class.getClassLoader();
try {
// Try Log4j 2.x API(尝试加载log4j2)
cl.loadClass("org.apache.logging.log4j.spi.ExtendedLogger");
logApi = LogApi.LOG4J;
}
catch (ClassNotFoundException ex1) {
try {
// Try SLF4J 1.7 SPI(尝试加载slf4j)
cl.loadClass("org.slf4j.spi.LocationAwareLogger");
logApi = LogApi.SLF4J_LAL;
}
catch (ClassNotFoundException ex2) {
try {
// Try SLF4J 1.7 API(尝试加载slf4j)
cl.loadClass("org.slf4j.Logger");
logApi = LogApi.SLF4J;
}
catch (ClassNotFoundException ex3) {
// Keep java.util.logging as default(如果都没有,就保持使用默认的jul)
}
}
}
}

从上面spring5的源码可以看到,spring5使用的日志是spring-jcl,默认是jul,然后会依次加载log4j2,slf4j。在都加载不到的情况下,就使用默认的jul日志技术了。
因为spring5使用的是log4j2,所以在加入了log4j的依赖和配置文件,是不生效的。

  • spring5使用log4j2日志技术,需要加入的依赖和配置文件。
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
xml复制代码<!-- spring5的依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.9.RELEASE</version>
</dependency>

<!-- log4j2依赖 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>

<!-- log4j2.xml配置文件 -->
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
  • 要将spring5改用log4j来记录日志,怎么实现?
  • 1.引入slf4j依赖,然后slf4j绑定log4j,并添加log4j的配置文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<!-- spring5的依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.9.RELEASE</version>
</dependency>

<!--slf4j依赖(可省略)-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>

<!--slf4jbind绑定器,将slf4j绑定到log4j(已经包含了slf4j和log4j的依赖)-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version>
</dependency>

扩展:

Mybatis日志体系

  • mybatis的日志技术实现
  • 在这里插入图片描述
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
typescript复制代码#LogFactory

private static void tryImplementation(Runnable runnable) {
//关键代码 logConstructor == null 没有找到实现则继续找
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}

tryImplementation(new Runnable() {
@Override
public void run() {
useSlf4jLogging();
}
});

public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}

private static void setImplementation(Class<? extends Log> implClass) {
try {
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}

具体实现类
mybatis提供很多日志的实现类,用来记录日志,取决于初始化的时候load到的class

  • jcl实现
  • 在这里插入图片描述
  • log4j2实现
  • 在这里插入图片描述
  • jul实现
  • 在这里插入图片描述

mybaits缓存问题:
1.mybaits在整合spring框架的时候,一级缓存会失效,原因是mybatis一级缓存是基于sqlSession的,整合了spring之后,spring会管理sqlSession,在查询完之后,会帮我们关闭sqlSession,所以导致缓存失效了。

2.mybatis的二级缓存。开启也是很简单在对应的mapper接口中加上@CacheNamespace注解即可。
备注:mybatis的二级缓存会有一个很大的坑。
因为mybatis的二级缓存是基于命名空间来实现了。当在不同的mapper接口操作了同一张表,这个就会有问题了,A接口更新了数据,B接口两次获取的数据都会是一样的。

mybatis打印sql日志分析

mybaits加载日志技术的顺序是:slf4j–>jcl—>log4j2—>log4j–>jul—>nolog
1.在spring4+mybatis+log4j的情况会有sql日志输出。

因为spring4使用的是jcl,jcl在引入log4j依赖下,就会使用了log4j技术打印sql日志。

2.在spring5+mybatis+log4j的情况不会有sql日志输出。

因为spring5使用的是spring-jcl(本质也是jcl),spring-jcl默认使用了jul(不再使用log4j而是用log4j2,在上面有详细说明)。
由于mybatis加载日志的顺序,jcl是先与log4j,所以该情况下导致了mybatis使用了jcl技术。

那么问题来了?为什么mybatis在使用jul的情况下,没有打印sql日志?

当使用了jul的时候,jul默认的日志级别是 INFO
原生的jdk的logging的日志级别是FINEST、FINE、INFO、WARNING、SEVERE分别对应我们常见的trace、debug、info、warn、error

  • 通过下图可以看到jdk14的isDebugEnabled()方法,是使用了Level.FINE,这个值是500
  • 在这里插入图片描述
  • 跟踪isLoggable()方法,可以看到下图,level.intValue()就是上面传递进去的Level.FINE是500,levelValue是800
  • 在这里插入图片描述
  • 这里跟踪一下levelValue的值,可以看到levelValue = Level.INFO.intValue() , INFO的值是800
  • 在这里插入图片描述
  • 在这里插入图片描述
  • 跟踪mybatis打印sql日志的源码,是判断了isDebugEnabled()返回的值,所以mybatis在使用jul的时候,是不能打印sql日志的。
  • 在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java网络爬虫- WebMagic 框架的使用 8月更

发表于 2021-08-02

一、WebMagic

WebMagic是一款爬虫框架,其底层用到之前学习到的HttpClient 和 Jsoup ,可以让我们更方便的开发爬虫。

WebMagic 项目代码分为核心和扩展两部分

  • 核心部分是一个精简的、模块化的爬虫实现;
  • 扩展部分则包括一些便利的、实用性强的功能。

  WebMagic 的设计目标是尽量的模块化,并体现爬虫的功能特点。这部分提供了非常简单、灵活的API,在基本不改变开发模式的情况下,编写爬虫。
  扩展部分提供一些便捷的功能,例如注解模式编写爬虫等。同事内置一些常用的组件,便于爬虫开发。

WebMagic架构

  WebMagic 的结构分为 Downloader、PageProcessor、Scheduler、Pipeline四大组件,并有 Spider 将它们彼此组织起来。这四大组件分别对应爬虫生命周期中的下载、处理、管理和持久化等功能。WebMagic 的设计参考了 Scrapy(Python中的),但是实现方式更Java化一些。

结构执行流程如下:

用于数据流转的对象

  1. Request
    Request(请求)跟我们web中学习的一样,Request是对 URL 地址的一层封装,一个Request对应一个URL地址;
      它是 PageProcessor 与 Downloader 交互的载体,也是 PageProcessor 控制 Downloader 唯一方式。
  2. Page
    Page 代表了从 Downloader 下载到的一个页面——可能是HTML,也可能是JSON或者其他文本格式的内容;
      Page 是 WebMagic 抽取过程的核心对象,它提供一些方法可供抽取、结果保存等。
  3. ResultItems
    ResultItems 相当于一个Map,它保存 PageProcessor 处理的结果,供 Pipeline 使用;
      它的API与Map很类似,其中它有一个字段 skip,为true时,则不应该被 Pipeline处理。

入门案例

1. 导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码    <dependencies>
<!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-core -->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>0.7.3</version>
</dependency>

<!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-extension -->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>0.7.3</version>
</dependency>
</dependencies>

ps: 0.7.3版本对SSL的并不完全,如果是直接从 Maven 中央仓库下载的依赖,在爬取只支持 SSL v1.2 的网站会有SSL的异常抛出。

解决方案:

  1. 等0.7.4版本的发布(放弃吧,有的人已经等三年了);
  2. 直接从 github 上下载最新的代码(github.com/code4craft/…;
    安装到本地仓库
    • 下载解压(照顾一下 git用的不溜的小伙伴,就不使用 git了)
    • 跳过测试

2. 加入配置文件

WebMagic 已使用slf4j-log4j12作为 slf4j 的实现,无需导包。

加配置文件 

1
2
3
4
5
properties复制代码log4j.rootLogger=INFO,A1

log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c] [%p] %m%n

3. 测试

3.1 编写代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class JobProcessor implements PageProcessor {
public static void main(String[] args) {
Spider.create(new JobProcessor()) // new一个自己所写的JobProcessor对象
.addUrl("http://112.124.1.187/index.html")
.run();
}
// 负责解析页面
@Override
public void process(Page page) {
// 解析返回的数据page,并且把解析的结果放到 ResultItems 中
page.putField("首页通知",page.getHtml().css("div.content h2").all());
}
// 这个,将就写着,入门案例嘛,后面讲到
private Site site = Site.me();
@Override
public Site getSite() {
return site;
}
}

ps: 这里解析页面数据:”page.putField(“首页通知”,page.getHtml().css(“div.content h2”).all()); ” 使用到的是 css抽取元素,除此之外,还有 XPath、正则表达式 的方法来抽取元素,他们还可以拼接使用,这个可以自己去了解。
在抽取的过程中,总有可能抽取到多条元素,可使用不同的API获取一个或多个元素:

1
2
3
java复制代码page.putField("首页通知",page.getHtml().css("div.content h2").gt()  // 一个元素(默认第一个)
page.putField("首页通知",page.getHtml().css("div.content h2").toString()  // 一个元素(默认第一个)
page.putField("首页通知",page.getHtml().css("div.content h2").all()  // 所有元素
3.2 获取链接
1
2
3
4
5
6
7
java复制代码// 放在上面的 process 方法中执行。       
     // 获取链接
// 这一行代码,获取,当前页面中抽取出来的带有其他 URL 地址的元素
page.addTargetRequests(page.getHtml().css(".excerpt_ban p").links().all());
// 这之间,做了一个跳转。下面的代码针对目标url地址的页面
// 这一行代码,获取,目标 url 的HTML页面中,你要抽取的元素
page.putField("链接抓取,博客标题",page.getHtml().css(".blog_title h4").all());

图解:

3.3 使用 Pipeline 保存数据

WebMagic 用于保存结果的组件叫 Pipeline,默认向控制台输出结果,由内置的Pipeline(ConsolePipeline)完成;
将结果保存到文件中,将 Pipeline 的实现换成 FilePipeline.

二、爬虫的配置、启动和终止

  Spider 是爬虫启动的入口。在启动爬虫之前,我们需要使用一个 PageProcessor 创建一个 Spider 对象,然后使用run()启动。
  同时,Spider 的其他组件(Downloder、Scheduler、Pipeline) 都可以配置。

  • create(PageProcessor)   创建Spider
  • addUrl(String .. urls)   添加初始的URL
  • thread(int n)   开启n个线程
  • run()   启动,会阻塞当前线程执行
  • start()/runAsync()   异步启动,当前线程继续执行
  • stop    停止爬虫
  • addPipeline(Pipeline p)   添加一个 Pipeline ,一个Spider可以有多个Pipeline
  • setScheduler(Scheduler s)   设置 Scheduler,一个Spider可以有多个 Scheduler
  • setDownloader(Downloader d)   设置 Downloader,一个Spider只能有一个 Downloader
  • get(String str)    同步调用,并直接取得结果
  • getAll(String ...str)   同步调用,并直接取得一堆结果
  • 站点本身的一些配置信息,例如编码、HTTP头、超时时间、重试策略、代理等,都可以通过 Site 对象进行配置。
1
2
3
4
5
6
7
8
java复制代码    // 这个,将就写着,入门案例嘛,后面讲到
private Site site = Site.me()
.setCharset("utf-8") // 设置编码
.setTimeOut(10 * 1000) // 设置超时时间,单位:毫秒
.setSleepTime(10) // 抓取间隔时间
.setCycleRetryTimes(10 * 1000) // 重试的时间
.setRetryTimes(3) // 重试次数
;
  • setCharset(String charSet)  设置编码
  • setUserAgent(String str)   设置 UserAgent
  • setTimeOut(int ms)   设置超时时间,单位:毫秒
  • setRetryTimes(int num)   设置重试次数
  • setCycleRetryTimes(int num)   设置循环重试次数
  • addCookie(String key,String val)   添加一条 cookie
  • setDomain(String str)    设置域名,需设置域名后,addCookie才可生效
  • addHeader(String s1,String s2)   添加一条 请求头部
  • setHttpProxy(HttpHost host)    设置 Http 代理

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot Aop 详解和多种使用场景|8月更文挑

发表于 2021-08-02

前言

aop面向切面编程,是编程中一个很重要的思想本篇文章主要介绍的是SpringBoot切面Aop的使用和案例

什么是aop

AOP(Aspect OrientedProgramming):面向切面编程,面向切面编程(也叫面向方面编程),是目前软件开发中的一个热点,也是Spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

使用场景

利用AOP可以对我们边缘业务进行隔离,降低无关业务逻辑耦合性。提高程序的可重用性,同时提高了开发的效率。一般用于日志记录,性能统计,安全控制,权限管理,事务处理,异常处理,资源池管理。使用场景

为什么需要面向切面编程

面向对象编程(OOP)的好处是显而易见的,缺点也同样明显。当需要为多个不具有继承关系的对象添加一个公共的方法的时候,例如日志记录、性能监控等,如果采用面向对象编程的方法,需要在每个对象里面都添加相同的方法,这样就产生了较大的重复工作量和大量的重复代码,不利于维护。面向切面编程(AOP)是面向对象编程的补充,简单来说就是统一处理某一“切面”的问题的编程思想。如果使用AOP的方式进行日志的记录和处理,所有的日志代码都集中于一处,不需要再每个方法里面都去添加,极大减少了重复代码。

技术要点

  1. 通知(Advice)包含了需要用于多个应用对象的横切行为,完全听不懂,没关系,通俗一点说就是定义了“什么时候”和“做什么”。
  2. 连接点(Join Point)是程序执行过程中能够应用通知的所有点。
  3. 切点(Poincut)是定义了在“什么地方”进行切入,哪些连接点会得到通知。显然,切点一定是连接点。
  4. 切面(Aspect)是通知和切点的结合。通知和切点共同定义了切面的全部内容——是什么,何时,何地完成功能。
  5. 引入(Introduction)允许我们向现有的类中添加新方法或者属性。
  6. 织入(Weaving)是把切面应用到目标对象并创建新的代理对象的过程,分为编译期织入、类加载期织入和运行期织入。

整合使用

导入依赖

在springboot中使用aop要导aop依赖

1
2
3
4
5
xml复制代码 <!--aop 切面-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

注意这里版本依赖于spring-boot-start-parent父pom中的spring-boot-dependencies

编写拦截的bean

这里我们定义一个controller用于拦截所有请求的记录

1
2
3
4
5
6
7
8
9
java复制代码@RestController
public class AopController {

@RequestMapping("/hello")
public String sayHello(){
System.out.println("hello");
return "hello";
}
}

定义切面

SpringBoot在使用切面的时候采用@Aspect注解对POJO进行标注,该注解表明该类不仅仅是一个POJO,还是一个切面容器

定义切点

切点是通过@Pointcut注解和切点表达式定义的。

@Pointcut注解可以在一个切面内定义可重用的切点。

由于Spring切面粒度最小是达到方法级别,而execution表达式可以用于明确指定方法返回类型,类名,方法名和参数名等与方法相关的部件,并且实际中,大部分需要使用AOP的业务场景也只需要达到方法级别即可,因而execution表达式的使用是最为广泛的。如图是execution表达式的语法:

execution表示在方法执行的时候触发。以“”开头,表明方法返回值类型为任意类型。然后是全限定的类名和方法名,“”可以表示任意类和任意方法。对于方法参数列表,可以使用“..”表示参数为任意类型。如果需要多个表达式,可以使用“&&”、“||”和“!”完成与、或、非的操作。

定义通知

通知有五种类型,分别是:

  1. 前置通知(@Before):在目标方法调用之前调用通知
  2. 后置通知(@After):在目标方法完成之后调用通知
  3. 环绕通知(@Around):在被通知的方法调用之前和调用之后执行自定义的方法
  4. 返回通知(@AfterReturning):在目标方法成功执行之后调用通知
  5. 异常通知(@AfterThrowing):在目标方法抛出异常之后调用通知

代码中定义了三种类型的通知,使用@Before注解标识前置通知,打印“beforeAdvice…”,使用@After注解标识后置通知,打印“AfterAdvice…”,使用@Around注解标识环绕通知,在方法执行前和执行之后分别打印“before”和“after”。这样一个切面就定义好了,代码如下:

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
java复制代码@Aspect
@Component
public class AopAdvice {

@Pointcut("execution (* com.shangguan.aop.controller.*.*(..))")
public void test() {

}

@Before("test()")
public void beforeAdvice() {
System.out.println("beforeAdvice...");
}

@After("test()")
public void afterAdvice() {
System.out.println("afterAdvice...");
}

@Around("test()")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
System.out.println("before");
try {
proceedingJoinPoint.proceed();
} catch (Throwable t) {
t.printStackTrace();
}
System.out.println("after");
}

}

运行结果

案例场景

这里我们通过一个日志记录场景来完整的使用Aop切面业务层只需关心代码逻辑实现而不用关心请求参数和响应参数的日志记录

那么首先我们需要自定义一个全局日志记录的切面类GlobalLogAspect

然后在该类添加@Aspect注解,然后在定义一个公共的切入点(Pointcut),指向需要处理的包,然后在定义一个前置通知(添加@Before注解),后置通知(添加@AfterReturning)和环绕通知(添加@Around)方法实现即可

日志信息类

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
java复制代码package cn.soboys.core;

import lombok.Data;

/**
* @author kenx
* @version 1.0
* @date 2021/6/18 18:48
* 日志信息
*/
@Data
public class LogSubject {
/**
* 操作描述
*/
private String description;

/**
* 操作用户
*/
private String username;

/**
* 操作时间
*/
private String startTime;

/**
* 消耗时间
*/
private String spendTime;

/**
* URL
*/
private String url;

/**
* 请求类型
*/
private String method;

/**
* IP地址
*/
private String ip;

/**
* 请求参数
*/
private Object parameter;

/**
* 请求返回的结果
*/
private Object result;

/**
* 城市
*/
private String city;

/**
* 请求设备信息
*/
private String device;



}

全局日志拦截

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
java复制代码package cn.soboys.core;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.reflect.Method;

/**
* @author kenx
* @version 1.0
* @date 2021/6/18 14:52
* 切面
*/
public class BaseAspectSupport {
public Method resolveMethod(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature)point.getSignature();
Class<?> targetClass = point.getTarget().getClass();

Method method = getDeclaredMethod(targetClass, signature.getName(),
signature.getMethod().getParameterTypes());
if (method == null) {
throw new IllegalStateException("无法解析目标方法: " + signature.getMethod().getName());
}
return method;
}

private Method getDeclaredMethod(Class<?> clazz, String name, Class<?>... parameterTypes) {
try {
return clazz.getDeclaredMethod(name, parameterTypes);
} catch (NoSuchMethodException e) {
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
return getDeclaredMethod(superClass, name, parameterTypes);
}
}
return null;
}
}

GlobalLogAspect类

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
java复制代码package cn.soboys.core;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.json.JSONUtil;
import cn.soboys.core.utils.HttpContextUtil;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* @author kenx
* @version 1.0
* @date 2021/6/18 15:22
* 全局日志记录器
*/
@Slf4j
@Aspect
@Component
public class GlobalLogAspect extends BaseAspectSupport {
/**
* 定义切面Pointcut
*/
@Pointcut("execution(public * cn.soboys.mallapi.controller.*.*(..))")
public void log() {

}


/**
* 环绕通知
*
* @param joinPoint
* @return
*/
@Around("log()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {

LogSubject logSubject = new LogSubject();
//记录时间定时器
TimeInterval timer = DateUtil.timer(true);
//执行结果
Object result = joinPoint.proceed();
logSubject.setResult(result);
//执行消耗时间
String endTime = timer.intervalPretty();
logSubject.setSpendTime(endTime);
//执行参数
Method method = resolveMethod(joinPoint);
logSubject.setParameter(getParameter(method, joinPoint.getArgs()));

HttpServletRequest request = HttpContextUtil.getRequest();
// 接口请求时间
logSubject.setStartTime(DateUtil.now());
//请求链接
logSubject.setUrl(request.getRequestURL().toString());
//请求方法GET,POST等
logSubject.setMethod(request.getMethod());
//请求设备信息
logSubject.setDevice(HttpContextUtil.getDevice());
//请求地址
logSubject.setIp(HttpContextUtil.getIpAddr());
//接口描述
if (method.isAnnotationPresent(ApiOperation.class)) {
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
logSubject.setDescription(apiOperation.value());
}

String a = JSONUtil.toJsonPrettyStr(logSubject);
log.info(a);
return result;

}


/**
* 根据方法和传入的参数获取请求参数
*/
private Object getParameter(Method method, Object[] args) {
List<Object> argList = new ArrayList<>();
Parameter[] parameters = method.getParameters();
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < parameters.length; i++) {
//将RequestBody注解修饰的参数作为请求参数
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
//将RequestParam注解修饰的参数作为请求参数
RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
String key = parameters[i].getName();
if (requestBody != null) {
argList.add(args[i]);
} else if (requestParam != null) {
map.put(key, args[i]);
} else {
map.put(key, args[i]);
}
}
if (map.size() > 0) {
argList.add(map);
}
if (argList.size() == 0) {
return null;
} else if (argList.size() == 1) {
return argList.get(0);
} else {
return argList;
}
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…583584585…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%