在实践中运用多线程

本文摘抄自《Java 多线程编程实战指南》核心篇 第四章小结

本章介绍了利用多线程实现并发计算的基本方法以及多线程编程实践中的注意事项及应对措施。


  挖掘出程序中的可并发点是实现多线程编程的目标——并发计算的前提。

  实现并发化的策略包括基于数据的分割策略和基于任务的分割策略。前者从程序处理的数据角度入手,将原始输入分解为若干规模更小的子输入,并将这些子输入指派给专门的工作者线程处理。其结果是产生若干同质的工作者线程。后者从程序的处理逻辑角度入手,将原始任务处理逻辑依照任务的资源消耗属性或者处理步骤分解为若干个子任务,并创建专门的工作者线程来执行这些子任务。其结果是产生多个相互协作的异质工作者线程。

  多线程编程实践中需要注意以下几下问题。

  • 考虑到多线程程序往往比相应的单线程程序要复杂,且未必比相应的单线程程序快,因此多线程编程的一个实施策略是考虑从单线程程序向多线程程序“进化”,而不是直接迈向“多线程”。

  • 线程数的合理设置。设置线程数的基本原则就是避免随意设置/使线程数可配置或者可以动态计算得来。设置合理的线程数需要考虑系统的资源状况(处理器数目/内存大小等)/线程所执行的任务的特性(CPU 密集型任务/I/O 密集型任务)/资源使用情况规划(CPU 使用率上限)以及程序运行过程中使用到的其他稀缺资源情况(如数据库连接/文件句柄数)等因素。

  • 多线程程序往往比相应的单线程程序产生更多的开销,且需要注意工作者线程的异常处理以及原始任务规模未知问题的应对。

本章知识结构图


Java 线程同步机制

本文摘抄自《Java 多线程编程实战指南》核心篇 第三章小结

本章介绍了 Java 平台提供的各种线程同步机制。


  Java 线程同步机制的幕后助手是内存屏障。不同同步机制的功能强弱不同,相应的开销以及可能导致的问题也不同,如下表所示。因此,我们需要根据实际情况选择一个功能适用且开销较小的同步机制。

Java 线程同步机制的功能与开销/问题

volatile CAS final static
原子性保障 具备 具备② 具备 不涉及 不涉及
可见性保障 具备 具备 不具备 不具备 具备③
有序性保障 具备 具备 不涉及 具备 具备④
上下文切换? 可能① 不会 不会 不会 可能⑤
备注 ①被争用的锁可能导致上下文切换 ②仅能够保障对 volatile 变量读/写操作本身的原子性 ③④仅在一个线程初次读取一个类的静态变量时起作用
⑤静态变量所属类的初始化可能导致上下文切换

  锁是 Java 平台中功能最强大的一种线程同步机制,同时其开销也最大,可能导致的问题也最多。被争用的锁会导致上下文切换,锁还可能导致死锁/锁死等线程活性故障。锁适用于存在多个线程对多个共享数据进行更新/check-then-act 操作或者 read-modify-write 操作这样的场景。

  锁的排他性以及 Java 虚拟机在临界区前后插入的内存屏障使得临界区中的操作具有原子性。由此,锁还保障了写线程在临界区中执行操作在读线程看来是有序的,即保障了有序性。Java 虚拟机在 MonitorExit 对应的机器码后插入的内存屏障则保障了可见性。锁能够保障线程安全的前提是访问同一组共享数据的多个线程必须同步在同一个锁之上,否则原子性/可见性和有序性均无法得以保障。在满足貌似串行语义的前提下,临界区内以及临界区外的操作可以在各自范围内重排序。临界区外的操作可能会被 JIT 编译器重排到临界区内,但是临界区内的操作不会被编译器/处理器重排到临界区之外。

  Java 中的所有锁都是可重入锁。内部锁(synchronized)仅支持非公平锁,因此它可能导致饥饿。而显式锁(ReentrantLock)既支持非公平锁又支持公平锁,显式锁可能导致锁泄露。内部锁和显式锁各有所长,各有所短。读写锁(ReadWriteLock)由于其内部实现的复杂性,仅适用于只读操作比更新操作要频繁得多且读线程持有锁的时间比较长的场景。读写锁(ReadWriteLock)中的读锁和写锁是一个锁实例所充当的两个角色,并不是两个独立的锁。

  线程转储中可以包含锁的相关信息——线程在等待哪些锁,这些锁又是被哪些线程持有的。

  volatile 相当于轻量级锁。在线程安全保障方面与锁相同的是,volatile 能够保障可见性/有序性;与锁不同的是 volatile 不具有排他性,也不会导致上下文切换。与锁类似,Java 虚拟机实现 volatile 对有序性和可见性的保障也是借助于内存屏障。从这个角度来看,volatile 变量写操作相当于释放锁,volatile 变量读操作相当于获得锁——Java 虚拟机通过在 volatile 变量写操作之前插入一个释放屏障,在 volatile 读操作之后插入一个获取屏障这种成对的释放屏障和获取屏障的使用实现了 volatile 对有序性的保障。类似地,Java 虚拟机在 volatile 变量写操作之后插入一个存储屏障,在 volatile 变量读操作之前插入一个加载屏障这种成对的存储屏障与加载屏障的使用实现了 volatile 对可见性的保障。

  在原子性方面,volatile 仅能够保障 long/double 型变量写操作的原子性。如果要保障对 volatile 变量的赋值操作的线程安全,那么赋值操作右边的表达式不能涉及任何共享变量(包括被赋值的变量本身)。volatile 关键字在可见性/有序性和原子性方面的保障并不会对其修饰的数组元素的读/写起作用。

  volatile 变量写操作的成本介于普通变量的写操作和在临界区内进行的写操作之间。读取一个 volatile 变量总是意味着(通过高速缓存进行的)读内存操作,而不是从寄存器中读取。因此,volatile 变量读操作的成本比读取普通变量要略高一些,但比在临界区中读取变量要低。

  volatile 的典型运用场景包括:一,使用 volatile 变量作为状态标志;二,使用 volatile 保障可见性;三,使用 volatile 变量替代锁;四,使用 volatile 实现简易版读写锁。

  CAS 使得我们可以在不借助锁的情况下保障 read-modify-write 操作/check-then-act 操作的原子性,但是它并不保障可见性。原子变量类相当于基于 CAS 实现的增强型 volatile 变量(保障 volatile 无法保障的那一部分操作的原子性)。常用的原子变量类包括 AtomicInteger/AtomicLong/AtomicBoolean 等。AtomicStampedReference 则可以用于规避 CAS 的 ABA 问题。

  static 关键字能够保证一个线程即使在未使用其他同步机制的情况下也总是可以读取到一个类的静态变量的初始值(而不是默认值)。对于引用型静态变量,static 还确保了该变量引用的对象已经初始化完毕。但是,static 的这种可见性和有序性保障仅在一个线程初次读取静态变量的时候起作用。

  final 关键字在多线程环境下也有其特殊作用:当一个对象被发布到其他线程的时候,该对象的所有 final 字段(实例变量)都是初始化完毕的。而非 final 字段没有这种保障,即这些线程读取该对象的非 final 字段时所读取到的值可能仍然是相应字段的默认值。对于引用型 final 字段,final 关键字还进一步确保该字段所引用的对象已经初始化完毕。

  实现对象的安全发布,通常可以依照以下顺序选择适用且开销最小的线程同步机制。

  • 使用 static 关键字修饰引用该对象的变量。
  • 使用 final 关键字修饰引用该对象的变量。
  • 使用 volatile 关键字修饰引用该对象的变量。
  • 使用 AtomicReference 来引用该对象。
  • 对访问该对象的代码进行加锁。

  为避免将 this 代表的当前对象逸出到其他线程,我们应该避免在构造器中启动工作者线程。通常我们可以定义一个 init 方法,在该方法中启动工作者线程。在此基础上,定义一个工厂方法来创建(并返回)相应的实例,并在该方法中调用该实例的 init 方法。

