本地缓存天花板-Caffeine

news2024/9/29 5:36:01

前言

caffeine是一款高性能的本地缓存组件,关于它的定义,官方描述如下:

Caffeine is a high performance, near optimal caching library.
翻译过来就是Caffeine是一款高性能、最优缓存库。

同时文档中也说明了caffeine是受Google guava启发的本地缓存(青出于蓝而胜于蓝),在Cafeine的改进设计中借鉴了 Guava 缓存和 ConcurrentLinkedHashMap。
在这里插入图片描述

1.Caffeine

1.1.项目地址

官方项目:https://github.com/ben-manes/caffeine
官方文档:https://github.com/ben-manes/caffeine/wiki/Home-zh-CN

1.2.添加依赖

我们需要在项目中增加Caffeine的依赖,这里以maven为例,其他管理工具可以参考官方文档:

<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
  <version>3.0.5</version>
</dependency>

这里需要注意,依赖的version对于jdk有要求,比如最新版本(3.0.5)对应的jdk为11,如果jdk版本太低,会报如下错误:
在这里插入图片描述
我的项目使用的JDK是8,所以我的Caffeine依赖使用的版本为2.9.3的:

<!-- Caffeine -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.3</version>
</dependency>

1.3.初始化缓存

你也能够基于以下,做降级,比方做异步操作…,这涉及到的是Caffeine的加载方式(手动加载、自动加载、手动异步加载 和 自动异步加载,下面会了解到)

package demo.springboot.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

/**
 * 本地缓存Caffeine配置
 *
 * @author jiangkd
 * @date 2022/12/30 8:29:40
 */
@Slf4j
@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<String, Object> cache() {
        //
        final Cache<String, Object> cache = Caffeine.newBuilder()
                // 最后一次写入或最后一次访问后的过期时间
                .expireAfterWrite(30, TimeUnit.SECONDS)
                // 初始的缓存空间大小
                .initialCapacity(10)
                // 缓存的最大条数
                .maximumSize(100)
                //记录下缓存的一些统计数据,例如命中率等
            	.recordStats()
                .build();

        log.info("本地缓存Caffeine初始化完成 ...");

        return cache;
    }

}

配置中不明白的地方不要紧,下面会有说明,这里只不过先来一个简单的Caffeine的配置。

2.四种缓存添加策略

Caffeine提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载。

2.1.手动加载

手动加载其实就是通过官方提供的api,比如get、put、invalidate等接口,手动操作缓存。

Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .build();

// 查找一个缓存元素, 没有查找到的时候返回null
Graph graph = cache.getIfPresent(key);
// 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回null
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.invalidate(key);

官方说明如下:

Cache 接口提供了显式搜索查找、更新和移除缓存元素的能力。

缓存元素可以通过调用 cache.put(key, value)方法被加入到缓存当中。如果缓存中指定的key已经存在对应的缓存元素的话,那么先前的缓存的元素将会被直接覆盖掉。因此,通过 cache.get(key, k -> value) 的方式将要缓存的元素通过原子计算的方式 插入到缓存中,以避免和其他写入进行竞争。值得注意的是,当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get 也许会返回 null 。

当然,也可以使用Cache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作。

2.2.自动加载

自动加载,顾名思义就是查不到数据时,系统会自动帮我们生成元素的缓存,只是这里构建的是LoadingCache,同时需要指定元素缓存的构造方法(也就是获取对象的方式,比如查库获取)。

LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

// 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回null
Graph graph = cache.get(key);
// 批量查找缓存,如果缓存不存在则生成缓存元素
Map<Key, Graph> graphs = cache.getAll(keys);

官方说明如下:

一个LoadingCache是一个Cache 附加上 CacheLoader能力之后的缓存实现。

通过 getAll可以达到批量查找缓存的目的。 默认情况下,在getAll 方法中,将会对每个不存在对应缓存的key调用一次 CacheLoader.load 来生成缓存元素。 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发CacheLoader.loadAll 方法来使你的缓存更有效率。

值得注意的是,你可以通过实现一个 CacheLoader.loadAll并在其中为没有在参数中请求的key也生成对应的缓存元素。打个比方,如果对应某个key生成的缓存元素与包含这个key的一组集合剩余的key所对应的元素一致,那么在loadAll中也可以同时加载剩下的key对应的元素到缓存当中。

