分类 Develop 下的文章

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

这里存在下面几个问题:

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

解决方案:

  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()));
            }
        }
    }
}

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

- 阅读剩余部分 -

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

- 阅读剩余部分 -

使用Linux作为服务器,很多时候会用到计划任务。
下面介绍下Cron计划任务的使用说明。
首先确认是否安装了Cron(如果系统已有,请跳过此步):

apt-get install cron

crontab命令常用参数

crontab   -l   #查看当前用户下的cron任务
crontab -e   #/编辑当前用户的定时任务
crontab -u  linuxso  -e   #/编辑用户linuxso的定时任务

添加要执行的计划任务

编辑文件/etc/crontab

vim /etc/crontab

语法格式说明

格式:m h dom mon dow user command

  • m: 表示分钟1~59 每分钟用或者 /1表示
  • h: 表示小时1~23(0表示0点)
  • dom: 表示日期1~31
  • mon: 表示月份1~12
  • dow: 表示星期0~6(0表示星期天)
  • user: 表示执行命令的用户
  • command: 表示具体命令

特殊符号说明

  • *: 表示任何时刻
  • ,: 表示分割
  • -: 表示一个段
  • /n:表示每隔单位执行一次(如第二段里,*/1, 就表示每隔1个小时执行一次命令。也可以写成1-23/1)

实战

00 8,12,16 * * * /data/app/scripts/monitor/df.sh
30 2 * * * /data/app/scripts/hotbackup/hot_database_backup.sh
10 8,12,16 * * * /data/app/scripts/monitor/check_ind_unusable.sh
10 8,12,16 * * * /data/app/scripts/monitor/check_maxfilesize.sh
10 8,12,16 * * * /data/app/scripts/monitor/check_objectsize.sh
 
43 21 * * *                              #每天21:43 执行
15 05 * * *                          #每天05:15 执行
0 17 * * *                               #每天17:00 执行
0 17 * * 1                               #每周一的 17:00 执行
0,10 17 * * 0,2,3                      #每周日,周二,周三的 17:00和 17:10 执行
0-10 17 1 * *                           #毎月1日从 17:00到7:10 毎隔1分钟 执行
0 0 1,15 * 1                             #毎月1日和 15日和 一日的 0:00 执行
42 4 1 * *                            #毎月1日的 4:42分 执行
0 21 * * 1-6                         #周一到周六 21:00 执行
0,10,20,30,40,50 * * * *          #每隔10分 执行
*/10 * * * *                  #每隔10分 执行
* 1 * * *                    #从1:0到1:59 每隔1分钟 执行
0 1 * * *                    #1:00 执行
0 */1 * * *                  #毎时0分 每隔1小时 执行
0 * * * *                    #毎时0分 每隔1小时 执行
2 8-20/3 * * *                #8:02,11:02,14:02,17:02,20:02 执行
30 5 1,15 * *                  #1日 和 15日的 5:30 执行

进程(process)和线程(thread)

实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包容的程序。
一个线程就是在进程中的一个单一的顺序控制流,单个进程可以拥有多个并发执行任务。

Runnable和Thread

通过Runnable定义任务

线程可以驱动任务,Java中是由Runnable接口提供,需要实现Runnable接口的run()方法。任务run()方法通常总会有某种形式的循环,使得任务一直运行下去直到不再需要。通常run()被写成无限循环的形式。
例如:

public class LiftOff implements Runnable {
    protected int countDown = 10;    //倒计数
    private static int taskCount;
    private final int id = taskCount ++;
    
    public LiftOff() {
        super();
    }
    public LiftOff(int countDown) {
        super();
        this.countDown = countDown;
    }
    
    public String status() {
        return String.format("#%d(%s).", id, (countDown > 0) ? countDown : "LiftOff!");
    }

    @Override
    public void run() {
        while(countDown-- > 0) {
            System.out.print(status());
            Thread.yield();
        }
    }
}

静态方法Thread.yield()

Thread.yield()线程调度器的一种建议,暗示:我已经执行完生命周期中最重要的部分了,现在正是将CPU资源切换给其他任务执行一段时间的时机。

Thread类

Runnable对象转变为工作任务的传统方式是将它提交给一个Thread的构造器。
使用方法如下:

public class BasicThreads {
    public static void main(String[] args) {
        Thread t = new Thread(new LiftOff());
        t.start();
        System.out.println("Waiting for LiftOff");
    }
}

Executor和线程池

Executor是Java SE5引入的,用来管理Thread对象,从而简化并发编程。Executor是启动任务的优选方法

ExecutorService

ExecutorService是具有生命周期的Executor,会管理如何构建恰当的上下文来执行Runnable对象。非常常见的情况是:单个Executor用来创建和管理系统中所有的任务。
在任何线程池中,在现有的线程的可能情况下,都会被自动复用

CachedThreadPool

CachedThreadPool为每个任务都创建一个线程,使用方法:

public class CachedThreadPool {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i = 0; i < 5; i ++) {
            exec.execute(new LiftOff());
        }
        exec.shutdown();
    }
}

- 阅读剩余部分 -

Android中常用的应用都会大量使用网络,比如QQ、微信、访问网页等等。通过使用网络,可以展示网页,发起Http请求获取数据或提交数据。

