Advanced Plugin Development¶
Master advanced techniques for building powerful Shiina-Web plugins.
Advanced Database Operations¶
Connection Pooling¶
The App.mysql object manages connection pooling automatically, but you should still write efficient queries.
// Good: Single query with JOIN
ResultSet rs = App.mysql.Query(
"SELECT u.*, s.playcount FROM users u " +
"LEFT JOIN stats s ON u.id = s.user_id " +
"WHERE u.id = ?",
userId
);
// Avoid: Multiple queries
ResultSet user = App.mysql.Query("SELECT * FROM users WHERE id = ?", userId);
ResultSet stats = App.mysql.Query("SELECT * FROM stats WHERE user_id = ?", userId);
Batch Operations¶
For multiple INSERT/UPDATE operations:
public void batchUpdateUsers(List<Integer> userIds) {
try {
// Begin transaction
App.mysql.Exec("START TRANSACTION");
for (Integer userId : userIds) {
App.mysql.Exec(
"UPDATE users SET last_updated = NOW() WHERE id = ?",
userId
);
}
// Commit transaction
App.mysql.Exec("COMMIT");
} catch (Exception e) {
// Rollback on error
App.mysql.Exec("ROLLBACK");
App.logger.error("Batch update failed: " + e.getMessage());
}
}
Pagination¶
Efficiently handle large result sets:
public class LeaderboardRoute extends Shiina {
@Override
public Object handle(Request req, Response res) throws Exception {
ShiinaRequest shiina = new ShiinaRoute().handle(req, res);
int page = Integer.parseInt(req.queryParams("page") != null ? req.queryParams("page") : "1");
int perPage = 50;
int offset = (page - 1) * perPage;
ResultSet rs = shiina.mysql.Query(
"SELECT * FROM users ORDER BY pp DESC LIMIT ? OFFSET ?",
perPage, offset
);
// Process results...
return renderPage(res, shiina, "leaderboard.html");
}
}
Security Best Practices¶
Input Validation¶
Always validate and sanitize user input:
public class UpdateProfileRoute extends Shiina {
@Override
public Object handle(Request req, Response res) throws Exception {
ShiinaRequest shiina = new ShiinaRoute().handle(req, res);
String bio = req.queryParams("bio");
// Validate length
if (bio == null || bio.length() > 500) {
res.status(400);
return "{\"error\": \"Bio must be between 1-500 characters\"}";
}
// Sanitize HTML/Scripts
bio = sanitizeInput(bio);
// Update database
shiina.mysql.Exec(
"UPDATE users SET bio = ? WHERE id = ?",
bio, shiina.user.id
);
return redirect(res, shiina, "/profile");
}
private String sanitizeInput(String input) {
// Remove HTML tags and dangerous characters
return input.replaceAll("<[^>]*>", "")
.replaceAll("[<>\"'&]", "");
}
}
Permission Checking¶
public class AdminRoute extends Shiina {
@Override
public Object handle(Request req, Response res) throws Exception {
ShiinaRequest shiina = new ShiinaRoute().handle(req, res);
// Check if user is logged in
if (!shiina.loggedIn) {
return redirect(res, shiina, "/login");
}
// Check admin privileges
ResultSet rs = shiina.mysql.Query(
"SELECT priv FROM users WHERE id = ?",
shiina.user.id
);
if (rs.next()) {
int privileges = rs.getInt("priv");
boolean isAdmin = (privileges & 4) != 0; // Check admin bit
if (!isAdmin) {
res.status(403);
return "{\"error\": \"Forbidden\"}";
}
}
// Admin-only code here
return "Welcome, admin!";
}
}
Rate Limiting¶
Protect your routes from abuse:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class RateLimitedRoute extends Shiina {
private static final ConcurrentHashMap<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>();
private static final int MAX_REQUESTS = 10; // per minute
@Override
public Object handle(Request req, Response res) throws Exception {
ShiinaRequest shiina = new ShiinaRoute().handle(req, res);
String clientIp = req.ip();
// Get or create counter
AtomicInteger counter = requestCounts.computeIfAbsent(
clientIp,
k -> new AtomicInteger(0)
);
// Check rate limit
if (counter.incrementAndGet() > MAX_REQUESTS) {
res.status(429);
return "{\"error\": \"Too many requests\"}";
}
// Process request...
return "{\"status\": \"success\"}";
}
}
Configuration Files¶
Store plugin settings in external configuration files.
Creating config.json¶
{
"enabled": true,
"apiKey": "your-api-key",
"refreshInterval": 30,
"features": {
"notifications": true,
"analytics": false
}
}
Loading Configuration¶
package com.example.plugin;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.io.FileReader;
import java.io.IOException;
public class PluginConfig {
private JsonObject config;
public PluginConfig(String configPath) {
try {
FileReader reader = new FileReader(configPath);
this.config = new Gson().fromJson(reader, JsonObject.class);
reader.close();
} catch (IOException e) {
App.logger.error("Failed to load config: " + e.getMessage());
this.config = new JsonObject();
}
}
public boolean isEnabled() {
return config.has("enabled") && config.get("enabled").getAsBoolean();
}
public String getApiKey() {
return config.has("apiKey") ? config.get("apiKey").getAsString() : "";
}
public int getRefreshInterval() {
return config.has("refreshInterval") ? config.get("refreshInterval").getAsInt() : 30;
}
}
Using Configuration in Plugin¶
public class Plugin extends ShiinaPlugin {
private PluginConfig config;
@Override
protected void onEnable(String pluginName, Logger logger) {
// Load configuration
config = new PluginConfig("plugins/" + pluginName + "/config.json");
if (!config.isEnabled()) {
logger.warn("Plugin is disabled in configuration");
return;
}
// Use configuration values
int interval = config.getRefreshInterval();
App.cron.registerTimedTask(interval, new UpdateTask());
logger.info("Plugin enabled with " + interval + " minute interval");
}
}
External API Integration¶
Call external services from your plugin.
HTTP Requests with OkHttp¶
import okhttp3.*;
import com.google.gson.Gson;
public class ApiTask extends RunnableCronTask {
private final OkHttpClient client = new OkHttpClient();
@Override
public void run() {
try {
// Build request
Request request = new Request.Builder()
.url("https://api.example.com/data")
.addHeader("Authorization", "Bearer YOUR_TOKEN")
.build();
// Execute request
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
String jsonData = response.body().string();
processApiResponse(jsonData);
} else {
App.logger.error("API request failed: " + response.code());
}
} catch (Exception e) {
App.logger.error("API error: " + e.getMessage());
}
}
private void processApiResponse(String json) {
// Parse and handle response
JsonObject data = new Gson().fromJson(json, JsonObject.class);
// ...
}
@Override
public String getName() {
return "ApiTask";
}
}
Discord Webhooks¶
public void sendDiscordNotification(String webhookUrl, String message) {
try {
OkHttpClient client = new OkHttpClient();
JsonObject payload = new JsonObject();
payload.addProperty("content", message);
RequestBody body = RequestBody.create(
payload.toString(),
MediaType.parse("application/json")
);
Request request = new Request.Builder()
.url(webhookUrl)
.post(body)
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
App.logger.error("Discord webhook failed: " + response.code());
}
} catch (Exception e) {
App.logger.error("Discord notification error: " + e.getMessage());
}
}
Caching¶
Implement caching to improve performance.
Simple In-Memory Cache¶
import java.util.concurrent.ConcurrentHashMap;
public class CacheManager {
private static final ConcurrentHashMap<String, CachedData> cache = new ConcurrentHashMap<>();
private static final long CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
public static class CachedData {
public Object data;
public long timestamp;
public CachedData(Object data) {
this.data = data;
this.timestamp = System.currentTimeMillis();
}
public boolean isExpired() {
return System.currentTimeMillis() - timestamp > CACHE_DURATION;
}
}
public static void put(String key, Object data) {
cache.put(key, new CachedData(data));
}
public static Object get(String key) {
CachedData cached = cache.get(key);
if (cached == null || cached.isExpired()) {
cache.remove(key);
return null;
}
return cached.data;
}
public static void clear() {
cache.clear();
}
}
Using Cache in Routes¶
public class LeaderboardRoute extends Shiina {
@Override
public Object handle(Request req, Response res) throws Exception {
ShiinaRequest shiina = new ShiinaRoute().handle(req, res);
// Try to get from cache
String cacheKey = "leaderboard_top100";
Object cachedData = CacheManager.get(cacheKey);
if (cachedData != null) {
App.logger.debug("Serving from cache");
res.type("application/json");
return cachedData;
}
// Cache miss - query database
ResultSet rs = shiina.mysql.Query(
"SELECT * FROM users ORDER BY pp DESC LIMIT 100"
);
// Build response
String jsonResponse = buildJsonResponse(rs);
// Store in cache
CacheManager.put(cacheKey, jsonResponse);
res.type("application/json");
return jsonResponse;
}
}
Working with Files¶
Read and write files in your plugin.
Reading Files¶
import java.nio.file.Files;
import java.nio.file.Paths;
public String readFile(String path) {
try {
return new String(Files.readAllBytes(Paths.get(path)));
} catch (IOException e) {
App.logger.error("Failed to read file: " + e.getMessage());
return "";
}
}
Writing Files¶
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public void writeFile(String path, String content) {
try {
Files.write(
Paths.get(path),
content.getBytes(),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
);
} catch (IOException e) {
App.logger.error("Failed to write file: " + e.getMessage());
}
}
Error Handling and Logging¶
Comprehensive Error Handling¶
public class RobustRoute extends Shiina {
@Override
public Object handle(Request req, Response res) throws Exception {
ShiinaRequest shiina = new ShiinaRoute().handle(req, res);
try {
// Your route logic
return processRequest(req, shiina);
} catch (SQLException e) {
App.logger.error("Database error: " + e.getMessage(), e);
res.status(500);
return "{\"error\": \"Database error occurred\"}";
} catch (IllegalArgumentException e) {
App.logger.warn("Invalid input: " + e.getMessage());
res.status(400);
return "{\"error\": \"Invalid input\"}";
} catch (Exception e) {
App.logger.error("Unexpected error: " + e.getMessage(), e);
res.status(500);
return "{\"error\": \"Internal server error\"}";
}
}
}
Structured Logging¶
// Info level - normal operations
App.logger.info("[MyPlugin] User " + userId + " performed action");
// Debug level - detailed information
App.logger.debug("[MyPlugin] Processing request with params: " + params);
// Warn level - potential issues
App.logger.warn("[MyPlugin] Slow query detected: " + duration + "ms");
// Error level - actual errors
App.logger.error("[MyPlugin] Failed to process request", exception);
Testing Your Plugin¶
Manual Testing Checklist¶
- Plugin loads without errors
- Routes respond correctly
- Database operations succeed
- Scheduled tasks execute on time
- Error handling works properly
- Logs are meaningful
- Performance is acceptable
- No memory leaks
Load Testing Routes¶
Use tools like Apache Bench to test route performance:
Dependency Management¶
Add external libraries to your plugin via pom.xml:
<dependencies>
<!-- OkHttp for HTTP requests -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- Gson for JSON -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
Further Resources¶
© 2026 Marc Andre Herpers. All rights reserved.