2.3.手动异步加载

手动异步加载和手动加载类似,唯一的区别是这里的缓存加载是异步的。

AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .buildAsync();

// 查找一个缓存元素, 没有查找到的时候返回null
CompletableFuture<Graph> graph = cache.getIfPresent(key);
// 查找缓存元素,如果不存在,则异步生成
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.synchronous().invalidate(key);

官方说明如下:

一个AsyncCache 是 Cache 的一个变体,AsyncCache提供了在 Executor上生成缓存元素并返回 CompletableFuture的能力。这给出了在当前流行的响应式编程模型中利用缓存的能力。

synchronous()方法给 Cache提供了阻塞直到异步缓存生成完毕的能力。

当然,也可以使用 AsyncCache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作。

默认的线程池实现是 ForkJoinPool.commonPool() ,当然你也可以通过覆盖并实现 Caffeine.executor(Executor)方法来自定义你的线程池选择。

2.4.自动异步加载

自动异步加载和自动加载对应,只是这里的加载是异步的,和手动异步加载一样,当然因为是自动加载,所以需要我们指定缓存加载方法。

AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    // 你可以选择: 去异步的封装一段同步操作来生成缓存元素
    .buildAsync(key -> createExpensiveGraph(key));
    // 你也可以选择: 构建一个异步缓存元素操作并返回一个future
    .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));

// 查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Graph> graph = cache.get(key);
// 批量查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);

官方说明如下:

一个 AsyncLoadingCache是一个 AsyncCache 加上 AsyncCacheLoader能力的实现。

在需要同步的方式去生成缓存元素的时候,CacheLoader是合适的选择。而在异步生成缓存的场景下, AsyncCacheLoader则是更合适的选择并且它会返回一个 CompletableFuture。

通过 getAll可以达到批量查找缓存的目的。 默认情况下,在getAll 方法中,将会对每个不存在对应缓存的key调用一次 AsyncCacheLoader.asyncLoad 来生成缓存元素。 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发AsyncCacheLoader.asyncLoadAll 方法来使你的缓存更有效率。

值得注意的是,你可以通过实现一个 AsyncCacheLoader.asyncLoadAll并在其中为没有在参数中请求的key也生成对应的缓存元素。打个比方,如果对应某个key生成的缓存元素与包含这个key的一组集合剩余的key所对应的元素一致,那么在asyncLoadAll中也可以同时加载剩下的key对应的元素到缓存当中。

3.属性参数

  • initialCapacity:初始的缓存空间大小,为什么要设置初始容量呢?因为如果提前能预估缓存的使用大小,那么可以设置缓存的初始容量,以免缓存不断地进行扩容,致使效率不高。
  • maximumSize:缓存的最大数量,如果缓存中的数据量超过这个数值,Caffeine 会有一个异步线程来专门负责清除缓存,按照指定的清除策略来清除掉多余的缓存。注意:比如最大容量是 2,此时已经存入了2个数据了,此时存入第3个数据,触发异步线程清除缓存,在清除操作没有完成之前,缓存中仍然有3个数据,且 3 个数据均可读,缓存的大小也是 3,只有当缓存操作完成了,缓存中才只剩 2 个数据,至于清除掉了哪个数据,这就要看清除策略了。
  • maximumWeight:缓存的最大权重,存入缓存的每个元素都要有一个权重值,当缓存中所有元素的权重值超过最大权重时,就会触发异步清除。
  • expireAfterAccess:最后一次访问之后,隔多久没有被再次访问的话,就过期。访问包括了 读 和 写。
  • expireAfterWrite:某个数据在多久没有被更新后,就过期。
  • expireAfter:创建缓存后指定过期时间
  • refreshAfterWrite:写操作完成后多久才将数据刷新进缓存中,只适用于 LoadingCache 和 AsyncLoadingCache。
  • weakKeys:打开key的弱引用
  • weakValues:打开value的弱引用
  • softValues:打开value的软引用
  • recordStats:开发统计功能

注意:

  • expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准。
  • maximumSize和maximumWeight不可以同时使用。
  • weakValues和softValues 不可以同时使用。

3.1.软引用与弱引用

  • 软引用: 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
  • 弱引用: 弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
// 软引用
Caffeine.newBuilder().softValues().build();

