第30章 使用Spring发送E-mail(二)

第30章 使用Spring发送E-mail

30.3 Spring的E-mail支持在实际开发中的应用

实际开发中,我们不可能像实例那样,直接通过字符串的拼接来创建所要发送的邮件的具体内容。更多时候,我们会使用系统指定的邮件模板。当发送给用户的邮件内容需要变更的时候(比如公司迁址,需要变更新的地址或者电话之类细节),我们不想挨个类地去改代码,而使用邮件模板的话,只要修改一下邮件模板就可以了。

从现在开始,我将向你展示如何在Spring中使用其E-mail支持来发送基于模板的电子邮件。虽然主要以Velocity作为使用的模板技术,但使用其他模板技术(比如FreeMarker)在道理上是相同的,只可能会存在少许具体操作上的差异。

在开始着手正式的工作之前,我们先构建一个要发送的邮件模板,使用Velocity的VTL编写模板如下:

来到魔法屋,就得听我的指示,我来选出符合条件的人,你来决定他们的命运!
嘿,轮到你了!${player}

以上邮件模板内容可以vm模板文件形式保存,也可以存入数据库中。这里先暂且将其存入magicHouse.vm文件,并将该文件放在Classpath的根路径下。为了能够重用使用邮件模板发送邮件这一功能,我们对该功能进行抽象,抽象后的接口如下方代码清单所示:

public interface ITemplateMailAgent {
    
    
	/**
	* convenient sending method with only recipient.TO <br/>
	*/
	void sendMail(String receiver, String subject, String templateKey, Map<String, Object> context);
	/**
	* central method to send E-mail with template support.<br/>
	* @param receivers Recipients as a Parameter wrapper object.
	* @param templateKey the identity of the template to use.
	* @param context contains data that will be merged into template.
	*/
	void sendMail(Recipients receivers, String subject, String templateKey, Map<String, Object> context);
}

当然,该抽象接口定义并不完善。如果要实际应用的话,或许要添加更多的方法来提供更为全面的支持,甚至,如果你觉得这种方法签名定义不爽,那就改成自己喜欢的风格。不管怎么样,我们将先就这一定义开始基于邮件模板的邮件发送整合之路。

ITemplateMailAgent的抽象允许我们提供基于不同模板技术的实现类,我们使用Velocity,所以,对应Velocity的ITemplateMailAgent实现类就此诞生了:

public class VelocityTemplateMailAgent implements ITemplateMailAgent {
    
    
	private final String DEFAULT_SENDER = "[email protected]";
	private final String DEFAULT_ENCODING = "UTF~8";
    
	private String mailFrom = DEFAULT_SENDER;
	private String mai1Encoding = DEFAULT_ENCODING;
	private JavaMailSender javaMailSender;
	private VelocityEngine velocityEngine;
    
	public void sendMail(String receiver, String subject, String templateKey, Map<String, Object> context) {
    
    
		Recipients recipients = new Recipients(receiver);
		sendMail(recipients, subject, templateKey, context);
    }
    
	public void sendMail(finalR?cipients receivers, final String subject, String templateKey, Map<String, Object> context) {
    
    
		validateRecipients(receivers);
		Validate.notEmpty(templateKey);
		StringWriter writer =new StringWriter();
		VelocityEngineUtils.mergeTemplate(velocityEngine, templateKey, getMailEncoding(), context, writer);
		final String mailText = writer.toString(); // mail content is ready

		getJavaMailSender().send(new MimeMessagePreparator() {
    
    
			public void prepare(MimeMessage message) throws Exception {
    
    
				MimeMessageHelper helper = new MimeMessageHelper(message, getMailEncoding());
				helper.setFrom(getMailFrom());
				helper.setTo(receivers.getTo());
				if(!CollectionUtils.isEmpty(receivers.getCcList())) {
    
    
					helper.setCc(receivers.getCc());
                }
				if(!CollectionUtils.isEmpty(receivers.getBccList())) {
    
    
					helper.setBcc(receivers.getBcc());
					helper.setSubject(subject);
					helper.setText(mailText);
                }
            }
		});
    }
                                 
