- 这个框架中的日志模块主要用到的技术:使用注解AOP切面自定义配置日志输出,只要有注解的地方都用日志做了代理输出。
- 这里日志的是使用了AOP代理,纵向代码,横向抽取。
- 在SpringBoot项目中做AOP日志代理步骤
-
首先导入springboot的aop依赖,之后就可以直接使用@Aspect切面注解,声明这个注解的类是切面类。
-
定义一个切入点,一般是写一个@Log注解类,作用就是所有被这个注解注释了的方法或者类都会走这边的切面代理方法。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Log { String value() default ""; }
- 切面类中需要有切面通知的方法
@Aspect:作用是把当前类标识为一个切面供容器读取 @Pointcut:Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为 此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。 @Around:环绕增强,相当于MethodInterceptor @AfterReturning:后置增强,相当于AfterReturningAdvice,方法正常退出时执行 @Before:标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有 @AfterThrowing:异常抛出增强,相当于ThrowsAdvice @After: final增强,不管是抛出异常或者正常退出都会执行
-
Logging模块剖析
整个logging模块的类图排列下边是eladmin中,开发时进行打日志的AOP日志通知配置方法
me.zhengjie.annotation.Log类(自定义切点注解)
此类是为了能够在程序中自由的使用切点,凡是被此注解标记的方法都是切点,都要执行切面中的方法,这个注解类也方便在切面中配置通知的切点。关于AOP中的切点,切面,连接点,通知概念戳这里了解一波
// 注解运行的级别
@Target(ElementType.METHOD)
// 注解运行时保存的时机,是源码级别的保存还是jvm级别的保存
// [详细了解可看看这篇博客](https://blog.csdn.net/u010002184/article/details/79166478)
// 也可以直接点进类中去看看java.lang.annotation.RetentionPolicy主要是这个类
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
me.zhengjie.aspect.LogAspect类(切面代码)
// 通过IOC创建对象放在Spring容器中
@Component
// 切面注解,需要导入aop依赖才能使用
@Aspect
// boot项目默认门面日志注解
@Slf4j
public class LogAspect {
/**
* 声明一个日志业务层对象,下边通过Spring中的依赖注入实现对象的赋值传递
*/
private final LogService logService;
/**
* java.lang.ThreadLocal 类提供线程局部变量,在这里主要是用来保存程序运行开始时的毫秒数
*/
ThreadLocal<Long> currentTime = new ThreadLocal<>();
/**
* 通过依赖注入,注入日志业务层(service)对象
*
* @author Hue Fu
* @param logService me.zhengjie.service.LogService
* @date 2021-01-27 13:03
* @since 0.0.1
*/
public LogAspect(LogService logService) {
this.logService = logService;
}
/**
* 配置切入点
* `@Pointcut` 注解
* 1. 作用
* 2. 参数意义
*
* @author Hue Fu
* @return void
* @date 2021-01-27 13:06
* @since 0.0.1
*/
@Pointcut("@annotation(me.zhengjie.annotation.Log)")
public void logPointcut() {
// 该方法无方法体,主要为了让同类中其他方法使用此切入点
}
/**
* `@Around`注解
* 1. 作用
* 2. 参数意义
*
* @author Hue Fu
* @param joinPoint org.aspectj.lang.ProceedingJoinPoint
* @return java.lang.Object
* @date 2021-01-27 13:07
* @since 0.0.1
*/
@Around("logPointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object result;
// 在当前的线程中设置一个毫秒数变量,方便后边统计程序的运行时间
currentTime.set(System.currentTimeMillis());
// 执行被代理的方法
result = joinPoint.proceed();
// 创建一个用于记录当前日志信息的日志对象,ThreadLocal T get()此方法返回此线程局部变量的当前线程副本中的值。
Log log = new Log("INFO",System.currentTimeMillis() - currentTime.get());
// 移除前边线程中设置的时间变量
currentTime.remove();
// 获得请求对象,里边包含了请求的浏览器,IP地址,等信息
HttpServletRequest request = RequestHolder.getHttpServletRequest();
logService.save(getUsername(), StringUtils.getBrowser(request), StringUtils.getIp(request),joinPoint, log);
return result;
}
/**
* `@AfterThrowing`注解
* 1. 作用
* 2. 参数意义
*
* @author Hue Fu
* @param joinPoint org.aspectj.lang.JoinPoint
* @param e java.lang.Throwable
* @return void
* @date 2021-01-27 13:08
* @since 0.0.1
*/
@AfterThrowing(pointcut = "logPointcut()", throwing = "e")
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
// 当环绕通知中的proceed()被代理方法执行错误时才会触发抛出异常通知切点
// 先创建一个日志对象,初始化日志类型和运行时间
Log log = new Log("ERROR",System.currentTimeMillis() - currentTime.get());
// 移除线程中的变量,因为我们在环绕通知中已经设置过了,(理解这一点要理解代码的执行顺序)
currentTime.remove();
log.setExceptionDetail(ThrowableUtil.getStackTrace(e).getBytes());
HttpServletRequest request = RequestHolder.getHttpServletRequest();
logService.save(
getUsername(),
StringUtils.getBrowser(request),
StringUtils.getIp(request),
(ProceedingJoinPoint)joinPoint,
log
);
}
/**
* 获取当前操作的用户对象
*
* @author Hue Fu
* @return java.lang.String
* @date 2021-01-27 13:08
* @since 0.0.1
*/
public String getUsername() {
try {
return SecurityUtils.getCurrentUsername();
}catch (Exception e){
return "";
}
}
}
以上就是eladmin中在程序中打日志的配置
下边是日志的服务模块,增删改查
me.zhengjie.domain.Log日志实体类剖析
此类是数据库中的实体类,我们一般在从数据库中查出来的时候用它装,在另外的数据传输中我们还需要进行DTO类的映射,用DTO类来进行传输(就是在service.dto包下的类)
// 表示此类是个数据库中的实体类,遵循默认的ORM规则,[详细的可以点这里查看](https://blog.csdn.net/wyf86/article/details/100124919)
@Entity
@Getter
@Setter
// 表示此类的映射的是数据库中的哪张表,因为默认是类名的表
@Table(name = "sys_log")
// lombok中的注解,提供无参构造
@NoArgsConstructor
public class Log implements Serializable {
// 标注这个变量是表中的主键[戳这里看看](https://www.cnblogs.com/hoojjack/p/6568920.html)
@Id
// 这个注解用来映射表中的字段
@Column(name = "log_id")
// 此注解用来指定主键的生成策略
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 操作用户 */
private String username;
/** 描述 */
private String description;
/** 方法名 */
private String method;
/** 参数 */
private String params;
/** 日志类型 */
private String logType;
/** 请求ip */
private String requestIp;
/** 地址 */
private String address;
/** 浏览器 */
private String browser;
/** 请求耗时 */
private Long time;
/** 异常详细 */
private byte[] exceptionDetail;
/** 创建日期 */
@CreationTimestamp
private Timestamp createTime;
public Log(String logType, Long time) {
this.logType = logType;
this.time = time;
}
}
me.zhengjie.repository.LogRepository相当于项目中的dao层,也就是和数据库交互的那一层解读
这个类主要是提供了操作数据库中日志表数据的方法
// 相当于@Service注解是Spring中引入的注解
// [详情可以戳这里](https://blog.csdn.net/aliushui/article/details/46042455)
@Repository
public interface LogRepository extends JpaRepository<Log,Long>, JpaSpecificationExecutor<Log> {
/**
* 根据日志类型删除信息
* @param logType 日志类型
*/
@Modifying
// 此注解在项目中被重写了位置在me.zhengjie.annotation.Query这个类
// 但在这里用的还是JPA中的@query注解
@Query(value = "delete from sys_log where log_type = ?1", nativeQuery = true)
void deleteByLogType(String logType);
}
me.zhengjie.rest.LogController类是日志文件的控制层,用来让用户控制保存在数据库中的日志文件
// Spring4之后新加入的注解,返回json字符
@RestController
// lombok中生成有参构造函数的注解
@RequiredArgsConstructor
// Spring配置url映射
@RequestMapping("/api/logs")
// Swagger中定义接口名字的注解
@Api(tags = "系统:日志管理")
public class LogController {
// 声明业务层对象
private final LogService logService;
@Log("导出数据")
@ApiOperation("导出数据")
@GetMapping(value = "/download")
@PreAuthorize("@el.check()")
public void download(HttpServletResponse response, LogQueryCriteria criteria) throws IOException {
criteria.setLogType("INFO");
logService.download(logService.queryAll(criteria), response);
}
@Log("导出错误数据")
@ApiOperation("导出错误数据")
@GetMapping(value = "/error/download")
@PreAuthorize("@el.check()")
public void downloadErrorLog(HttpServletResponse response, LogQueryCriteria criteria) throws IOException {
criteria.setLogType("ERROR");
logService.download(logService.queryAll(criteria), response);
}
@GetMapping
@ApiOperation("日志查询")
@PreAuthorize("@el.check()")
public ResponseEntity<Object> query(LogQueryCriteria criteria, Pageable pageable){
criteria.setLogType("INFO");
return new ResponseEntity<>(logService.queryAll(criteria,pageable), HttpStatus.OK);
}
@GetMapping(value = "/user")
@ApiOperation("用户日志查询")
public ResponseEntity<Object> queryUserLog(LogQueryCriteria criteria, Pageable pageable){
criteria.setLogType("INFO");
criteria.setBlurry(SecurityUtils.getCurrentUsername());
return new ResponseEntity<>(logService.queryAllByUser(criteria,pageable), HttpStatus.OK);
}
@GetMapping(value = "/error")
@ApiOperation("错误日志查询")
@PreAuthorize("@el.check()")
public ResponseEntity<Object> queryErrorLog(LogQueryCriteria criteria, Pageable pageable){
criteria.setLogType("ERROR");
return new ResponseEntity<>(logService.queryAll(criteria,pageable), HttpStatus.OK);
}
@GetMapping(value = "/error/{id}")
@ApiOperation("日志异常详情查询")
@PreAuthorize("@el.check()")
public ResponseEntity<Object> queryErrorLogs(@PathVariable Long id){
return new ResponseEntity<>(logService.findByErrDetail(id), HttpStatus.OK);
}
@DeleteMapping(value = "/del/error")
@Log("删除所有ERROR日志")
@ApiOperation("删除所有ERROR日志")
@PreAuthorize("@el.check()")
public ResponseEntity<Object> delAllErrorLog(){
logService.delAllByError();
return new ResponseEntity<>(HttpStatus.OK);
}
@DeleteMapping(value = "/del/info")
@Log("删除所有INFO日志")
@ApiOperation("删除所有INFO日志")
@PreAuthorize("@el.check()")
public ResponseEntity<Object> delAllInfoLog(){
logService.delAllByInfo();
return new ResponseEntity<>(HttpStatus.OK);
}
}
me.zhengjie.service.LogService接口类,没得说
public interface LogService {
/**
* 分页查询
* @param criteria 查询条件
* @param pageable 分页参数
* @return /
*/
Object queryAll(LogQueryCriteria criteria, Pageable pageable);
/**
* 查询全部数据
* @param criteria 查询条件
* @return /
*/
List<Log> queryAll(LogQueryCriteria criteria);
/**
* 查询用户日志
* @param criteria 查询条件
* @param pageable 分页参数
* @return -
*/
Object queryAllByUser(LogQueryCriteria criteria, Pageable pageable);
/**
* 保存日志数据
* @param username 用户
* @param browser 浏览器
* @param ip 请求IP
* @param joinPoint /
* @param log 日志实体
*/
@Async
void save(String username, String browser, String ip, ProceedingJoinPoint joinPoint, Log log);
/**
* 查询异常详情
* @param id 日志ID
* @return Object
*/
Object findByErrDetail(Long id);
/**
* 导出日志
* @param logs 待导出的数据
* @param response /
* @throws IOException /
*/
void download(List<Log> logs, HttpServletResponse response) throws IOException;
/**
* 删除所有错误日志
*/
void delAllByError();
/**
* 删除所有INFO日志
*/
void delAllByInfo();
}
me.zhengjie.service.impl.LogServiceImpl日志业务接口实现类非常有的说,但是还没能力去看懂,先歇歇吧
@Service
@RequiredArgsConstructor
public class LogServiceImpl implements LogService {
private static final Logger log = LoggerFactory.getLogger(LogServiceImpl.class);
private final LogRepository logRepository;
private final LogErrorMapper logErrorMapper;
private final LogSmallMapper logSmallMapper;
@Override
public Object queryAll(LogQueryCriteria criteria, Pageable pageable) {
Page<Log> page = logRepository.findAll(((root, criteriaQuery, cb) -> QueryHelp.getPredicate(root, criteria, cb)), pageable);
String status = "ERROR";
if (status.equals(criteria.getLogType())) {
return PageUtil.toPage(page.map(logErrorMapper::toDto));
}
return page;
}
@Override
public List<Log> queryAll(LogQueryCriteria criteria) {
return logRepository.findAll(((root, criteriaQuery, cb) -> QueryHelp.getPredicate(root, criteria, cb)));
}
@Override
public Object queryAllByUser(LogQueryCriteria criteria, Pageable pageable) {
Page<Log> page = logRepository.findAll(((root, criteriaQuery, cb) -> QueryHelp.getPredicate(root, criteria, cb)), pageable);
return PageUtil.toPage(page.map(logSmallMapper::toDto));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void save(String username, String browser, String ip, ProceedingJoinPoint joinPoint, Log log) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
me.zhengjie.annotation.Log aopLog = method.getAnnotation(me.zhengjie.annotation.Log.class);
// 方法路径
String methodName = joinPoint.getTarget().getClass().getName() + "." + signature.getName() + "()";
// 描述
if (log != null) {
log.setDescription(aopLog.value());
}
assert log != null;
log.setRequestIp(ip);
log.setAddress(StringUtils.getCityInfo(log.getRequestIp()));
log.setMethod(methodName);
log.setUsername(username);
log.setParams(getParameter(method, joinPoint.getArgs()));
log.setBrowser(browser);
logRepository.save(log);
}
/**
* 根据方法和传入的参数获取请求参数
*/
private String getParameter(Method method, Object[] args) {
List<Object> argList = new ArrayList<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
//将RequestBody注解修饰的参数作为请求参数
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
if (requestBody != null) {
argList.add(args[i]);
}
//将RequestParam注解修饰的参数作为请求参数
RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
if (requestParam != null) {
Map<String, Object> map = new HashMap<>();
String key = parameters[i].getName();
if (!StringUtils.isEmpty(requestParam.value())) {
key = requestParam.value();
}
map.put(key, args[i]);
argList.add(map);
}
}
if (argList.size() == 0) {
return "";
}
return argList.size() == 1 ? JSONUtil.toJsonStr(argList.get(0)) : JSONUtil.toJsonStr(argList);
}
@Override
public Object findByErrDetail(Long id) {
Log log = logRepository.findById(id).orElseGet(Log::new);
ValidationUtil.isNull(log.getId(), "Log", "id", id);
byte[] details = log.getExceptionDetail();
return Dict.create().set("exception", new String(ObjectUtil.isNotNull(details) ? details : "".getBytes()));
}
@Override
public void download(List<Log> logs, HttpServletResponse response) throws IOException {
List<Map<String, Object>> list = new ArrayList<>();
for (Log log : logs) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("用户名", log.getUsername());
map.put("IP", log.getRequestIp());
map.put("IP来源", log.getAddress());
map.put("描述", log.getDescription());
map.put("浏览器", log.getBrowser());
map.put("请求耗时/毫秒", log.getTime());
map.put("异常详情", new String(ObjectUtil.isNotNull(log.getExceptionDetail()) ? log.getExceptionDetail() : "".getBytes()));
map.put("创建日期", log.getCreateTime());
list.add(map);
}
FileUtil.downloadExcel(list, response);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delAllByError() {
logRepository.deleteByLogType("ERROR");
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delAllByInfo() {
logRepository.deleteByLogType("INFO");
}
}
网友评论