在业务需求中,有时对与危险操作,比如删除需要提醒用户删除该资源可能造成的影响。特别是对于基础资源被创建后,其他业务资源有可能处于正在使用该基础资源的状态。对于该基础资源的删除,有必要提醒用户当前有哪些业务资源在使用。下面将重点介绍一种通过数据库结合AOP来维护管理基础资源使用情况的方案。其他基于事件的方案将做简要分析。
对于资源的使用情况记录,在这里只是简单的记录下当前操作的业务类型、做了什么操作、操作结果是正常还是失败、操作时间,实际项目中,如果我们需要记录的更详细,可以记录当前操作人的详细信息。我们还可以记录用户调用了哪个类的哪个方法,我们可以使用JoinPoint参数获取或者利用环绕通知ProceedingJoinPoint去获取。可以精确的定位到类、方法、参数,如果有必要我们就可以记录在操作记录数据库中中,看业务需求和我们的资源使用情况来进行设计。如果再细致的记录资源使用情况,我们可以针对错误再建立一个错误操作表,在发生错误的情况下(异常通知里)记录操作错误的信息。
实现的大致思路是:
1.前期准备,设计资源使用记录表和资源使用记录类,编写资源使用记录entity和repository以及实现
2.自定义注解,注解中加入几个属性,属性可以标识操作的类型(方法是做什么的)
3.编写切面,切点表达式使用上面的注解直接定位到使用注解的方法,
4.编写通知,通过定位到方法,获取上面的注解以及注解的属性,然后从请求参数和数据库操作对象repository中获取信息,最后根据业务处理一些资源使用信息之后调用资源使用Service存储资源使用情况信息。
其实资源使用记录可以针对Controller层进行切入,也可以选择Service层进行切入,当前选择的是基于Service层进行资源使用情况记录。有些AOP操作记录有的用前置通知,有的用环绕通知,当前选择在环绕通知中完成,因为环绕通知中可以完成前置、后置、最终、异常通知的所有功能。
一、数据库实现
@Data
@Entity
@NoArgsConstructor
@Table(name = "tb_resource_usage_record")
@EqualsAndHashCode(callSuper = false)
public class ResourceUsageRecordEntity {
@Id
@Column(name="id" ,nullable = false , unique = true )
private String id;
/**
* 资源id, 如entId
*/
@Column(name="resource_id" ,nullable = false)
private String resourceId;
/**
* 资源类型: 如ent, region, building,
*/
@Column(name="resource_type" ,nullable = false)
private String resourceType;
/**
* 操作业务
*/
@Column(name="operate_business")
private String operateBusiness;
/**
* 操作结果, 0-正常, -1 异常
*/
@Column(name="operate_result")
private Integer operateResult;
@Column(name="create_time")
private Date createTime;
}
二、数据库Jpa操作对象实现
@Repository
public interface ResourceUsageRepository extends JpaRepository<ResourceUsageRecordEntity, String>,
JpaSpecificationExecutor<ResourceUsageRecordEntity> {
@Transactional
@Modifying
@Query("delete from ResourceUsageRecordEntity rs where rs.resourceId in (:resourceIds)")
Integer deleteByResourceIds(@Param("resourceIds") List<String> resourceIds);
}
三、自定义增加和删除使用情况记录注解
使用情况增加注解
/**
* 运行时可见
* 方法注解, 类注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AddRecord {
/**
* 资源类型,ent, building, region
* @return
*/
String resourceType() default "ent";
/**
* 当前操作业务, 参考OperateTypeEnum
* @return
*/
String operateBusiness();
}
使用情况删除注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DeleteRecord {
/**
* 包含有entId或buildingId的数据库操作对象名, 如:controlRoomConfigRepository
* @return
*/
String repositoryName();
}
增加和删除使用情况的AOP代理实现类
@Aspect
@Slf4j
@Component
public class AddRecordAspect {
@Autowired
private ResourceUsageRepository resourceUsageRepository;
@Pointcut("@annotation(AddRecord)")
public void pointCut() {}
@Around(value = "pointCut()")
public Object aroundAddRecord(ProceedingJoinPoint joinPoint) throws Throwable {
// 1.方法执行前的处理,相当于前置通知
// 获取方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取方法
Method method = signature.getMethod();
// 获取方法上面的注解
AddRecord recordAnno = method.getAnnotation(AddRecord.class);
//获取资源类型
String resourceType = recordAnno.resourceType();
// 获取操作业务类型
String operateBusiness = recordAnno.operateBusiness();
String[] parameterNames = signature.getParameterNames();
String resourceId = "";
if (null != parameterNames && parameterNames.length >0){
for (int i = 0; i < parameterNames.length; i++) {
String value = joinPoint.getArgs()[i] != null ? JSON.toJSONString(joinPoint.getArgs()[i]) : Strings.EMPTY;
if (StringUtils.isNotEmpty(value)) {
resourceId = this.findResourceId(value);
}
}
}
Object result = new Object();
ResourceUsageRecordEntity entity = new ResourceUsageRecordEntity();
if(StringUtils.isNotBlank(resourceId)){
entity.setResourceId(resourceId);
}else {
return result;
}
entity.setId(UUIDUtil.generateId());
entity.setResourceType(resourceType);
entity.setOperateBusiness(operateBusiness);
try {
//让代理方法执行
result = joinPoint.proceed();
// 2.相当于后置通知(方法成功执行之后走这里)
entity.setOperateResult(0);// 设置操作结果
} catch (Exception e) {
// 3.相当于异常通知部分
entity.setOperateResult(-1);// 设置操作结果异常
log.error(SystemLogUtil.logWithErrorCodeAndParams(CommonApiErrorCodeEnum.SERVICE_ERROR.getErrorCode(),
"aroundAddRecord.error", "operateBusiness", "resourceId"), operateBusiness,
resourceId);
}
// 4.相当于最终通知
entity.setCreateTime(new Date());// 设置操作日期
resourceUsageRepository.save(entity);// 添加资源使用记录
return result;
}
private String findResourceId(String value){
String resourceId = StringUtils.EMPTY;
if(StringUtils.isBlank(value)){
return resourceId;
}
JSONObject jsonObject = JSONObject.parseObject(value);
String entId = (String)jsonObject.get("entId");
String enterpriseId = (String)jsonObject.get("enterpriseId");
String buildingId = (String)jsonObject.get("buildingId");
if(StringUtils.isNotBlank(entId)){
resourceId = entId;
}else if(StringUtils.isNotBlank(enterpriseId)){
resourceId = enterpriseId;
} else if(StringUtils.isNotBlank(buildingId)){
resourceId = buildingId;
}
return resourceId;
}
}
@Aspect
@Slf4j
@Component
public class DeleteRecordAspect {
@Autowired
private ResourceUsageRepository resourceUsageRepository;
@Pointcut("@annotation(DeleteRecord)")
public void pointCut() {}
@Around(value = "pointCut()")
public Object aroundDeleteRecord(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = new Object();
// 1.方法执行前的处理,相当于前置通知
// 获取方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取方法
Method method = signature.getMethod();
// 获取方法上面的注解
DeleteRecord recordAnno = method.getAnnotation(DeleteRecord.class);
//获取jpa repositoryName
String repositoryName = recordAnno.repositoryName();
Object bean = SpringBeanContext.getBean(repositoryName);
JpaRepository jpaRepository = (JpaRepository)bean;
String[] parameterNames = signature.getParameterNames();
List<String> deleteIds = Lists.newArrayList();
if (null != parameterNames && parameterNames.length >0){
for (int i = 0; i < parameterNames.length; i++) {
String value = joinPoint.getArgs()[i] != null ? JSON.toJSONString(joinPoint.getArgs()[i]) : Strings.EMPTY;
if (StringUtils.isNotEmpty(value)) {
deleteIds = JSONObject.parseArray(value, String.class);
}
}
}
List<Object> deleteObjects = jpaRepository.findAllById(deleteIds);
if(CollectionUtils.isEmpty(deleteObjects)){
return result;
}
List<String> deleteResourceIds = deleteObjects.stream().map(this::findResourceId).collect(Collectors.toList());
if(CollectionUtils.isEmpty(deleteResourceIds)){
return result;
}
try {
//让代理方法执行
result = joinPoint.proceed();
// 2.相当于后置通知(方法成功执行之后走这里)
} catch (Exception e) {
// 3.相当于异常通知部分
log.error(SystemLogUtil.logWithErrorCodeAndParams(CommonApiErrorCodeEnum.SERVICE_ERROR.getErrorCode(),
"aroundDeleteRecord.error", "repositoryName", "deleteIds"), repositoryName,
JSON.toJSONString(deleteIds));
}
// 4.相当于最终通知
resourceUsageRepository.deleteByResourceIds(deleteResourceIds);// 添加资源使用记录
return result;
}
private String findResourceId(Object value){
String resourceId = StringUtils.EMPTY;
if(ObjectUtils.isEmpty(value)){
return resourceId;
}
JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(value));
String entId = (String)jsonObject.get("entId");
String enterpriseId = (String)jsonObject.get("enterpriseId");
String buildingId = (String)jsonObject.get("buildingId");
if(StringUtils.isNotBlank(entId)){
resourceId = entId;
}else if(StringUtils.isNotBlank(enterpriseId)){
resourceId = enterpriseId;
} else if(StringUtils.isNotBlank(buildingId)){
resourceId = buildingId;
}
return resourceId;
}
}
四、测试结果
使用示例
@Override
@Transactional
@AddRecord(resourceType = "ent", operateBusiness = OperateTypeEnum.CONTROLROOM_CODE)
public ControlRoomConfigVO addControlRoom(ControlRoomConfigAddQuery query) {
ControlRoomConfigEntity configEntity = mapperFacade.map(query, ControlRoomConfigEntity.class);
// ...
}
测试结果