jyoryo 发布的文章

背景:存在一个耗时的任务,优先使用异步方式进行调用,但是有时有需要同步调用等待获取任务执行结果。

这里存在下面几个问题:

  • 同一任务已经异步提交执行了,现在需要同步执行等待结果
  • 同一任务正在同步执行等待结果,后面后进行了异步重复提交,需要确保不会重复调用该任务
  • 系统资源有效,耗时任务不能无限的添加执行

解决方案:

  1. 可以使用CompletableFutureAtomicReference来保存异步调用的结果,并在同步调用时检查是否有正在进行的任务。如果有,等待结果;如果没有,启动任务。
  2. 使用Semaphore来控制同时能有多少个任务可执行

示例代码:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;

public class TaskManager {
    private final ConcurrentHashMap<String, AtomicReference<CompletableFuture<String>>> tasks = new ConcurrentHashMap<>();
    private final Semaphore semaphore = new Semaphore(5);  // 限制最多同时执行5个任务

    // 同步执行任务
    public String executeTaskSynchronously(String taskId) {
        AtomicReference<CompletableFuture<String>> taskRef = tasks.computeIfAbsent(taskId, k -> new AtomicReference<>());
        CompletableFuture<String> task = taskRef.get();

        if (task == null) {
            task = startTask(taskId);
            if (!taskRef.compareAndSet(null, task)) {
                task = taskRef.get();
            }
        }

        return task.join();  // 等待异步任务完成并返回结果
    }

    // 异步执行任务
    public CompletableFuture<String> executeTaskAsynchronously(String taskId) {
        AtomicReference<CompletableFuture<String>> taskRef = tasks.computeIfAbsent(taskId, k -> new AtomicReference<>());
        CompletableFuture<String> task = taskRef.get();

        if (task == null) {
            task = startTask(taskId);
            if (!taskRef.compareAndSet(null, task)) {
                task = taskRef.get();
            }
        }

        return task;
    }

    // 实际启动任务
    private CompletableFuture<String> startTask(String taskId) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                semaphore.acquire();  // 获取许可,最多只能有5个任务同时执行
                // 模拟耗时任务
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                return "Result for task " + taskId;
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return "Task interrupted";
            } finally {
                semaphore.release();  // 释放许可,允许下一个任务执行
                tasks.remove(taskId);
            }
        });
    }
}

代码说明:

  1. ConcurrentHashMap: tasks 是一个 ConcurrentHashMap,其中的每个任务都有一个唯一的 taskId 作为键。每个任务的执行状态用 AtomicReference<CompletableFuture<String>> 表示。
  2. Semaphore: Semaphore 初始化为 5,表示最多允许 5 个任务同时执行。如果一个任务完成了,会释放许可,允许下一个任务执行。
  3. 同步调用 (executeTaskSynchronously): 同步调用会获取任务的 CompletableFuture,如果任务还未启动,则启动任务并存储在 AtomicReference 中,然后等待任务完成并返回结果。
  4. 异步调用 (executeTaskAsynchronously): 异步调用与同步调用类似,只是不等待任务完成,而是直接返回 CompletableFuture,以便调用者在未来某个时间点处理结果。
  5. 任务完成后清理: 当任务完成后,tasks 中与该任务相关的记录会被移除,以便下次可以重新启动任务。

Java Mail发送邮件email 支持SSL加密、TSL加密

类型服务名称服务器地址SSL协议端口非SSL协议端口TSL协议端口
收件服务器POPpop.163.com995110
收件服务器IMAPmap.163.com993143
发件服务器SMTPsmtp.163.com465/99425587

POM依赖:
注意:不要依赖javax.mail javax.mail-api 1.6.2,不然会提示:java.lang.NoClassDefFoundError: com/sun/mail/util/MailLogger

<dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>javax.mail</artifactId>
    <version>1.6.2</version>
</dependency>

Java Mail代码如下:

/**
 * 发送邮件
 *
 * @param toEmails    收件人列表
 * @param ccEmails    抄送人列表
 * @param subject     主题
 * @param htmlContent 邮件正文
 * @param files       附件。Key:附件的文件 Value:显示文件名
 */
