首先看下業(yè)務(wù)需求:

如圖所示,就是將用戶的操作行為記錄到日志表中,而且有些內(nèi)容是動(dòng)態(tài)的,如圖是六種操作,但是后期肯定是要增加的,也就是說就這六種需要記錄到日志表中,日志表所對應(yīng)的對象是
AnalysisMessage,標(biāo)題,內(nèi)存,接收時(shí)間,對應(yīng)對象中字段名為title,content,create_time;看到需求的第一眼就想到AOP來實(shí)現(xiàn);但是只是使用AOP的會寫一些if else,后期如果要增加一些是不是要增加if else,后期維護(hù)起來是相當(dāng)麻煩,我就想到了使用策略模式;
這里有個(gè)前提就是必須這些操作是成功之后才可以入庫,所以我選擇了使用@Around,在切入點(diǎn)前后切入內(nèi)容;因?yàn)檫@樣可以在調(diào)用接口結(jié)束之后拿到接口返回的參數(shù),從而判斷接口是否調(diào)用成功;下面就用代碼來實(shí)現(xiàn)一下
首先我會將這個(gè)操作以key value 的形式存放到j(luò)son文件中,以url 為key,操作title為value;
在項(xiàng)目的resources目錄下有個(gè)test.json文件,內(nèi)容如下:
{
"/user/addUser": "賬號添加",
"/user/isLock": "賬號禁用",
"/user/delUser": "賬號刪除"
}
這里只列舉三種操作,之所以存放到j(luò)son文件中,目的是想讓本案例更簡化,不想涉及到數(shù)據(jù)庫,所以這個(gè)演示案例只放到了json文件中;在生成環(huán)境中是配置在數(shù)據(jù)庫中的;
在項(xiàng)目啟動(dòng)的時(shí)候?qū)son文件中的內(nèi)容以key value 的形式加載到map中;
代碼實(shí)現(xiàn)如下:
@Component
@Slf4j
public class MessageInitHandler implements CommandLineRunner {
/**
* 文件名
*/
private static String fileName = "test.json";
/**
* 初始化 Map
*/
private Map<String, String> initMap = Maps.newHashMap();
@Override
public void run(String... args) throws Exception {
JSONObject jsonObject = loadingJSONFile();
log.info("json Data are as follows:{}", jsonObject);
//使用Gson將json轉(zhuǎn)成map
Gson gson = new Gson();
initMap = gson.fromJson(jsonObject.toJSONString(), Map.class);
//將initMap賦值給MapCacheUtils.mapCaheInit
MapCacheUtils.mapCaheInit = initMap;
}
private JSONObject loadingJSONFile() {
log.info("開始加載resources/test.json");
Enumeration<URL> resources;
JSONObject jsonObject = new JSONObject();
try {
resources = getClassLoader().getResources(fileName);
} catch (IOException e) {
log.warn("getJsonResource fail {}", fileName, e);
return jsonObject;
}
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
try {
String json = Resources.toString(url, Charsets.UTF_8);
jsonObject.putAll(JSON.parseObject(json)); // 有多個(gè)的時(shí)候,后面的覆蓋前面的
} catch (IOException e) {
log.warn("addJsonFile fail url:{}", url, e);
}
}
return jsonObject;
}
private static ClassLoader getClassLoader() {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader != null) {
return classLoader;
}
return MessageInitHandler.class.getClassLoader();
}
}
MessageIntiHandler實(shí)現(xiàn)CommandLineRuner,并實(shí)現(xiàn)run方法;
這樣MapCacheUtils.mapCaheInit中就有三條數(shù)據(jù);初始化工作完成;
然后自定義一個(gè)注解@MessageLog,標(biāo)注在需要aop攔截的接口上;也就是上圖中需要存庫的操作接口上;例如 賬號添加,賬號刪除...等
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MessageLog {
}
然后編寫接口:包括賬號添加,賬號刪除,等。。接口,并標(biāo)識MessageLog注解,
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private AnalysisUserService analysisUserService;
/**
* 添加用戶
*/
@RequestMapping("addUser")
@MessageLog
public String addUser(@RequestParam("roleId") Long roleId, AnalysisUser user){
return analysisUserService.addUser(roleId,user);
}
/**
* 刪除用戶
* 需要參數(shù):userid username realname
* 因?yàn)檫@里把用戶刪除之后在策略類中就查詢不到該用戶的信息
*/
@RequestMapping("delUser")
@MessageLog
public String delUser(@RequestBody AnalysisUser analysisUser){
return analysisUserService.delUser(analysisUser.getId());
}
/**
* 鎖定用戶這里就不模擬了。。。
*/
}
然后編寫策略類
首先 定義個(gè)StrategyBase接口
//策略父類
public interface StrategyBase {
String run(Object[] args);
}
接下來就是StrategyBase的子類:
賬號添加策略
@Component(value="/user/addUser")
public class MessageAddUserStrategy implements StrategyBase {
@Autowired
private AnalysisRoleService analysisRoleService;
@Override
public String run(Object[] args) {
Long roleId = null;
AnalysisUser analysisUser = null;
for (Object arg : args) {
if (arg instanceof Long) {
roleId = (Long) arg;
} else if (arg instanceof AnalysisUser) {
analysisUser = (AnalysisUser) arg;
} else {
return null;
}
}
String username = analysisUser.getUsername();
AnalysisRole role = analysisRoleService.findByRoleId(roleId);
return "添加了賬號" + username + "(" + role.getName() + ")";
}
}
賬號刪除策略
@Component("/user/delUser")
public class MessageDelUserStrategy implements StrategyBase {
@Override
public String run(Object[] args) {
for (Object arg : args) {
if (arg instanceof AnalysisUser) {
AnalysisUser analysisUser = (AnalysisUser) arg;
return "刪除了賬號" + analysisUser.getUsername() + "(" + analysisUser.getRealname() + ")";
}
}
return null;
}
}
禁用賬號策略
@Component("/user/isLock")
public class MessageIsLockStrategy implements StrategyBase {
@Autowired
private AnalysisUserService analysisUserService;
@Override
public String run(Object[] args) {
AnalysisUser analysisUser = null;
for (Object arg : args) {
if (arg instanceof AnalysisUser) {
analysisUser = (AnalysisUser) arg;
if (analysisUser.getStatus().equals((byte) 1)) {
analysisUser = analysisUserService.selectById(analysisUser.getId());
return "啟用了賬號" + analysisUser.getUsername() + " (" + analysisUser.getRealname() + ")";
} else if (analysisUser.getStatus().equals((byte) 2)) {
analysisUser = analysisUserService.selectById(analysisUser.getId());
return "禁用了賬號" + analysisUser.getUsername() + " (" + analysisUser.getRealname() + ")";
}
}
}
return null;
}
}
這里稍微的注意一下@Component注解中的value值,
策略控制器
@Component
public class DataSourceContextAware {
@Autowired
private final Map<String, StrategyBase> strategyMap = new ConcurrentHashMap<>(3);
public StrategyBase getStrategyInstance(String dsType) {
StrategyBase strategyBase = strategyMap.get(dsType);
return strategyBase;
}
}
這里定義一個(gè)ConcurrentHashMap,這個(gè)類的作用就是將策略名(@Component注解中的value值),和實(shí)現(xiàn)StrategyBase的類,以key,value的形式保存到了ConcurrentHashMap中;
到了這里@MessageLog注解只是定義出來了,還沒有正式的用上,下面該輪到@MessageLog和aop上場了。
定義切面
- 使用
@Aspect注解將一個(gè)java類定義為切面類 - 使用
@Pointcut定義一個(gè)切入點(diǎn),可以是一個(gè)規(guī)則表達(dá)式,比如某個(gè)package下的所有函數(shù),也可以是一個(gè)注解等。 - 根據(jù)需要在切入點(diǎn)不同位置的切入內(nèi)容
- 使用@Before在切入點(diǎn)開始處切入內(nèi)容
- 使用@After在切入點(diǎn)結(jié)尾處切入內(nèi)容
- 使用@AfterReturning在切入點(diǎn)return內(nèi)容之后切入內(nèi)容(可以用來對處理返回值做一些加工處理)
- 使用@Around在切入點(diǎn)前后切入內(nèi)容,并自己控制何時(shí)執(zhí)行切入點(diǎn)自身的內(nèi)容
- 使用@AfterThrowing用來處理當(dāng)切入內(nèi)容部分拋出異常之后的處理邏輯
上面也說到了,就是必須這些操作是成功之后才可以入庫,所以我選擇了使用@Around,在切入點(diǎn)前后切入內(nèi)容;因?yàn)檫@樣可以在調(diào)用接口結(jié)束之后拿到接口返回的參數(shù),從而判斷接口是否調(diào)用成功;
@Aspect
@Component
public class MessageMonitorHandler {
private Logger logger = LoggerFactory.getLogger(MessageMonitorHandler.class);
@Autowired
private AnalysisMessageService messageService;
@Autowired
private MessageStrategyService messageStrategyService;
@Pointcut("@annotation(cn.haoxy.strategy.aop.annotation.MessageLog)")
public void checkMessageHandler() {
}
@Around("checkMessageHandler()")
public void doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
logger.info("start run doAround.....");
Object obj = proceedingJoinPoint.proceed();//調(diào)用執(zhí)行目標(biāo)方法
//判斷調(diào)用是否成功
//省略判斷 ......
//如果調(diào)用成功
processOutPutObj(proceedingJoinPoint);
}
private void processOutPutObj(ProceedingJoinPoint proceedingJoinPoint) {
Object[] args = proceedingJoinPoint.getArgs();
//得到HttpServletRequest
HttpServletRequest request = getHttpServletRequest();
//得到請求url
String url = request.getServletPath();
//根據(jù)url從MapCacheUtils.mapCaheInit中取出操作title,
// 這里是從test.json文件中讀取的,當(dāng)然也可以配置在數(shù)據(jù)庫中
String operatorLog = MapCacheUtils.mapCaheInit.get(url);
//根據(jù)url取出對應(yīng)的策略類,這里的url也就是和策略類上@Component注解的value值
StrategyBase messageChild = messageStrategyService.run(url);
//拿到策略類執(zhí)行相應(yīng)的策略方法
String content = messageChild.run(args);
AnalysisMessage analysisMessage = new AnalysisMessage();
analysisMessage.setId(1L);
analysisMessage.setTitle(operatorLog);
analysisMessage.setContent(content);
//在這里模擬存庫
messageService.insert(analysisMessage);
logger.info(" end run doAround....." + content);
}
/**
* 獲取 HttpServletRequest
*/
private HttpServletRequest getHttpServletRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
return servletRequestAttributes.getRequest();
}
}
這里@Pointcut是使用注解的方式;在檢測接口上含有@MessageLog注解時(shí)就會被AOP攔截;
這里貼一下MessageStrategyService類:
@Component
public class MessageStrategyService {
@Autowired
private DataSourceContextAware dataSourceContextAware;
public StrategyBase run(String dsType) {
//這里調(diào)用策略控制器中的getStrategyInstance方法,來獲取對應(yīng)的策略類
StrategyBase strategyInstance = dataSourceContextAware.getStrategyInstance(dsType);
return strategyInstance;
}
}
下面進(jìn)行測試
使用postman 測試賬號添加:

賬號添加測試結(jié)果打?。?/p>

賬號刪除測試結(jié)果打?。?/p>

打印content結(jié)果缺少主語(當(dāng)前登陸的用戶),需求是:xxx添加了賬號xxx(角色名);這里有很多種方式可以拿到當(dāng)前用戶,如果使用token的話,可以從token中解析出當(dāng)前用戶的id,我這里使用的是shrio,從而也很方便的拿到當(dāng)前用戶,為了減少本演示案例的復(fù)雜度就沒去引入,這里只突出主要部分;
到這里就結(jié)束了,是不是避免很多的 if else,如果有更好的方式歡迎探討。