// 弱引用
Caffeine.newBuilder().weakKeys().weakValues().build();

4.Caffeine工具类

package demo.springboot.util;

import com.github.benmanes.caffeine.cache.Cache;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;

/**
 * 本地缓存工具类
 *
 * @author jiangkd
 * @date 2022/12/30 10:10:43
 */
@Component
@RequiredArgsConstructor
public class CacheUtil<K, V> {

    private final Cache<K, V> cache;

    // =================================================================
    // 获取缓存

    /**
     * 依据key获取value, 如果未找到, 返回null
     *
     * @return Object
     */
    public V get(K key) {
        // 就是相当于cache.getIfPresent(key)
        return cache.asMap().get(key);
    }

    /**
     * 依据key获取value, 如果未找到, 返回null
     *
     * @return Object
     */
    public V getIfPresent(K key) {
        // 就是相当于get(key)
        return cache.getIfPresent(key);
    }

    /**
     * 批量依据key获取value
     *
     * @return Object
     */
    public Map<K, V> getBatch(List<String> key) {
        //
        return cache.getAllPresent(key);
    }

    /**
     * 得到缓存Map
     *
     * @return ConcurrentMap<K, V>
     */
    public ConcurrentMap<K, V> get() {
        return cache.asMap();
    }

    // =================================================================
    // 插入,修改缓存

    /**
     * 插入一个缓存
     *
     * @param key   key
     * @param value value
     */
    public void put(K key, V value) {
        //
        cache.put(key, value);
    }

    /**
     * 插入缓存,如果不存在,则将value放入缓存
     *
     * @param key   key
     * @param value value
     */
    public V getIfNotExist(K key, V value) {
        //
        return cache.get(key, k -> value);
    }

    /**
     * 将一个map插入或修改缓存
     */
    public void putBatch(Map<? extends K, ? extends V> map) {
        //
        cache.asMap().putAll(map);
    }

    /**
     * 更新一个指定key的缓存
     *
     * @param key   key
     * @param value value
     */
    public void update(K key, V value) {
        //
        cache.put(key, value);
    }

    // =================================================================
    // 判断缓存

    /**
     * 是否含有指定key的缓存
     *
     * @param key key
     */
    public boolean contains(K key) {
        //
        return cache.asMap().containsKey(key);
    }

    // =================================================================
    // 删除缓存

    /**
     * 删除指定key的缓存
     *
     * @param key key
     */
    public void delete(K key) {
        //
        cache.asMap().remove(key);
    }

    /**
     * 批量删除指定key的缓存
     *
     * @param key key
     */
    public void delete(List<String> key) {
        //
        cache.invalidateAll(key);
    }
    
    /**
     * 删除指定key的缓存
     *
     * @param key key
     */
    public void invalidate(K key) {
        //
        cache.invalidate(key);
    }

    /**
     * 清除所有缓存
     */
    public void deleteAll() {
        //
        cache.invalidateAll();
    }

}

简单的测试如下:

package demo.springboot.caffeine;

import cn.hutool.core.collection.CollUtil;
import demo.springboot.DemoSpringbootApplication;
import demo.springboot.util.CacheUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;

/**
 * 本地缓存Caffeine测试
 *
 * @author jiangkd
 * @date 2022/12/30 10:17:12
 */
@SpringBootTest(classes = DemoSpringbootApplication.class)
@RunWith(SpringRunner.class)
@Slf4j
public class CaffeineTest {

    @Resource
    CacheUtil<String, String> cacheUtil;

    @Resource
    CacheUtil<String, List<String>> listCacheUtil;

    @Test
    public void test(){
        // 插入一个缓存
        cacheUtil.put("name", "TEST1");

        // 获取缓存
        final String name = cacheUtil.get("name");
        log.info("获取指定key的value:{}", name);
    }

    @Test
    public void listTest(){
        //
        listCacheUtil.put("names", CollUtil.newArrayList("AA","BB"));

        final List<String> names = listCacheUtil.get("names");
        log.info("获取指定key的value:{}", names);
    }

    @Test
    public void Test3(){
        //
        final String name = cacheUtil.get("name");
        log.info("获取指定key的value:{}", name);

        final String name2 = cacheUtil.getIfPresent("name2");
        log.info("获取指定key的value:{}", name2);
    }

