以下转载和参考自:集成JavaMail - 廖雪峰的官方网站
1、发送Email

如上图所示,我们把类似Outlook这样的能够发送邮件的软件称为MUA:Mail User Agent,意思是给用户服务的邮件代理;邮件服务器则称为MTA:Mail Transfer Agent,意思是邮件中转的代理;最终到达的邮件服务器称为MDA:Mail Delivery Agent,意思是邮件到达的代理。电子邮件一旦到达MDA,就不再动了。实际上,电子邮件通常就存储在MDA服务器的硬盘上,然后等收件人通过软件或者登陆浏览器查看邮件。
从MUA发送邮件到MTA使用的是SMTP协议,它是Simple Mail Transport Protocol的缩写,是一个TCP协议,使用标准端口25,也可以使用加密端口465或587。如果我们想要实现发送邮件的功能的话,在Java中可以使用JavaMail这个标准API就能直接发送邮件,在Spring中,同样可以集成JavaMail来收发邮件。以下是一些常用邮件服务商的SMTP信息:
- QQ邮箱:SMTP服务器是smtp.qq.com,端口是465/587;
- 163邮箱:SMTP服务器是smtp.163.com,端口是465;
- Gmail邮箱:SMTP服务器是smtp.gmail.com,端口是465/587。
在Spring中集成JavaMail的话,需要在pom.xml中添加以下依赖:
- org.springframework:spring-context-support:5.2.0.RELEASE
- javax.mail:javax.mail-api:1.6.2
- com.sun.mail:javax.mail:1.6.2
发送邮件的话需要使用一个JavaMailSender对象,以下为提供了创建JavaMailSender实例的方法:
/*smtp.properties文件*/
smtp.host=smtp.163.com
smtp.port=465
smtp.auth=true
smtp.username=xsl@163.com
smtp.password=123456;
smtp.from=xsl@163.com
@Configuration //标识为IOC容器配置类
@ComponentScan //告诉IOC容器:自动搜索当前类所在的包以及子包,把所有标注为@Component的Bean自动创建出来并根据@Autowired、@Value进行注入
@PropertySource("smtp.properties") // 读取classpath下的app.properties文件的配置信息
public class AppConfig {
...
@Bean
JavaMailSender createJavaMailSender(
@Value("${smtp.host}") String host,/*注入配置信息中key为smtp.host的值到host*/
@Value("${smtp.port}") int port,
@Value("${smtp.auth}") String auth,
@Value("${smtp.username}") String username,
@Value("${smtp.password}") String password,
@Value("${smtp.debug:true}") String debug)
{
var mailSender = new JavaMailSenderImpl();
mailSender.setHost(host); //SMTP服务器(MTA邮件服务器)地址
mailSender.setPort(port); //SMTP服务器(MTA邮件服务器)端口号
mailSender.setUsername(username); //SMTP服务器(MTA邮件服务器)登录名(通常是使用自己的邮件地址)
mailSender.setPassword(password); //SMTP服务器(MTA邮件服务器)登录口令(自己邮箱的口令或者一个独立设置的SMTP口令)
//通过Properties设置JavaMailSender的其它配置信息
Properties props = mailSender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp"); //使用协议
props.put("mail.smtp.auth", auth); //是否需要用户认证
if (port == 587) {
props.put("mail.smtp.starttls.enable", "true"); // 启用TLS加密
}
if (port == 465) {
props.put("mail.smtp.socketFactory.port", "465");
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
}
props.put("mail.debug", debug); // 设置debug模式,打开调试模式可以看到SMTP通信的详细内容,便于调试
return mailSender;
}
}
下面是使用JavaMailSender发送邮件的功能类,邮件的发件人、收件人等信息可以通过一个MimeMessage对象来设置:
@Component
public class MailService {
@Value("${smtp.from}")
String from;
@Autowired
JavaMailSender mailSender;
public void sendRegistrationMail(User user) {
try {
MimeMessage mimeMessage = mailSender.createMimeMessage();
//通过MimeMessageHelper来配置MimeMessage的信息
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, "utf-8");
helper.setFrom(from); //设置发件人
helper.setTo(user.getEmail()); //设置收件人
helper.setSubject("Welcome to Java course!"); //设置右键主题
String html = String.format("<p>Hi, %s,</p><p>Welcome to Java course!</p><p>Sent at %s</p>", user.getName(), LocalDateTime.now());
helper.setText(html, true); //设置邮件内容,这里为一个HTML页面,如果是普通文本的话,第二个参数设为false
mailSender.send(mimeMessage); //发送邮件
} catch (MessagingException e) {
throw new RuntimeException(e);
}
}
}
因为发送邮件是一种耗时的任务,所以可以启动一个线程来执行发送任务:
User user = userService.register(email, password, name);
...
//用户注册成功后给其邮箱发送邮件
new Thread(() -> {
mailService.sendRegistrationMail(user);
}).start();
关于发送邮件的其它事项,以及发送带附件的邮件、内嵌图片的邮件、怎样收邮件,可以参考:发送Email - 廖雪峰的官方网站。
2、消息中间件
1)、概述
消息中间件,又称Message Server消息服务,就是两个进程之间,通过消息服务传递消息,如下所示:

使用消息服务,而不是双方直接收发消息(比如直接调用对方的API向其发消息),有以下好处:
- 消息可以异步处理,将消息投递到中间服务后就可以进行其他操作,不必等待对方的处理。
- 如果Producer发送的消息频率高于Consumer的处理能力,消息可以积压在消息服务器,不至于压垮Consumer,而且消息服务器会在Consumer离线的时候自动缓存消息。
- 一个消息服务器可以连接多个Producer和多个Consumer,比如原来有3个Consumer,我们像其推送消息的话,一个Consumer处理消息就要1秒,那么向3个Consumer推送消息总共就需要3秒,使用消息中间件的话,只需要将消息发送到中间服务,然后接口立即返回,由中间件负责向Consumer发送消息,提高了Producer系统的效率。
- 降低耦合度:比如一个Producer,提供给A公司的Consumer来使用,现在又增加了一个B公司的Consumer,我们又要调整Producer以增加对B公司Consumer通信协议的支持。如果使用中间件的话,就相当于统一了通信协议,所有的Consumer都使用中间件约定的数据结构进行消息接收,因为中间件是知名的,其它公司易于接受。
引入消息中间件后,也会带来一些其它问题:
- 需要考虑消息中间件的高可用性,如果消息中间件出现宕机,推送的消息即会丢失,所以消息中间件需要支持主从或分布式架构,以应对宕机问题。
- 原来使用接口进行消息推送,我们只需要考虑接口超时以及接口推送消息失败的问题,入消息中间件后,就需要考虑消息丢失、消息重复消费问题。
- 原来使用接口进行消息推送,推送消息我们可以放在事务中处理,如果推送过程中出现异常,我们可以进行数据回滚。引入消息中间件后,就需要考虑消息推送后,消费失败的问题,以及如果我们同时推送消息到BCD系统中,如何保证他们的事务一致性。
2)、常用消息中间件
常见的消息中间件有ActiveMQ、RabbitMQ、RocketMQ、Kafka,如下图所示。
ActiveMQ是JMS的实现产品,JMS即Java Message Service,JavaEE消息服务,它是java消息服务标准接口,ActiveMQ有ActiveMQ Classic和ActiveMQ Artemis两个版本,其中后者相当于是前者的升级扩展版。
RabbitMQ是AMQP协议的实现,AMQP的全称是Advanced Message Queuing Protocol,即高级消息队列协议。JMS是JavaEE的消息服务标准接口,但是,如果Java程序要和另一种语言编写的程序通过消息服务器进行通信,那么ActiveMQ就不太适合了。AMQP是一种使用广泛的独立于语言的消息协议,它定义了一种二进制格式的消息流,任何编程语言都可以实现该协议,所以跨语言通信的话可以选择RabbitMQ。
RocketMQ和Kafka没有实现其它的标准或协议,它们是自实现的消息服务。
我们可以将消息中间件单独部署到一台服务器上,然后多个服务使用这个公用的消息中间件,通过使用不同的通道。

3)、JMS
ActiveMQ Artemis(JMS)有两种类型的通信模式,一种是Queue,一种是Topic,如下所示。Topic是发布-订阅模式,是一种一对多通道,一个Producer发出的消息,会被多个Consumer同时收到。Queue则是一对一(点对点)模式, 如果有多个Consumer的话,消息只会被交给其中一个Consumer来处理,比如发送方发送的消息是A,B,C,D,E,F,两个Consumer可能分别收到A,C,E和B,D,F。