本章知识结构图


多线程编程的目标与挑战

本文摘抄自《Java 多线程编程实战指南》核心篇 第二章小结

本章通过一些具体概念介绍了多线程编程的目标及其面临的挑战。

  • 单线程程序所进行的计算本质上是串行。多线程编程的目标是将原本串行的计算改为并发乃至并行。

  • 竞态(Race Condition)是指计算的正确性依赖于相对时间顺序(Relative Timing)或者线程的交错(Interleaving)。竞态表现为计算的结果时而正确时而错误,它并不意味着计算的结果一定是错误的,其往往伴随着读脏数据/丢失更新的问题。竞态是访问(读取/更新)同一组共享变量的多个线程所执行的操作相互交错(Interleave)而导致的干扰(读取脏数据)或者冲突(丢失更新)的结果。二维表分析法是分析和解释竞态的有效和常用工具。一个类能够导致竞态,那么它就不是线程安全的。线程安全意味着不存在竞态,但是不存在竞态却未必意味着线程安全。

  • 线程安全问题表现为原子性/可见性和有序性这三个方面。这几个方面既相互区别,又相互联系。原子性的保障能够消除竞态。可见性描述了一个线程对共享变量的更新对于另外一个线程而言是否可见,或者说什么情况下可见的问题。原子性和可见性一同得以保障了一个线程能够共享变量的相对新值,而不是一个“半成品”的值。有序性描述了一个处理器上运行的一个线程对共享变量所做的更新,在另外一个处理器上运行的其他线程看来,这些线程是以什么样的顺序观察到这些更新的问题。可见性是有序性的基础,而有序性又可能影响可见性。

  • 原子操作是“不可分割”的操作。所谓“不可分割”包括两层含义:其一,访问(读/写)某个共享变量的操作从其执行线程以外的任何线程来看,该操作要么已经执行结束,要么尚未发生,即其他线程不会“看到”该操作执行了部分的中间效果;其二,访问同一组共享变量的原子操作是不能够被交错的,这通常意味着互斥(Mutual Exclusion),即对于访问同一组共享变量的多个原子操作,一个线程执行其中一个操作的时候其他线程无法访问这组共享变量中的任意一个变量。将 read-modify-write 操作和 check-then-act 转换为原子操作能够消除竞态。在 Java 语言中,对 long/double 型以外的任何变量的写操作都是原子的。volatile 关键字修饰的 long/double 型写操作也具有原子性。针对任何变量的读操作都是原子操作。

  • 可见性问题不是必然出现的,而一旦出现则可能导致灾难性后果。导致可见性问题的因素既有软件因素(JIT 编译器)也有硬件因素(处理器和内存等存储设备)。可见性的保障仅仅意味着一个线程能够读取到共享变量的相对新值,而不能保障更新对这个子线程可见,子线程执行期间对共享变量所做的更新对该线程的 join() 执行线程可见(从 join() 返回处开始才是可见的)。

  • 编译器/处理器/存储子系统(写缓冲器和高速缓存等)和运行时(JIT 编译器)都可能导致重排序。重排序是出于性能的需要并在满足“貌似串行语义”的前提下进行的,它可能导致线程安全问题。于可见性问题类似,重排序也不是必然出现的。有序性的保障是通过部分地从逻辑上禁止重排序实现的。可见性是有序性的基础,而有序性反过来又可能影响可见性。

  • 上下文切换可以被看作多线程编程的必然产物,一方面它使得充分利用极其有限的处理器资源成为可能;另一方面它也增加了系统的开销。因此,多线程编程未必比单线程的计算效率要高。程序运行过程中发生的上下文切换既有自发性上下文切换,也有非自发性上下文切换。Linux 内核提供的 perf 命令可以帮助我们测量程序运行过程中发生的上下文切换的次数和频率。

  • 多线程程序可能由于资源稀缺性或者程序自身的错误和缺陷而一直处于非 RUNNABLE 状态,或者即使是处于 RUNNABLE 状态,但是其要执行的任务一直无法进展,即产生了活性故障。

  • 非公平调度策略是我们多数情况下的首选资源调度策略。其优点是吞吐率较大;缺点是资源申请者申请资源所需的时间偏差可能较大,并可能导致饥饿现象。公平调度策略适合在资源的持有线程占用资源的时间相对长或资源的平均申请时间间隔相对长的情况下,或者对资源申请所需的时间偏差有所要求的情况下使用。其优点是线程申请资源所需的时间偏差较小,并且不会导致饥饿现象;其缺点是吞吐率较小。