public void sendByJavaMail(String[] toEmails, String[] ccEmails, String subject, String htmlContent, Map<String, String> files) {
    // 获取邮件配置
    final String mailServerUsername = "{你的邮箱地址,例如:xxx@163.com}",
        mailServerPassword = "{你的邮箱密码或授权密码}",      
        mailFrom = "{你的邮箱发件人地址,例如:xxx@163.com}",
        mailFromDisplayName = "{希望收件人看到的发件人显示名称,例如:报警服务}";
    if (Strings.isBlank(mailServerUsername) || Strings.isBlank(mailServerPassword) || Strings.isBlank(mailFrom)) {
        throw new ApiException("Email sending service is not set up, please contact the platform if there are any issues.");
    }
    // 设置邮箱服务属性
    Properties props = new Properties();
    props.setProperty("mail.smtp.host", "smtp.163.com");
    props.setProperty("mail.smtp.auth", "true");
    // "mail.smtp.host":"smtp.gmail.com" "mail.smtp.auth":"true" "mail.smtp.port":"587" "mail.smtp.starttls.enable":"true" "mail.smtp.ssl.protocols":"TLSv1.2"
    // TLS需要内容项:"mail.smtp.starttls.enable":"true" "mail.smtp.port":"587"
    // SSL需要内容项:"mail.smtp.ssl.enable":"true" "mail.smtp.port":"465" "mail.smtp.socketFactory.class":"javax.net.ssl.SSLSocketFactory"
    props.setProperty("mail.smtp.ssl.enable", "true");
    props.setProperty("mail.smtp.port", "465");
    props.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");    

    // 获取默认的 Session 对象。
    Session session = Session.getInstance(props, new Authenticator() {
        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
            return new PasswordAuthentication(mailServerUsername, mailServerPassword);
        }
    });
    // 是否配置debug打印日志信息
    session.setDebug(true);
    try {
        // 创建默认的 MimeMessage 对象。
        MimeMessage message = new MimeMessage(session);
        // 发送方邮箱
        Address fromAddress = Strings.isBlank(mailFromDisplayName) ? new InternetAddress(mailFrom) : new InternetAddress(mailFrom, mailFromDisplayName, "utf-8");
        message.setFrom(fromAddress);
        // 接收方邮箱
        for (String toEmail : toEmails) {
            message.addRecipient(Message.RecipientType.TO, new InternetAddress(toEmail));
        }
        // 抄送邮件列表
        if (null != ccEmails && ccEmails.length > 0) {
            for (String ccEmail : ccEmails) {
                message.addRecipient(Message.RecipientType.CC, new InternetAddress(ccEmail));
            }
        }
        // 设置主题
        message.setSubject(subject);

        /* 消息部分 */
        // 正文
        Multipart multipart = new MimeMultipart();
        // HTML消息
        BodyPart htmlBodyPart = new MimeBodyPart();
        htmlBodyPart.setContent(htmlContent, "text/html;charset=UTF-8");
        multipart.addBodyPart(htmlBodyPart);
        // 设置附件
        if (null != files && !files.isEmpty()) {
            for (Map.Entry<String, String> entry : files.entrySet()) {
                final String file = entry.getKey(), filename = entry.getValue();
                BodyPart fileBodyPart = new MimeBodyPart();
                DataSource source = new FileDataSource(file);
                fileBodyPart.setDataHandler(new DataHandler(source));
                // fileBodyPart.setFileName(filename);
                fileBodyPart.setFileName(MimeUtility.encodeText(filename, "UTF-8", "B"));
                multipart.addBodyPart(fileBodyPart);
            }
        }
        message.setContent(multipart);
        // 发送消息
        log.debug(">>>>>>>>>>sendByJavaMail---sending--<<<<<<<<<<<<<<");
        Transport.send(message);
    } catch (Exception e) {
        log.error("", e);
    } finally {
        // 根据业务需求,是否删除附件的文件
        if (null != files && !files.isEmpty()) {
            for (Map.Entry<String, String> entry : files.entrySet()) {
                Files.deleteQuietly(new File(entry.getKey()));
            }
        }
    }
}

Nginx搭建WebDAV服务器

搭建webDAV服务前,先确认已安装nginx

配置webdav.conf

创建用于webdav的nginx配置文件:

vim /etc/nginx/conf.d/webdav.conf

设置内容:

server {
    listen 80;
    listen [::]:80;

    server_name webdav.youdomain.com;

    # 认证方式
    auth_basic              realm_name;
    # 存放认证用户名、密码文件(确认有对应权限)
    auth_basic_user_file    /data/www/webdav/.credentials.list;
    root /data/www/webdav/;

    # dav allowed method
    dav_methods     PUT DELETE MKCOL COPY MOVE;
    # Allow current scope perform specified DAV method
    dav_ext_methods PROPFIND OPTIONS;

    # In this folder, newly created folder or file is to have specified permission. If none is given, default is user:rw. If all or group permission is specified, user could be skipped
    dav_access      user:rw group:rw all:r;

    # MAX size of uploaded file, 0 mean unlimited
    client_max_body_size    0;

    # Allow autocreate folder here if necessary
    create_full_put_path    on;
}

创建用户

用户名

echo -n '$yourname:' | tee -a /data/www/webdav/.credentials.list

设置密码

openssl passwd -apr1 | tee -a /data/www/webdav/.credentials.list

