Add player caching

This commit is contained in:
Braydon 2024-04-06 15:24:03 -04:00
parent e146fa1030
commit f9426395a0
9 changed files with 169 additions and 12 deletions

16
pom.xml

@ -41,6 +41,22 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis for caching -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- Libraries -->
<dependency>
<groupId>org.projectlombok</groupId>

@ -5,8 +5,13 @@ import com.google.gson.GsonBuilder;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.File;
import java.nio.file.Files;
@ -23,6 +28,30 @@ public class RESTfulMC {
.serializeNulls()
.create();
/**
* The Redis server host.
*/
@Value("${spring.data.redis.host}")
private String redisHost;
/**
* The Redis server port.
*/
@Value("${spring.data.redis.port}")
private int redisPort;
/**
* The Redis database index.
*/
@Value("${spring.data.redis.database}")
private int redisDatabase;
/**
* The optional Redis password.
*/
@Value("${spring.data.redis.auth}")
private String redisAuth;
@SneakyThrows
public static void main(@NonNull String[] args) {
// Handle loading of our configuration file
@ -39,4 +68,34 @@ public class RESTfulMC {
// Start the app
SpringApplication.run(RESTfulMC.class, args);
}
/**
* Build the config to use for Redis.
*
* @return the config
* @see RedisTemplate for config
*/
@Bean @NonNull
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
return template;
}
/**
* Build the connection factory to use
* when making connections to Redis.
*
* @return the built factory
* @see JedisConnectionFactory for factory
*/
@Bean @NonNull
public JedisConnectionFactory jedisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort);
config.setDatabase(redisDatabase);
if (!redisAuth.trim().isEmpty()) { // Auth with our provided password
config.setPassword(redisAuth);
}
return new JedisConnectionFactory(config);
}
}