本章知识结构图


焦躁六月(2019)

送走了愉快的五月,就来了令人焦躁不已的六月。

不知道是不是温度高了天气热了,总是在这样的季节使得焦躁浑身难受然后发生各种事情。


租房

在外漂没有自己的房子真的是很难受,换工作不在附近得搬,房东不续租说搬也就得搬。

住的地是三室户,这不马上就到期了,一室友不续租了跟女朋友住去结果导致空一间房,找不到人接盘,房东也就不跟我们续租了。也罢,老房子什么设施都是坏的,是得要对自己好一点了,搬吧,那就找房子吧。

找房也还算顺利,虽然是通过中介的,多付了好几千中介费,这也算是找了个新住处,无缝对接。这房子也是个三室,不过面积要稍微大点,环境也好一点,出门就是欧尚超市不远还有个生活广场。原来住的三小伙要搬去浦东刚好也要到期了,房子是真的贵,感觉还合适那就快刀斩乱麻,直接签了得。

过几天得收拾东西准备搬了,其它的倒是还好,就我那个大鱼缸有点费事,也是我要搬之所以头疼的事。说到还是个三室,那还是要准备拉一个人来的,刚好有个新同事过来,先住着,后面就再说吧。也不知道搬过去还能住多久,沪漂什么时候是个头。事情真多,头疼。

淄博

公司以前的一个项目突然就把我派过去考察了,山东淄博潭溪山景区。地方是真的挺偏的,下了高铁,打了个出租一直开了将近两小时,其中山路开了近一个小时,还是盘山上去的。据了解来这的都是自驾的多,不然交通确实是不方便。

在那边呆了差不多四天吧,他们吃粗粮的多。我就入乡随俗,跟着吃了几天的菜饼馒头,一天吃个两顿,还真管饱。

家族企业自建的景区,项目确实是多,玻璃桥建得也很奇,十五根索支持,还是值得去一趟的,就是地方偏了点。有一天中午我从那边跟着人群走山路下来,八百多米啊,走得后来腿都打颤了。现在小腿还酸着呢。在这边我就不吐槽公司。

回来路上开车师傅路边买了点油桃,才两块钱一斤,那边的水果确实是好吃点,跟气候环境关系很大吧。看看他们那边居民生活也挺自在的,景点工作人员早上七点集合一起去山上上班,平时没什么人没什么事就坐在那玩玩手机睡睡觉,等到下午五点就下班回去吃饭了。

回来的火车不知道因为什么事还晚点了半小时左右,我是中转去济南西再到上海的,还好给自己留的时间够长,不然又得要费一番劲了。回到上海晚上八点多,回到住的地方晚上十点多,喝了点稀饭吃了两个家里带的粽子,澡就第二天洗了,弄弄就睡了。

回来公司还有一堆事。

搬搬搬

回头再来说说搬家这个事。

其它东西倒还好,搬的地方也不是很远,还是就那个缸是真的不好弄。思来想去还是叫了辆依维柯,一趟省事,六七十倒也不贵。

周末大雨啊,必是一番雨淋淋。之前一直是直接用的投影,没用上显示器,这下那边房间虽然小但是有桌子,显示器也有地方可以放了,刚好家里有个闲置的显示器,这不得要叫我姐给我寄过来,顺丰21,我最喜欢的数字。家里电脑配置都是七八年前的了,看年底的时候要不要把这边的电脑给他们带回去,自己再重新配一台,要努力赚钱攒钱咯。

这么一搬,房租肯定是上去了,不过还好,还能接受吧。独自在外无人关怀,也不知道这样还要再过上几年啊。


