AOP+自定義注解+策略模式 記錄操作日志,避免過多的if else

演示案例代碼

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

image.png

如圖所示,就是將用戶的操作行為記錄到日志表中,而且有些內(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注解只是定義出來了,還沒有正式的用上,下面該輪到@MessageLogaop上場了。

定義切面

  • 使用@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 測試賬號添加:


image.png

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

image.png

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

image.png

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

到這里就結(jié)束了,是不是避免很多的 if else,如果有更好的方式歡迎探討。

演示案例代碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容