Skip to content
JackSparrow414
Go back

Sending Email with Velocity in Spring Boot

Table of contents

Open Table of contents

Sending Email with Velocity in Spring Boot

Introduction

What Can Velocity Be Used For?

Many people know Velocity as a template engine for MVC views. Today, with separate frontend and backend applications, I think a large proportion of companies use frontend frameworks such as React or Vue. Using template engines to render frontend pages seems less common.

Besides page rendering, the official documentation mentions an important use case: sending email. An application may use a shared email template while displaying different content for each user. The template needs to remain separate from the Java code and rarely changes. This is where Velocity is useful.

Getting Started

In simple terms, Velocity syntax defines variables and retrieves their values in a template, while Java code supplies those values and renders the template.

Templates

Template syntax mainly falls into two categories:

References

See the reference documentation. I will only make a few brief notes here.

  1. Naming rules.

    A reference consists of $ plus a variable or method name. The name must begin with a letter; subsequent characters may be letters, digits, or underscores (_).

  2. Accessing properties and methods.

    Use a name followed by a dot.

    ## Access customerName on order.
    $order.customerName

    For whether a reference accesses a property or a method, see the property lookup rules.

  3. Formal reference notation.

    When other words immediately follow a variable, Velocity may treat them as part of the variable name. To prevent this, use formal reference notation.

    Jack is a ${vice}maniac

    This looks up only vice.

Directives

Directive documentation.

Directives begin with #. The main common ones are:

  1. set: assign a value.

    #set($temList = $order.items)

    Assign items to the variable temList.

  2. If / ElseIf / Else: conditions.

    #if( $foo < 10 )
        Go North
    #elseif( $foo == 10 )
        Go East
    #elseif( $bar == 6 )
        Go South
    #else
        Go West
    #end

    foo may be an object, collection, array, and so on. The condition is: Velocity documentation listing conditions under which a variable evaluates to true

  3. foreach: loops.

    #set($temList = $order.items)
    #foreach($item in $temList)
            $item
    #end
  4. include and parse: reuse other templates.

    <html>
    ## Specify the full path under resources here.
        #parse("templates/emails/header.vm")
    <body>
    </body>
    </html>

For other features, such as escaping and arithmetic, see the relevant documentation.

Java Code

After writing the template, we need to assign its variables. See the developer guide.

Create a Spring Boot application.

Use Velocity or VelocityEngine according to preference. Context is the main API for assigning variables.

An example template:

<html>
    #parse("templates/emails/header.vm")
<body>
Hi, $order.customerName<br>
## Use formal reference notation.
Here are the details of the order you completed at ${order.paymentTime}:
<table border="1">
    <tr>
        <th>Item</th>
    </tr>
    #set($temList = $order.items)
    #foreach($item in $temList)
        <tr>
            <td>$item</td>
        </tr>
    #end
</table>
<br>
Total amount: $order.paymentAmount<br>
Delivery method: $order.deliveryMethod
</body>
</html>

Assign values to the template above:

@Service
@Log
public class SendEmailService {

    private static final VelocityEngine ve = new VelocityEngine();

    /**
     * https://velocity.apache.org/engine/2.3/configuration.html#resource-management
     *
     * https://velocity.apache.org/engine/2.3/configuration.html#configuration-examples
     * Pay attention to the final paragraph.
     * Node that the three names 'file', 'class', and 'jar' are merely for your convenience and sanity.
     * They can be anything you want - they are just used to associate a set of properties together.
     * However, it is recommended that you use names that give some hint of the function
     *
     * It explains that file, class, and jar can be replaced with other names. Hence resource.loader.class.class below; class is used here.
     */
    @PostConstruct
    public void initVelocity() {
        ve.setProperty(RuntimeConstants.RESOURCE_LOADERS, "class");
        ve.setProperty("resource.loader.class.class", ClasspathResourceLoader.class.getName());
        ve.setProperty("resource.loader.class.cache", true);
        ve.init();
    }