	private void validateRecipients(Recipients receivers) {
    
    
		// ...
    }
	// 用于依赖注入的gettcr和setter方法定义
}

VelocityTemplateMailAgent的实现逻辑依赖于两个主要组件,这可以推而广之到使用其他模板技术的ITemplateMailAgent实现类中,如下所述。

  • 模板引擎——VelocityEngine。我们需要使用具体的模板引擎来合并具体的邮件模板和要发送的数据,合并后的结果自然就成为我们将要最终发送的邮件内容。

  • JavaMailSender。没有JavaMailSender的支持,我们好像也发送不了邮件吧?(当然,使用其他Email封装方案则另当别论。)

至于其他的实现细节,我想从代码上来看,并不是太难理解,所以,这里就不做太多解释了。

注意:VelocityTemplateMailAgent的当前实现只支持基于邮件模板的普通文本邮件的发送。如果要支持MIME邮件,那需要做更多的工作,你不妨自己尝试一下。

现在只要将velocityTemplateMailAgent和它的相关依赖添加到Spring的IoC容器,或者编程组装在一起,我们就能发送基于邮件模板的邮件了。当前,我们不妨像通常那样,通过Spring的IoC容器来做这部分工作,如下方代码清单所示。

<bean id="cpLoaderVelocityEngine" class="org.springframework.ui.velocity.VelocityEngineFactoryBean">
	<property name="configLocation" value="classpath:cn/spring21/conf/velocity-config.properties"/>
</bean>
  
<bean id="javaMailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
	<property name="host" value=".."/>
	<property name="username" value=".."/>
	<property name="password" value=".."/>
	<property name="javaMailProperties">
		<props>
			<prop key="mail.smtp.auth">true</prop>
		</props>
</property>
</bean>
    
<bean id="templatEmailAgent" class="org.darrenstudio.books.unveilspring.mail.template.VelocityTemplateMailAgent">
	<property name="javaMailSender" ref="javaMailSender"></property>
	<property name="velocityEngine" ref="cpLoaderVelocityEngine"/>
</bean>

VelocityTemplateMailAgent所依赖的VelocityEngine是我们通过Spring提供的VelocityEngineFactoryBean注入给它的。VelocityEngineFactoryBean作为一个FactoryBean实现,封装了一个VelocityEngine相关的设定,使我们可以简单bean的形式将VelocityEngine添加到Spring的IoC容器进行管理。在Spring MVC部分,我们也应该接触过该类了。VelocityEngineFactoryBean使用的配置文件内容比较简单,如下所示,当然,如果需要,你可以在使用的时候追加更多控制项:

# velocity-config.properties
resource.loader = classpath
classpath.resource.loader.description = Classpath Resource Loader
classpath.resource.loader.class = org.apache.velocity.runtime.resource.loader.ClasspathResourceloader

velocimacro.library =

我们的velocity-config.properties配置,使得VelocityEngine将从Classpath加载vm模板文件。所以,现在我们可以通过如下的形式发送最初模板所设定的邮件内容了:

velocityTemplateMailAgentmailAgent = ... // 通过IoC容器注入或查找相应实例
Map<String, Object> context = new HashMap<String,Object>();
context.put("player", "孙小美");
mailAgent.sendMail("[email protected]", "邮件标题""magicHouse.vm", context);

像收件人、邮件标题,以及合并到邮件模板的数据,通常需要我们根据应用程序的上下文来获得。不管怎么样,如果你想看一下效果的话,那图30-2是我的Gmail所呈现的。

image-20220719174919379

看起来已经达到预期的效果了!

注意:有关结合Velocity模板和Spring发送邮件的内容,最新的Spring文档中都有介绍。不过,如果我没有记错的话,最早提出这种实践思路的是一篇文章“Sending Velocity-Based E-Mail With Spring”。如果你感兴趣的话,不妨根据本书后面的参考资料找这篇文章读读。