最近发现一个很好听的声音,小C英乐,学英语又有动力了!


走近 Java 世界中的线程

本文摘抄自《Java 多线程编程实战指南》核心篇 第一章小结

本章介绍了线程、多线程编程这两个基本概念以及 Java 平台对线程的实现。


  • 进程是程序的运行实例,一个进程可以包含多个线程,这些线程共享其所在进程的资源。

  • 线程是进程中可独立执行的最小单位。Java 标准库类 java.lang.Thread 就是 Java 平台对线程的实现。特定线程总是在执行特定的任务,线程的 run 方法就是线程所要执行任务的处理逻辑的入口方法,该方法由 Java 虚拟机直接调用执行。Java 标准库接口 java.lang.Runnable 就是对任务的抽象,Thread 类就是 Runnable 接口的一个实现类

  • 应用程序负责线程的创建与启动,而线程调度器负责线程的调度和执行。Java 平台中有两种方式创建线程:创建 Thread 的子类和以 Runnable 接口实例为构造器参数直接通过 new 创建 Thread 实例。

  • 在 Java 平台中,任何一段代码总是执行在确定的代码中的。同一段代码可以被不同的线程执行。代码可以通过 Thread.currentThread() 调用来获取其当前执行线程。

  • 为每个线程设置一个简短而含义明确的名称属性有助于多线程程序的调试和问题定位。

  • 一个线程从其创建到运行结束的整个生命周期会经历若干状态。线程执行过程中调用一些对象的方法(如 Thread.sleep(long millis))或者执行特定的操作(如 I/O 操作)往往导致其状态的变更。线程转储是对线程进行监视的重要媒介。操作系统以及 JDK 都提供了一些工具(jvisualvm、jstack 和 Java Mission Control),可以用来获取线程转储。

  • Java 平台是一个多线程的平台,线程的身影在 Java 平台中无处不在。按照线程间的创建关系,我们可以将多个线程间的关系理解为一个层次关系。Java 并无相关 API 用于获取一个线程的父线程或子线程,父线程和子线程之间的生命周期并无必然联系。

  • 线程是多线程编程的基本单位。多线程编程一方面有助于提高系统的吞吐率、提高软件的响应性、充分利用多核处理器资源、最小化对系统资源的使用和简化程序的结构,另一方面面临线程安全问题、线程活性问题、上下文切换和可靠性等问题。因此,多线程编程绝不仅仅是使用多个线程进行编程那么简单,多线程编程有其自身需要解决的问题,而这正是后续章节的主要内容。

本章知识结构图


SpringBoot2 实现邮件发送功能

springboot2 实现邮件发送功能,QQ/Gmail/163/126..

个人博客:DoubleFJ の Blog

效果图如下:springboot 实现邮件发送

技术选型

  • Spring Boot 2.1.3.RELEASE (原本官网推荐 2.1.5.RELEASE,可是搭建途中发现部分注解未生效,故改之)
  • Thymeleaf (用作邮件模板)
  • JDK 1.8

简要讲解

依赖以及配置

这里还是用的 Spring Boot 来整合,自带了模块依赖

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

随着 Spring Boot 热度越来越大,现在从事 Java 的要是不知道 Spring Boot 的存在那就真的很不应该了。本来只是为了取代繁琐的 EJB,一直发展到了如今无所不在的地步。

然后在配置文件中进行对应的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server:
port: 8887

spring:
mail:
host: smtp.163.com
username: ffj0721@163.com
password: xxxx # 授权码
protocol: smtp
properties.mail.smtp.auth: true
properties.mail.smtp.port: 994
properties.mail.display.sendmail: DoubleFJ
properties.mail.display.sendname: Spring Boot Email
properties.mail.smtp.starttls.enable: true
properties.mail.smtp.starttls.required: true
properties.mail.smtp.ssl.enable: true
default-encoding: utf-8
from: ffj0721@163.com

一切就是这样的简单清晰。不同的邮件个别配置数据不同,请自行查阅,这里只用 163 做测试。

配置了之后我们开始操刀敲代码,其实只需要调用 JavaMailSender 接口即可,传参实现,已经给我们封装好了。

常用邮件接口

这里是几个常用的邮件接口:

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
package com.example.springbootmail.service;
import javax.mail.MessagingException;

/**
* 常用邮件接口
*/
public interface IMailService {
/**
* 发送文本邮件
* @param to
* @param subject
* @param content
*/
public void sendSimpleMail(String to, String subject, String content);

public void sendSimpleMail(String to, String subject, String content, String... cc);

/**
* 发送HTML邮件
* @param to
* @param subject
* @param content
* @throws MessagingException
*/
public void sendHtmlMail(String to, String subject, String content) throws MessagingException;

public void sendHtmlMail(String to, String subject, String content, String... cc);

/**
* 发送带附件的邮件
* @param to
* @param subject
* @param content
* @param filePath
* @throws MessagingException
*/
public void sendAttachmentsMail(String to, String subject, String content, String filePath) throws MessagingException;

public void sendAttachmentsMail(String to, String subject, String content, String filePath, String... cc);

/**
* 发送正文中有静态资源的邮件
* @param to
* @param subject
* @param content
* @param rscPath
* @param rscId
* @throws MessagingException
*/
public void sendResourceMail(String to, String subject, String content, String rscPath, String rscId) throws MessagingException;

public void sendResourceMail(String to, String subject, String content, String rscPath, String rscId, String... cc);

}

