Using Spring Expression Language (SpEL) to Create a Simple Rule Engine
Spring Expression Language (SpEL) is a powerful feature that enables dynamic querying and manipulation of objects at runtime. In this article, we will explore how to leverage SpEL in a Spring Boot 3 application to define and evaluate business rules derived from records stored in a database. We will focus on an example involving discount rules for an e-commerce application.
Overview of SpEL
SpEL allows developers to execute expressions on objects, making it particularly useful for creating flexible business rules that can change based on data in your database.
Use Case
Imagine an e-commerce application where you want to apply discounts based on criteria such as customer age and membership level. These criteria will be stored in a database, and SpEL will evaluate them dynamically to determine applicable discounts.
Setting Up the Spring Boot 3 Project
1. Create a Spring Boot Project
You can set up a new Spring Boot project using Spring Initializr. Include the following dependencies:
2. Add Dependencies in pom.xml
Here’s an example of what your pom.xml might look like for Spring Boot 3:
<project xmlns="https://maven.apache.org/POM/4.0.0"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>discount-rules</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>discount-rules</name>
<description>Demo project for Spring Boot 3</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Defining the Database Entity
Create an entity class for the discount rules.
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class DiscountRule {
@Id
@GeneratedValue
private Long id;
private String condition; // SpEL condition
private double discountPercentage;
// Constructors
public DiscountRule() {}
public DiscountRule(String condition, double discountPercentage) {
this.condition = condition;
this.discountPercentage = discountPercentage;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCondition() {
return condition;
}
public void setCondition(String condition) {
this.condition = condition;
}
public double getDiscountPercentage() {
return discountPercentage;
}
public void setDiscountPercentage(double discountPercentage) {
this.discountPercentage = discountPercentage;
}
}
Creating the Repository
Now, create a Spring Data JPA repository to manage the DiscountRule entities.
import org.springframework.data.jpa.repository.JpaRepository;
public interface DiscountRuleRepository extends JpaRepository<DiscountRule, Long> {
}
Sample Data in Database
Here are some example discount rules that you might store in the database:
Populate the Database
You can use the CommandLineRunner to populate the database with these rules when the application starts.
领英推荐
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
public class DataInitializer implements CommandLineRunner {
private final DiscountRuleRepository repository;
public DataInitializer(DiscountRuleRepository repository) {
this.repository = repository;
}
@Override
public void run(String... args) throws Exception {
repository.saveAll(Arrays.asList(
new DiscountRule("customerAge > 60", 10),
new DiscountRule("membershipLevel == 'GOLD'", 15),
new DiscountRule("membershipLevel == 'SILVER'", 5),
new DiscountRule("totalAmount > 100", 20)
));
}
}
Evaluating Rules with SpEL
Create a service that retrieves the discount rules and evaluates them based on the current order.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DiscountService {
@Autowired
private DiscountRuleRepository discountRuleRepository;
public double calculateDiscount(Order order) {
List<DiscountRule> rules = discountRuleRepository.findAll();
double totalDiscount = 0;
ExpressionParser parser = new SpelExpressionParser();
for (DiscountRule rule : rules) {
StandardEvaluationContext context = new StandardEvaluationContext(order);
boolean isMatch = parser.parseExpression(rule.getCondition()).getValue(context, Boolean.class);
if (isMatch) {
totalDiscount += order.getTotalAmount() * (rule.getDiscountPercentage() / 100);
}
}
return totalDiscount;
}
}
Example Order Class
Here’s a simple Order class representing an order in your application.
public class Order {
private double totalAmount;
private int customerAge;
private String membershipLevel;
// Getters and Setters
public double getTotalAmount() {
return totalAmount;
}
public void setTotalAmount(double totalAmount) {
this.totalAmount = totalAmount;
}
public int getCustomerAge() {
return customerAge;
}
public void setCustomerAge(int customerAge) {
this.customerAge = customerAge;
}
public String getMembershipLevel() {
return membershipLevel;
}
public void setMembershipLevel(String membershipLevel) {
this.membershipLevel = membershipLevel;
}
}
Creating a REST Controller
Create a REST controller to expose an endpoint for calculating discounts based on the order.
Example Request
To test the application, you can use a tool like Postman or curl. Here’s an example of how to send a request:
Request :
POST /api/discount
Content-Type: application/json
{
"totalAmount": 150,
"customerAge": 65,
"membershipLevel": "GOLD"
}
Expected Response
Given the rules in the database, the expected response would be:
Total discount = 15.00 + 22.50 + 30.00 = 67.50.
Conclusion
By leveraging Spring Expression Language (SpEL) in a Spring Boot 3 application, you can create dynamic and flexible business rules evaluated at runtime based on data stored in your database. This approach allows for greater adaptability in business logic, making it easier to implement complex rules without hardcoding them into your application.
Further Reading
With this setup, your e-commerce application can dynamically apply discounts based on changing business rules, ensuring it remains responsive to customer needs.