    @Test
    public void Test4(){
        //
        Map<String, String> map = new HashMap<>();
        map.put("name1", "TEST1");
        map.put("name2", "TEST2");
        map.put("name3", "TEST3");

        cacheUtil.putBatch(map);

        ConcurrentMap<String, String> cacheMap = cacheUtil.get();
        log.info("缓存Map:{}", cacheMap.toString());

        map.put("name2", "TEST22");
        map.put("name3", "TEST33");

        cacheUtil.putBatch(map);

        cacheMap = cacheUtil.get();
        log.info("缓存Map:{}", cacheMap.toString());
    }

}

结语

由于内容篇幅和时间的限制,这里只能做一个简单的分享,更多详细内容还需要各位自己探索或参考官方文档。

参考文档:

  • 参考1
  • 参考2
  • 参考3
  • 参考4
  • 官方

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/129042.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Git】一文带你入门Git分布式版本控制系统(分支管理策略、Bug分支)

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;也会涉及到服务端 &#x1f4c3;个人状态&#xff1a; 在校大学生一枚&#xff0c;已拿多个前端 offer&#xff08;秋招&#xff09; &#x1f680;未…

Eth04 - Eth分层模块架构和索引方案

文章目录 1 Eth分层模块架构2 索引方案3 发送成功和接收成功回调函数传送门 ==>> AutoSAR入门和实战系列总目录 1 Eth分层模块架构 下面的图片表明了以太网控制器驱动程序和硬件的关系;从EthIf看来,要通过以太网控制器层去访问以太网控制器硬件,以太网控制器层有多…

AcWing1204.错误票据——学习笔记

题目&#xff1a;1204. 错误票据 - AcWing题库https://www.acwing.com/problem/content/description/1206/ import java.util.Scanner;public class Main {public static void main(String args[]){Scanner input new Scanner(System.in);int line input.nextInt();int loseI…

Python开发环境

1. Python开发环境 开发环境&#xff0c;英文是IDE&#xff08;Integrated Development Environment 集成开发环境&#xff09;。 不要纠结于使用哪个开发环境。开发环境本质上就是对Python解释器python.exe的封装&#xff0c;核心都一样。可以说:“开发环境IDE&#xff0c;只…

SpringCloud(10)— Elasticsearch集群

SpringCloud&#xff08;10&#xff09;— Elasticsearch集群 一 搭建ES集群 单机的 Elasticsearch 做数据存储&#xff0c;必然面临两个问题&#xff1a;海量数据存储问题&#xff0c;单点故障等 海量数据存储问题&#xff1a;将索引库从逻辑上拆分为 N 个分片&#xff08;…

直播回顾 | 如何运用数智化助力光伏上游产业节能降碳?

12月29日&#xff0c;【始祖双碳研习社-行业解决方案】系列直播课第一期直播顺利举办。 始祖科技解决方案专家张开宇在本次直播上进行了以《如何运用数智化助力光伏上游产业节能降碳》的主题分享&#xff0c;详细介绍了光伏行业产业链分析、光伏行业节能减排的现状与挑战、数智…

【Javassist】快速入门系列12 当检测到catch语句时在catch前插入代码

系列文章目录 01 在方法体的开头或结尾插入代码 02 使用Javassist实现方法执行时间统计 03 使用Javassist实现方法异常处理 04 使用Javassist更改整个方法体 05 当有指定方法调用时替换方法调用的内容 06 当有构造方法调用时替换方法调用的内容 07 当检测到字段被访问时使用语…

【C++学习】vector的使用及模拟实现

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《C学习》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; vector的使用及模拟实现&#x1f387;构造函数&#x1f9e8;模拟实现&#x1f9e8;vector的扩容机制&…

力扣(LeetCode)363. 矩形区域不超过 K 的最大数值和(2022.12.30)

给你一个 m x n 的矩阵 matrix 和一个整数 k &#xff0c;找出并返回矩阵内部矩形区域的不超过 k 的最大数值和。 题目数据保证总会存在一个数值和不超过 k 的矩形区域。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,0,1],[0,-2,3]], k 2 输出&#xff1a;2 解释&…

2022年-年度总结报告