接口实现类注入 JavaMailSender

实现类分别实现上述接口 注入 JavaMailSender

1
2
3
4
5
6
7
8
9
10
11
/**
* 发送邮件实现类
*/
@Service
public class IMailServiceImpl implements IMailService {

@Autowired
private JavaMailSender mailSender;

@Value("${spring.mail.from}")
private String from;

其中 from 就是配置中我们配置的发送方,直接读取使用。

实现发送文本邮件

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
/**
* 发送文本邮件
*
* @param to
* 邮件接收方
* @param subject
* 邮件标题
* @param content
* 邮件内容
*/
@Override
public void sendSimpleMail(String to, String subject, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
}

/**
*
* @param to
* 邮件接收方
* @param subject
* 邮件标题
* @param content
* 邮件内容
* @param cc
* 抄送方
*/
@Override
public void sendSimpleMail(String to, String subject, String content, String... cc) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setCc(cc);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
}

如上所示,三个参数的,第一个是你要发邮件的对象,第二个是邮件标题,第三个是邮件发送的内容,第二个 cc 字符串数组就是需要抄送的对象。

实现发送 HTML 邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 发送 HTML 邮件
*
* @param to
* @param subject
* @param content
*/
@Override
public void sendHtmlMail(String to, String subject, String content) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();

// true 表示需要创建一个multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);

mailSender.send(message);
}

官网 MimeMessageHelper 使用介绍。

实现发送带附件邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 发送带附件的邮件
*
* @param to
* @param subject
* @param content
* @param filePath
*/
public void sendAttachmentsMail(String to, String subject, String content, String filePath) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();

MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);

FileSystemResource file = new FileSystemResource(new File(filePath));
// 截取附件名
String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
helper.addAttachment(fileName, file);

mailSender.send(message);
}

这里附件名的截取要按照自己的实际需求来,有人喜欢用 \\,有人喜欢用 /,看具体情况具体分析了。

实现发送正文中有静态资源邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 发送正文中有静态资源(图片)的邮件
*
* @param to
* @param subject
* @param content
* @param rscPath
* @param rscId
*/
public void sendResourceMail(String to, String subject, String content, String rscPath, String rscId) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();

MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);

FileSystemResource res = new FileSystemResource(new File(rscPath));
helper.addInline(rscId, res);

mailSender.send(message);
}

其中 rscId 是资源的唯一 id,rscPath 就是对应资源的路径。具体待会我们看 Controller 调用方法。

实现发送模板邮件

前面说了我们选择使用 Thymeleaf 作为邮件的模板,那就需要在 POM 文件中加入对应依赖。

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

然后在 templates 文件夹下新建 mailTemplate.html 页面,我的内容如下:

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
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>spring-boot-mail test</title>
<style>
body {
text-align: center;
margin-left: auto;
margin-right: auto;
}
#welcome {
text-align: center;
}
</style>
</head>
<body>
<div id="welcome">
<h3>Welcome To My Friend!</h3>

GitHub:
<a href="#" th:href="@{${github_url}}" target="_bank">
<strong>GitHub</strong>
</a>
<br />
<br />
个人博客:
<a href="#" th:href="@{${blog_url}}" target="_bank">
<strong>DoubleFJ の Blog</strong>
</a>
<br />
<br />
<img width="258px" height="258px"
src="https://raw.githubusercontent.com/Folgerjun/materials/master/blog/img/WC-GZH.jpg">
<br />微信公众号(诗词鉴赏)
</div>
</body>
</html>

这个模板自己 DIY 即可,不过对应填充参数不可弄错。例如我上面的是 github_urlblog_url

Thymeleaf 官网

调用实现

MailController.java

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

import com.example.springbootmail.service.impl.IMailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

