当前位置: 代码迷 >> 综合 >> 使用Spring AOP、ThreadLocal、自定义注解完成操作日志的记录
  详细解决方案

使用Spring AOP、ThreadLocal、自定义注解完成操作日志的记录

热度:48   发布时间:2024-02-23 18:39:52.0

        之前搞过操作日志的东西,这里简单的使用Spring AOP、ThreadLocal、自定义注解来实现对于操作日志的记录,在学习技术的同时,熟悉对于日志的记录。

        一般情况下系统打印的日志分成了三种:

1:系统日志(便于研发人员调试排查问题的)。
2:追踪日志(多个组件相互调用时单纯只依赖系统日志效率低下,该日志便于追踪复杂业务的调用链)。
3:操作日志(也叫业务日志,记录一笔业务)。

         这里仅介绍操作日志,操作日志(即业务日志)是在软件运行时记录一笔业务的描述,执行结果,产生的影响。

         以下场景需要记录操作日志:

1. 系统用户对系统的业务操作。如用户添加,修改,删除信息。

2. 服务自身对(数据库的)数据产生影响的操作。如服务自身的定时任务运行,对数据进行了新增、修改、删除操作。

         记录规范操作日志的原因

1. 系统日志一般只是是给研发人员排查问题看的,不一定非要打印日志。而操作日志是记录所有平台用户和系统对平台的操作,是给平台的管理员用户看的,因此需要记录每一笔业务操作,并且要有一定可交互的、美观的UI界面。

       操作日志中一般需要这些字段:

操作者、操作环境、操作动作、被操作对象、结果、附属信息和其他

      基于以上介绍,我们可以简单的来实现自定义注解记录操作日志的代码:

     1.自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {/*** 操作业务模块标示。模块标识的多语言词条在服务封装时提供* @return*/String moduleId() default "";/*** 操作动作标识,* 表示登录、查询、新增、修改、删除、上传、部署、下载等操作。* 操作动作标识的多语言词条在组件封装时提供。* @return*/String action();/*** 操作内容详情,* 在其他所有字段无法满足用户操作内容描述的情况下能通过本项尽可能准确、详细的描述用户的操作内容。* 支持多语言,写操作参数,采用“,”号分隔;不支持多语言,写操作内容* @return*/String actionDetail() default "";/*** actionDetail支持多语言时,写操作内容详情标识,标识的多语言词条在组件封装时提供。* 标识的多语言值支持占位符,采用%1,%2,%n形式,n表示第n个参数;不支持多语言,留空* @return*/String actionMessageId() default "";/*** 终端类型* @return*/String terminalType() default BusLogConstants.TERMINAL_TYPE_WEB;/*** 对象类型* @return*/String objectType() default "";}

       2.业务日志服务类

public interface ILogHelper {/*** 获取当前远程ID信息* @return*/String getRemoteIp();/*** 获取当前操作人员* @return*/String getUserId();/*** 当前操作人员* @return*/default String getUserName(){return "";}/*** 获取当前操作的mac地址* @return*/String getMac();/*** 获取serviceId* @return*/String getServiceId();/*** 获取组件ID* @return*/String getComponentId();
}

      该接口主要用于获取操作者的ip、用户名、组件id等信息。

   3. 业务日志接口实现

@Service("logHelper")
public class LogHelperServiceImpl implements ILogHelper {@Autowiredprivate IAgentService agentService;@Overridepublic String getRemoteIp() {HttpServletRequest request = RequestContextUtil.getRequest();if (request == null) {return "localhost";}return RequestContextUtil.getRemoteIp(request);}/*** 操作者为用户时,填写用户名;* 操作者为系统内部任务时,填写执行任务的服务实例名,格式为“组件标识.段标识.实例序号”** @return*/@Overridepublic String getUserId() {String userIndexCode = RequestContextUtil.getUserId();if (StringUtils.isEmpty(userIndexCode)) {return agentService.getTaskUserId();}return userIndexCode;}/*** 操作者为用户时,填写用户对于人员姓名;* 操作者为系统内部任务时,为空** @return userName*/@Overridepublic String getUserName() {//单点登录后的session中无该字段,这里默认为空,如有需要,组件自己查询用户信息return null;}@Overridepublic String getMac() {return null;}@Overridepublic String getComponentId() {return agentService.getComponentId();}
}

       可以看到在获取操作信息时,是从RequestContextUtil和RequestSourceUtils中去获取的。

4.  请求上下文工具类

/*** 请求上下文工具类,需要配置filter使用* @author gonghao*/
public class RequestContextUtil {private static Logger log = LoggerFactory.getLogger(RequestContextUtil.class);private static Locale  DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;private static ThreadLocal<HttpServletRequest> request = new ThreadLocal<>();private static ThreadLocal<HttpServletResponse> response = new ThreadLocal<>();private static ThreadLocal<UserSession> userSession = new ThreadLocal<>();/*** 平台启动的时候调用,当获取不到语言信息的时候,返回的默认语言* @param locale*/public static void setDefaultLocale(Locale locale){DEFAULT_LOCALE = locale;}private static List<String> SUPPORT_LOCALE = new ArrayList<>(10);/*** 平台启动的时候调用,设置支持的语言列表* @param supportLocales*/public static void setSupportLocales(List<Locale> supportLocales){if(supportLocales!=null){SUPPORT_LOCALE = supportLocales.stream().map(e->e.toString()).collect(Collectors.toList());}}public static void setUserSession(UserSession info) {RequestContextUtil.userSession.set(info);}public static UserSession getUserSession() {return RequestContextUtil.userSession.get();}public static void setRequest(HttpServletRequest request) {RequestContextUtil.request.set(request);}public static void setResponse(HttpServletResponse response) {RequestContextUtil.response.set(response);}public static HttpServletRequest getRequest() {return RequestContextUtil.request.get();}public static HttpServletResponse getResponse() {return RequestContextUtil.response.get();}/*** 获取默认的语言信息* @return*/private static Locale getDefaultLocale() {if (RequestContextUtil.getRequest() != null) {Locale locale = RequestContextUtil.getRequest().getLocale();if (locale != null && !StringUtils.isEmpty(locale.toString())) {if(!CollectionUtils.isEmpty(SUPPORT_LOCALE) && SUPPORT_LOCALE.contains(locale.toString())){return locale;}return DEFAULT_LOCALE;} else {return DEFAULT_LOCALE;}} else {return DEFAULT_LOCALE;}}/*** 获取当前用户的语言信息* @return*/public static Locale getLocale() {if (RequestContextUtil.getUserSession() == null) {log.trace("- userSession is null,getLocal return default language :SIMPLIFIED_CHINESE");return getDefaultLocale();}Locale  language = RequestContextUtil.getUserSession().getLanguage();if (language == null) {log.trace("- language is null, return default language :SIMPLIFIED_CHINESE");return getDefaultLocale();}return language;}/*** 清空threadLocal,需要再filter中的finally中调用*/public static void clear() {request.remove();response.remove();userSession.remove();}/*** 获取操作人ID* @return 操作人ID*/public static String getUserId() {HttpServletRequest request = getRequest();if (Objects.nonNull(request)) {String userId = request.getHeader(USER_ID);if (StringUtils.isNotBlank(userId)) {return userId;}}UserSession userSession = getUserSession();if (Objects.nonNull(userSession)) {return userSession.getUserId();}return EboardConstant.BLANK_STRING;}/*** 获取请求客户端地址** @param request* @return ip*/public static String getRemoteIp(HttpServletRequest request) {String ip = request.getHeader("X-Forwarded-For");if (ip != null) {if (!ip.isEmpty() && !"unKnown".equalsIgnoreCase(ip)) {int index = ip.indexOf(",");if (index != -1) {return ip.substring(0, index);} else {return ip;}}}ip = request.getHeader("X-Real-IP");if (ip != null) {if (!ip.isEmpty() && !"unKnown".equalsIgnoreCase(ip)) {return ip;}}ip = request.getHeader("Proxy-Client-IP");if (ip != null) {if (!ip.isEmpty() && !"unKnown".equalsIgnoreCase(ip)) {return ip;}}ip = request.getHeader("WL-Proxy-Client-IP");if (ip != null) {if (!ip.isEmpty() && !"unKnown".equalsIgnoreCase(ip)) {return ip;}}ip = request.getRemoteAddr();return ip.equals("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip;}}

         可以看到RequestContextUtil使用到了ThreadLocal来获取当前线程设置的值,那么想获取的话,需要先进行设置。

        UserSession类,记录用户的登陆信息。

/*** 单点登录后的用户身份信息**/
public class UserSession {/*** 用户的ID(登录名)*/private String userId;/*** 人员ID*/private String personId;/*** 客户端IP*/private String clientIp;/*** 客户端mac*/private String clientMac;/*** TGC*/private String tgc;/*** 多语言标识*/private Locale language;public String getUserId() {return userId;}public void setUserId(String userId) {this.userId = userId;}public String getPersonId() {return personId;}public void setPersonId(String personId) {this.personId = personId;}public String getClientIp() {return clientIp;}public void setClientIp(String clientIp) {this.clientIp = clientIp;}public String getClientMac() {return clientMac;}public void setClientMac(String clientMac) {this.clientMac = clientMac;}public String getTgc() {return tgc;}public void setTgc(String tgc) {this.tgc = tgc;}public Locale getLanguage() {return language;}public void setLanguage(Locale language) {this.language = language;}}

5. 过滤器 RequestContextFilter

public class RequestContextFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {try {HttpServletRequest httpRequest = (HttpServletRequest) request;RequestContextUtil.setRequest(httpRequest);RequestContextUtil.setResponse((HttpServletResponse) response);HttpSession session = httpRequest.getSession();if (session != null) {Assertion assertion = (Assertion) httpRequest.getSession().getAttribute("_const_cas_assertion_");if (assertion != null && assertion.getPrincipal() != null) {String name = assertion.getPrincipal().getName();String[] infos = name.split("&&");if (infos.length >= 6) {UserSession userSession = new UserSession();userSession.setUserId(infos[0]);userSession.setPersonId(infos[1]);userSession.setClientIp(infos[2]);userSession.setClientMac(infos[3]);userSession.setTgc(infos[4]);//处理多语言标识,多语言可能是中划线或下划线String language = infos[5];String[] arrays = language.split("-");if (arrays.length == 1) {String[] tempArray = language.split("_");if (tempArray.length > 1) {arrays = tempArray;}}if (arrays.length == 2) {userSession.setLanguage(new Locale(arrays[0], arrays[1]));} else if (arrays.length == 1) {userSession.setLanguage(new Locale(arrays[0], ""));}RequestContextUtil.setUserSession(userSession);}}}chain.doFilter(request, response);} finally {RequestContextUtil.clear();}}@Overridepublic void destroy() {}}

     将相关信息保存在RequestContextUtil里面
    1.保存当前request和response,以便在Controller和service层代码直接拿到请求和返回信息,使用方式如:

HttpServletRequest request = RequestContextUtil.getRequest()

    2.保存登录用户的session信息及多语言标识的处理,使用方式如: 

UserSession userSession = RequestContextUtil.getUserSession()
 Local userLocal = RequestContextUtil.getLocale()

6. 将过滤器添加到Spring容器中

 

  相关解决方案