Add caching

This commit is contained in:
Kenny Bastani
2016-12-09 20:33:47 -08:00
parent 39f195a994
commit b4579855f2
10 changed files with 173 additions and 86 deletions

View File

@@ -31,6 +31,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>

View File

@@ -31,14 +31,14 @@ public class AccountController {
public ResponseEntity createAccount(@RequestBody Account account) {
return Optional.ofNullable(createAccountResource(account))
.map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
.orElseThrow(() -> new IllegalArgumentException("Account creation failed"));
.orElseThrow(() -> new RuntimeException("Account creation failed"));
}
@PutMapping(path = "/accounts/{id}")
public ResponseEntity updateAccount(@RequestBody Account account, @PathVariable Long id) {
return Optional.ofNullable(updateAccountResource(id, account))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
.orElseThrow(() -> new IllegalArgumentException("Account update failed"));
.orElseThrow(() -> new RuntimeException("Account update failed"));
}
@GetMapping(path = "/accounts/{id}")
@@ -48,25 +48,32 @@ public class AccountController {
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
@DeleteMapping(path = "/accounts/{id}")
public ResponseEntity deleteAccount(@PathVariable Long id) {
return Optional.ofNullable(accountService.deleteAccount(id))
.map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT))
.orElseThrow(() -> new RuntimeException("Account deletion failed"));
}
@GetMapping(path = "/accounts/{id}/events")
public ResponseEntity getAccountEvents(@PathVariable Long id) {
return Optional.ofNullable(getAccountEventResources(id))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
.orElseThrow(() -> new IllegalArgumentException("Could not get account events"));
.orElseThrow(() -> new RuntimeException("Could not get account events"));
}
@PostMapping(path = "/accounts/{id}/events")
public ResponseEntity createAccount(@PathVariable Long id, @RequestBody AccountEvent event) {
return Optional.ofNullable(appendEventResource(id, event))
.map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
.orElseThrow(() -> new IllegalArgumentException("Append account event failed"));
.orElseThrow(() -> new RuntimeException("Append account event failed"));
}
@GetMapping(path = "/accounts/{id}/commands")
public ResponseEntity getAccountCommands(@PathVariable Long id) {
return Optional.ofNullable(getCommandsResource(id))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
.orElseThrow(() -> new IllegalArgumentException("The account could not be found"));
.orElseThrow(() -> new RuntimeException("The account could not be found"));
}
@GetMapping(path = "/accounts/{id}/commands/confirm")
@@ -74,7 +81,7 @@ public class AccountController {
return Optional.ofNullable(getAccountResource(
accountService.applyCommand(id, AccountCommand.CONFIRM_ACCOUNT)))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
.orElseThrow(() -> new IllegalArgumentException("The command could not be applied"));
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
}
@GetMapping(path = "/accounts/{id}/commands/activate")
@@ -82,7 +89,7 @@ public class AccountController {
return Optional.ofNullable(getAccountResource(
accountService.applyCommand(id, AccountCommand.ACTIVATE_ACCOUNT)))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
.orElseThrow(() -> new IllegalArgumentException("The command could not be applied"));
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
}
@GetMapping(path = "/accounts/{id}/commands/suspend")
@@ -90,7 +97,7 @@ public class AccountController {
return Optional.ofNullable(getAccountResource(
accountService.applyCommand(id, AccountCommand.SUSPEND_ACCOUNT)))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
.orElseThrow(() -> new IllegalArgumentException("The command could not be applied"));
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
}
@GetMapping(path = "/accounts/{id}/commands/archive")
@@ -98,7 +105,7 @@ public class AccountController {
return Optional.ofNullable(getAccountResource(
accountService.applyCommand(id, AccountCommand.ARCHIVE_ACCOUNT)))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
.orElseThrow(() -> new IllegalArgumentException("The command could not be applied"));
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
}
/**

View File

@@ -3,6 +3,9 @@ package demo.account;
import demo.event.AccountEvent;
import demo.event.AccountEventType;
import demo.event.EventService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
@@ -11,6 +14,8 @@ import java.util.Arrays;
import java.util.Objects;
import static demo.account.AccountStatus.*;
import static demo.account.AccountStatus.ACCOUNT_ACTIVE;
import static demo.account.AccountStatus.ACCOUNT_ARCHIVED;
/**
* The {@link AccountService} provides transactional support for managing {@link Account}
@@ -33,81 +38,13 @@ public class AccountService {
this.eventService = eventService;
}
/**
* Apply an {@link AccountCommand} to the {@link Account} with a specified identifier.
*
* @param id is the unique identifier of the {@link Account}
* @param accountCommand is the command to apply to the {@link Account}
* @return a hypermedia resource containing the updated {@link Account}
*/
public Account applyCommand(Long id, AccountCommand accountCommand) {
Account account = getAccount(id);
Assert.notNull(account, "The account for the supplied id could not be found");
AccountStatus status = account.getStatus();
switch (accountCommand) {
case CONFIRM_ACCOUNT:
Assert.isTrue(status == ACCOUNT_PENDING, "The account has already been confirmed");
// Confirm the account
Account updateAccount = account;
updateAccount.setStatus(ACCOUNT_CONFIRMED);
account = updateAccount(id, updateAccount);
appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED));
break;
case ACTIVATE_ACCOUNT:
Assert.isTrue(status != ACCOUNT_ACTIVE, "The account is already active");
Assert.isTrue(Arrays.asList(ACCOUNT_CONFIRMED, ACCOUNT_SUSPENDED, ACCOUNT_ARCHIVED)
.contains(status), "The account cannot be activated");
// Activate the account
account.setStatus(ACCOUNT_ACTIVE);
account = updateAccount(id, account);
appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED));
break;
case SUSPEND_ACCOUNT:
Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be suspended");
// Suspend the account
account.setStatus(ACCOUNT_SUSPENDED);
account = updateAccount(id, account);
appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED));
break;
case ARCHIVE_ACCOUNT:
Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be archived");
// Archive the account
account.setStatus(ACCOUNT_ARCHIVED);
account = updateAccount(id, account);
appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED));
break;
default:
Assert.notNull(accountCommand,
"The provided command cannot be applied to this account in its current state");
}
return account;
}
/**
* Get an {@link Account} entity for the supplied identifier.
*
* @param id is the unique identifier of a {@link Account} entity
* @return an {@link Account} entity
*/
public Account getAccount(Long id) {
return accountRepository.findOne(id);
}
/**
* Create a new {@link Account} entity.
*
* @param account is the {@link Account} to create
* @return the newly created {@link Account}
*/
@CacheEvict(value = "account", key = "#account.getAccountId().toString()")
public Account createAccount(Account account) {
// Assert for uniqueness constraint
Assert.isNull(accountRepository.findAccountByUserId(account.getUserId()),
@@ -125,6 +62,17 @@ public class AccountService {
return account;
}
/**
* Get an {@link Account} entity for the supplied identifier.
*
* @param id is the unique identifier of a {@link Account} entity
* @return an {@link Account} entity
*/
@Cacheable(value = "account", key = "#id.toString()")
public Account getAccount(Long id) {
return accountRepository.findOne(id);
}
/**
* Update an {@link Account} entity with the supplied identifier.
*
@@ -132,6 +80,7 @@ public class AccountService {
* @param account is the {@link Account} containing updated fields
* @return the updated {@link Account} entity
*/
@CachePut(value = "account", key = "#id.toString()")
public Account updateAccount(Long id, Account account) {
Assert.notNull(id, "Account id must be present in the resource URL");
Assert.notNull(account, "Account request body cannot be null");
@@ -143,15 +92,31 @@ public class AccountService {
account.setAccountId(id);
}
Account currentAccount = getAccount(id);
currentAccount.setStatus(account.getStatus());
currentAccount.setDefaultAccount(account.getDefaultAccount());
currentAccount.setAccountNumber(account.getAccountNumber());
Assert.state(accountRepository.exists(id),
"The account with the supplied id does not exist");
Account currentAccount = accountRepository.findOne(id);
currentAccount.setUserId(account.getUserId());
currentAccount.setAccountNumber(account.getAccountNumber());
currentAccount.setDefaultAccount(account.getDefaultAccount());
currentAccount.setStatus(account.getStatus());
return accountRepository.save(currentAccount);
}
/**
* Delete the {@link Account} with the supplied identifier.
*
* @param id is the unique identifier for the {@link Account}
*/
@CacheEvict(value = "account", key = "#id.toString()")
public Boolean deleteAccount(Long id) {
Assert.state(accountRepository.exists(id),
"The account with the supplied id does not exist");
this.accountRepository.delete(id);
return true;
}
/**
* Append a new {@link AccountEvent} to the {@link Account} reference for the supplied identifier.
*
@@ -168,4 +133,63 @@ public class AccountService {
accountRepository.save(account);
return event;
}
/**
* Apply an {@link AccountCommand} to the {@link Account} with a specified identifier.
*
* @param id is the unique identifier of the {@link Account}
* @param accountCommand is the command to apply to the {@link Account}
* @return a hypermedia resource containing the updated {@link Account}
*/
@CachePut(value = "account", key = "#id.toString()")
public Account applyCommand(Long id, AccountCommand accountCommand) {
Account account = getAccount(id);
Assert.notNull(account, "The account for the supplied id could not be found");
AccountStatus status = account.getStatus();
switch (accountCommand) {
case CONFIRM_ACCOUNT:
Assert.isTrue(status == ACCOUNT_PENDING, "The account has already been confirmed");
// Confirm the account
Account updateAccount = account;
updateAccount.setStatus(ACCOUNT_CONFIRMED);
account = this.updateAccount(id, updateAccount);
this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED));
break;
case ACTIVATE_ACCOUNT:
Assert.isTrue(status != ACCOUNT_ACTIVE, "The account is already active");
Assert.isTrue(Arrays.asList(ACCOUNT_CONFIRMED, ACCOUNT_SUSPENDED, ACCOUNT_ARCHIVED)
.contains(status), "The account cannot be activated");
// Activate the account
account.setStatus(ACCOUNT_ACTIVE);
account = this.updateAccount(id, account);
this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED));
break;
case SUSPEND_ACCOUNT:
Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be suspended");
// Suspend the account
account.setStatus(ACCOUNT_SUSPENDED);
account = this.updateAccount(id, account);
this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED));
break;
case ARCHIVE_ACCOUNT:
Assert.isTrue(status == ACCOUNT_ACTIVE, "An inactive account cannot be archived");
// Archive the account
account.setStatus(ACCOUNT_ARCHIVED);
account = this.updateAccount(id, account);
this.appendEvent(id, new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED));
break;
default:
Assert.notNull(accountCommand,
"The provided command cannot be applied to this account in its current state");
}
return account;
}
}