重新加载nginx

# 检验配置文件
nginx -t

# 重启nginx
nginx -s reload

参考文章

vim使用

vim-key

官方文档:http://vimdoc.sourceforge.net/htmldoc/ 中文文档:http://vimcdoc.sourceforge.net/doc/

vim配置

vim 的配置文件分为系统配置文件 /etc/vimrc/usr/share/vim/ 和用户配置文件 ~/.vimrc~/.vim/

vim 的配置文件载入过程为:

  1. /etc/vimrc
  2. $HOME/.vim, $HOME/.vimrc
  3. $VIMRUNTIME/.vim,$VIMRUNTIME/.vimrc
  4. $HOME/.vim/after/
通过vim -V可以查看整个初始化过程。

vim常用的一些配置选项:

" .vimrc
" See: http://vimdoc.sourceforge.net/htmldoc/options.html for details
" For multi-byte character support (CJK support, for example):
" set fileencodings=ucs-bom,utf-8,cp936,big5,euc-jp,euc-kr,gb18030,latin1       
set tabstop=4       " Number of spaces that a <Tab> in the file counts for.

set shiftwidth=4    " Number of spaces to use for each step of (auto)indent.
 
set expandtab       " Use the appropriate number of spaces to insert a <Tab>.
                    " Spaces are used in indents with the '>' and '<' commands
                    " and when 'autoindent' is on. To insert a real tab when
                    " 'expandtab' is on, use CTRL-V <Tab>.
 
set smarttab        " When on, a <Tab> in front of a line inserts blanks
                    " according to 'shiftwidth'. 'tabstop' is used in other
                    " places. A <BS> will delete a 'shiftwidth' worth of space
                    " at the start of the line.
 
set showcmd         " Show (partial) command in status line.

set number          " Show line numbers.

set showmatch       " When a bracket is inserted, briefly jump to the matching
                    " one. The jump is only done if the match can be seen on the
                    " screen. The time to show the match can be set with
                    " 'matchtime'.
 
set hlsearch        " When there is a previous search pattern, highlight all
                    " its matches.
 
set incsearch       " While typing a search command, show immediately where the
                    " so far typed pattern matches.
 
set ignorecase      " Ignore case in search patterns.
 
set smartcase       " Override the 'ignorecase' option if the search pattern
                    " contains upper case characters.
 
set backspace=2     " Influences the working of <BS>, <Del>, CTRL-W
                    " and CTRL-U in Insert mode. This is a list of items,
                    " separated by commas. Each item allows a way to backspace
                    " over something.
 
set autoindent      " Copy indent from current line when starting a new line
                    " (typing <CR> in Insert mode or when using the "o" or "O"
                    " command).
 
set textwidth=79    " Maximum width of text that is being inserted. A longer
                    " line will be broken after white space to get this width.
 
set formatoptions=c,q,r,t " This is a sequence of letters which describes how
                    " automatic formatting is to be done.
                    "
                    " letter    meaning when present in 'formatoptions'
                    " ------    ---------------------------------------
                    " c         Auto-wrap comments using textwidth, inserting
                    "           the current comment leader automatically.
                    " q         Allow formatting of comments with "gq".
                    " r         Automatically insert the current comment leader
                    "           after hitting <Enter> in Insert mode. 
                    " t         Auto-wrap text using textwidth (does not apply
                    "           to comments)
 
set ruler           " Show the line and column number of the cursor position,
                    " separated by a comma.
 
set background=dark " When set to "dark", Vim will try to use colors that look
                    " good on a dark background. When set to "light", Vim will
                    " try to use colors that look good on a light background.
                    " Any other value is illegal.
 
set mouse=a         " Enable the use of the mouse.
 
filetype plugin indent on
syntax on

vim光标移动

常用移动:

h,j,k,l:分别对应光标⬅️,⬇️,⬆️,➡️。

w,b,$,^,gg,G:分别对应下一单词,前一单词,行末,行首,文件头,文件末尾。

- 阅读剩余部分 -

Debian Ubuntu下Redis源码安装

安装依赖

# 更新
apt-get update
# 安装依赖工具
apt-get install -y build-essential tcl pkg-config

安装Redis

工作目录未/tmp

cd /tmp
# 下载
wget https://download.redis.io/releases/redis-6.2.1.tar.gz
# 解压
tar zxf redis-6.2.1.tar.gz
# 进入源码文件目录
cd redis-6.2.1/
# 编译源码
make
# 测试
make test
# 测试通过后,进行安装redis
make install

make test 完成

image-20210326090559219

配置Redis

设置配置文件redis.conf

- 阅读剩余部分 -

Debian安装Certbot获取SSL证书

安装snapd

# 安装snapd
apt-get update
apt-get install snapd

