Managing and Enhancing your Spring Boot application with MuleSoft
Part 2: How to implement your business logic using Spring Boot application and the Spring MVC RAML plugin
By Karim DJAAFAR, Tech Lead Architect and MuleSoft Ambassador
Welcome to the second part of this series dedicated to the implementation of the RAML specification that we have made in the first step using Anypoint Designer.
We suppose that you have installed all the prerequisites in the first blog article series.
Implementing the specification using the Spring-MVC RAML plugin
We will generate in the second step the java code using the Spring MVC RAML plugin for the Client RAML entity.
Let’s examine the steps to implement our specification.
First, we will create a new Spring Boot project with a Web dependency as follows (we will use VS Code Spring Boot Application Development Extension Pack)
- Enter Shift+ P enter fill Spring Initializr: Generate a Maven Project :
- Select the last version of the Spring Boot (3.3.2 in my case) and specify Java language
- Fill the group Id for your project like you want (for example com.jasmineconseil) and springbootmuledemo as Artifact Id and specify the package as JAR file
- Specify the Java version (I choose Java 17)
- Choose Spring Web Dependencies and click Enter
- Generate your Spring Boot Web project inside your favorite folder you should have the final structure bellow (notice the Mule Code Builder extension already installed):
Ok, now that we have a Spring Boot application generated we need to modify the pom using the following dependencies to integration the Spring-MVC RAML plugin:
<plugin>
<groupId>com.phoenixnap.oss</groupId>
<artifactId>springmvc-raml-plugin</artifactId>
<version>2.0.5</version>
<configuration>
<ramlPath>src/main/resources/api/webstock-system.raml</ramlPath>
<outputRelativePath>src/main/java</outputRelativePath>
<basePackage>com.jasmineconseil.web</basePackage>
<baseUri>/api</baseUri>
<rule>com.phoenixnap.oss.ramlplugin.raml2code.rules.Spring4ControllerInterfaceRule</rule>
</configuration>
<executions>
<execution>
<id>generate-springmvc-endpoints</id>
<phase>compile</phase>
<goals>
<goal>generate-springmvc-endpoints</goal>
</goals>
</execution>
</executions>
</plugin>
As you can notice in the plugin Spring-MVC RAML configuration, the configuration includes the path to the RAML file (ramlPath) that we will use to generate the request URIs, the path ofto the directory where the API contract will be generated (outputRelativePath), the request URI at the top of each Controller class (baseUri) ,…
If you need more options and details please refer to the official documentation of the Spring-MVC RAML plugin available here.
- Create an api directory inside the resources directory of your Spring Boot project (DemoSpringBootMule/springbootmuledemo/src/main/resources) and copy the content of the API specification exported in the step before of the series in this api directory (notice the RAML syntax supported by the VSCode extension):
- Click on the mvn clean install command you should have the following content generated inside web:
Let’s examine the Model package, you can notice that all the getter/setter are generated and an array list is generated to modelize the relation between the entity customer and the entity address (notice the annotation @Size and @NotNull for validating the field generated from the RAML code (length and required):
public class Customer implements Serializable
{
final static long serialVersionUID = -8840655438559532660L;
protected Double id;
protected String firstName;
protected String lastName;
protected String email;
protected String phone;
@JsonFormat(pattern = "yyyy-MM-dd")
protected Date dateOfBirth;
protected Gender gender;
protected String occupation;
protected String company;
protected List<Address> addresses = new ArrayList<Address>();
/**
* Creates a new Customer.
*
*/
public Customer() {
super();
}
/**
* Creates a new Customer.
*
*/
public Customer(Double id, String firstName, String lastName, String email, String phone, Date dateOfBirth, Gender gender, String occupation, String company, List<Address> addresses) {
super();
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phone = phone;
this.dateOfBirth = dateOfBirth;
this.gender = gender;
this.occupation = occupation;
this.company = company;
this.addresses = addresses;
}
/**
* Returns the id.
*
* @return
* id
*/
public Double getId() {
return id;
}
/**
* Set the id.
*
* @param id
* the new id
*/
public void setId(Double id) {
this.id = id;
}
/**
* Returns the firstName.
*
* @return
* firstName
*/
@NotNull
@Size(min = 2, max = 20)
public String getFirstName() {
return firstName;
}
/**
* Set the firstName.
*
* @param firstName
* the new firstName
*/
public void setFirstName(String firstName) {
this.firstName = firstName;
}
/**
* Returns the lastName.
*
* @return
* lastName
*/
@NotNull
@Size(min = 2, max = 50)
public String getLastName() {
return lastName;
}
/**
* Set the lastName.
*
* @param lastName
* the new lastName
*/
public void setLastName(String lastName) {
this.lastName = lastName;
}
/**
* Returns the email.
*
* @return
* email
*/
@Size(max = 50)
public String getEmail() {
return email;
}
/**
* Set the email.
*
* @param email
* the new email
*/
public void setEmail(String email) {
this.email = email;
}
/**
* Returns the phone.
*
* @return
* phone
*/
@Size(max = 20)
public String getPhone() {
return phone;
}
/**
* Set the phone.
*
* @param phone
* the new phone
*/
public void setPhone(String phone) {
this.phone = phone;
}
/**
* Returns the dateOfBirth.
*
* @return
* dateOfBirth
*/
public Date getDateOfBirth() {
return dateOfBirth;
}
/**
* Set the dateOfBirth.
*
* @param dateOfBirth
* the new dateOfBirth
*/
public void setDateOfBirth(Date dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
/**
* Returns the gender.
*
* @return
* gender
*/
@Valid
public Gender getGender() {
return gender;
}
/**
* Set the gender.
*
* @param gender
* the new gender
*/
public void setGender(Gender gender) {
this.gender = gender;
}
/**
* Returns the occupation.
*
* @return
* occupation
*/
@Size(min = 1)
public String getOccupation() {
return occupation;
}
/**
* Set the occupation.
*
* @param occupation
* the new occupation
*/
public void setOccupation(String occupation) {
this.occupation = occupation;
}
/**
* Returns the company.
*
* @return
* company
*/
public String getCompany() {
return company;
}
/**
* Set the company.
*
* @param company
* the new company
*/
public void setCompany(String company) {
this.company = company;
}
/**
* Returns the addresses.
*
* @return
* addresses
*/
@Valid
public List<Address> getAddresses() {
return addresses;
}
/**
* Set the addresses.
*
* @param addresses
* the new addresses
*/
public void setAddresses(List<Address> addresses) {
this.addresses = addresses;
}
public int hashCode() {
return new HashCodeBuilder().append(id).append(firstName).append(lastName).append(email).append(phone).append(dateOfBirth).append(gender).append(occupation).append(company).append(addresses).toHashCode();
}
public boolean equals(Object other) {
if (other == null) {
return false;
}
if (other == this) {
return true;
}
if (this.getClass()!= other.getClass()) {
return false;
}
Customer otherObject = ((Customer) other);
return new EqualsBuilder().append(id, otherObject.id).append(firstName, otherObject.firstName).append(lastName, otherObject.lastName).append(email, otherObject.email).append(phone, otherObject.phone).append(dateOfBirth, otherObject.dateOfBirth).append(gender, otherObject.gender).append(occupation, otherObject.occupation).append(company, otherObject.company).append(addresses, otherObject.addresses).isEquals();
}
public String toString() {
return new ToStringBuilder(this).append("id", id).append("firstName", firstName).append("lastName", lastName).append("email", email).append("phone", phone).append("dateOfBirth", dateOfBirth).append("gender", gender).append("occupation", occupation).append("company", company).append("addresses", addresses).toString();
}
}
For the controller part we have the following code for each endpoints:
/**
* List all customers
* (Generated with springmvc-raml-parser v.2.0.5)
*
*/
@RestController
@Validated
@RequestMapping("/api/customers")
public interface CustomerController {
/**
* Retrieve the list of the resource customers
*
*/
@RequestMapping(value = "", method = RequestMethod.GET)
public ResponseEntity<List<Customer>> getCustomers();
/**
* Add a new Customer
*
*/
@RequestMapping(value = "", method = RequestMethod.POST)
public ResponseEntity<?> createCustomer(
@Valid
@RequestBody
Customer customer);
}
@RestController
@Validated
@RequestMapping("/api/customers/{customersId}")
public interface CustomersIdController {
/**
* No description
*
*/
@RequestMapping(value = "", method = RequestMethod.GET)
public ResponseEntity<?> getObjectByCustomersId(
@PathVariable
String customersId);
/**
* Update customer profile information
*
*/
@RequestMapping(value = "", method = RequestMethod.PUT)
public ResponseEntity<?> updateCustomer(
@PathVariable
String customersId,
@Valid
@RequestBody
Customer customer);
/**
* No description
*
*/
@RequestMapping(value = "", method = RequestMethod.DELETE)
public ResponseEntity<?> deleteCustomerByCustomersId(
@PathVariable
String customersId);
}
Of course this is only a skeleton of you backend and you have to implement all your specific business logic for each endpoint and configure Spring JPA using a database like Posgress in our example to implement it but the plugin give you all the facilities to structure and standardize your project.
I propose to give you an example of a Spring implementation for the endpoint /customers/{id} based on the existing generated code by the plugin Spring MCV RAML and I let you implement the other endpoint which are straightforward for any spring backend developer using the service and repository components:
public class CustomerController {
@Autowired
CustomerRepository clientRepository;
@Autowired
CustomerService customerService;
@GetMapping("customers")
public List<Customer> getAllCustomers(){
return customerRepository.findAll();
}
@PostMapping("/customers")
public Customer createCustomer(@RequestBody Customer customer) {
customer.setAdresse_id(null);
return customerService.addCustomer(customer);
}
@PutMapping("/customers/{id}")
public ResponseEntity<Customer> updateCustomer(@PathVariable(value = "id") int clientId,
@RequestBody Customer detailsCustomer) throws ResourceNotFoundException {
Customer customer = clientRepository.findById(customerId)
.orElseThrow(() -> new ResourceNotFoundException("Customer not found :: ID ::" + clientId));
customer.setLastName(detailsCustomer.getLastName());
customer.setFirstName(detailsCustomer.getFirstName());
customer.setEmail(detailsCustomer.getEmail());
customer.setPhone(detailsCustomer.getPhone());
customer.setBirthDate(detailsCustomer.getBirthDate());
customer.setGenre(detailsCustomer.getGenre());
customer.setProfession(detailsCustomer.getProfession());
customer.setCompany(detailsCustomer.getCompany());
final Customer updatedCustomer = customerRepository.save(customer);
return ResponseEntity.ok(updatedCustomer);
}
@DeleteMapping("/customers/{id}")
public Map<String, Boolean> deleteCustomer(@PathVariable(value = "id") Integer customerId)
throws ResourceNotFoundException {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new ResourceNotFoundException("Customer doesnt exist ! ID :: " + customerId));
customerRepository.delete(customer);
Map<String, Boolean> response = new HashMap<>();
response.put("deleted", Boolean.TRUE);
return response;
}
To conclude the Spring implementation part here the config of your connexion and the JPA configuration for your database of choice here Postgres and it is declared in the application.properties file:
spring.application.name=clientmanagement
spring.datasource.url=jdbc:postgresql://<IP of your server>:5433/mule_database
spring.datasource.username=demo
spring.datasource.password=XXXXX
spring.datasource.driverClassName=org.postgresql.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
Let’s conclude this code implementation to see the React part and integration with keycloack, I just put here the main points for managing the access to keycloak SSO server in order not overload this article .
In this extract we manage the authentification of the user based on the keycloack client configuration defined (react-client on the master keycloack realm):
import "./App.css";
import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
import Navbar from "./layout/Navbar";
import Home from "./pages/Home";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import AddClient from "./clients/AddClient";
import EditClient from "./clients/EditClient";
import ViewClient from "./clients/ViewClient";
import Keycloak from "keycloak-js";
import axios from "axios";
/*
Init Options
*/
let initOptions = {
// the Url of your keycloak server
url: 'http://localhost:9080/',
realm: 'master',
clientId: 'react-client',
}
let kc = new Keycloak(initOptions);
kc.init({
onLoad: 'login-required', // Supported values: 'check-sso' , 'login-required'
checkLoginIframe: true,
pkceMethod: 'S256'
}).then((auth) => {
if (!auth) {
window.location.reload();
} else {
/* Remove below logs if you are using this on production */
console.info("Authenticated");
console.log('auth', auth)
console.log('Keycloak', kc)
console.log('Access Token', kc.token)
/* http client will use this header in every request it sends */
axios.defaults.headers.common['Authorization'] = `Bearer ${kc.token}`;
kc.onTokenExpired = () => {
console.log('token expired')
}
}
}, () => {
/* Notify the user if necessary */
console.error("Authentication Failed");
});
function App() {
return (
<div className="App">
<Router>
<Navbar kc={kc}/>
<Routes>
<Route exact path="/" element={<Home kc={kc}/>} />
<Route exact path="/addClient" element={<AddClient kc={kc}/>} />
<Route exact path="/editClient/:id" element={<EditClient kc={kc}/>} />
<Route exact path="/viewClient/:id" element={<ViewClient kc={kc}/>} />
</Routes>
</Router>
</div>
);
}
export default App;
As you can understand by reading the extract, the role are extract from keycloak (admin or developer) and following the role and using the hasRealmRole method, the access to the CRUD actions on some buttons of the react application are enabled/disabled (delete, display, edit):
export default function Home({ kc }) {
const [clients, setClients] = useState([]);
const [error, setError] = useState(""); // State for error message
const { id } = useParams();
useEffect(() => {
loadClients();
}, []);
const loadClients = async () => {
try {
const result = await axios.get("http://localhost:8080/api/clients", {
headers: {
Authorization: "Basic " + btoa("react-app:root"),
},
});
setClients(result.data);
console.log(result);
} catch (err) {
setError("Application non autorisée");
}
};
const deleteClient = async (id) => {
try {
await axios.delete(`http://localhost:8080/api/clients/${id}`);
loadClients();
} catch (err) {
setError("Application non autorisée");
}
};
return (
<div className="container">
<div className="py-4">
{/* {!error &&( */}
<h2 className="text-center m-0">Clients Jasmine</h2>
{/* )} */}
{kc.hasRealmRole("admin") && (
<div className="add-client-button-container">
<Link className="btn btn-outline-dark" to="/addClient">
+ Nouveau Client
</Link>
</div>
)}
{error && <div className="alert alert-danger">{error}</div>}{" "}
{/* Display error message for app authorization */}
<table className="table border shadow">
{/* {!error && ( */}
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Nom</th>
<th scope="col">Prenom</th>
<th scope="col">Email</th>
<th scope="col">Actions</th>
</tr>
</thead>
{/* )} */}
<tbody>
{clients.map((client, index) => (
<tr key={client.id}>
<th scope="row" key={index}>
{client.id}
</th>
<td>{client.nom}</td>
<td>{client.prenom}</td>
<td>{client.email}</td>
<td>
<Link
className="btn btn-outline-dark mx-3"
to={`/viewClient/${client.id}`}
>
Afficher
</Link>
{kc.hasRealmRole("admin") && (
<Link
className="btn btn-outline-primary mx-3"
to={`/editClient/${client.id}`}
>
Modifier
</Link>
)}
{kc.hasRealmRole("admin") && (
<button
className="btn btn-outline-danger mx-3"
onClick={() => deleteClient(client.id)}
>
Supprimer
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
Here an example of the user admin which have access to the all facilities of the application after been authenticated:
And the other screen with a simple user developer authenticated but with limited access to the webstock application:
Hope you enjoy this second part of my series the last part will show you how to apply Mule policies on the deployed Spring Boot React Webstock application, stay tuned !
All the available React and Spring code is available on a simple request to the author directly at kdjaafar@jasmineconseil.com