    public boolean sendOrderDetailEmail() {
        Order order = new Order();
        order.setCustomerName("jack");
        List<String> items = Arrays.asList("猪肉", "牛肉", "鱼肉");
        order.setItems(items);
        order.setPaymentAmount(BigDecimal.valueOf(78.365));
        order.setPaymentTime(LocalDateTime.now());
        order.setDeliveryMethod("顺丰");
        // Assign values.
        VelocityContext context = new VelocityContext();
        context.put("order", order);
        context.put("header", "OrderDetail");
        // Get the template.
        Template template = ve.getTemplate(SendEmailUtil.obtainTemplateRealPath("orderDetail"));
        StringWriter writer = new StringWriter();
        // Render the template with the variable values.
        template.merge(context, writer);
        return true;
    }
}

Note: pay particular attention to configuring the location from which Velocity reads templates. See the ResourceLoader documentation.

When Must ResourceLoader Be Configured?

  1. When using velocityEngine.mergeTemplate or template.merge, configure ResourceLoader during initialization.
  2. A .vm file containing #include or #parse also requires ResourceLoader configuration.
/**
 * Two approaches: https://velocity.apache.org/engine/devel/developer-guide.html#using-velocity
 * One uses a string directly with velocityEngine.evaluate.
 * The other uses a template file with velocityEngine.mergeTemplate and requires resourceLoader configuration at initialization.
 *
 * Velocity configuration: https://velocity.apache.org/engine/devel/configuration.html#configuring-velocity
 * Default configuration file location: org/apache/velocity/runtime/defaults/velocity.properties
 * Any values specified before init() time will replace the default values.
 * Therefore, you only have to configure velocity with the values for the keys    that you need to change,
 * and not worry about the rest
 */
@Log
@Component
public class VelocityUsageService {

    /**
     * A string containing #parse or #include requires resourceLoader configuration.
     * @return
     */
    public String velocityEvaluateHasParseOrIncludeDirective() {
        StringWriter writer = new StringWriter();
        String template = "<html>\n" + "## Specify the full path under resources here.\n" + "    #parse(\"templates/emails/header.vm\")\n" + "<body>\n" + "Hi, $order.customerName<br>\n" + "## Use formal reference notation.\n" +
            "Here are the details of the order you completed at ${order.paymentTime}:\n" + "<table border=\"1\">\n" + "    <tr>\n" + "        <th>Item</th>\n" + "    </tr>\n" + "    #set($temList = $order.items)\n" +
            "    #foreach($item in $temList)\n" + "        <tr>\n" + "            <td>$item</td>\n" + "        </tr>\n" + "    #end\n" + "</table>\n" + "<br>\n" + "Total amount: $order.paymentAmount<br>\n" +
            "Delivery method: $order.deliveryMethod\n" + "</body>\n" + "</html>";
        VelocityEngine velocityEngine = new VelocityEngine();
        velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADERS, "class");
        velocityEngine.setProperty("resource.loader.class.class", ClasspathResourceLoader.class.getName());
        velocityEngine.setProperty("resource.loader.class.cache", true);
        VelocityContext velocityContext = new VelocityContext();
        Order order = new Order();
        order.setCustomerName("jack");
        List<String> items = Arrays.asList("猪肉", "牛肉", "鱼肉");
        order.setItems(items);
        order.setPaymentAmount(BigDecimal.valueOf(78.365));
        order.setPaymentTime(LocalDateTime.now());
        order.setDeliveryMethod("顺丰");
        velocityContext.put("order", order);
        velocityContext.put("header", "OrderDetail");
//        velocityContext.put("name", "sparrow");
        velocityEngine.evaluate(velocityContext, writer, UUID.randomUUID().toString(), template);
        log.info(writer.toString());
        return writer.toString();
    }

    public String velocityEvaluateNoParseAndIncludeDirective() {
        StringWriter writer = new StringWriter();
        String template = "hello $name";
        VelocityEngine velocityEngine = new VelocityEngine();
        VelocityContext velocityContext = new VelocityContext();
        velocityContext.put("name", "sparrow");
        velocityEngine.evaluate(velocityContext, writer, UUID.randomUUID().toString(), template);
        log.info(writer.toString());
        return writer.toString();
    }

    /**
     * https://velocity.apache.org/engine/devel/developer-guide.html#resource-loaders
     * When using velocityEngine.mergeTemplate or template.merge, configure ResourceLoader during initialization.
     * A .vm file containing #include or #parse also requires ResourceLoader configuration.
     *
     * because the resource management system will also handle non-template reasources, specifically things that are loaded via the #include() directive
     */
    public String velocityMergeTemplate() {
        StringWriter writer = new StringWriter();
        VelocityEngine velocityEngine = new VelocityEngine();
        velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADERS, "class");
        velocityEngine.setProperty("resource.loader.class.class", ClasspathResourceLoader.class.getName());
        velocityEngine.setProperty("resource.loader.class.cache", true);
        VelocityContext velocityContext = new VelocityContext();
        velocityContext.put("name", "sparrow");
        // Approach 1
//        Template template = velocityEngine.getTemplate(SendEmailUtil.obtainTemplateRealPath("registerSuccess"));
//        template.merge(velocityContext, writer);
        // Approach 2
        velocityEngine.mergeTemplate(SendEmailUtil.obtainTemplateRealPath("registerSuccess"), Charset.defaultCharset().name(), velocityContext, writer);
        log.info(writer.toString());
        return writer.toString();
    }
}

Integrating Email Sending

Here we send email using a 163.com mailbox.

Two main protocols are involved in sending and receiving email:

Use JavaMailSender, the mail utility integrated by Spring Boot. See documentation 1 and documentation 2.

Prerequisites

Enable POP3/SMTP/IMAP in the 163.com mailbox and obtain an authorization code.

Implementation

Configuration File

server:
  port: 18080
  servlet:
    context-path: /v
spring:
  mail:
    host: smtp.163.com
    username: YOUR_EMAIL_ADDRESS
    # Authorization code for the 163.com mailbox
    password: YOUR_AUTHORIZATION_CODE
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true
  # https://velocity.apache.org/engine/devel/developer-guide.html#logging
  logging:
    level:
      org.apache.velocity: trace

Sending the Email

@Service
@Log
public class SendEmailService {

    private static final VelocityEngine ve = new VelocityEngine();

    @Autowired
    private JavaMailSender javaMailSender;

    @PostConstruct
    public void initVelocity() {
        ve.setProperty(RuntimeConstants.RESOURCE_LOADERS, "class");
        ve.setProperty("resource.loader.class.class", ClasspathResourceLoader.class.getName());
        ve.setProperty("resource.loader.class.cache", true);
        ve.init();
    }

    public boolean sendOrderDetailEmail() {
        VelocityContext context = new VelocityContext();
        Order order = new Order();
        order.setCustomerName("jack");
        List<String> items = Arrays.asList("猪肉", "牛肉", "鱼肉");
        order.setItems(items);
        order.setPaymentAmount(BigDecimal.valueOf(78.365));
        order.setPaymentTime(LocalDateTime.now());
        order.setDeliveryMethod("顺丰");
        context.put("order", order);
        context.put("header", "OrderDetail");
        Template template = ve.getTemplate(SendEmailUtil.obtainTemplateRealPath("orderDetail"));
        StringWriter writer = new StringWriter();
        template.merge(context, writer);
        javaMailSender.send(buildMessage("OrderDetail Email", writer.toString()));
        return true;
    }

    @SneakyThrows
    private MimeMessage buildMessage(String subject, String emailContent) {
        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message);
        helper.setFrom("SENDER_EMAIL_ADDRESS");
        helper.setTo("RECIPIENT_EMAIL_ADDRESS");
        helper.setSubject(subject);
        message.setText(emailContent, Charset.defaultCharset().name(), "html");
        return message;
    }
}

Result

OrderDetail email generated from a Velocity template

Official Documentation

Reading the official documentation falls into two main parts: User's Guide and Developer's Guide entries on the Velocity website

Example Code


Share this post:

Previous Post
Getting Started with React (Part 2): Sending Requests and Mocking Data
Next Post
Getting Started with JMX: JMX Exporter Monitoring and OpenTelemetry Integration

Comments

Questions, corrections, and experiences are welcome. Sign in with GitHub to comment; both language versions share this discussion.

Comments are available on the live site only.