将模板文件放入应用程序的Classpath或者直接放到文件系统中,有时候会因为应用程序场景的变化产生些许不便。比如,多个应用程序都需要访问同一模板内容的话,这些应用程序可能需要将同一份模板分别打包,或者通过其他途径,来访问文件系统中存放的同一份模板文件。所以,出于这些场景的考虑,我们的模板文件也会被放入某种类型的数据存储服务中,比如数据库或者目录服务器上。这种情况下,我们就需要在使用VelocityTemplateMailAgent之前,做点儿附加工作,以便让VelocityEngine可以加载位于特定数据存储服务中的邮件模板。

假设我们将magicHouse.vm以及系统中其他邮件模板的内容存入数据库。为了让Velocity能够从数据库加载这些模板资源,我们可以替换Velocity最初使用的ResourceLoader实现,从ClasspathResourceLoader转而使用DataSourceResourceLoader。整个切换过程涉及的,实际上也只是velocity-config.properties配置内容的少许改变。应该说,这种思路是正确的。不过,实施当中我们可能会碰到点儿小问题!

通常,使用DataSourceResourceLoader的配置文件内容可能是如下方代码清单所示的样子。

resource.loader = ds
ds.resource.loader.public.name = DataSource
?s.resource.loader.description = VelocityDataSourceResourceLoader
ds.resource.loader.class = org.apache.velocity.runtime.resource.loader.DataSourceResourceLoader
ds.resource.loader.resource.datasource = java:comp/env/jdbc/Velocity
ds.resource.loader.resource.table = mail_templates
ds.resource.loader.resource.keycolumn = TEMPLATEKEY
ds.resource.loader.resource.templatecolumn = TEMPLATE_DEFINIATION
ds.resource.loader.resource.timestampcolumn = UPDATE_DATE
ds.resource.loader.cache = false
ds.resource.loader.modificationCheckInterval = 60

从中可以看出,DataSourceResourceLoader只能使用从JNDI获取的DataSource来加载模板资源。如果要使用外部独立的数据源,那么需要做一些类似如下的编码工作:

DataSourceResourceLoader ds = new DataSourceResourceLoader();
DataSourcedataSource = ...; // 可以从其他地方注入
ds.setDataSource(dataSource);
velocityEngine.setProperty("ds.resource.loader.instance", ds);

如果Spring的VelocityEngineFactoryBean提供了setResourceLoader(..)的方法的话(实际上该名称的方法确实提供了,但却是接受Spring的ResourceLoader类型作为方法参数,而不是Velocity的ResourceLoader类型),我们可以通过一个FactoryBean来封装以上代码所示的对DataSourceResourceLoader进行设定的相关逻辑,但不巧,事情没有按照我们所预想的方向发展,我们得另寻他路。

VelocityEngineFactoryBean定义有一个postProcessVelocityEngine(VelocityEngine)方法。该方法为protected,可以允许子类覆写它,以对VelocityEngineFactoryBean所管理的velocityEngine实例做进一步的定制。所以,我们可以从这里着手,解决DataSourceResourceLoader使用外部独立数据源的问题。既然VelocityEngineFactoryBean没有提供我们所需要的设置选项,那我们就扩展它。扩展后的VelocityEngineFactoryBean如下方代码清单所示。

public class ExtendedVelocityEngineFactoryBean extends VelocityEngineFactoryBean {
    
    
	private DataSource dataSource;
  
	@Override
	protected void postProcessVelocityEngine(VelocityEngine velocityEngine) throws IOException,VelocityException {
    
    
		super.postProcessVelocityEngine(velocityEngine);
		DataSourceResourceLoader resourceLoader = new DataSourceResourceLoader();
		resourceLoader.setDataSource(dataSource);
		velocityEngine.setProperty("ds.resource.loader.instance", resourceLoader);
  }

