美文网首页
AOP拦截自调用方法

AOP拦截自调用方法

作者: 晚歌歌 | 来源:发表于2019-10-22 11:28 被阅读0次

    背景

    因公司一后台管理系统现可能由非技术人员进行操作,为防止错误操作或出问题时找不到背锅人的情况,现计划在原有框架下加入日志切面。

    代码

    CREATE TABLE `sys_log` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `username` varchar(50) DEFAULT '' COMMENT '用户名',
      `operation` varchar(50) DEFAULT '' COMMENT '用户操作',
      `method` varchar(200) DEFAULT '' COMMENT '请求方法',
      `params` varchar(2000) DEFAULT '' COMMENT '请求参数',
      `exception` varchar(1000) DEFAULT '' COMMENT '异常信息',
      `ip` varchar(64) DEFAULT '' COMMENT 'IP地址',
      `create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
      PRIMARY KEY (`id`),
      KEY `idx_cd_op` (`create_date`,`operation`)
    ) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8 COMMENT='系统后台操作日志'
    

    实体

    public class SysLog implements Serializable {
        private Long id;
    
        private String username;
    
        private String operation;
    
        private String method;
    
        private String params;
    
        private String exception;
    
        private String ip;
    
        private Date createDate;
    

    注解

    @Documented
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Log {
        LogOperationEnum value();
    }
    

    枚举

    public enum LogOperationEnum {
    
        UPDATE_ORDER_INFO("更新订单信息"),
        ADD_SYS_CTRL("新增系统配置"),
        UPDATE_SYS_CTRL("更新系统配置"),
        UPDATE_SYS_LOAN_NUM("更新资方账期"),
        ADD_SIGH_CONF("新增签约配置"),
        UPDATE_SIGH_CONF("更新签约配置"),
        ADD_COPYWRITING_CONF("新增文案配置"),
        UPDATE_COPYWRITING_CONF("更新文案配置"),
        ADD_COMPANY_INFO("新增公司信息"),
        UPDATE_COMPANY_INFO("更新公司信息"),
        ;
    
        public final String desc;
    
        LogOperationEnum(String desc) {
            this.desc = desc;
        }
    
        public String getDesc(){
            return desc;
        }
    }
    

    切面

    @Aspect
    @Component
    public class LogAspect {
    
        public static final int PARAMS_MAX_LENGTH = 2000;
        public static final int EXCEPTION_MAX_LENGTH = 1000;
    
        @Resource
        private ISysLogService sysLogService;
    
        @Pointcut("@annotation(com.q.pay.order.admin.annotation.Log)")
        public void logPointCut() {
        }
    
        @Around("logPointCut()")
        public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    
            Object result;
            SysLog sysLog = new SysLog();
    
            //操作名
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            Log LogAnnotation = method.getAnnotation(Log.class);
            sysLog.setOperation(LogAnnotation.value().desc);
    
            //方法名
            String className = joinPoint.getTarget().getClass().getSimpleName();
            String methodName = signature.getName();
            sysLog.setMethod(className + "." + methodName);
    
            //参数
            Object[] args = joinPoint.getArgs();
            if (ObjectUtil.isNotEmpty(args[0])) {
                String params = JsonUtil.toJSONString(args[0]);
                int paramsLength = params.length();
                int maxLength = paramsLength > PARAMS_MAX_LENGTH ? PARAMS_MAX_LENGTH : paramsLength;
                sysLog.setParams(params.substring(0, maxLength));
            }
    
            //操作人
            if (ObjectUtil.isNotEmpty(args[1])) {
                sysLog.setUsername(args[1].toString());
            }
    
            //IP地址
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            sysLog.setIp(IPUtils.getIpAddr(request));
    
            try {
                result = joinPoint.proceed();
            } catch (Throwable e) {
                //异常信息
                String exception = e.toString();
                int exceptionLength = exception.length();
                int maxLength = exceptionLength > EXCEPTION_MAX_LENGTH ? EXCEPTION_MAX_LENGTH : exceptionLength;
                sysLog.setException(exception.substring(0, maxLength));
    
                //异步插入操作日志
                sysLogService.asyncInsertSelective(sysLog);
                throw e;
            }
    
            //异步插入操作日志
            sysLogService.asyncInsertSelective(sysLog);
            return result;
        }
    
    }
    

    IPUtils

    public class IPUtils {
        private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
    
        /**
         * 获取IP地址
         * 
         * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
         * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
         */
        public static String getIpAddr(HttpServletRequest request) {
    
            String ip = request.getHeader("x-forwarded-for");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
            return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
        }
    
    }
    

    接着在对应Controller中加入响应的@Log注解即可。
    但由于原有Controller框架原有导致日志拦截失效。

    切面失效原因

    首先有一个抽象基类BaseController

    public abstract class BaseController<PO, VO> {
    
        /**
         * 查询列表
         */
        @RequestMapping("/query")
        @ResponseBody
        public Object list(@Validated VO vo,
                           @RequestParam(value = "pageIndex", required = false) Integer pageIndex,
                           @RequestParam(value = "pageSize", required = false) Integer pageSize, BindingResult bindingResult) throws Exception {
            BindingResultUtil.judgeParam(bindingResult);
            PO config = buildQuery(vo);
            Page page = query(config, pageIndex, pageSize);
            return ResponseUtil.buildSuccess(page);
        }
    
        /**
         * 更新
         */
        @RequestMapping(value = "/update", method = RequestMethod.POST)
        @ResponseBody
        public Object updateInfo(String loginUserName, @Validated(value = {Update.class}) VO vo, BindingResult bindingResult) throws Exception {
            BindingResultUtil.judgeParam(bindingResult);
            PO po = buildQuery(vo);
            return update(po, loginUserName) > 0 ? ResponseUtil.buildSuccess() : ResponseUtil.buildError();
        }
    
        /**
         * 新增
         */
        @RequestMapping(value = "/add", method = RequestMethod.POST)
        @ResponseBody
        public Object addInfo(String loginUserName, @Validated(value = {Save.class}) VO vo, BindingResult bindingResult) throws Exception {
            BindingResultUtil.judgeParam(bindingResult);
            PO po = buildQuery(vo);
            return add(po, loginUserName) > 0 ? ResponseUtil.buildSuccess() : ResponseUtil.buildError();
        }
    
    
        protected abstract PO buildQuery(VO vo);
    
        protected abstract Page query(PO po, Integer pageIndex, Integer pageSize);
    
        public abstract Integer update(PO po, String loginUserName) throws Exception;
    
        public abstract Integer add(PO po, String loginUserName) throws Exception;
    }
    

    而子类Controller大致如下:

    @Controller
    @RequestMapping(path = "/companyInfo")
    public class CompanyInfoController extends BaseController<CompanyInfo, CompanyInfoVO> {
    
        @Resource
        private ICompanyInfoService companyInfoService;
    
        @Log(LogOperationEnum.ADD_COMPANY_INFO)
        @Override
        public Integer add(CompanyInfo po, String loginUserName) {
            companyInfoService.insert(po);
            return 1;
        }
    
        @Log(LogOperationEnum.UPDATE_COMPANY_INFO)
        @Override
        public Integer update(CompanyInfo po, String loginUserName) {
            companyInfoService.update(po);
            return 1;
        }
    
        @Override
        protected Page query(CompanyInfo po, Integer pageIndex, Integer pageSize) {
            return companyInfoService.query(po, pageIndex, pageSize);
        }
    
        @Override
        protected CompanyInfo buildQuery(CompanyInfoVO vo) {
            return new CompanyInfo()
                    .setId(vo.getId())
                    .setCompanyId(vo.getCompanyId())
                    .setCompanyName(vo.getCompanyName())
                    .setAdvanceRepayment(vo.getAdvanceRepayment())
                    .setCompanyStatus(vo.getCompanyStatus())
                    .setCreateDate(vo.getCreateDate())
                    .setUpdateDate(vo.getUpdateDate())
                    .setExt1(vo.getExt1())
                    .setExt2(vo.getExt2())
                    .setExt3(vo.getExt3());
        }
    }
    

    一开始不知道拦截为啥失效,做了非常多尝试,本来已经打算把注解打在Service层了,后来发现是方法自调用导致的失效。
    因为子类加了日志注解并重写的update和add方法是在BaseController中的updateInfo和addInfo中调用,这样事实上就出现了同一个类中方法的自我调用,这跟Spring事务、缓存、异步注解是同样道理,AOP默认同一个类中方法的自我调用是不会生效的,因为此时调用的是原目标对象方法而不是代理对象方法。

    这是由于 Spring AOP (包括动态代理和 CGLIB 的 AOP) 的限制导致的. Spring AOP 并不是扩展了一个类(目标对象), 而是使用了一个代理对象来包装目标对象, 并拦截目标对象的方法调用. 这样的实现带来的影响是: 在目标对象中调用自己类内部实现的方法时, 这些调用并不会转发到代理对象中, 甚至代理对象都不知道有此调用的存在.

    image.png
    因此此处的add和update代表的是this对象,而不是代理对象
    可以参考以下生成的反编译代理类,代理方法中的自调用方法是不会再次被代理的
    https://blog.csdn.net/luzhensmart/article/details/84866599

    解决方案

    1、开启expose-proxy
    spring中expose-proxy的作用与原理
    作用:将代理对象暴露出来,可以使用使用AopContext.currentProxy()获取当前代理

    <aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>
    

    2、修改自调用方法为代理方法

    ((BaseController)AopContext.currentProxy()).update
    
    image.png

    相关文章

      网友评论

          本文标题:AOP拦截自调用方法

          本文链接:https://www.haomeiwen.com/subject/pjtqvctx.html