View File

@@ -0,0 +1,42 @@
package demo.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
@EnableCaching
public class CacheConfiguration {
@Bean
public JedisConnectionFactory redisConnectionFactory(
@Value("${spring.redis.port}") Integer redisPort,
@Value("${spring.redis.host}") String redisHost) {
JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory();
redisConnectionFactory.setHostName(redisHost);
redisConnectionFactory.setPort(redisPort);
return redisConnectionFactory;
}
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory cf) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(cf);
return redisTemplate;
}
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
cacheManager.setDefaultExpiration(3000);
return cacheManager;
}
}

View File

@@ -9,5 +9,5 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
*/
@Configuration
@EnableJpaAuditing
public class JpaConfig {
public class JpaConfiguration {
}

View File

@@ -10,7 +10,7 @@ import org.springframework.hateoas.ResourceProcessor;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
@Configuration
public class AccountResourceConfig {
public class ResourceConfiguration {
/**
* Enriches the {@link Account} resource with hypermedia links.

View File

@@ -6,5 +6,5 @@ import org.springframework.context.annotation.Configuration;
@Configuration
@EnableBinding(Source.class)
public class AccountStreamConfig {
public class StreamConfiguration {
}

View File

@@ -7,10 +7,11 @@ import org.springframework.hateoas.ResourceSupport;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.io.Serializable;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity extends ResourceSupport {
public class BaseEntity extends ResourceSupport implements Serializable {
@CreatedDate
private Long createdAt;

View File

@@ -10,5 +10,8 @@ spring:
output:
destination: account
contentType: 'application/json'
redis:
host: localhost
port: 6379
server:
port: 0

View File

@@ -70,6 +70,7 @@ public class AccountServiceTests {
accountEvent.setEventId(1L);
given(this.accountRepository.findOne(1L)).willReturn(account);
given(this.accountRepository.exists(1L)).willReturn(true);
given(this.accountRepository.save(account)).willReturn(account);
given(this.eventService.createEvent(new AccountEvent(AccountEventType.ACCOUNT_SUSPENDED)))
.willReturn(accountEvent);
@@ -91,6 +92,7 @@ public class AccountServiceTests {
accountEvent.setEventId(1L);
given(this.accountRepository.findOne(1L)).willReturn(account);
given(this.accountRepository.exists(1L)).willReturn(true);
given(this.accountRepository.save(account)).willReturn(account);
given(this.eventService.createEvent(new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED)))
.willReturn(accountEvent);
@@ -112,6 +114,7 @@ public class AccountServiceTests {
accountEvent.setEventId(1L);
given(this.accountRepository.findOne(1L)).willReturn(account);
given(this.accountRepository.exists(1L)).willReturn(true);
given(this.accountRepository.save(account)).willReturn(account);
given(this.eventService.createEvent(new AccountEvent(AccountEventType.ACCOUNT_ARCHIVED)))
.willReturn(accountEvent);
@@ -133,6 +136,7 @@ public class AccountServiceTests {
accountEvent.setEventId(1L);
given(this.accountRepository.findOne(1L)).willReturn(account);
given(this.accountRepository.exists(1L)).willReturn(true);
given(this.accountRepository.save(account)).willReturn(account);
given(this.eventService.createEvent(new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED)))
.willReturn(accountEvent);
@@ -154,6 +158,7 @@ public class AccountServiceTests {
accountEvent.setEventId(1L);
given(this.accountRepository.findOne(1L)).willReturn(account);
given(this.accountRepository.exists(1L)).willReturn(true);
given(this.accountRepository.save(account)).willReturn(account);
given(this.eventService.createEvent(new AccountEvent(AccountEventType.ACCOUNT_CONFIRMED)))
.willReturn(accountEvent);
@@ -175,6 +180,7 @@ public class AccountServiceTests {
accountEvent.setEventId(1L);
given(this.accountRepository.findOne(1L)).willReturn(account);
given(this.accountRepository.exists(1L)).willReturn(true);
given(this.accountRepository.save(account)).willReturn(account);
given(this.eventService.createEvent(new AccountEvent(AccountEventType.ACCOUNT_ACTIVATED)))
.willReturn(accountEvent);