目录
背景
由于项目有很多场景,类似点击时间控制、网络判断、文件上传等,所以记录一下
AOP实现上传
首先定义一个日志实体:
public class FileUploadLog {
private Integer id;
// 唯一编码
private String batchNo;
// 上传到文件服务器的文件key
private String key;
// 错误日志文件名
private String fileName;
//上传状态
private Integer status;
//上传人
private String createName;
//上传类型
private String uploadType;
//结束时间
private Date endTime;
// 开始时间
private Date startTime;
}
然后定义一个上传的类型枚举,用于记录是哪里操作的:
public enum UploadType {
未知(1,"未知"),
类型2(2,"类型2"),
类型1(3,"类型1");
private int code;
private String desc;
private static Map<Integer, UploadType> map = new HashMap<>();
static {
for (UploadType value : UploadType.values()) {
map.put(value.code, value);
}
}
UploadType(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
public static UploadType getByCode(Integer code) {
return map.get(code);
}
}
最后,定义一个注解,用于标识切点:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Upload {
// 记录上传类型
UploadType type() default UploadType.未知;
}
然后,编写切面:
@Component
@Aspect
@Slf4j
public class UploadAspect {
public static ThreadFactory commonThreadFactory = new ThreadFactoryBuilder().setNameFormat("upload-pool-%d")
.setPriority(Thread.NORM_PRIORITY).build();
public static ExecutorService uploadExecuteService = new ThreadPoolExecutor(10, 20, 300L,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024), commonThreadFactory, new ThreadPoolExecutor.AbortPolicy());
@Pointcut("@annotation(com.aaa.bbb.Upload)")
public void uploadPoint() {}
@Around(value = "uploadPoint()")
public Object uploadControl(ProceedingJoinPoint pjp) {
// 获取方法上的注解,进而获取uploadType
MethodSignature signature = (MethodSignature)pjp.getSignature();
Upload annotation = signature.getMethod().getAnnotation(Upload.class);
UploadType type = annotation == null ? UploadType.未知 : annotation.type();
// 获取batchNo
String batchNo = UUID.randomUUID().toString().replace("-", "");
// 初始化一条上传的日志,记录开始时间
writeLogToDB(batchNo, type, new Date)
// 线程池启动异步线程,开始执行上传的逻辑,pjp.proceed()就是你实现的上传功能
uploadExecuteService.submit(() -> {
try {
String errorMessage = pjp.proceed();
// 没有异常直接成功
if (StringUtils.isEmpty(errorMessage)) {
// 成功,写入数据库,具体不展开了
writeSuccessToDB(batchNo);
} else {
// 失败,因为返回了校验信息
fail(errorMessage, batchNo);
}
} catch (Throwable e) {
LOGGER.error("导入失败:", e);
// 失败,抛了异常,需要记录
fail(e.toString(), batchNo);
}
});
return new Object();
}
private void fail(String message, String batchNo) {
// 生成上传错误日志文件的文件key
String s3Key = UUID.randomUUID().toString().replace("-", "");
// 生成文件名称
String fileName = "错误日志_" +
DateUtil.dateToString(new Date(), "yyyy年MM月dd日HH时mm分ss秒") + ExportConstant.txtSuffix;
String filePath = "/home/xxx/xxx/" + fileName;
// 生成一个文件,写入错误数据
File file = new File(filePath);
OutputStream outputStream = null;
try {
outputStream = new FileOutputStream(file);
outputStream.write(message.getBytes());
} catch (Exception e) {
LOGGER.error("写入文件错误", e);
} finally {
try {
if (outputStream != null)
outputStream.close();
} catch (Exception e) {
LOGGER.error("关闭错误", e);
}
}
// 上传错误日志文件到文件服务器,我们用的是s3
upFileToS3(file, s3Key);
// 记录上传失败,同时记录错误日志文件地址到数据库,方便用户查看错误信息
writeFailToDB(batchNo, s3Key, fileName);
// 删除文件,防止硬盘爆炸
deleteFile(file)
}
}
至此整个异步上传功能就完成了,是不是很简单?(笑)
那么怎么使用呢?更简单,只需要在 service 层加入注解即可,顶多就是把错误信息 return 出去。
@Upload(type = UploadType.类型1)
public String upload(List<ClassOne> items) {
if (items == null || items.size() == 0) {
return;
}
//校验
String error = uploadCheck(items);
if (StringUtils.isNotEmpty) {
return error;
}
//删除旧的
deleteAll();
//插入新的
batchInsert(items);
}
AOP实现点击间隔
依赖导入
// AOP 插件库:https://mvnrepository.com/artifact/org.aspectj/aspectjrt
api 'org.aspectj:aspectjrt:1.9.6'
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SingleClick {
/**
* 快速点击的间隔
*/
long value() default 2000;
}
@Aspect
public class SingleClickAspect {
/** 最近一次点击的时间 */
private long mLastTime;
/** 最近一次点击的标记 */
private String mLastTag;
/**
* 方法切入点
*/
@Pointcut("execution(@com.maxvision.fyj.common.aop.SingleClick * *(..))")
public void method() {}
/**
* 在连接点进行方法替换
*/
@Around("method() && @annotation(singleClick)")
public void aroundJoinPoint(ProceedingJoinPoint joinPoint, SingleClick singleClick) throws Throwable {
CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
// 方法所在类
String className = codeSignature.getDeclaringType().getName();
// 方法名
String methodName = codeSignature.getName();
// 构建方法 TAG
StringBuilder builder = new StringBuilder(className + "." + methodName);
builder.append("(");
Object[] parameterValues = joinPoint.getArgs();
for (int i = 0; i < parameterValues.length; i++) {
Object arg = parameterValues[i];
if (i == 0) {
builder.append(arg);
} else {
builder.append(", ")
.append(arg);
}
}
builder.append(")");
String tag = builder.toString();
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis - mLastTime < singleClick.value() && tag.equals(mLastTag)) {
Timber.tag("SingleClick");
Timber.i("%s 毫秒内发生快速点击:%s", singleClick.value(), tag);
return;
}
mLastTime = currentTimeMillis;
mLastTag = tag;
// 执行原方法
joinPoint.proceed();
}
}
AOP实现网络判断
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CheckNet {}
@Aspect
public class CheckNetAspect {
/**
* 方法切入点
*/
@Pointcut("execution(@com.maxvision.fyj.common.aop.CheckNet * *(..))")
public void method() {}
/**
* 在连接点进行方法替换
*/
@Around("method() && @annotation(checkNet)")
public void aroundJoinPoint(ProceedingJoinPoint joinPoint, CheckNet checkNet) throws Throwable {
Application application = ActivityManager.getInstance().getApplication();
if (application != null) {
ConnectivityManager manager = ContextCompat.getSystemService(application, ConnectivityManager.class);
if (manager != null) {
NetworkInfo info = manager.getActiveNetworkInfo();
// 判断网络是否连接
if (info == null || !info.isConnected()) {
ToastUtils.show(R.string.common_network_error);
return;
}
}
}
//执行原方法
joinPoint.proceed();
}
结语
在Android开发中,aop的使用场景有很多,上面只是列举出一些例子,后面会不断完善,也欢迎小伙伴们留言,会第一时间解答