ActiveMQ Artemis(JMS)收发的消息类型有以下几种:
- TextMessage:文本消息;
- BytesMessage:二进制消息;
- MapMessage:包含多个Key-Value对的消息;
- ObjectMessage:直接序列化Java对象的消息;
- StreamMessage:一个包含基本类型序列的消息。
关于JMS/ActiveMQ Artemis的具体使用,可以参考:集成JMS - 廖雪峰的官方网站。
3、定时执行任务
Java标准库本身就提供了定时执行任务的功能——通过使用线程池。在Spring中,开启定时任务只需要两个注解@EnableScheduling和@Scheduled即可:
@Configuration
@ComponentScan
@EnableScheduling //在Ioc配置类中开启对定时任务的支持
public class AppConfig {
...
}
@Component //spring会自动实例化该定时器类
public class TaskService {
...
@Scheduled(initialDelay = 60_000, fixedRate = 60_000) //启动延迟60秒,并以60秒的间隔执行的方法任务,如果60秒到了但上次任务还在执行的话,会等待上次任务执行完毕后再执行当次任务
public void checkSystemStatusEveryMinute1() {
...
}
@Scheduled(initialDelay = 60_000, fixedDelay = 60_000) //启动延迟60秒,并以60秒的间隔执行的方法任务,该60秒是从上次执行完毕后开始计时的
public void checkSystemStatusEveryMinute2() {
...
}
@Scheduled(initialDelay = 30_000,
fixedDelayString = "${task.checkDiskSpace:30000}") //启动延迟30秒,并以配置文件中设置的间隔(没有的话30秒)执行的方法任务
public void checkSystemStatusEveryMinute3() {
...
}
@Scheduled(initialDelay = 30_000,
fixedDelayString = "${task.checkDiskSpace:PT2M30S}") //启动延迟30秒,并以配置文件中设置的间隔(没有的话2分30秒,该格式可以参考LocalDateTime的Duration相关部分)执行的方法任务
public void checkSystemStatusEveryMinute4() {
...
}
}
可以使用Cron表达式来执行每天凌晨2:15执行、每个工作日12:00执行这种任务:
@Component
public class TaskService {
...
@Scheduled(cron = "${task.report:0 15 2 * * *}") //每天2点15分0秒(天 月份 星期为*,年可以忽略不写)执行
//@Scheduled(cron = "${task.report:0 0 12 * * MON-FRI}") //每个工作日12:00执行
//@Scheduled(cron = "${task.report:0 0 12 1-3,10 * *}") //每个月1号,2号,3号和10号12:00执行
//@Scheduled(cron = "${task.report:0 */10 * * * *}") //每10分钟执行
@Scheduled(cron = "${task.report:0 * 9-15 * * MON-FRI}") //每个工作日的9点到15点每分钟执行一次
public void cronDailyReport() {
}
}
如果我们以以集群的方式运行服务,在这个服务中包含一个每天23:00执行检查的子任务,如果我们想只要集群中的一台机器运行这个子任务的话,可以考虑使用redisson分布式锁或者Quartz,Quartz可以配置一个JDBC数据源,以便存储所有的任务调度计划以及任务执行状态。
@Scheduled开启的定时器任务是由Spring的定时任务线程池中线程执行的,而Controller方法是由Spring(Tomcat)的请求处理线程池中线程执行的。@Scheduled使用的定时任务线程池默认大小为1,即所有定时任务都在一个线程中执行,可以通过在配置类中实现SchedulingConfigurer接口来配置线程池大小,如下所示。也可以定义TaskScheduler或ScheduledExecutorService类型的Bean来配置线程池大小(Spring会自动发现这两个类型的Bean,并将其用作定时任务的执行器 )。也有人说可以仅配置application.properties / application.yml文件来设置定时线程池,如spring.task.scheduling.pool.size=5,这个不太确定是否可行。
@Configuration
@EnableScheduling
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 设定一个包含10个线程的调度线程池
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
}
}
4、JMX
JMX即Java管理监控接口(Java Management Extensions),使用它可以知道Java应用程序当前的状态,例如,占用了多少内存,分配了多少内存,当前有多少活动线程,有多少休眠线程等等。
如下所示,JMX把所有被监控管理的资源都称为MBean(Managed Bean),这些MBean全部由MBeanServer管理,如果要访问MBean,可以通过MBeanServer对外提供的访问接口,例如通过RMI或HTTP访问。MBeanServer已经内置在Java SE中,JavaSE还提供了一个jconsole程序,它通过RMI连接到MBeanServer来监控管理Java进程。

在命令行窗口,输入jconsole启动它,如下所示,可以在Local Process中找到要监控的Java进程,如AppConfig,点击Connect即可连接到这个Web应用,然后就可以直接看到其内存、CPU等资源的监控,我们也可以点击MBean,来具体查看每个MBean的情况,比如可以在java.lang查看内存等信息,可以点击Tomcat、HikariCP查看该程序包含的Tomcat和HikariCP连接池的监控信息。