@RestController
@RequestMapping("/Mail")
public class MailController {

private static final String SUCC_MAIL = "邮件发送成功!";
private static final String FAIL_MAIL = "邮件发送失败!";

// 图片路径
private static final String IMG_PATH = "C:/Users/zjj/Desktop/github/materials/blog/img/WC-GZH.jpg";
// 发送对象
private static final String MAIL_TO = "folgerjun@gmail.com";

@Autowired
private IMailServiceImpl mailService;
@Autowired
private TemplateEngine templateEngine;

@RequestMapping("/Email")
public String index(){
try {
mailService.sendSimpleMail(MAIL_TO,"这是一封普通的邮件","这是一封普通的SpringBoot测试邮件");
}catch (Exception ex){
ex.printStackTrace();
return FAIL_MAIL;
}
return SUCC_MAIL;
}

@RequestMapping("/htmlEmail")
public String htmlEmail(){
try {
mailService.sendHtmlMail(MAIL_TO,"这是一HTML的邮件","<body>\n" +
"<div id=\"welcome\">\n" +
" <h3>Welcome To My Friend!</h3>\n" +
"\n" +
" GitHub:\n" +
" <a href=\"#\" th:href=\"@{${github_url}}\" target=\"_bank\">\n" +
" <strong>GitHub</strong>\n" +
" </a>\n" +
" <br />\n" +
" <br />\n" +
" 个人博客:\n" +
" <a href=\"#\" th:href=\"@{${blog_url}}\" target=\"_bank\">\n" +
" <strong>DoubleFJ の Blog</strong>\n" +
" </a>\n" +
" <br />\n" +
" <br />\n" +
" <img width=\"258px\" height=\"258px\"\n" +
" src=\"https://raw.githubusercontent.com/Folgerjun/materials/master/blog/img/WC-GZH.jpg\">\n" +
" <br />微信公众号(诗词鉴赏)\n" +
"</div>\n" +
"</body>");
}catch (Exception ex){
ex.printStackTrace();
return FAIL_MAIL;
}
return SUCC_MAIL;
}

@RequestMapping("/attachmentsMail")
public String attachmentsMail(){
try {
mailService.sendAttachmentsMail(MAIL_TO, "这是一封带附件的邮件", "邮件中有附件,请注意查收!", IMG_PATH);
}catch (Exception ex){
ex.printStackTrace();
return FAIL_MAIL;
}
return SUCC_MAIL;
}

@RequestMapping("/resourceMail")
public String resourceMail(){
try {
String rscId = "DoubleFJ";
String content = "<html><body>这是有图片的邮件<br/><img src=\'cid:" + rscId + "\' ></body></html>";
mailService.sendResourceMail(MAIL_TO, "这邮件中含有图片", content, IMG_PATH, rscId);

}catch (Exception ex){
ex.printStackTrace();
return FAIL_MAIL;
}
return SUCC_MAIL;
}

@RequestMapping("/templateMail")
public String templateMail(){
try {
Context context = new Context();
context.setVariable("github_url", "https://github.com/Folgerjun");
context.setVariable("blog_url", "http://putop.top/");
String emailContent = templateEngine.process("mailTemplate", context);

mailService.sendHtmlMail(MAIL_TO, "这是模板邮件", emailContent);
}catch (Exception ex){
ex.printStackTrace();
return FAIL_MAIL;
}
return SUCC_MAIL;
}
}

程序运行后若访问 http://localhost:8887/Mail/Email 页面出现邮件发送成功!字样就说明邮件发送已经实现了。

相关链接


愉快五月(2019)

这个五月过得是真的快啊~

五一回家在家呆了三天,跟父母老姐和姐夫(已订婚)去了趟老家县城附近的自建的一个玻璃栈道景点。过年期间就在朋友圈看到刷屏了,这次趁着空一起去逛了逛。老家小县城本就四面环山,景色相当不错,也有几个国家 4A 级景点,什么仙华山、神丽峡、江南第一家啊……

我老爸恐高啊,我也有点。全程在桥上老爸搭着老妈的肩膀小心翼翼地走在玻璃桥的边缘,事后一直被老妈嘲笑,家里也是很少有这样的机会一家人一起出去玩玩,我很喜欢一家人在一起的感觉。

一定要多跟他们在一起!


五一回来上班一周,又出去耍了!公司旅游投票菲律宾和塞班岛,结果毫无疑问,塞班岛走起~大晚上的飞机四个半小时凌晨飞到那边,一下飞机热浪扑面而来,弄弄行李到达酒店,天就亮了,太快了吧。

进房间洗洗躺下,大太阳就刺进来了,朝东的房间,太丫的亮了。结果躺了起,起了躺,后来终于睡着了,本来是十点钟门口集合导游大巴带我们去景点逛,这一睡也太舒服了,空调打得低。微信群里无人反馈(睡死了都),随着同事破门而入,一阵慌乱中奔向大巴。

岛不大,逛逛一圈也用不了多久。

景色是真的不错,碧海蓝天白云。

(拍了不少帅比照,哈哈)

就是太阳毒了点,回来黑了不少,后背轻微脱皮。本想晒成古天乐的,差点晒了个熊猫眼,下次再晒。在海边一点腥味都没闻到,在这样的海边上住着才是真的享受啊。

不得不说,在老美的地盘花着美金消费就是高,资金不允许啊就没怎么玩多少项目,不过也是体验了拖伞和浮潜。大头都留在那边的赌场了……

逛了一圈那边的礼品店,乍一眼都是从中国的地摊上运过来的,本地也没什么特色,我也就没买东西,哈哈(反正买了也没人送)

玩着耍着,六天五晚的旅程就结束了,要回来上班咯。

再上两三个星期就端午了,再之后就只有到中秋国庆了,能回家还是要回家的啊。


刚从塞班岛回来就又去了临港出差,开了个房还没走进去就听到老大声的“老公老公,啊啊啊嗯……”,真特么刺激,真好。

晚上就在他们的欢声笑语中入睡。


生活都不易,且活且珍惜。

做最好的自己。


惊险四月(2019)

就在昨天,公司自己刚搭没多久的小服务器中了 ETH 勒索病毒,还好刚起步数据量不大,真可怕!

把环境又给装了一遍,轻车熟路了。

专业事情还是需要专业人士来做,网络安全也是一个相当大的方面,只要被不法分子盯上就很有可能是一笔不小的损失了。

虽然说现在的技术科技发展很快,但是安全方面却不是那么跟得上脚步。中午跟同事聊起来,有些公司还是用最初的刻盘来保存数据,银行系统还有很多是 XP,回到本初,最原始的即最安全也是不无道理,整得再花再虚,我最需要的还是安全实用。

从这些角度继续往下想,有时候我们会埋怨某些系统太过于古老,技术太过于落后,这也是前几年我看到一些古老项目的想法。技术的世界日新月异,新轮子不断出现,内涵包裹越来越深,过于追求抽象化。