  public DataSource getDataSource() {
    
    
		return dataSource;
  }
  
	public void setDataSource(DataSource dataSource) {
    
    
		this.dataSource = dataSource;
  }
}

现在,以ExtendedVelocityEngineFactoryBean替换掉最初的VelocityEngineFactoryBean,并注入给VelocityTemplateMailAgent,我们的VelocityTemplateMailAgent就可以从数据库来加载邮件模板了。

不过好事多磨,在此之前,需要注意余下的几个问题。

(1)启用了使用外部数据源的DataSourceResourceLoader之后,velocity-config.properties中有关内容需要注释掉(或者直接删掉),否则,Velocity依然会沿用配置文件中的配置内容。注释后的配置文件如下方代码清单所示。

ds.resource.loader.public.name = DataSource
ds.resource.loader.description = VelocityDataSourceResourceLoader
# ds.resource.loader.class = org.apache.velocity.runtime.resource.loader.DataSourceResourceLoader
# ds.resource.loader.resource.datasource = java:comp/env/jdbc/Veloci.ty
ds.resource.loader.resource.table = mail_templates
ds.resource.loader.resource.keycolumn = TEMPLATE_KEY
ds.resource.loader.resource.templatecolumn = TEMPLATE_DEFINIATION
ds.resource.loader.resource.timestampcolumn = UPDATE_DATE

(2)说是从数据库加载模板,我们还没有将模板内容导入数据库,所以,根据配置内容所示,我们需要建立一个名为mail_templates的表,然后声明模板对应的标志列和模板的定义内容列等,表定义DDL如下所示:

CREATE TABLE mail_templates (
	TEMPLATE_KEY varchar(25) NOT NULL,
	TEMPLATE_DEFINIATION text NOT NULL,
	UPDATEDATE datetime NOT NULL,
	CREATEDDATE datetime NOT NULL,
	PRIMARYKEY(TEMPLATE_KEY)
)

至于实际系统中,要以什么作为表名,以什么列唯一标志模板,以及以什么列来保存模板内容,那是你自己的事情了,无非就是更改一下velocity-config.properties的配置内容而已。

假如我们通过如下SQL插入模板内容的话:

insert into mail_templates values('rich4', '来到魔法屋,就得听我的指示,我来选出符合条件的人,你来决定他们的命运!\n嘿,轮到你了!${player}', NOW(), NOW())

那使用VelocityTemplateMailAgent进行邮件发送的如下代码,可以获得与从Classpath或者文件系统加载邮件模板同样的效果:

VelocityTermplateMailAgent mailAgent = // 通过IoC容器注入或者查找相应实例;
Map<String, Object> context = new HashMap<String, Object>();
context.put("player", 孙小美");
mailAgent.sendMail("[email protected]", "邮件标题", "rich4", context);

其实只是模板文件名换成了数据库表中的标志键。

提示:以上几种场景都是基于Velocity进行介绍的,如果你愿意使用其他模板技术,可以在此基础上进行适度的调整,也可以达到同样的目的。实际上,不使用Velocity的DataSourceResourceLoader,我们同样可以达到从数据库获取邮件模板的目的,而且,还要更加灵活,适用范围也更广,能猜到是什么吗?(想想Spring的数据访问相关内容。)

30.4 小结

与JavaEE平台上许多API的境遇类似,JavaMailAPI虽强大,但依然没能摆脱实际应用中的烦琐。本章我们先忆苦,简单回顾了早些时候JavaMailAPI的实践之路,在此基础上,我们引入Spring的Mail抽象层相关内容。我们一起了解了Spring的Mail抽象层的方方面面,同时也对Spring的Mail抽象层在实际开发用的应用场景进行了简单的探索。希望你结束本章的阅读之后,能够借助Spring的Mail抽象层的支持,“多快好省”地完成日常开发中各种Mai相关功能。

猜你喜欢

转载自blog.csdn.net/qq_34626094/article/details/125956069