目录1.攻克的技术难题问题1&#xff1a;2.学习的新技术1.system系统的学习2.网络3.游戏22年总结23年的计划1.先给自己画个大饼2.计划内的小饼1.攻克的技术难题 问题1&#xff1a; 跑VTS测试的时候&#xff0c;mkfs.exfat挂测失败&#xff0c;VTS刷最新的谷歌gsi没有过&#x…

大文件传输如何帮助媒体行业

过去几年&#xff0c;随着分辨率从4k到6k再到8k的升级&#xff0c;观众已经适应了高分辨率的时代。然而&#xff0c;许多媒体工作室的工作流程还停留在过去。 TB甚至PB大小的材料的传输让从业者无所适从。这就是高速文件传输对媒体行业有很大帮助的原因。 什么是大文件传输&am…

81.【SpringMVC】

SpringMVC(一)、认识MVC三层架构1.回顾MVC(1).什么是MVC三层框架(2).MVC要做那些事情?(3).常见的MVC三层框架结构(4).知识拓展2.代码回顾3.什么是SpringMVC(二)、第一个SpringMVC0.前提1.搭建环境2.配置WEB-INF的XML配置文件3.在资源Resource的包下设置springmvc-servlet.xml4…

30-深入Hotspot源码与Linux内核理解NIO/BIO/AIO

IO模型 IO模型就是说用什么样的通道进行数据的发送和接收&#xff0c;Java共支持3种网络编程IO模式&#xff1a;BIO&#xff0c;NIO&#xff0c;AIO BIO(Blocking IO) 同步阻塞模型&#xff0c;一个客户端连接对应一个处理线程 缺点&#xff1a; 1、IO代码里read操作是阻塞操…

Spreadsheet与FineReport数据集对比

什么是数据集&#xff1f;在BI工具中指的是在报表开发前的取数过程&#xff0c;把需要的数据整合成一个数据集合&#xff0c;以便于在报表开发中使用。可以把它理解为我们基于数据库获取我们需要的数据。而数据库获取数据是有多种方式的&#xff0c;比如可以通过直接写SQL语句、…

基于verilog实现序列相关检测

题目来源牛客网&#xff0c;完整工程源码&#xff1a;https://github.com/ningbo99128/verilog 目录 1、VL25 输入序列连续的序列检测 题目介绍 思路分析 代码实现 仿真文件 2、VL26 含有无关项的序列检测 题目介绍 思路分析 代码实现 仿真文件 3、VL27 不重叠序列检…

Sleuth+Zipkin架构

为什么要链路追踪 小结&#xff1a; nacos 【name server】&#xff1a;注册中心&#xff0c;解决服务的注册与发现 nacos【config】&#xff1a;配置中心&#xff0c;微服务配置文件的中心化管理&#xff0c;同时配置信息的动态刷新 Ribbon&#xff1a;客户端负载均衡器&#…

《设计模式》享元模式

《设计模式》享元模式《设计模式》设计模式的基本原则 《设计模式》单例模式 《设计模式》工厂模式 《设计模式》原型模式 《设计模式》建造者模式 《设计模式》适配器模式 《设计模式》桥接模式 《设计模式》装饰者模式 《设计模式》组合模式 《设计模式》外观模式 《设计模式…

数据结构课设:迷宫问题

文章目录前言一、概要设计1、基本信息2、功能模块图3、功能描述4、调用关系图5、结果演示① 创建迷宫② 求解③ 清除多余路径二、完整代码前言 最近刚好在写自己的课设&#xff0c;匆匆忙忙写出来的课设系统&#xff0c;仍有不足&#xff0c;拿出来和大家分享一下&#xff0c;…

C. p-binary(二进制位)

Problem - 1225C - Codeforces Vasya会看中任何数字&#xff0c;只要它是2的整数次方。另一方面&#xff0c;Petya非常保守&#xff0c;只喜欢单一的整数p&#xff08;可以是正数、负数或零&#xff09;。为了结合他们的口味&#xff0c;他们发明了2xp形式的p-二进制数&#xf…

jmeter接口测试之导入测试用例/get请求中Url存在参数(工作日记2)

导入接口用例进行接口测试 以运营中心测试计划中的企业菜单管理为例 【前提条件】 1、有接口数据 2、有接口用例 我们需要把接口测试用例转换为CSV格式步骤如下&#xff1a; 右键选择打开方式为Notepad 需要将文件设置一下编码 文件留着备用 【步骤】 1、新建一个企业…