@ -4,7 +4,7 @@ import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import me.braydon.mc.exception.impl.BadRequestException;
import me.braydon.mc.exception.impl.ResourceNotFoundException;
import me.braydon.mc.model.Player;
import me.braydon.mc.model.cache.CachedPlayer;
import me.braydon.mc.service.MojangService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
@ -41,7 +41,7 @@ public final class PlayerController {
*/
@GetMapping("/{query}")
@ResponseBody
public ResponseEntity<Player> getPlayer(@PathVariable @NonNull String query) throws BadRequestException, ResourceNotFoundException {
public ResponseEntity<CachedPlayer> getPlayer(@PathVariable @NonNull String query) throws BadRequestException, ResourceNotFoundException {
return ResponseEntity.ofNullable(mojangService.getPlayer(query));
}
}

@ -1,7 +1,7 @@
package me.braydon.mc.exception;
import lombok.NonNull;
import me.braydon.mc.model.ErrorResponse;
import me.braydon.mc.model.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;

@ -1,6 +1,8 @@
package me.braydon.mc.model;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import java.util.UUID;
@ -9,12 +11,17 @@ import java.util.UUID;
*
* @author Braydon
*/
@NoArgsConstructor @AllArgsConstructor @Setter @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
public final class Player {
@NoArgsConstructor
@AllArgsConstructor
@Setter @Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString
@RedisHash(value = "player", timeToLive = 60L * 60L) // 1 hour (in seconds)
public class Player {
/**
* The unique id of this player.
*/
@EqualsAndHashCode.Include @NonNull private UUID uniqueId;
@Id @EqualsAndHashCode.Include @NonNull private UUID uniqueId;
/**
* The username of this player.
@ -22,7 +29,7 @@ public final class Player {
@NonNull private String username;
/**
* The profile actions this player has.
* The profile actions this player has, null if none.
*/
@NonNull private ProfileAction[] profileActions;
private ProfileAction[] profileActions;
}

@ -0,0 +1,29 @@
package me.braydon.mc.model.cache;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import me.braydon.mc.model.Player;
import me.braydon.mc.model.ProfileAction;
import java.io.Serializable;
import java.util.UUID;
/**
* A cacheable {@link Player}.
*
* @author Braydon
*/
@Setter @Getter
public final class CachedPlayer extends Player implements Serializable {
/**
* The unix timestamp of when this
* player was cached, -1 if not cached.
*/
private long cached;
public CachedPlayer(@NonNull UUID uniqueId, @NonNull String username, ProfileAction[] profileActions, long cached) {
super(uniqueId, username, profileActions);
this.cached = cached;
}
}

@ -1,4 +1,4 @@
package me.braydon.mc.model;
package me.braydon.mc.model.response;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;

@ -0,0 +1,14 @@
package me.braydon.mc.repository;
import me.braydon.mc.model.Player;
import me.braydon.mc.model.cache.CachedPlayer;
import org.springframework.data.repository.CrudRepository;
import java.util.UUID;
/**
* A cache repository for {@link Player}'s.
*
* @author Braydon
*/
public interface PlayerCacheRepository extends CrudRepository<CachedPlayer, UUID> { }

@ -8,11 +8,16 @@ import me.braydon.mc.common.web.JsonWebRequest;
import me.braydon.mc.exception.impl.BadRequestException;
import me.braydon.mc.exception.impl.ResourceNotFoundException;
import me.braydon.mc.model.Player;
import me.braydon.mc.model.ProfileAction;
import me.braydon.mc.model.cache.CachedPlayer;
import me.braydon.mc.model.token.MojangProfileToken;
import me.braydon.mc.model.token.MojangUsernameToUUIDToken;
import me.braydon.mc.repository.PlayerCacheRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.UUID;
/**
@ -28,6 +33,16 @@ public final class MojangService {
private static final String UUID_TO_PROFILE = SESSION_SERVER_ENDPOINT + "/session/minecraft/profile/%s";
private static final String USERNAME_TO_UUID = API_ENDPOINT + "/users/profiles/minecraft/%s";
/**
* The cache repository for {@link Player}'s.
*/
@NonNull private final PlayerCacheRepository playerCacheRepository;
@Autowired
public MojangService(@NonNull PlayerCacheRepository playerCacheRepository) {
this.playerCacheRepository = playerCacheRepository;
}
/**
* Get a player by their username or UUID.
*
@ -37,7 +52,7 @@ public final class MojangService {
* @throws ResourceNotFoundException if the player is not found
*/
@NonNull
public Player getPlayer(@NonNull String query) throws BadRequestException, ResourceNotFoundException {
public CachedPlayer getPlayer(@NonNull String query) throws BadRequestException, ResourceNotFoundException {
log.info("Requesting player with query: {}", query);
UUID uuid; // The player UUID to lookup
@ -54,6 +69,13 @@ public final class MojangService {
log.info("Found UUID for username {}: {}", query, uuid);
}
// Check the cache for the player
Optional<CachedPlayer> cached = playerCacheRepository.findById(uuid);
if (cached.isPresent()) { // Respond with the cache if present
log.info("Found player in cache: {}", uuid);
return cached.get();
}
// Send a request to Mojang requesting
// the player profile by their UUID
try {
@ -61,9 +83,19 @@ public final class MojangService {
MojangProfileToken token = JsonWebRequest.makeRequest(
UUID_TO_PROFILE.formatted(uuid), HttpMethod.GET
).execute(MojangProfileToken.class);
ProfileAction[] profileActions = token.getProfileActions();
// Return our player model representing the requested player
return new Player(uuid, token.getName(), token.getProfileActions());
// Build our player model, cache it, and then return it
CachedPlayer player = new CachedPlayer(
uuid, token.getName(),
profileActions.length == 0 ? null : profileActions,
System.currentTimeMillis()
);
playerCacheRepository.save(player);
log.info("Cached player: {}", uuid);
player.setCached(-1L); // Set to -1 to indicate it's not cached in the response
return player;
} catch (JsonWebException ex) {
// No profile found, return null
if (ex.getStatusCode() == 400) {