当我们选择项目中要用的技术栈时,肯定会优先选择那些稳定的、有人长期维护的、社区较活跃的,这些现象也正说明了这个技术关注度高,较为成熟。不然找偏门技术栈的可能出现了 Bug,其创始人都不知道为什么会出现该怎么解决,那不就废了吗。现在开源社区大多都是拿来即用,也很少会有人去专门研究源码,这就是技术选型。

当你接手了一个老项目,你了解了其作用与每个地方的功能后,发现市面上完全有成熟的技术可以取代之,且换架构后项目会更加清晰易理解。这个时候你就可以向上申报或者自己闲暇时间尝试给它换一个壳(当然是因人而异,也有可能是费力不讨好之事,若对自身有益且有闲时,可一做)。当完成这个“手术”之后,往往自己会对这个项目有了一个更深的理解,很可能还会找到几处优化的地方。改造成功一个项目,看着它,就像是你赋予了它生命一般,后期需要改动或是加功能打补丁都会更得心应手。

最近在看《深入理解计算机系统》,本是一本基础教材。明白了“万变不离其宗”的道理之后,深知基础的重要性。别人问我:你们搞计算机的要记的东西不少,感觉很难啊。我说:只要你脑子不笨不呆,灵活会转会用心记就行。

调调几个 API 谁不会呢,我才不管你是怎么实现的,我只要拿到我想要的数据得到我想要的结果就行了,不就是一堆数据传过来传过去。现在框架轮子多得死,一拼一凑就是一个“项目”。

可不是嘛,平时要写的就是一大坨的业务逻辑处理代码,正常的有几个会拎不清呢?当然也是有的,所以这层就开始出现了能力的差别。越往细小的领域,这差距就会越大。

业务逻辑写个几年,天花板就到头了,若是脑子不活络无管理能力的,那就是真的“废”了。虽说饿不死,至少以后找个公司维护维护老项目都能养活自己。这就导致了以后没有了 竞争力,年级越大越无奈,这一阶段的人可以说是到处都是,市场完全不会缺。

前一段时间招了个实习生(上海理工研二),自动化专业想要来写代码(跨专业,基础确实是差),说实话已是一片红海,若是像我上面说的那样还要去那个阶段一起游泳,那我觉得真是有点不值了。


前几天智齿长出来了!右腮帮就像被人挥了一拳一样,感觉是真的不爽!本命年才会长智齿,真的是准啊,长大了长大了。

同事搬过来变成了新室友,好家伙现在天天晚上都要粥煮起来吃,不吃睡不着,这吃下去再不长肉就真不科学了,再加上每天下午去楼下全家买两个包子或是两个馒头。搞了张尊享会员卡,也开始了没事喝喝咖啡的日子了(送的不喝那我就是有病了),血压低,多喝喝。

哈哈,室友将他那辆二手永久车送我了!我自己去补了个胎,就花了二十换了辆自行车,怎么都不亏啊哈哈。刚好摩拜季卡到期了,共享单车又集体涨价,这辆三手车(可能不止三手了……)至少能骑个半年咯。

等夏天到了,早上戴着墨镜骑着车听听音乐去上班,然后一身汗,湿透。


有名气有底蕴的大学是真的不一样,看了附近同济、复旦的校区后,自己的母校是何等的简陋。。。以后还要多去其它学校看看,欣赏欣赏别人大学的风景。

生活在于折腾,前提是还能经得起折腾。年纪轻轻就不要想那么多,想要的想做的就尽量去满足自己,一旦念头消散可能就再无机会了。

至少还能有可以愿意去回想的东西,还有内心觉得美好的日子,还有至今无悔的事情。

也许现在的生活并不是那么尽人意,但若是有可以回去的机会我还是会选择那样过,还是想要经历那些经历过的事情,虽然也许无果但无悔、无愧,青春,知足。


我这个人啊,耳根子软,心也软又善,还重情义。你若对我一丝好,我便会含泪感动入睡。

不过这不代表一向如此,上述只是我大多状态。人啊,久了都会不懂自己。

最近在补权游,Valar Morghulis.


LeetCode 之总持续时间可被 60 整除的歌曲(Pairs of Songs With Total Durations Divisible by 60)

题目虽然有点长,不过可以化简为同一个类型的,就是两两配对其和是某个数的倍数。

原题描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在歌曲列表中,第 i 首歌曲的持续时间为 time[i] 秒。

返回其总持续时间(以秒为单位)可被 60 整除的歌曲对的数量。形式上,我们希望索引的数字 i < j 且有 (time[i] + time[j]) % 60 == 0。

示例 1:
输入:[30,20,150,100,40]
输出:3
解释:这三对的总持续时间可被 60 整数:
(time[0] = 30, time[2] = 150): 总持续时间 180
(time[1] = 20, time[3] = 100): 总持续时间 120
(time[1] = 20, time[4] = 40): 总持续时间 60

示例 2:
输入:[60,60,60]
输出:3
解释:所有三对的总持续时间都是 120,可以被 60 整数。

提示:

1 <= time.length <= 60000
1 <= time[i] <= 500

初看题目,脑子都不要动,我相信大多人也是和我一样,两个 for 搞定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 超时
*
* @param time
* @return
*/
public int numPairsDivisibleBy60(int[] time) {
int result = 0;
for (int i = 0; i < time.length - 1; i++) {
for (int j = i + 1; j < time.length; j++) {
int value = (time[i] + time[j]) % 60;
if (value == 0)
result++;
}
}
return result;
}