WebView控件

有时我们可能需要让我们自己的应用展示网页,我们可以使用Android内置的控件WebView

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        WebView webView = (WebView)findViewById(R.id.web_view);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.setWebViewClient(new WebViewClient());
        webView.loadUrl("http://www.baidu.com");
    }
}

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <WebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

由于使用了网络功能,需要声明网络权限。在AndroidManifest.xml添加android.permission.INTERNET:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jyoryo.app.android.study">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

- 阅读剩余部分 -

内容提供器Content Provider用于不同程序之间进行数据共享:允许其它程序访问操作提供内容提供器的程序,同时保证提供被访问的数据的安全性。

内容提供器Content Provider一般有两种用法:

  • 使用其他现有提供内容提供器的程序来读取和操作该程序的数据;
  • 自身程序创建内容提供器供其他程序读取和操作自身程序的数据,即对外提供接口。

比如常用的电话簿、短信、媒体库等都提供了内容提供器供其他程序读取和操作数据。

内容提供器Content Provider基本用法

通过Context中的getContentResolver()获取ContentResolver类的实例来访问其他程序的内容提供器的数据,进行相关CRUD操作。相关方法与SQLiteDatabase基本相同,只不过方法参数有些不同。

  • insert():添加数据;
  • update():更新数据;
  • delete():删除数据;
  • query():查询数据。

ContentResolverCRUD操作不接受表名,而是使用Uri参数(内容URI)。内容URI给内容提供器提供了唯一标志符,它是有两部分组成:authoritypath

  • authority:用于区分应用程序,一般为了更好的区分,都会采用程序包名再加上provider来命名。比如程序的包名com.demo.app,那么该程序的authority可以命名为com.demo.app.provider
  • path:用于区分同一程序中不同表,通常都会添加在authority后面。比如程序存在2张表table1table2,对应的path就可以分别命名为/table1/table2
  • 内容URI:是将上面的authoritypath进行组合,然后再在头部加上协议声明。比如content://com.demo.app.provider/table1content://com.demo.app.provider/table2

- 阅读剩余部分 -

Android自发布以来就已经存在权限,但是之前版本(Android 6.0以前)对用户安全和隐私数据保护是很有限的,特别是常用离不开的App,很容易造成用户数据泄露。为了解决这个问题,Android6.0开始引入运行时权限

Android权限机制

Android中如果要访问网络、监听开机广播、监听来电或者短信息、监听网络变化等等,就必须在AndroidManifest.xml文件中添加权限声明,比如:

<!-- 网络状态 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 系统开机 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

运行时权限:用户在安装软件的时候可以不用一次性授权所有权限,而是在软件的实际使用过程中再逐一对某一权限进行申请授权。
Android的权限可以归为2类:普通权限、危险权限。

普通权限:不会直接威胁到用户安全和隐私的权限,对于这部分权限,系统会自动帮我们进行授权,而不用用户进行手动全操作授权;

危险权限:是指会获取用户敏感数据或对设备安全性造成影响的权限,如联系人、定位位置、拨打电话等,对于这部分权限,必须由用户进行手动点击进行授权,否则程序无法使用该权限对应的功能。

危险权限一共是9组24个权限,9组分别为日历、拍照、联系人、位置、麦克风、电话、传感器、短信及存储:

- 阅读剩余部分 -

Android中针对数据的持久化存储主要提供了3种:文件存储SharedPreference存储SQLite数据库存储。除了上面3种外,也可也将数据保存在设备的SD卡中,不过使用这3种相对更简单易用。

文件存储

Android中对文件存储的内容不进行任何格式化处理,所有的数据都是原样保存至文件中,因此比较适合存储一些文本内容或二进制数据。
存储的文件默认都是存储在路径:/data/data/{package_name}/files这个目录下,{package_name}为程序的包名。

将数据保存至文件

Context类中提供openFileOutput()方法,获取向文件写入数据的输出流。方法需要2个参数:第一个参数是文件名;第二个参数是写入模式,主要有2种(MODE_PRIVATEMODE_APPEND)。
MODE_PRIVATE:是默认的写入模式,该模式下,如果存在同名的时候,会将写入的内容完全覆盖源文件的内容。
MODE_APPEND:如果已存在该文件,就往该文件追加写入数据;如果文件不存在,则新建该文件。

    private void save(String content) {
        FileOutputStream out = null;
        BufferedWriter writer = null;
        try {
            out = openFileOutput("data", Context.MODE_PRIVATE);
            writer = new BufferedWriter(new OutputStreamWriter(out));
            writer.write(content);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if(null != writer) {
                    writer.close();
                }
                if(null != out) {
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

从文件中读取数据

Context类中提供openFileInput()方法,获取从文件读取数据的输入流。

    private String load() {
        FileInputStream in = null;
        BufferedReader reader = null;
        StringBuilder sbd = new StringBuilder();
        try {
            in = openFileInput("data");
            reader = new BufferedReader(new InputStreamReader(in));
            String line = "";
            while (null != (line = reader.readLine())) {
                sbd.append(line);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if(null != reader) {
                    reader.close();
                }
                if(null != in) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return sbd.toString();
    }

- 阅读剩余部分 -