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> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </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 --> <!-- Libraries -->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>

@ -5,8 +5,13 @@ import com.google.gson.GsonBuilder;
import lombok.NonNull; import lombok.NonNull;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; 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.io.File;
import java.nio.file.Files; import java.nio.file.Files;
@ -23,6 +28,30 @@ public class RESTfulMC {
.serializeNulls() .serializeNulls()
.create(); .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 @SneakyThrows
public static void main(@NonNull String[] args) { public static void main(@NonNull String[] args) {
// Handle loading of our configuration file // Handle loading of our configuration file
@ -39,4 +68,34 @@ public class RESTfulMC {
// Start the app // Start the app
SpringApplication.run(RESTfulMC.class, args); 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 lombok.extern.log4j.Log4j2;
import me.braydon.mc.exception.impl.BadRequestException; import me.braydon.mc.exception.impl.BadRequestException;
import me.braydon.mc.exception.impl.ResourceNotFoundException; 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 me.braydon.mc.service.MojangService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -41,7 +41,7 @@ public final class PlayerController {
*/ */
@GetMapping("/{query}") @GetMapping("/{query}")
@ResponseBody @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)); return ResponseEntity.ofNullable(mojangService.getPlayer(query));
} }
} }

@ -1,7 +1,7 @@
package me.braydon.mc.exception; package me.braydon.mc.exception;
import lombok.NonNull; 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.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;

@ -1,6 +1,8 @@
package me.braydon.mc.model; package me.braydon.mc.model;
import lombok.*; import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import java.util.UUID; import java.util.UUID;
@ -9,12 +11,17 @@ import java.util.UUID;
* *
* @author Braydon * @author Braydon
*/ */
@NoArgsConstructor @AllArgsConstructor @Setter @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString @NoArgsConstructor
public final class Player { @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. * The unique id of this player.
*/ */
@EqualsAndHashCode.Include @NonNull private UUID uniqueId; @Id @EqualsAndHashCode.Include @NonNull private UUID uniqueId;
/** /**
* The username of this player. * The username of this player.
@ -22,7 +29,7 @@ public final class Player {
@NonNull private String username; @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 com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*; 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.BadRequestException;
import me.braydon.mc.exception.impl.ResourceNotFoundException; import me.braydon.mc.exception.impl.ResourceNotFoundException;
import me.braydon.mc.model.Player; 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.MojangProfileToken;
import me.braydon.mc.model.token.MojangUsernameToUUIDToken; 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.http.HttpMethod;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.UUID; 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 UUID_TO_PROFILE = SESSION_SERVER_ENDPOINT + "/session/minecraft/profile/%s";
private static final String USERNAME_TO_UUID = API_ENDPOINT + "/users/profiles/minecraft/%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. * Get a player by their username or UUID.
* *
@ -37,7 +52,7 @@ public final class MojangService {
* @throws ResourceNotFoundException if the player is not found * @throws ResourceNotFoundException if the player is not found
*/ */
@NonNull @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); log.info("Requesting player with query: {}", query);
UUID uuid; // The player UUID to lookup UUID uuid; // The player UUID to lookup
@ -54,6 +69,13 @@ public final class MojangService {
log.info("Found UUID for username {}: {}", query, uuid); 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 // Send a request to Mojang requesting
// the player profile by their UUID // the player profile by their UUID
try { try {
@ -61,9 +83,19 @@ public final class MojangService {
MojangProfileToken token = JsonWebRequest.makeRequest( MojangProfileToken token = JsonWebRequest.makeRequest(
UUID_TO_PROFILE.formatted(uuid), HttpMethod.GET UUID_TO_PROFILE.formatted(uuid), HttpMethod.GET
).execute(MojangProfileToken.class); ).execute(MojangProfileToken.class);
ProfileAction[] profileActions = token.getProfileActions();
// Return our player model representing the requested player // Build our player model, cache it, and then return it
return new Player(uuid, token.getName(), token.getProfileActions()); 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) { } catch (JsonWebException ex) {
// No profile found, return null // No profile found, return null
if (ex.getStatusCode() == 400) { if (ex.getStatusCode() == 400) {