# 安装 snapd core
snap install core
snap refresh core

通过snapd安装certbot

snap install --classic certbot

# 软链接
ln -s /snap/bin/certbot /usr/bin/certbot

如果遇到无法下载,可能需要翻墙,请给snap设置代理:

sudo snap set system proxy.http=http://127.0.0.1:1081
sudo snap set system proxy.https=http://127.0.0.1:1081

获取SSL证书

执行certbot certonly

certbot certonly -d "*.youdomain.com" --manual --preferred-challenges dns-01  --server https://acme-v02.api.letsencrypt.org/directory
root@docker00:/usr/bin# certbot certonly -d "*.youdomain.com" --manual --preferred-challenges dns-01  --server https://acme-v02.api.letsencrypt.org/directory
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Enter email address (used for urgent renewal and security notices)
 (Enter 'c' to cancel): xxx@xxx.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: N
Account registered.
Requesting a certificate for *.youdomain.com
Performing the following challenges:
dns-01 challenge for youdomain.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.youdomain.com with the following value:

<your_dns_txt_value>

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/youdomain.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/youdomain.com/privkey.pem
   Your certificate will expire on 2021-07-01. To obtain a new or
   tweaked version of this certificate in the future, simply run
   certbot again. To non-interactively renew *all* of your
   certificates, run "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

- 阅读剩余部分 -

清除SVN版本控制

关于清除SVN版本控制,本人目前是采用如下方法:通过运行一个.bat文件,删除此目录下所有.svn的文件夹包括子目录下的.svn文件夹

简易版

新建一个clean.bat的文件,用记事本打开,写入:

@echo On @Rem 删除SVN版本控制目录 @PROMPT [Com]
@for /r . %%a in (.) do @if exist "%%a\.svn" rd /s /q "%%a\.svn" @Rem for /r . %%a in (.) do @if exist "%%a\.svn" @echo "%%a\.svn"
@echo Mission Completed. @pause

完整版

@echo off
echo ***********************************************************
echo 清除SVN版本信 息
echo ***********************************************************
:start
::启动过程,切换目录
:set pwd=%cd%
:cd %1
echo 工作目录是:& chdir
:input
::获取输入,根据输入进行处理
set source=:
set /p source=确定要清楚当前目录下的.svn信息吗?[Y/N/Q]
set "source=%source:"=%"
if "%source%"=="y" goto clean
if "%source%"=="Y" goto clean
if "%source%"=="n" goto noclean
if "%source%"=="N" goto noclean
if "%source%"=="q" goto end
if "%source%"=="Q" goto end
goto input
:clean
::主处理过程,执行清理工作
@echo on
@for /d /r %%c in (.svn) do @if exist %%c ( rd /s /q %%c & echo 删除目录%%c)
@echo off
echo "当前目录下的svn信息已清除"
goto end
:noclean
::分支过程,取消清理工作
echo "svn信息清楚操作已取消"
goto end
:end
::退出程序
cd "%pwd%"
pause

Debian jessie升级至buster

注意:不要跨版本升级,需要逐版本升级。

备份数据

升级前请做好备份操作,防止升级后相关服务不可用或数据无法正常读取。

  1. 程序代码
  2. 数据库文件
  3. 配置文件,如:Nginx、MySQL、PHP等
  4. 系统添加的用户和SSH Key等。
说明,下面操作建议使用root账号。

更新当前系统

apt-get update
apt-get upgrade
apt-get dist-upgrade

# 更新好后,建议重启下
reboot

升级至stretch

替换软件源

# 备份软件源
cp /etc/apt/sources.list /etc/apt/sources.list_bak

# 替换jessie为stretch
sed -i 's/jessie/stretch/g' /etc/apt/sources.list

- 阅读剩余部分 -

Lombok介绍

Lombok 是一种 Java 实用工具,可用来帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注释实现这一目的。通过在开发环境中实现 Lombok,开发人员可以节省构建诸如 hashCode() 和 equals() 这样的方法以及以往用来分类各种 accessor 和 mutator 的大量时间。

SpringToolSuite4安装Lombok

下载 lombok.jar

可以通过下面地址进行下载:

https://repo1.maven.org/maven2/org/projectlombok/lombok/1.18.18/lombok-1.18.18.jar

拷贝lombok.jar至STS目录

目录:/Applications/SpringToolSuite4.app/Contents/MacOS

接着需要将lombok-1.18.18.jar重命名为lombok.jar
(下面截图名称没有改,实际操作时,请将lombok-1.18.18.jar改为lombok.jar)

7c2yiab8hz4.png

在STS路径运行命令

cd /Applications/SpringToolSuite4.app/Contents/MacOS

java -jar lombok.jar

- 阅读剩余部分 -