我们也可以自己编写一个MBean,然后注册到JMX,把监控信息以MBean的形式暴露给JMX Server,这样就能够通过jconsole这种JMX客户端来与我们自己的MBean进行监控管理和交互。
编写MBean的话需要在Ioc配置类中添加@EnableMBeanExport注解,告诉Spring自动注册MBean:
@Configuration
@ComponentScan
@EnableMBeanExport // 自动注册MBean
public class AppConfig {
...
}
比如我们的一个程序启动后会读取黑名单文件,在黑名单上的IP地址禁止访问接口,如果我们要添加黑名单中IP地址的话,只有重启程序才能生效。现在,我们可以编写一个MBean,在这里面提供对黑名单数据的写操作接口,然后就可以使用jconsole来调用MBean里面的接口来实现对黑名单数据的热更新。编写MBean也就是编写一个类,但是需要使用相关注解让Spring把相关方法注册到MBeanServer中:
@Component
@ManagedResource(objectName = "sample:name=blacklist"/*MBean名称,通常以company:name=Xxx来分类MBean*/, description = "Blacklist of IP addresses")//表示这是一个MBean
public class BlacklistMBean {
private Set<String> ips = new HashSet<>(); //保存黑名单数据
@ManagedAttribute(description = "Get IP addresses in blacklist") //指示为属性
public String[] getBlacklist() {
return ips.toArray(String[]::new);
}
@ManagedOperation //指示为操作
@ManagedOperationParameter(name = "ip", description = "Target IP address that will be added to blacklist")//操作参数
public void addBlacklist(String ip) {
ips.add(ip);
}
@ManagedOperation
@ManagedOperationParameter(name = "ip", description = "Target IP address that will be removed from blacklist")
public void removeBlacklist(String ip) {
ips.remove(ip);
}
public boolean shouldBlock(String ip) {//普通类的方法,不会暴露给JMX
return ips.contains(ip);
}
}
我们可以在拦截器中对当前连接和黑名单中IP进行查找,在黑名单中的话就禁止其访问:
@Order(1)
@Component
public class BlacklistInterceptor implements HandlerInterceptor {
@Autowired
BlacklistMBean blacklistMBean;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ip = request.getRemoteAddr();
if (blacklistMBean.shouldBlock(ip)) { //在黑名单中
// 发送403错误响应:
response.sendError(403);
return false;
}
return true;
}
}
然后就可以启动jconsole来调用BlacklistMBean中的相关方法,如下所示,先找到程序进程,选择MBean标签,然后在sample下就可以看到我们自己的MBean——blacklist,其属性和操作也能看到,如下所示,我们可以点击addBlacklist操作,填入参数127.0.0.1后点击addBlacklist按钮,这就相当于调用了BlacklistMBean的addBlacklist()方法,即对黑名单添加了IP(如果使用IPv6,那么需要把0:0:0:0:0:0:0:1这个本机地址加到黑名单):


使用jconsole还可以远程连接连接JVM,这时候需要打开JMX端口,在启动Java程序的时候,需要传入以下JVM启动参数:
- -Dcom.sun.management.jmxremote.port=19999
- -Dcom.sun.management.jmxremote.authenticate=false
- -Dcom.sun.management.jmxremote.ssl=false
第一个参数表示在19999端口监听JMX连接,第二个和第三个参数表示无需验证,不使用SSL连接,在开发测试阶段比较方便,生产环境必须指定验证方式并启用SSL。详细参数可参考Oracle官方文档。这样jconsole可以用ip:19999的远程方式连接JMX。连接后的操作是完全一样的。
常用的运维监控如Zabbix、Nagios等工具对JVM本身的监控都是通过JMX获取的信息,许多JavaEE服务器如JBoss的管理后台也是通过JMX提供管理接口,并由Web方式访问。
可以使用Spring提供的HandlerInterceptor和DeferredResultProcessingInterceptor跟踪API性能,它们分别用于拦截同步API和异步API。
5、StatsD
我们可以通过JMX构造一个监控系统来监控系统状态、性能等实时信息,但从零开始是不现实的,选择一个通用的标准协议比使用JMX要更简单。StatsD就是目前最流行的监控方案,它可以将监控数据实时发送给聚合服务器如Graphite,再以可视化的形式展示出来。
如下所示,应用程序本身负责收集监控数据,然后以UDP协议发给StatsD守护进程,StatsD进程通常和应用程序运行在同一台机器上,它非常轻量级,并且StatsD是否运行都不影响应用程序的正常运行(因为UDP协议只管发不管能不能收到)。

StatsD只是一个解决方案,既可以自己用开源组件搭建,也可以选择第三方商业服务商,例如DataDog。如果使用DataDog, 它会提供一个dd-java-agent.jar,在启动应用程序时,以agent的方式注入到JVM中来采集监控数据,如下启动命令所示。DataDog提供的agent除了能采集应用程序的数据,还可以直接监控JVM、Linux系统,能大大简化监控配置。
$ java -javaagent:dd-java-agent.jar -jar app.jar
下面为使用DataDog的相关依赖:
<dependency>
<groupId>com.datadoghq</groupId>
<artifactId>dd-trace-api</artifactId>
<version>{version}</version>
</dependency>
【转】&spm=1001.2101.3001.5002&articleId=126747961&d=1&t=3&u=61bc4c8caf00491e8db4683fd5662b2d)
332

被折叠的 条评论
为什么被折叠?



