Refactoring a Struts2 Authentication System with Flat-File Storage
System Architecture and Optimization Strategy
Implementing user authentication using Apache Struts 2 and flat-file storage is a common exercise for understanding the Model-View-Controller (MVC) pattern. While production environments typically rely on relational databases, optimizing a TXT-based user system provides valuable insights into layer separation and code maintainability. This technical breakdown outlines the refactoring of a legacy Struts2 login module to resolve architectural debt, eliminate redundant I/O operations, and standardize data persistence logic.
Identified Structural Issues
The initial implementation suffered from several design anti-patterns that hindered maintainability:
- Tightly Coupled Logic: The Service layer directly invoked file I/O operations, mixing business logic with data access details.
- Code Redundancy: Duplicate file parsing routines existed within both the Service implementation and utility classes, increasing the risk of divergent behavior.
- Inconsistent Serialization: User records were stored using mixed delimiters (commas and colons), causing parsing errors when different modules accessed the same
users.txtfile. - Unnecessary Abstractions: Action classes with no logic other than returning a success string were used for navigation, which is better handled via configuration files.
Refactored Directory Layout
The project was reorganized into a standard package structure to enforce strict separation of concerns:
com.techref.auth
│
├── controller
│ LoginAction
│ RegistrationAction
│
├── domain
│ Account
│
├── service
│ AuthService
│ AuthServiceImpl
│
└── persistence
FileDataManager
Persistence Layer Implementation
All file read/write operations are centralized in the FileDataManager class. This ensures a consistent serialization format using a colon delimiter (id:secret) and utilizes try-with-resources for automatic resource management.
public class FileDataManager {
private static final String STORAGE_PATH = "/data/users.txt";
public static boolean saveAccount(Account account) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(STORAGE_PATH, true))) {
writer.write(account.getId() + ":" + account.getSecretKey());
writer.newLine();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
public static boolean checkCredentials(String id, String secretKey) {
try (BufferedReader reader = new BufferedReader(new FileReader(STORAGE_PATH))) {
String record;
while ((record = reader.readLine()) != null) {
String[] parts = record.split(":");
if (parts.length == 2 && parts[0].equals(id) && parts[1].equals(secretKey)) {
return true;
}
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
public static boolean isIdAvailable(String id) {
// Implementation reads file and checks for existence of ID
// Omitted for brevity
return false;
}
}
Service Layer Logic
The AuthServiceImpl class acts as an intermediary, delegating persistence tasks to FileDataManager. This layer remains agnostic to the underlying storage mechanism.
public class AuthServiceImpl implements AuthService {
@Override
public Account authenticate(String id, String secretKey) {
boolean isValid = FileDataManager.checkCredentials(id, secretKey);
return isValid ? new Account(id, secretKey) : null;
}
@Override
public boolean createAccount(Account account) {
if (FileDataManager.isIdAvailable(account.getId())) {
return FileDataManager.saveAccount(account);
}
return false;
}
}
Controller Layer and Validation
The LoginAction handles request processing. Input validation is enforced at this layer before invoking the service. Redundant actions used solely for page forwarding have been removed in favor of struts.xml result configurations.
public class LoginAction extends ActionSupport {
private String id;
private String secretKey;
private AuthService authService = new AuthServiceImpl();
public String execute() {
if (id == null || id.trim().isEmpty()) {
addFieldError("id", "Username is required");
return INPUT;
}
if (secretKey == null || secretKey.trim().isEmpty()) {
addFieldError("secretKey", "Password is required");
return INPUT;
}
Account account = authService.authenticate(id, secretKey);
if (account != null) {
return SUCCESS;
}
return "login";
}
// Getters and Setters
}
Configuration Updates
Navigation logic previously embedded in empty Action classes is moved to the Struts configuration file. This simplifies the controller codebase and centralizes routing rules.
<action name="registerForm" class="com.opensymphony.xwork2.ActionSupport">
<result name="success">/register.jsp</result>
</action>
This refactoring results in a cleaner architecture where the controller handles HTTP requests, the service manages business rules, and the utility layer manages raw data access, effectively decoupling the application logic from the file system.