果然不意外,提交显示超时了。

我们静下心来想想,两数之和是某个数的倍数。既然这样循环查找过于繁琐,那有什么方式可以快速精准查找呢?

哐…当然有了。

首先我们会想到数组,还有键值对形式存储的 Map

这里我使用的是数组,那要怎么使用它呢,再细想。

数组精准查找,那得要根据下标,两数之和为某数,自然,那就将数字作为下标存储到对应块中,值就是其对应的个数。

思路有了,代码如行云流水,请看:

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
public int numPairsDivisibleBy601(int[] time) {
// 每个元素取60余
time = Arrays.stream(time).map(x -> x % 60).toArray();
int[] arrCount = new int[60];
// 统计对应下标数
Arrays.stream(time).forEach(x -> {
arrCount[x]++;
});
// 余数是 0 或 30 的两两配对
int result = qiuhe(arrCount[0]) + qiuhe(arrCount[30]);
// 其余互相配对
for (int i = 1; i < 30; i++) {
int count1 = arrCount[i];
int count2 = arrCount[60 - i];
result += count1 * count2;
}
return result;
}

/**
* 求两两配对数
*
* @param num
* @return
*/
private int qiuhe(int num) {
if (num < 2)
return 0;
// (num-1)!
return num * (num - 1) / 2;
}

很好理解,不过速度还是慢,通过阅读其它人的代码后发现了另一种 O(n) 解法,cool~

1
2
3
4
5
6
7
8
9
10
11
12
13
public int numPairsDivisibleBy602(int[] time) {
int result = 0;
int[] arrCount = new int[60];
for (int t : time) {
// 对应的下标
int index = t == 0 ? 0 : 60 - t % 60;
// 与已经统计的数进行匹配 可防止两两重复匹配
result += arrCount[index];
// 对应数字加一
arrCount[t % 60]++;
}
return result;
}

一个 for 完美解决问题,想必这就是算法的魅力吧。


忙碌三月(2019)

三月头到三月底算是在这公司至今最忙的一段时间了

从三月初去昆明之后回来又去临港来回三趟,之后又是世博文化中心项目急得要死,加班赶项目还花了两周末。

还不错,今年公司五月去海岛旅游,该加紧锻炼了!


昆明

说来惭愧,昆明这趟是我的“处女飞”,去的时候坐的是吉祥航空经济舱,体验是真特么差,便宜啊便宜。回来坐的是昆明航空经济舱,贵是贵了,体验也确实好很多,不知什么时候能体验下头等舱的魅力。

去了昆明长水机场,那确实是偏了点,坐了两个多小时才到了目的地。之后在昆明医科大学吃了顿饭,附近找了个宾馆,乖乖,全程微信沟通,就过来送了个发票。

昆明路上拍摄的黄金龙头

你说这是“凤凰”?没差,龙凤呈祥!

去的时候那边温度比上海要高个 10℃,宾馆中都不带空调的,听说是四季如春啊,蓝天白云真不错。虽然没有大都市那么繁华,也很安谧,宜居。

晚上没事干搜了附近,去了家汗蒸店,结果一进去发现全是女的,尴尬。附近学校多嘛,女同学经常会来汗蒸放松放松,我就找了个角落闭目凝神静气。大概蒸了一个半小时,全身真的蒸透了,很舒服。

临港

临港创新科技城,规模不小设计造型独特,几年后估计又是一片天地。临港现在没有像市区那么繁华,街道上也几乎没什么人,打个车也没那么方便,不过看这发展的样子,未来也是个不错的地方,如果生活工作都在临港,那绝对幸福感不会低,看了下,均价三万一平吧。

去临港三月去了三趟,过去要 8 号线 -> 2 号线 -> 16 号线,将近三小时的路程。不过还好,每次都待了个几天不然一天来回估计脑袋够呛。

公司接的项目都这么高级,着实厉害。

原先我们都是现场用 485 串口服务器直连现场服务器进行数据采集,包括这个临港项目也是。之后准备用 485 转 GPRS 透传功能直接与公司外网 IP 暴露出去的端口进行数据传输,这样就不用在现场布置服务器了,直接在公司服务器部署采集即可。不过这样必然对现场工人们的技术要求提高些,不光要保证传感器的完好还要保证采集设备的完好还要过程中接线的正确性,由于现场技术人员不在就需要他们要有一定的问题排查能力。有利有弊吧,不过总体是方便了不少。

世博

上海世博文化中心项目最近要收尾了,公司干活人数有限,之前催死了。这项目还是用的现场部署服务器进行数据采集,不过用的是 4G 路由器,无固定外网 IP,所以现场能访问公司服务器,而外部访问不了,这就需要之前我总结过的使用 ssh 反向隧道穿透 NAT 访问 Linux 内网主机。然后还有一个问题就是数据需要同步到公司服务器,不然无法给业主实时查看。针对业务需求,通过一系列的资料查找,最终选择阿里开源的项目 otter,具体可看记录 otter 数据同步

加了几天班,终于也是告一段落了。

还没缓过来,松江云廊屋盖项目也要来了,今年真是项目都堆积一起了。


期待五月的海岛游(听说是塞班岛,俺土鳖,哪都成!)