Design and Implementation of a Unified Log Recording Framework for Java Backend Systems
This article introduces a unified log recording solution for Java backend services, covering the background need, expected UI effects, database schema, core principles, detailed annotation and service implementations, and multiple usage scenarios including simple log objects, inheritance, and nested objects.
1. Background
Backend systems increasingly require log recording functionality, and existing log implementations are not reusable; a unified log format is needed to improve development efficiency.
2. Expected Effect Display
New action:
Modify action:
Delete action:
3. Data Storage
Note: other storage methods can be used; the following is a simple example.
`biz_id` bigint(20) NOT NULL DEFAULT 0 COMMENT 'business id',
`biz_type` tinyint(4) NOT NULL DEFAULT 0 COMMENT 'business type',
`operator_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'operator',
`operate_content` text COMMENT 'operation content',
`change_before` text COMMENT 'before change',
`change_after` text COMMENT 'after change',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'creation time'4. Principle Brief
The log construction focuses on two objects: the state before modification and the state after modification.
Before: null → After: X = create
Before: Y → After: X = update
Before: Y → After: null = delete
The system compares each property of the two objects; if a property changes, it is recorded in the log. Fields to be logged are marked with a custom annotation.
5. Specific Implementation
Annotation definition:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogField {
String name() default "";
String valueFun() default "";
boolean spliceValue() default true;
}Service class (simplified):
@Service
@Slf4j
public class OperateLogService {
@Resource
private CommonOperateLogService commonOperateLogService;
enum ActionEnum {
ADD("新建"),
UPDATE("修改"),
DELETE("删除");
public String desc;
ActionEnum(String desc) { this.desc = desc; }
}
private int insertLog(CommonOperatorLog log) {
String result = commonOperateLogService.insertLog(JSON.toJSONString(log));
Response response = JSON.parseObject(result, Response.class);
return (response == null || ApiResponse.Status.fail.equals(response.getStatus())) ? 0 : (int) response.getContent();
}
public
void saveLog(String operatorId, Long bizId, Integer bizType, T target, T original) {
if (StringUtils.isBlank(operatorId) || (target == null && original == null)) {
throw new IllegalArgumentException();
}
if (target != null && original != null && !target.getClass().isAssignableFrom(original.getClass())) {
throw new IllegalArgumentException();
}
ActionEnum action = getAction(target, original);
List
> changeInfos = getChangeInfoList(target, original);
List
changeInfoList = new ArrayList<>();
if (CollectionUtils.isEmpty(changeInfos) && action != ActionEnum.UPDATE) {
changeInfoList.add(0, action.desc);
} else if (!CollectionUtils.isEmpty(changeInfos)) {
for (Triple
i : changeInfos) {
String part = action.desc + (i.getRight().spliceValue() ? "为:" + i.getLeft() + i.getMiddle() : "了" + i.getLeft());
changeInfoList.add(part);
}
} else {
return;
}
String operateContext = StringUtils.join(changeInfoList, "\n").replaceAll("\"", "").replaceAll("\\[", "").replaceAll("\\]", "");
CommonOperatorLog operatorLog = new CommonOperatorLog();
operatorLog.setBizId(bizId);
operatorLog.setBizType(bizType);
operatorLog.setOperateContent(operateContext);
operatorLog.setOperatorId(operatorId);
operatorLog.setChangeBefore(JSON.toJSONString(original));
operatorLog.setChangeAfter(JSON.toJSONString(target));
this.insertLog(operatorLog);
}
private ActionEnum getAction(Object target, Object original) {
if (target != null && original != null) return ActionEnum.UPDATE;
if (target != null) return ActionEnum.ADD;
if (original != null) return ActionEnum.DELETE;
return ActionEnum.ADD;
}
// Methods getChangeInfoList, compareTo, allFields, findField, getFieldName, getFieldValue, getStrValue omitted for brevity – they implement reflection‑based field comparison using the @LogField annotation.
}6. Usage Examples
1) Log object dedicated to logging:
public class SubsidyRateLog {
@LogField(name = "补贴率名称")
private String name;
@LogField(name = "适用城市", valueFun = "getCityNames")
private List
cityIds;
private List
cityNames;
}Here name is logged directly, while cityIds uses getCityNames to obtain a displayable value.
2) No dedicated log object – inherit from existing entity:
public class SubsidyRate {
@LogField(name = "补贴率名称")
private String name;
@LogField(name = "适用城市", valueFun = "getCityNames")
private List
cityIds;
}
@Data
public class SubsidyRateLog extends SubsidyRate {
private List
cityNames;
}This approach preserves the original entity while adding logging fields.
3) Objects containing nested objects:
public class SubsidyRateLog {
@LogField(name = "补贴率名称")
private String name;
@LogField
private Address address;
}
public class Address {
@LogField(name = "省份")
private String province;
@LogField(name = "城市")
private String city;
}If the nested object has no @LogField annotations, its whole value is logged; otherwise only annotated fields are considered.
7. End Notes
The article concludes with a call to follow the public account for more architecture guidance and provides several promotional links to external resources.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.