background
SpringBoot has made it one of the most mainstream Java web development frameworks today because it provides a variety of out-of-the-box plugins. Mybatis is a very lightweight and easy-to-use ORM framework. Redis is a very mainstream distributed key-value database today. In web development, we often use it to cache database query results.
This blog will introduce how to quickly build a web application using SpringBoot and use Mybatis as our ORM framework. To improve performance, we use Redis as the second level cache for Mybatis. To test our code, we wrote unit tests and used the H2 in-memory database to generate our test data. Through this project, we hope readers can quickly master the skills and best practices of modern Java Web development.
The sample code for this article can be downloaded in Github: https://github.com/Lovelcp/spring-boot-mybatis-with-redis/tree/master
environment
Development environment: mac 10.11
ide: Intellij 2017.1
jdk:1.8
Spring-Boot: 1.5.3.RELEASE
Redis: 3.2.9
Mysql: 5.7
Spring-Boot
Create a new project
First, we need to initialize our Spring-Boot project. Through Intellij's Spring Initializer, it is very easy to create a new Spring-Boot project. First, we select New Project in Intellij:
Then in the interface to select dependencies, check Web, Mybatis, Redis, Mysql, H2:
After the new project is successful, we can see the initial structure of the project as shown in the figure below:
Spring Initializer has helped us automatically generate a startup class - SpringBootMybatisWithRedisApplication. The code of this class is very simple:
@SpringBootApplicationpublic class SpringBootMybatisWithRedisApplication { public static void main(String[] args) { SpringApplication.run(SpringBootMybatisWithRedisApplication.class, args); }}@SpringBootApplication annotation means enabling the automatic configuration feature of Spring Boot. Okay, our project skeleton has been successfully built so interested readers can start the results through Intellij.
Create a new API interface
Next, we will write a Web API. Suppose our web engineering is responsible for handling the merchant's products (Product). We need to provide a get interface that returns product information based on the product id and a put interface that updates product information. First we define the Product class, which includes the product id, product name, and price:
public class Product implements Serializable { private static final long serialVersionUID = 1435515995276255188L; private long id; private String name; private long price; // getters setters}Then we need to define the Controller class. Since Spring Boot uses Spring MVC as its web component internally, we can quickly develop our interface class through annotation:
@RestController@RequestMapping("/product")public class ProductController { @GetMapping("/{id}") public Product getProductInfo( @PathVariable("id") Long productId) { // TODO return null; } @PutMapping("/{id}") public Product updateProductInfo( @PathVariable("id") Long productId, @RequestBody Product newProduct) { // TODO return null; }}Let's briefly introduce the functions of the annotations used in the above code:
@RestController: means that the class is a Controller and provides a Rest interface, that is, the values of all interfaces are returned in Json format. This annotation is actually a combination annotation of @Controller and @ResponseBody, which facilitates us to develop the Rest API.
@RequestMapping, @GetMapping, @PutMapping: represents the URL address of the interface. The @RequestMapping annotation annotated on the class means that the URLs of all interfaces under the class start with /product. @GetMapping means this is a Get HTTP interface, @PutMapping means this is a Put HTTP interface.
@PathVariable, @RequestBody: represents the mapping relationship of parameters. Assuming that a Get request accesses /product/123, the request will be processed by the getProductInfo method, where 123 in the URL will be mapped into the productId. Similarly, if it is a Put request, the requested body will be mapped into the newProduct object.
Here we only define the interface, and the actual processing logic has not been completed yet, because the information of the product is present in the database. Next we will integrate mybatis in the project and interact with the database.
Integration of Mybatis
Configure data source
First we need to configure our data source in the configuration file. We use mysql as our database. Here we use yaml as the format of our configuration file. We create a new application.yml file in the resources directory:
spring:# Database configuration datasource: url: jdbc:mysql://{your_host}/{your_db} username: {your_username} password: {your_password} driver-class-name: org.gjt.mm.mysql.DriverSince Spring Boot has the automatic configuration feature, we do not need to create a new DataSource configuration class. Spring Boot will automatically load the configuration file and establish a database connection pool based on the configuration file information, which is very convenient.
The author recommends that you use yaml as the configuration file format. XML looks lengthy and properties have no hierarchical structure. Yaml just makes up for the shortcomings of both. This is also the reason why Spring Boot supports yaml format by default.
Configure Mybatis
We have introduced the mybatis-spring-boot-starte library in pom.xml through Spring Initializer, which will automatically help us initialize mybatis. First, we fill in the relevant configuration of mybatis in application.yml:
# mybatis configure mybatis: # Configure the package name where the mapping class is located type-aliases-package: com.wooyoo.learning.dao.domain # Configure the path where the mapper xml file is located, here is an array mapper-locations: - mappers/ProductMapper.xml
Then, define the ProductMapper class in the code:
@Mapperpublic interface ProductMapper { Product select( @Param("id") long id); void update(Product product);}Here, as long as we add the @Mapper annotation, Spring Boot will automatically load the mapper class when initializing mybatis.
The biggest reason why Spring Boot is so popular is its automatic configuration feature. Developers only need to pay attention to the configuration of components (such as database connection information) without caring about how to initialize individual components, which allows us to focus on business implementation and simplify the development process.
Accessing the database
After completing the Mybatis configuration, we can access the database in our interface. We introduce the mapper class through @Autowired under ProductController and call the corresponding method to implement query and update operations on product. Here we take the query interface as an example:
@RestController@RequestMapping("/product")public class ProductController { @Autowired private ProductMapper productMapper; @GetMapping("/{id}") public Product getProductInfo( @PathVariable("id") Long productId) { return productMapper.select(productId); } // Avoid too long and omit the code of updateProductInfo}Then insert a few product information into your mysql and you can run the project to see if the query is successful.
So far, we have successfully integrated Mybatis into our project, adding the ability to interact with the database. But that's not enough. A modern web project will definitely speed up our database query on cache. Next, we will introduce how to scientifically integrate Redis into Mybatis' secondary cache to realize automatic cache of database queries.
Integrated Redis
Configure Redis
Just like accessing a database, we need to configure Redis connection information. Add the following configuration to the application.yml file:
spring: redis: # redis database index (default is 0). We use a database with index 3 to avoid conflicts with other databases database: 3 # redis server address (default is localhost) host: localhost # redis port (default is 6379) port: 6379 # redis access password (default is null) password: # redis connection timeout (unit is milliseconds) timeout: 0 # redis connection pool configuration pool: # Maximum number of available connections (default is 8, negative numbers represent infinite) max-active: 8 # Maximum number of idle connections (default is 8, negative numbers represent infinite) max-idle: 8 # Minimum number of idle connections (default is 0, this value is only effective) min-idle: 0 # Get the maximum connection waiting time from the connection pool (default is -1, unit is milliseconds, negative numbers indicate infinite) max-wait: -1
All listed above are commonly used configurations, and readers can understand the specific role of each configuration item through comment information. Since we have introduced the spring-boot-starter-data-redis library in pom.xml, Spring Boot will help us automatically load Redis connections and specific configuration classes
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration. Through this configuration class, we can find that the underlying layer uses the Jedis library by default, and provides redisTemplate and stringTemplate out of the box.
Use Redis as a Level 2 cache
The principle of Mybatis' secondary caching will not be described in this article. Readers only need to know that Mybatis' secondary caching can automatically cache database queries, and can automatically update the cache when updating data.
Implementing Mybatis' secondary caching is very simple. You only need to create a new class to implement the org.apache.ibatis.cache.Cache interface.
There are five methods for this interface:
String getId(): The identifier of the mybatis cache operation object. A mapper corresponds to a mybatis cache operation object.
void putObject(Object key, Object value): stuff the query results into the cache.
Object getObject(Object key): Get the cached query result from the cache.
Object removeObject(Object key): Remove the corresponding key and value from the cache. Only fired when rolling back. Generally, we don’t need to implement it. For specific usage methods, please refer to: org.apache.ibatis.cache.decorators.TransactionalCache.
void clear(): Clear cache when an update occurs.
int getSize(): Optional implementation. Returns the number of caches.
ReadWriteLock getReadWriteLock(): Optional implementation. Used to implement atomic cache operations.
Next, we create a new RedisCache class to implement the Cache interface:
public class RedisCache implements Cache { private static final Logger logger = LoggerFactory.getLogger(RedisCache.class); private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private final String id; // cache instance id private RedisTemplate redisTemplate; private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis expiration time public RedisCache(String id) { if (id == null) { throw new IllegalArgumentException("Cache instances require an ID"); } this.id = id; } @Override public String getId() { return id; } /** * Put query result to redis * * @param key * @param value */ @Override @SuppressWarnings("unchecked") public void putObject(Object key, Object value) { RedisTemplate redisTemplate = getRedisTemplate(); ValueOperations opsForValue = redisTemplate.opsForValue(); opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES); logger.debug("Put query result to redis"); } /** * Get cached query result from redis * * @param key * @return */ @Override public Object getObject(Object key) { RedisTemplate redisTemplate = getRedisTemplate(); ValueOperations opsForValue = redisTemplate.opsForValue(); logger.debug("Get cached query result from redis"); return opsForValue.get(key); } /** * Remove cached query result from redis * * @param key * @return */ @Override @SuppressWarnings("unchecked") public Object removeObject(Object key) { RedisTemplate redisTemplate = getRedisTemplate(); redisTemplate.delete(key); logger.debug("Remove cached query result from redis"); return null; } /** * Clears this cache instance */ @Override public void clear() { RedisTemplate redisTemplate = getRedisTemplate(); redisTemplate.execute((RedisCallback) connection -> { connection.flushDb(); return null; }); logger.debug("Clear all the cached query result from redis"); } @Override public int getSize() { return 0; } @Override public ReadWriteLock getReadWriteLock() { return readWriteLock; } private RedisTemplate getRedisTemplate() { if (redisTemplate == null) { redisTemplate = ApplicationContextHolder.getBean("redisTemplate"); } return redisTemplate; }}Let me explain some key points in the above code:
The second-level cache you implement must have a constructor with id, otherwise an error will be reported.
We use the Spring encapsulated redisTemplate to operate Redis. All articles online that introduce redis to the secondary cache of level 2 use the jedis library directly, but the author believes that this is not enough Spring Style. Moreover, redisTemplate encapsulates the underlying implementation. If we don’t use jedis in the future, we can directly replace the underlying library without modifying the upper code. What's more convenient is that using redisTemplate, we don't have to care about the release of redis connections, otherwise it will be easy for novices to forget to release the connection and cause the application to get stuck.
It should be noted that redisTemplate cannot be referenced through autowire, because RedisCache is not a bean in the Spring container. So we need to manually call the getBean method of the container to get this bean. For specific implementation methods, please refer to the code in Github.
The redis serialization method we use is the default jdk serialization. Therefore, the database query object (such as Product class) needs to implement the Serializable interface.
In this way, we implement an elegant, scientific and Redis cache class with Spring Style.
Turn on Level 2 cache
Next, we need to enable Level 2 cache in ProductMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.wooyoo.learning.dao.mapper.ProductMapper"> <!-- Enable redis-based secondary cache--> <cache type="com.wooyoo.learning.util.RedisCache"/> <select id="select" resultType="Product"> SELECT * FROM products WHERE id = #{id} LIMIT 1 </select> <update id="update" parameterType="Product" flushCache="true"> UPDATE products SET name = #{name}, price = #{price} WHERE id = #{id} LIMIT 1 </update></mapper><cache type="com.wooyoo.learning.util.RedisCache"/> means to enable redis-based secondary cache, and in the update statement, we set flushCache to true, so that when updating product information, the cache can be automatically invalidated (essentially, the clear method is called).
test
Configure H2 memory database
At this point, we have completed all the code development, and next we need to write unit test code to test the quality of our code. In the development process, we used mysql database, and generally we often used in-memory databases during testing. Here we use H2 as the database used in our test scenario.
It is also very simple to use H2, you just need to configure it when using mysql. In the application.yml file:
---spring: profiles: test # Database configuration datasource: url: jdbc:h2:mem:test username: root password: 123456 driver-class-name: org.h2.Driver schema: classpath:schema.sql data: classpath:data.sql
In order to avoid conflicts with the default configuration, we use --- to start a new paragraph and use profiles: test to indicate that this is the configuration in the test environment. Then just add the @ActiveProfiles(profiles = "test") annotation to our test class to enable the configuration in the test environment, so that you can switch from the mysql database to the h2 database with one click.
In the above configuration, schema.sql is used to store our table creation statement, and data.sql is used to store insert data. In this way, when we test, h2 will read these two files, initialize the table structure and data we need, and then destroy it at the end of the test, which will not have any impact on our mysql database. This is the benefit of in-memory databases. Also, don't forget to set the scope of h2's dependency to test in pom.xml.
Using Spring Boot is that simple, you can easily switch databases in different environments without modifying any code.
Writing test code
Because we are initialized through Spring Initializer, we already have a test class - SpringBootMybatisWithRedisApplicationTests.
Spring Boot provides some tool classes that facilitate us to conduct web interface testing, such as TestRestTemplate. Then in the configuration file, we adjust the log level to DEBUG to facilitate observation of debug logs. The specific test code is as follows:
@RunWith(SpringRunner.class)@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)@ActiveProfiles(profiles = "test")public class SpringBootMybatisWithRedisApplicationTests { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test public void test() { long productId = 1; Product product = restTemplate.getForObject("http://localhost:" + port + "/product/" + productId, Product.class); assertThat(product.getPrice()).isEqualTo(200); Product newProduct = new Product(); long newPrice = new Random().nextLong(); newProduct.setName("new name"); newProduct.setPrice(newPrice); restTemplate.put("http://localhost:" + port + "/product/" + productId, newProduct); Product testProduct = restTemplate.getForObject("http://localhost:" + port + "/product/" + productId, Product.class); assertThat(testProduct.getPrice()).isEqualTo(newPrice); }}In the above test code:
We first call the get interface and use the assert statement to determine whether the expected object has been obtained. At this time, the product object will be stored in redis.
Then we call the put interface to update the product object, and the redis cache will be invalidated.
Finally, we call the get interface again to determine whether we have obtained a new product object. If an old object is obtained, it means that the cache invalid code failed to execute and there is an error in the code, otherwise it means that our code is OK.
Writing unit testing is a good programming habit. Although it will take up a certain amount of time for you, when you need to do some refactoring work in the future, you will be grateful to yourself who has written unit tests in the past.
View the test results
We click to execute the test case in Intellij, and the test results are as follows:
The green is displayed, indicating that the test case has been executed successfully.
Summarize
This article introduces how to quickly build a modern web project with Spring Boot, Mybatis and Redis, and also introduces how to write unit tests gracefully under Spring Boot to ensure the quality of our code. Of course, there is another problem with this project, that is, mybatis' level 2 cache can only be cached invalidated by flushing the entire DB. At this time, some caches that do not need to be invalidated may also be invalidated, so it has certain limitations.