系统需求分析
业务描述
用户在sso-resource工程访问我们的资源数据时,获取用户的行为日志信息,然后传递给sso-system工程,将日志信息存储到数据库.
业务架构分析
系统服务中的日志存储设计
本次设计中,系统服务负责将其它服务获取的用户行为日志写入到数据库。
Pojo逻辑实现
定义一个Log对象,用于在内存中存储用户行为日志信息,例如:
package com.jt.system.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 基于此对象封装用户行为日志?
* 谁在什么时间执行了什么操作,访问了什么方法,传递了什么参数,访问时长是多少.
*/
@Data
@TableName("tb_logs")
public class Log implements Serializable {
private static final long serialVersionUID = 3054471551801044482L;
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String operation;
private String method;
private String params;
private Long time;
private String ip;
@TableField("createdTime")
private Date createdTime;
private Integer status;
private String error;
}
Dao 逻辑实现
第一步:创建用户行为日志数据层对象,用于处理数据持久化逻辑,例如
package com.jt.system.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.system.pojo.Log;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户行为日志数据层对象
*/
@Mapper
public interface LogMapper extends BaseMapper<Log> {
}
第二步:定义单元测试,对数据层方法进行单元测试
package com.jt;
import com.jt.system.dao.LogMapper;
import com.jt.system.pojo.Log;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
@SpringBootTest
public class LogMapperTests {
@Autowired
private LogMapper logMapper;
@Test
void testInsert(){
//构建用户行为日志对象(基于此对象存储一些用户行为日志,先用假数据)
Log log=new Log();
log.setUsername("cgb2107");
log.setIp("192.168.100.200");
log.setOperation("查询资源");
log.setMethod("pkg.ResourceController.doSelect");
log.setParams("");
log.setStatus(1);
log.setTime(100L);
log.setCreatedTime(new Date());
//将日志持久化到数据库
logMapper.insert(log);
}
}
Service逻辑实现
第一步:定义日志业务接口,例如:
package com.jt.system.service;
import com.jt.system.pojo.Log;
/**
* 用户行为日志业务逻辑接口定义
*/
public interface LogService {
/**
* 保存用户行为日志.
* @param log
*/
void insertLog(Log log);
//.....
}
第二步:定义日志业务接口实现类,例如:
package com.jt.system.service.impl;
import com.jt.system.dao.LogMapper;
import com.jt.system.pojo.Log;
import com.jt.system.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogMapper logMapper;
/**
* @Async描述的方法底层会异步执行(不由web服务线程执行,
* 而是交给spring自带的线程池中的线程去执行)但是@Async注解的
* 应用有个前提,需要启动类上启动异步执行(添加@EnableAsync注解描述).
* 优点:不会长时间阻塞web服务(例如tomcat)线程
*/
@Async
@Override
public void insertLog(Log log) {
logMapper.insert(log);
}
}
Controller 逻辑实现
第一步:定义LogController对象,用于实现日志控制逻辑,例如:
package com.jt.system.controller;
import com.jt.system.pojo.Log;
import com.jt.system.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/log")
public class LogController {
@Autowired
private LogService logService;
@PostMapping
public void doInsertLog(@RequestBody Log log){
logService.insertLog(log);
}
}
在启动类加上
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync //启动异步操作
@SpringBootApplication
public class SystemApplication {
public static void main(String[] args) {
SpringApplication.run(SystemApplication.class,args);
}
}
第二步:启动服务,基于postman进行访问测试,例如:
资源服务中行为日志操作设计
业务描述
在不修改目标业务方法代码实现的基础之上,访问目标方法时,获取用户行为日志.
Pojo逻辑对象定义
定义日志对象,用户封装获取到的用户行为日志,例如:
package com.jt.resource.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 基于此对象封装用户行为日志?
* 谁在什么时间执行了什么操作,访问了什么方法,传递了什么参数,访问时长是多少.
*/
@Data
public class Log implements Serializable {
private static final long serialVersionUID = 3054471551801044482L;
private Long id;
private String username;
private String operation;
private String method;
private String params;
private Long time;
private String ip;
private Date createdTime;
private Integer status;
private String error;
}
切入点注解定义
构建一个自定义注解,名字为RequiredLog,后续会基于此注解描述作为切入点,定义切入点方法.例如:
package com.jt.resource.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义RequiredLog注解,通过此注解对需要
* 进行日志记录的方法进行描述
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
String value() default "";
}
AOP方式获取并记录日志
定义一个日志切面,基于此切面中的通知方法实现用户行为日志的获取和记录
package com.jt.resource.aspect;
import com.jt.resource.annotation.RequiredLog;
import com.jt.resource.pojo.Log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.util.Date;
/**
* @Aspect 注解描述的类型为一个切面类型,在此类中可以定义:
* 1)切入点(切入扩展逻辑的位置~例如权限控制,日志记录,事务处理的位置),在
* @Aspect描述的类中,通常使用@Pointcut注解进行定义.使用切入点描述的方法为切入点方法.
*
* 2)通知方法(在切入点对应的目标方法执行前后要执行逻辑需要写到这样的方法中),在
* @Aspect描述的类中,通过@Before,@After,@Aroud,@AfterReturning,@AfterThrowing
* 这样的注解进行描述.
* a: @Before切入点方法执行之前执行
* b: @After 切入点方法执行之后执行(不管切入点方法是否执行成功了,它都会执行)
* c: @Aroud 切入点方法执行之前和之后都可以执行(最重要)
* d: @AfterReturning 切入点方法成功执行之后执行
* e: @AfterThrowing 切入点方法执行时出了异常会执行
*/
@Aspect
@Component
public class LogAspect {
/**
* @Pointcut 注解用于定义切入点,此注解中的内容为切入点表达式
* @annotation 为注解方式的切入点表达式,此方式的表达式为一种细粒度的切入点表达式,
* 因为它可以精确到方法,例如我们现在使用RequiredLog注解描述方法时,由它描述的方法
* 就是一个切入点方法.
*/
@Pointcut("@annotation(com.jt.resource.annotation.RequiredLog)")
public void doLog(){
//此方法中不需要写任何内容,只负责承载@Pointcut注解
}
/**
* @Around 注解描述的方法为Aspect中的一个环绕通知方法,在此方法
* 内部可以控制对目标方法的调用.
* @param joinPoint 连接点对象,此对象封装了你要执行的切入点方法信息,可以基于
* 此对象对切入点方法进行反射调用
* @return 目标执行链中切入点方法的返回值.
* @throws Throwable
*/
@Around("doLog()")
public Object doAround(ProceedingJoinPoint joinPoint)throws Throwable{
int status=1;//状态
String error=null;//错误信息
long time=0l;//执行时长
long t1=System.currentTimeMillis();
try {
//手动调用目标执行链(这个执行链中包含切入点方法~目标方法)
Object result = joinPoint.proceed();
long t2=System.currentTimeMillis();
time=t2-t1;
return result;
}catch (Throwable e){
long t3=System.currentTimeMillis();
time=t3-t1;
status=0;
error=e.getMessage();
throw e;
}finally {
saveLog(joinPoint,time,status,error);
}
}
//存储用户行为日志
private void saveLog(ProceedingJoinPoint joinPoint,long time,
int status,String error)throws Throwable{
//1.获取用户行为日志
//1.1获取目标对象类型(切入点方法所在类的类型)
Class<?> targetClass = joinPoint.getTarget().getClass();
//1.2.获取目标方法
//1.2.1获取方法签名(包含方法信息,....)
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
//1.2.2获取方法对象
Method targetMethod=
targetClass.getDeclaredMethod(signature.getName(),signature.getParameterTypes());
//1.3获取方法上的RequiredLog注解内容
//1.3.1获取目标方法上注解
RequiredLog requiredLog=targetMethod.getAnnotation(RequiredLog.class);
//1.3.2获取注解中的内容(这个内容为我们定义的操作名)
String operation=requiredLog.value();
//1.4获取目标方法名(类名+方法名)
String targetMethodName=targetClass.getName()+"."+targetMethod.getName();
//1.5获取目标方法执行时传入的参数
String params=new ObjectMapper().writeValueAsString(joinPoint.getArgs());
//1.6获取登录用户名(参考了Security官方的代码)
String username=(String)
SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
//1.7获取ip地址(从当前线程获取request对象,然后基于request获取ip地址)
//String ip="192.168.1.100";
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ip=requestAttributes.getRequest().getRemoteAddr();
//2.将用户行为日志,封装到Log对象
Log logInfo=new Log();
logInfo.setIp(ip);//后续获取
logInfo.setUsername(username);
logInfo.setOperation(operation);
logInfo.setMethod(targetMethodName);
logInfo.setParams(params);
logInfo.setTime(time);
logInfo.setStatus(status);
logInfo.setError(error);
logInfo.setCreatedTime(new Date());
System.out.println("logInfo="+logInfo);
}
}
启动服务进行访问测试
依次启动nacos,sso-system,sso-auth,sso-resource,sso-gateway,sso-ui工程服务,然后执行登陆,登陆成功后查询我的资源,检测日志输出。
服务中AOP技术应用原理分析
AOP是一种设计思想,它要实现的功能就是"锦上添花",就是在尽量不修改原有目标方法的基础上,添加一些扩展功能,例如日志的记录,权限的控制,事务的控制,异步任务的执行等等,其应用原理如图所示:
说明:当我们在项目中定义了AOP切面以后,系统启动时,会对有@Aspect注解描述的类进行加载分析,基于切入点的描述为目标类型对象,创建代理对象,并在代理对象内部创建一个执行链,这个执行链中包含拦截器(封装了切入点信息),通知(Around,…),目标对象等,我们请求目标对象资源时,会直接按执行链的顺序对资源进行调用
Feign方式将日志传递给系统服务
第一步:确保sso-resource工程中添加了openfeign依赖,例如:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
第二步:确保sso-resource工程的启动上添加了@EnableFeignClients注解,例如:
package com.jt;
@EnableFeignClients
@SpringBootApplication
public class ResourceApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceApplication.class, args);
}
}
第三步:定义日志远程服务调用接口,例如:
package com.jt.resource.service;
import com.jt.resource.pojo.Log;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(value = "sso-system",contextId = "remoteLogService")
public interface RemoteLogService {
@PostMapping("/log")
public void insertLog(@RequestBody Log log);
}
第四步:在LogAspect类中注入RemoteLogService对象,并通过此对象将日志对象传递到sso-system服务
@Autowired
private RemoteLogService remoteLogService;
....
第五步:依次启动服务进行访问测试。