Build a JSON API Microservice With Spring Boot and Elide
Click here to learn how you can securely expose JPA entities via a testable JSON API interface with Spring Boot and Elide.
Join the DZone community and get the full member experience.
Join For FreeIn the past, I have written about how Elide can be used with Spring to create JSON API-compatible REST services. In this article, I want to show you how to create a self-contained microservice that implements Elide’s security layers, and also makes use of Spring and H2 to provide a testable environment that has no dependencies on external databases.
To get started, let's take a look at the Gradle build file.
There is nothing particularly exciting here. Most of this code was generated with Spring Initializr. There are some changes worth mentioning though:
- The H2 database is only compiled for the tests, as we are not using it for the main application, but are using it to mock up a database environment for unit tests.
- We’ve used Hibernate 5.1.2.Final. I had issues with Hibernate 5.2 with Elide, but these might be resolved by the time you read this article.
buildscript {
ext {
springBootVersion = '1.4.1.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'
jar {
baseName = 'microservice-template'
version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-data-rest')
testCompile('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
compile group: 'com.yahoo.elide', name: 'elide-core', version: '2.3.17'
compile group: 'com.yahoo.elide', name: 'elide-datastore-hibernate5', version: '2.3.17'
compile group: 'org.hibernate', name: 'hibernate-core', version: '5.1.2.Final'
compile group: 'org.hibernate', name: 'hibernate-entitymanager', version: '5.1.2.Final'
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.39'
}
The entry point to the application is in MicroserviceTemplateApplication. Again, this is boilerplate code.
package com.matthewcasperson;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MicroserviceTemplateApplication {
public static void main(String[] args) {
SpringApplication.run(MicroserviceTemplateApplication.class, args);
}
}
The next four classes are used to configure access to the database.
- DatasourceConfiguration, which holds some Spring annotations that configure data access, and return a bean that configures the embedded Tomcat server with a data source.
- DatasourceConnection, which either returns the data source hosted by Tomcat, or the H2 database that we’ll create as part of the TEST profile.
- DatasourceEmfConfiguration, which configures the JPA EntityManager Factory.
- DatasourceTXManagerConfiguration, which configures that transaction manager.
package com.matthewcasperson.config;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.apache.tomcat.util.descriptor.web.ContextResource;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* Spring JPA configuration for app quote microservice data
*/
@Configuration
@EnableJpaRepositories(
// This needs to match the entity manager factory created below
entityManagerFactoryRef = "MicroserviceEMF",
// This needs to match the transaction manager created below
transactionManagerRef = "MicroserviceTX",
// This is the package that holds the Spring repository using the classes exposed by this EMF
basePackages = "com.matthewcasperson.repository"
)
@EnableTransactionManagement
@Import({
DatasourceConnection.class,
DatasourceEmfConfiguration.class,
DatasourceTXManagerConfiguration.class
})
public class DatasourceConfiguration {
@Bean
public TomcatEmbeddedServletContainerFactory tomcatFactory() {
return new TomcatEmbeddedServletContainerFactory() {
@Override
protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(final Tomcat tomcat) {
tomcat.enableNaming();
return super.getTomcatEmbeddedServletContainer(tomcat);
}
@Override
protected void postProcessContext(Context context) {
ContextResource resource = new ContextResource();
resource.setName("jdbc/microservice");
resource.setType(DataSource.class.getName());
resource.setProperty("factory", "org.apache.tomcat.jdbc.pool.DataSourceFactory");
resource.setProperty("driverClassName", "com.mysql.jdbc.Driver");
resource.setProperty("username", "root");
resource.setProperty("password", "password1!");
resource.setProperty("url", "jdbc:mysql://localhost:3306/microservice");
context.getNamingResources().addResource(resource);
}
};
}
}
The DatasourceConnection class is where we swap between the production database (MySQL in this case, but could be any other database with some slight configuration changes), and the H2 in-memory database based on the Spring profile that is being used. The in-memory database makes testing easy as we can run real tests against a real database without any additional infrastructure.
As you can see, it is very easy to use the H2 database. Once a connection is made, the database is created automatically and the INIT parameter on the JBDC URL gives us a way to initialize any required schemas.
package com.matthewcasperson.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnJndi;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.jndi.JndiTemplate;
import java.util.Properties;
import javax.sql.DataSource;
/**
* Setup a connection to the database
*/
@Configuration
public class DatasourceConnection {
private static final String H2_JDBC = "jdbc:h2:mem:microservice;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS MicroserviceDatabase";
private static final String H2_USER = "";
private static final String H2_PASSWORD = "";
private static final String H2_DRIVER = "org.h2.Driver";
/**
* This is where Tomcat has created our datasource
*/
private static final String LOOKUP_DATASOURCE_JNDI = "java:/comp/env/jdbc/microservice";
@Bean(name = "MicroserviceDS")
@Profile("!TEST")
public DataSource jndiLookupDataSource() throws Exception {
return (DataSource) new JndiTemplate().lookup(LOOKUP_DATASOURCE_JNDI);
}
/**
* When running tests we connect to an in memory H2 database
*/
@Bean(name = "MicroserviceDS")
@Profile("TEST")
public DataSource driverManagerLookupDataSource() throws Exception {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(H2_DRIVER);
dataSource.setUrl(H2_JDBC);
dataSource.setUsername(H2_USER);
dataSource.setPassword(H2_PASSWORD);
return dataSource;
}
}
package com.matthewcasperson.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
/**
* Set up JPA EMF for lookup database
*/
@Configuration
public class DatasourceEmfConfiguration {
/**
* Prod code uses MySQL databases
*/
@Bean(name = "MicroserviceEMF")
@Profile("!TEST")
public EntityManagerFactory lookupEntityManagerFactory(
@Qualifier("MicroserviceDS") final DataSource lookupDataSource
) {
final HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(false);
final Map<String, String> properties = new HashMap<>();
properties.put("hibernate.dialect", "org.hibernate.dialect.MySQLDialect");
final LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setJpaPropertyMap(properties);
factory.setPackagesToScan("com.matthewcasperson.domain.**");
factory.setDataSource(lookupDataSource);
factory.setPersistenceUnitName("LookupPersistenceUnit");
factory.afterPropertiesSet();
return factory.getObject();
}
/**
* When running tests we will be using an in memory h2 database
*/
@Bean(name = "MicroserviceEMF")
@Profile("TEST")
public EntityManagerFactory lookupTestEntityManagerFactory(
@Qualifier("MicroserviceDS") final DataSource lookupDataSource
) {
final HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(false);
final Map<String, String> properties = new HashMap<>();
properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
properties.put("hibernate.current_session_context_class", "org.springframework.orm.hibernate5.SpringSessionContext");
final LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setJpaPropertyMap(properties);
factory.setPackagesToScan("com.matthewcasperson.domain.**");
factory.setDataSource(lookupDataSource);
factory.setPersistenceUnitName("LookupPersistenceUnit");
factory.afterPropertiesSet();
return factory.getObject();
}
}
package com.matthewcasperson.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManagerFactory;
/**
* Set up transaction manager for lookup database
*/
public class DatasourceTXManagerConfiguration {
@Bean(name = "MicroserviceTX")
public PlatformTransactionManager lookupTransactionManager(
@Qualifier("MicroserviceEMF") EntityManagerFactory lookupEntityManagerFactory
) {
final JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(lookupEntityManagerFactory);
return txManager;
}
}
The JPA entity is defined in the MicroserviceKeyValue class. Again, this class is mostly boilerplate code, with the exception of the @ReadPermission and @ComputedAttribute annotations, which are used by Elide to enforce some security restrictions. These will be covered later.
package com.matthewcasperson.domain;
import com.yahoo.elide.annotation.ComputedAttribute;
import com.yahoo.elide.annotation.ReadPermission;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
/**
* Auotgenerated JPA entity, with some additions to support Elide.
* Notably we have added the @ReadPermission annotation, which will
* only allow entities that pass the rule we have called
* "Client supplied secret".
*/
@ReadPermission(expression = "Client supplied secret")
@Entity
@Table(name = "Microservice", schema = "MicroserviceDatabase")
public class MicroserviceKeyValue {
private Integer id;
private String key;
private String value;
private String secret;
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "id")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Basic
@Column(name = "key")
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
@Basic
@Column(name = "value")
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MicroserviceKeyValue that = (MicroserviceKeyValue) o;
if (id != that.id) {
return false;
}
if (key != null ? !key.equals(that.key) : that.key != null) {
return false;
}
if (value != null ? !value.equals(that.value) : that.value != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + (key != null ? key.hashCode() : 0);
result = 31 * result + (value != null ? value.hashCode() : 0);
return result;
}
@Transient
@ComputedAttribute
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
}
In order to expose the JPA entity to Elide, we need an @Include annotation in the package-info.java file.
/**
* JPA entities that represent the domain objects of this service
*/
@Include(rootLevel=true)
package com.matthewcasperson.domain;
import com.yahoo.elide.annotation.Include;
A Spring repository gives us easy CRUD access to the database. This is more boilerplate code.
package com.matthewcasperson.repository;
import com.matthewcasperson.domain.MicroserviceKeyValue;
import org.springframework.data.repository.CrudRepository;
/**
* A repo for working with jpa entities
*/
public interface MicroserviceRepository extends CrudRepository<MicroserviceKeyValue, Integer> {
}
Now let's take a look at the REST controller. Here we have implemented the GET HTTP method, and used Elide to respond to the request. I won’t go into to much detail here as most of this code has been covered in Create a JSON API REST Service With Spring Boot and Elide, with a few exceptions.
package com.matthewcasperson;
import com.matthewcasperson.domain.MicroserviceKeyValue;
import com.matthewcasperson.security.OpaqueUser;
import com.matthewcasperson.security.ValidateSecretDetails;
import com.yahoo.elide.Elide;
import com.yahoo.elide.ElideResponse;
import com.yahoo.elide.audit.AuditLogger;
import com.yahoo.elide.audit.Slf4jLogger;
import com.yahoo.elide.core.DataStore;
import com.yahoo.elide.core.EntityDictionary;
import com.yahoo.elide.core.Initializer;
import com.yahoo.elide.datastores.hibernate5.HibernateStore;
import com.yahoo.elide.security.checks.Check;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import javax.persistence.EntityManagerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import java.util.HashMap;
import java.util.Map;
import static com.matthewcasperson.MicroserviceController.BASE_URL;
/**
* This is the service that applications can call to determine what application was
* used to generate a quote.
*/
@RestController
@RequestMapping(BASE_URL)
public class MicroserviceController {
protected static final String BASE_URL = "/microservice/1.0";
@Autowired
private EntityManagerFactory emf;
@Autowired
private Initializer initializer;
/**
* Converts a plain map to a multivalued map
* @param input The original map
* @return A MultivaluedMap constructed from the input
*/
private MultivaluedMap<String, String> fromMap(final Map<String, String> input) {
return new MultivaluedHashMap<String, String>(input);
}
@RequestMapping(
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE,
value={"/{entity}", "/{entity}/{id}/relationships/{entity2}", "/{entity}/{id}/{child}", "/{entity}/{id}"})
@Transactional
@ResponseBody
public String jsonApiGet(
@RequestParam final Map<String, String> allRequestParams,
@QueryParam("secret") final String secret,
final HttpServletRequest request) {
/*
Get thr request URI, and normalise it against the microservice endpoint prefix
*/
final String restOfTheUrl = request.getRequestURI().replaceFirst(BASE_URL, "");
/*
Elide works with the Hibernate SessionFactory, not the JPA EntityManagerFactory.
Fortunately we san unwrap the JPA EntityManagerFactory to get access to the
Hibernate SessionFactory.
*/
final SessionFactory sessionFactory = emf.unwrap(SessionFactory.class);
/*
Elide takes a hibernate session factory
*/
final DataStore dataStore = new HibernateStore(sessionFactory);
/*
Define a logger
*/
final AuditLogger logger = new Slf4jLogger();
/*
These are the Elide rules that will be checked to ensure that only matching information
has been sent to the client
*/
final Map<String, Class<? extends Check>> checks = new HashMap<>();
checks.put("Client supplied secret", ValidateSecretDetails.class);
final EntityDictionary dictionary = new EntityDictionary(checks);
/*
Create the Elide object
*/
final Elide elide = new Elide.Builder(dataStore)
.withAuditLogger(logger)
.withEntityDictionary(dictionary)
.build();
/*
This is the object that we use to enrich the JPA entities with security
information.
*/
dictionary.bindInitializer(initializer, MicroserviceKeyValue.class);
/*
Convert the map to a MultivaluedMap, which we can then pass to Elide
*/
final MultivaluedMap<String, String> params = fromMap(allRequestParams);
/*
This is where the magic happens. We pass in the path, the params, and a place holder security
object (which we won't be using here in any meaningful way, but can be used by Elide
to authorize certain actions), and we get back a response with all the information
we need.
*/
final ElideResponse response = elide.get(restOfTheUrl, params, new OpaqueUser(secret));
/*
Return the JSON response to the client
*/
return response.getBody();
}
}
There are two things about this implementation that are worth pointing out.
The first is the mapping of the ValidateSecretDetails class to the security rule "Client supplied secret."
final Map<String, Class<? extends Check>> checks = new HashMap<>();
checks.put("Client supplied secret", ValidateSecretDetails.class);
final EntityDictionary dictionary = new EntityDictionary(checks);
You might be wondering why you would need to use Elide’s security when Spring offers a wealth of security either at the method level or via AOP. The reason is because the method that is responding to the REST request is very generic. Take a look at the URL that is assigned to the request mapping for the REST method.
@RequestMapping(
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE,
value={"/{entity}", "/{entity}/{id}/relationships/{entity2}", "/{entity}/{id}/{child}", "/{entity}/{id}"})
It has been written in such a way as to respond to any GET request that is defined in the JSON API specification. This method doesn’t know if you are returning a collection of “message of the day” quotes or nuclear launch codes. Not only that, but because JSON API supports returning compound documents, you could conceivably craft a request that traversed and returned many JPA relationships. The Elide documentation has a more detailed explanation of these security issues.
The general nature of the method and the expressive functionality of the JSON API specification make writing security at the method level quite difficult. You would need to be able to understand the format of the various query parameters and unwind the relationships being requested before you could make any meaningful security decisions.
Thankfully, Elide provides a security layer that inherently understands the relationship between the request parameters and the resulting entity graph that is returned to the user. In our case, we are using the ValidateSecretDetails class to only allow entities that pass a custom security check to be included in the returned result.
In our case, we require the end user to supply a secret string, and only entities that share that secret string will be returned. This may seem like a trivial example, but in the real world, you might do something similar with those secret questions that are often used to perform a password reset.
package com.matthewcasperson.security;
import com.matthewcasperson.domain.MicroserviceKeyValue;
import com.yahoo.elide.security.ChangeSpec;
import com.yahoo.elide.security.RequestScope;
import com.yahoo.elide.security.User;
import com.yahoo.elide.security.checks.InlineCheck;
import java.util.Optional;
/**
* This security check verifies that the secret string supplied by the client
* matches the secret assigned to the entity.
*/
public class ValidateSecretDetails extends InlineCheck<MicroserviceKeyValue> {
@Override
public boolean ok(final MicroserviceKeyValue object, final RequestScope requestScope, final Optional<ChangeSpec> changeSpec) {
final OpaqueUser user = (OpaqueUser)requestScope.getUser().getOpaqueUser();
return user.getSecret().equals(object.getSecret());
}
@Override
public boolean ok(final User user) {
return true;
}
}
So where does this secret string come from? If you look back at the JPA entity in the MicroserviceKeyValue class, you’ll see that the getSecret() method has both a @Transient and a @ComputedAttribute annotation. @Transient is a JPA annotation and means that the field will not be persisted to the database. @ComputedAttribute is an Elide annotation, and it means that the value will be returned as part of the JSON API result, even though it will not be persisted in the database.
We use this transient property to hold the secret string that our clients must supply in order to have the entity returned. But if this value doesn’t come from the database, where does it come from? That is where the second significant change comes in, where we assign an Elide Initializer to enrich the JPA entities as they are read from the database.
/*
This is the object that we use to enrich the JPA entities with security
information.
*/
dictionary.bindInitializer(initializer, MicroserviceKeyValue.class);
The initializer object is an instance of the SecretEnricher class. It was assigned via a Spring injection, which is important because it demonstrates that we can use Spring objects with Elide, meaning that our enrichment code has full access to the Spring library.
Even though it wasn’t necessary in this example, the SecretEnricher class is a Spring Component. Inside we generate a new secret string for each entity returned from the database. Obviously, this implementation doesn’t reflect how you would generate security codes in real life, but it is a simple example of using Elide classes to enrich JPA entities with additional information at run time.
package com.matthewcasperson.security;
import com.matthewcasperson.domain.MicroserviceKeyValue;
import com.yahoo.elide.core.Initializer;
import org.springframework.stereotype.Component;
/**
* This class is used to populate a computed attribute on the JPA entity.
* In this way we are enriching the JPA entity at run time with information
* that isn't stored in the database.
*
* Note that we have made this class a Spring component. We could inject
* anything supported by Spring into this object, and it will work
* with Elide.
*/
@Component
public class SecretEnricher implements Initializer<MicroserviceKeyValue> {
@Override
public void initialize(MicroserviceKeyValue entity) {
/*
Here we define the secret that is associated with the JPA entity.
In this example the secret is just the key combined with the
suffix "Secret", which obviously is not good security, but
goes to show how this class can be used to enrich a JPA entity.
*/
entity.setSecret(entity.getKey() + "Secret");
}
}
The last piece of the puzzle is how we get the secret string passed by the user into the method in the ValidateSecretDetails class which does the actual authorization. This is done with a POJO called OpaqueUser. This class serves as our Elide user and contains whatever information we require to perform our security checks.
/*
This is where the magic happens. We pass in the path, the params, and a place holder security
object (which we won't be using here in any meaningful way, but can be used by Elide
to authorize certain actions), and we get back a response with all the information
we need.
*/
final ElideResponse response = elide.get(restOfTheUrl, params, new OpaqueUser(secret));
The OpaqueUser class itself is nothing more than a data object that holds the secret string.
package com.matthewcasperson.security;
import static com.google.common.base.Preconditions.checkArgument;
import org.apache.commons.lang3.StringUtils;
/**
* An Elide user that contains the details we need to match before returning a result
*/
public class OpaqueUser {
private final String secret;
public String getSecret() {
return secret;
}
public OpaqueUser(final String secret) {
checkArgument(StringUtils.isNotBlank(secret));
this.secret = secret;
}
}
With these classes in place, a GET request will perform the following:
- User makes a GET request with a secret query param.
- The MicroserviceController class intercepts the request and runs the jsonApiGet method.
- Elide is configured with the Check class ValidateSecretDetails and the Initializer SecretEnricher.
- The MicroserviceKeyValue entities are generated from the information in the database.
- The MicroserviceKeyValue entities are enriched by the SecretEnricher instance, and given a secret string.
- The MicroserviceKeyValue entities are filtered by the ValidateSecretDetails, and any whose secret string doesn’t match the value supplied by the user are removed from the result.
- The JSON API document is returned to the user.
Of course, every good microservice has a suite of tests to validate their functionality. Spring makes testing a mock REST controller easy, and the H2 memory database we defined earlier means that we can test the entire process of requesting, filtering and returning results without any external infrastructure.
Our test class does two important things.
- It creates the database in H2 and populates it with dummy data.
- It executes a simulated HTTP request against the REST controller and validates the results.
package com.matthewcasperson;
import com.matthewcasperson.domain.MicroserviceKeyValue;
import com.matthewcasperson.repository.MicroserviceRepository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Test suite that verifies the behaviour of the microservice. This test uses a H2
* in memory database to simulate the datastore, providing a convenient way to test all
* the JPA/Repository/Elide things without needing any external services.
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("TEST")
public class MicroserviceTemplateApplicationTests {
private static final Logger LOGGER = LoggerFactory.getLogger(MicroserviceTemplateApplicationTests.class);
/**
* We'll use these to populate the in memory H2 database
*/
private static final Map<String, String> TEST_DATA = new HashMap<String, String>() {{
put("Key1", "Value1");
put("Key2", "Value2");
put("Key3", "Value3");
put("Key4", "Value4");
put("Key5", "Value5");
}};
/**
* The H2 database needs to have the correct tables created so JPA can work with it.
*/
private static final String CREATE_TABLE = "CREATE TABLE MicroserviceDatabase.Microservice(" +
"id int auto_increment primary key, key varchar(45), value varchar(45))";
@Autowired
private MockMvc mockMvc;
@Autowired
private WebApplicationContext webApplicationContext;
@Autowired
private MicroserviceRepository microserviceRepository;
@Autowired
private EntityManagerFactory emf;
/**
* Create and populate H2 tables
*/
@Before
public void setup() throws Exception {
EntityManager em = null;
try {
/*
A native query that creates the tables
*/
em = emf.createEntityManager();
em.getTransaction().begin();
em.createNativeQuery(CREATE_TABLE).executeUpdate();
em.getTransaction().commit();
/*
Save some mock data
*/
for (final String key : TEST_DATA.keySet()) {
final MicroserviceKeyValue appQuoteMicroserviceEntity = new MicroserviceKeyValue();
appQuoteMicroserviceEntity.setValue(TEST_DATA.get(key));
appQuoteMicroserviceEntity.setKey(key);
microserviceRepository.save(appQuoteMicroserviceEntity);
}
} catch (final Exception ex) {
LOGGER.error("Error preparing database", ex);
if (em != null && em.getTransaction().isActive()) {
em.getTransaction().rollback();
}
} finally {
if (em != null) {
em.close();
}
}
}
/**
* In this test we are calling the JSON API GET endpoint, which would normally
* return a collection of the entities in the absence of a filter. However,
* only one entity will pass the security test, so our returned collection
* only has one entity.
* @throws Exception
*/
@Test
public void elideFilterTest() throws Exception {
mockMvc.perform(
get("/microservice/1.0/microserviceKeyValue?secret=Key1Secret"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data", hasSize(1)));
}
}
The end result of this tutorial is a standalone microservice that exposes JPA entities via the JSON API specification with custom security routines and tests that can be run independently of any external database. This is a great foundation on which to build a robust and standards-compliant microservice.
Grab the source code from GitHub.
Published at DZone with permission of Matthew Casperson, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments