Daily-Blog项目后台日志(上)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
日志服务 SLS,月写入数据量 50GB 1个月
简介: Daily-Blog项目后台日志(上)

博客后台



image-20230328200728728.png


AOP实现日志记录


需求


通过日志记录接口调用信息,便于后期排查


格式如下 :


image-20230320163423192.pngimage-20230320163644874.png



实现


1.先定义注解类

/**
 * 自定义注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SystemLog {
    String businessName();
}


2.定义切面类


/**
 * 切面类
 */
@Component
@Aspect
@Slf4j
public class LogAspect {
}


3.定义切点,及其通知方法


@Component
@Aspect
@Slf4j
public class LogAspect {
    //确定切点
    @Pointcut("@annotation(com.blog.annotation.SystemLog)")
    public void pt(){
    }
    //通知方法(使用环绕通知)
    @Around("pt()")
    public Object printLog(ProceedingJoinPoint joinPoint) throws Throwable {
        joinPoint.getArgs();
        Object res; //得到目标方法调用的返回值
        try {
            handleBefore(joinPoint);
            //目标方法的调用
            res = joinPoint.proceed();
            //打印响应信息
            handleAfter(res);
        }//无论有没有异常都需要打印异常信息
        finally {
            //换行
            log.info("============End============" + System.lineSeparator());
        }
        return res;
    }
    private void handleBefore(ProceedingJoinPoint joinPoint) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        //获取被增强方法上的注解对象
        SystemLog systemLog = getSystemLog(joinPoint);
        log.info("============Start============");
        // 打印请求 URL
        log.info("URL            : {}",request.getRequestURL());
        // 打印描述信息
        log.info("BusinessName   : {}",systemLog.businessName());
        // 打印 Http method
        log.info("HTTP Method    : {}",request.getMethod());
        // 打印调用 controller 的全路径以及执行方法
        log.info("Class Method   : {}.{}",joinPoint.getSignature().getDeclaringTypeName(),((MethodSignature) joinPoint.getSignature()).getName());
        // 打印请求的 IP
        log.info("IP             : {}",request.getRemoteHost());
        // 打印请求入参
        log.info("Request Args   : {}", JSON.toJSONString(joinPoint.getArgs()) );
    }
    private void handleAfter(Object res) {
        // 打印出参
        log.info("Response       : {}", JSON.toJSONString(res));
    }
    //todo 获取被增强方法上的注解对象
    private SystemLog getSystemLog(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        SystemLog systemLog = methodSignature.getMethod().getAnnotation(SystemLog.class);
        return systemLog;
    }
}

4.在需要增强的方法上添加自定义注解


@SystemLog(businessName="更新用户信息")
@GetMapping("/userInfo")
public ResponseResult userInfo(){
    return userService.userInfo();
}


Swagger2


依赖


<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
</dependency>

image-20230321215827890.png

基本使用


@ApiOperation(value = "友链评论列表",notes = "获取一页友链评论")
@GetMapping("/linkCommentList")
public ResponseResult listCommentList(Integer pageNum , Integer pageSize){
    return commentService.commentList(SystemConstants.COMMENT_TYPE_FRIEND,null,pageNum,pageSize);
}

使用@ApiOperation(value = "友链评论列表",notes = "获取一页友链评论")来进行标注


配置形参


@ApiImplicitParams({
        @ApiImplicitParam(name = "pageNum", value = "页号"),
        @ApiImplicitParam(name = "pageSize", value = "每页大小")
})


image-20230322163816207.pngimage-20230322164820155.png


**实体类接口 : **


一般一个实体类不止在一个接口中被用到,所以如果直接在实体类中添加的话就是使代码耦合,所以我们需要进行拆解


@ApiModel(description = "文章实体类")
public class Article{
}

所以上面的写法是不正规的


所以我们需要使用DTO对象


【dto对象 :数据传输对象】


按照开发规范,所有的controller层需要的实体类参数,我们都需要将其转换为dto对象


//todo 添加评论
@PostMapping
public ResponseResult addComment(@RequestBody AddCommentDto addCommentDto){
    Comment comment = BeanCopyUtils.copyBean(addCommentDto, Comment.class);
    return commentService.addComment(comment);
}
@ApiModel(description = "添加评论实体类")
public class AddCommentDto {
    private Long id;
    //评论类型(0代表文章评论,1代表友链评论)
    private String type;
    //文章id
    @ApiModelProperty(notes = "文章id")
    private Long articleId;
    //根评论id
  //......   
}

所有的dto都是需要添加的

image-20230322170449009.png


获取所有标签

接口

image-20230323184335879.png


实现

/**
 * 标签请求
 */
@RestController
@RequestMapping("/content/tag")
public class TagController {
    @Resource
    private TagService tagService;
    @GetMapping("/list")
    public ResponseResult list(){
        return ResponseResult.okResult(tagService.list());
    }
}


后台登录、登出


接口

image-20230323184730929.png


登录


①自定义登录接口


调用ProviderManager的方法进行认证 如果认证成功生成jwt


把信息存入redis中


②自定义UserDetailsServic e


在这个实现类中进行查询数据库操作


注意配置密码加密BCryptPasswordCoder


校验


①自定义jwt认证过滤器


获取token


解析token获取其中的userId


从redis中获取用户信息


存入securityContextHolder

image-20230323190712580.png


实现


@RestController
@RequestMapping("/user")
public class loginController {
    @Resource
    private AdminLoginService loginService;
    @PostMapping("/login")
    public ResponseResult login(@RequestBody User user){
        if (!StringUtils.hasText(user.getUserName())){
            //提示 要传用户名
            throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
        }
        return loginService.login(user);
    }
    @PostMapping("/logout")
    public ResponseResult logout(){
        return loginService.logout();
    }
}


/**
 * 后台登陆实现
 */
@Service
public class AdminLoginServiceImpl implements AdminLoginService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Resource
    private RedisCache redisCache;
    //todo 登录业务
    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //判断是否认证通过
        //获取userId ,生成token
        //判断是否认证通过
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("用户名或密码错误");
        }
        //获取userid 生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        //把用户信息存入redis
        redisCache.setCacheObject(SystemConstants.LOGIN_KEY + userId,loginUser);
        //封装响应  : 把token 和userInfoVo(由user转换而成) 封装 ,然后返回
        Map<String,String> map = new HashMap<>();
        map.put("token",jwt);
        return ResponseResult.okResult(map);
    }
    @Override
    public ResponseResult logout() {
        //获取 token 解析获取 userId
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userId = loginUser.getUser().getId();
        redisCache.deleteObject(SystemConstants.LOGIN_KEY + userId);
        return ResponseResult.okResult();
    }
}


后台权限控制及其动态路由


表分析

权限表image-20230323193145026.png


对应的页面image-20230323193349478.png


权限表


image-20230323194436827.png

角色权限表

image-20230323194501433.png


获取当前用户的权限和角色信息


接口(getInfo)


image-20230323194702962.pngimage-20230323195059120.png



实现getInfo


最终实现结果

{
  "code": 200,
  "data": {
  "permissions": [
    "system:menu:list",
    "system:menu:query",
    "system:menu:add",
    "system:menu:edit",
    "system:menu:remove",
    "content:article:writer"
  ],
  "roles": [
    "common",
    "link"
  ],
  "user": {
    "avatar": "http://rrpanx30j.hd-bkt.clouddn.com/images/91529822720e0cf3efed815e0446f21fbe09aa79.png",
    "email": "23412532@qq.com",
    "id": 4,
    "nickName": "红红火火恍恍惚惚",
    "sex": "1"
  }
  },
  "msg": "操作成功"
}
controller


@RestController
public class UserController {
    @Resource
    private RoleService roleService;
    @Resource
    private MenuService menuService;
    @GetMapping("/getInfo")
    public ResponseResult<AdminUserInfoVo> getInfo(){
        //1. 查询当前登陆的用户
        LoginUser loginUser = SecurityUtils.getLoginUser();
        //2. 根据用户id查询权限
        List<String> perms = menuService.selectPermsByUserId(loginUser.getUser().getId());
        // 根据id查询角色信息
        List<String> roleKeyList = roleService.selectRoleKeyByUserId(loginUser.getUser().getId());
        //3. 封装 返回
        User user = loginUser.getUser();
        UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class);
        AdminUserInfoVo adminUserInfoVo = new AdminUserInfoVo(perms,roleKeyList,userInfoVo);
        return ResponseResult.okResult(adminUserInfoVo);
    }
}
service层两个实现类
/**
 * 查询角色权限信息
 */
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {
    /**
     * 根据用户id查询权限信息<br>
     * 如果用户id为1 代表管理员 ,menus中需要有所有菜单类型为c或者F的,状态为,未被删除的权限
     * @param id 用户id
     * @return 返回该用户权限集合
     */
    @Override
    public List<String> selectPermsByUserId(Long id) {
        //管理员返回所有的权限
        if(id == 1){
            LambdaQueryWrapper<Menu> wrapper = new LambdaQueryWrapper<>();
            wrapper.in(Menu::getMenuType, SystemConstants.MENU_TYPE_C,SystemConstants.MENU_TYPE_F);  //菜单类型为C 和 F
            wrapper.eq(Menu::getStatus,SystemConstants.LINK_STATUS_NORMAL);//状态正常
            List<Menu> menus = list(wrapper);
            List<String> Perms = menus.stream().map(Menu::getPerms).collect(Collectors.toList());
            return Perms;
        }
        // 反之返回相对应用户所具有的权限
        //1. 先查询sys_user_roles查询用户角色id
        //2. 查到角色id之后再到sys_roles_menu查询对应的权限id(menuId)
        //3. 最后通过menuId查询对应的menu信息
        //4. 封装返回
        return getBaseMapper().selectPermsByUserId(id);
    }
}


/**
 * 查询角色信息
 */
@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
    @Override
    public List<String> selectRoleKeyByUserId(Long id) {
        //判断是否为管理员角色
        if(id == 1){
            List<String> roleKeys = new ArrayList<>();
            roleKeys.add("admin");  //管理员角色
            return roleKeys;
        }
        //如果不是返回对应id的角色信息(连表查询)
        return getBaseMapper().selectRoleKeyByUserId(id);
    }
}

对应多表联查的xml文件


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.blog.mapper.RoleMapper">
<!--            selectRoleKeyByUserId-->
    <select id="selectRoleKeyByUserId" resultType="java.lang.String">
        SELECT
            r.`role_key`
        FROM
            `sys_user_role` ur
                LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
        WHERE
            ur.`user_id` = #{userId} AND
            r.`status` = 0 AND
            r.`del_flag` = 0
    </select>
    <select id="selectRoleIdByUserId" resultType="java.lang.Long">
        select r.id
        from sys_role r
                 left join sys_user_role ur on ur.role_id = r.id
        where ur.user_id = #{userId}
    </select>
</mapper>


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.blog.mapper.MenuMapper">
    <select id="selectPermsByUserId" resultType="java.lang.String">
        SELECT
            DISTINCT m.perms
        FROM
            `sys_user_role` ur
                LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
                LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
        WHERE
            ur.`user_id` = #{userId} AND
            m.`menu_type` IN ('C','F') AND
            m.`status` = 0 AND
            m.`del_flag` = 0
    </select>
    <select id="selectAllRouterMenu" resultType="com.blog.domain.entity.Menu">
        SELECT
            DISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, IFNULL(m.perms,'') AS perms, m.is_frame,  m.menu_type, m.icon, m.order_num, m.create_time
        FROM
            `sys_menu` m
        WHERE
            m.`menu_type` IN ('C','M') AND
            m.`status` = 0 AND
            m.`del_flag` = 0
        ORDER BY
            m.parent_id,m.order_num
    </select>
    <select id="selectRouterMenuTreeByUserId" resultType="com.blog.domain.entity.Menu">
        SELECT
            DISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, IFNULL(m.perms,'') AS perms, m.is_frame,  m.menu_type, m.icon, m.order_num, m.create_time
        FROM
            `sys_user_role` ur
                LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
                LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
        WHERE
            ur.`user_id` = #{userId} AND
            m.`menu_type` IN ('C','M') AND
            m.`status` = 0 AND
            m.`del_flag` = 0
        ORDER BY
            m.parent_id,m.order_num
    </select>
    <select id="selectMenuListByRoleId" resultType="java.lang.Long">
        select m.id
        from sys_menu m
                 left join sys_role_menu rm on m.id = rm.menu_id
        where rm.role_id = #{roleId}
        order by m.parent_id, m.order_num
    </select>
</mapper>


动态路由接口(getRouters)

image-20230323195215441.png


响应格式


前端为了实现动态路由的效果,需要后端有接口能够返回所有的菜单数据


注意 :返回的菜单需要体现父子菜单的层级关系


如果用户id为1 代表管理员 ,menus中需要有所有菜单类型为c或者M的,状态为,未被删除的权限

image-20230323195723943.png


{
  "code": 200,
  "data": {
  "menus": [
    {
    "children": [],
    "component": "content/article/write/index",
    "createTime": "2022-01-08 03:39:58",
    "icon": "build",
    "id": 2023,
    "isFrame": 1,
    "menuName": "写博文",
    "menuType": "C",
    "orderNum": 0,
    "parentId": 0,
    "path": "write",
    "perms": "content:article:writer",
    "status": "0",
    "visible": "0"
    },
    {
    "children": [
      {
      "children": [],
      "component": "system/menu/index",
      "createTime": "2021-11-12 10:46:19",
      "icon": "tree-table",
      "id": 102,
      "isFrame": 1,
      "menuName": "菜单管理",
      "menuType": "C",
      "orderNum": 3,
      "parentId": 1,
      "path": "menu",
      "perms": "system:menu:list",
      "status": "0",
      "visible": "0"
      }
    ],
    "createTime": "2021-11-12 10:46:19",
    "icon": "system",
    "id": 1,
    "isFrame": 1,
    "menuName": "系统管理",
    "menuType": "M",
    "orderNum": 1,
    "parentId": 0,
    "path": "system",
    "perms": "",
    "status": "0",
    "visible": "0"
    }
  ]
  },
  "msg": "操作成功"
}


实现getRouters


controller


@GetMapping("/getRouters")
public ResponseResult<RoutersVo> getRouters(){
    Long userId = SecurityUtils.getUserId();
    //查询menu 结果是tree形状
    List<Menu> menus = menuService.selectRouterMenuTreeByUserId(userId);
    RoutersVo routersVo = new RoutersVo(menus);
    //封装返回
    return ResponseResult.okResult(routersVo);
}


service层实现


@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {  
  /**
     * 根据用户id查询相关的权限菜单信息
     * @param userId 用户id
     * @return 返回符合要求的val
     */
    @Override
    public List<Menu> selectRouterMenuTreeByUserId(Long userId) {
        MenuMapper menuMapper = getBaseMapper();
        List<Menu> menus = null;    //封装Menu
        //如果是管理员,返回所有的菜单
        if(SecurityUtils.isAdmin()){
            menus = menuMapper.selectAllRouterMenu();
        }
        else{
            //如果不是管理员 那么返回对应有权限的菜单按钮
            menus = menuMapper.selectRouterMenuTreeByUserId(userId);
        }
        //通过上述得到的Menu无法得到我们需要的父子菜单管理,所以我们需要通过(buildMenuTree)来构建这种父子菜单关系
        //构建Tree
        //先找出第一层的菜单,接着找到他们的子菜单,然后就可以设置children属性中
        List<Menu> menuTree = buildMenuTree(menus,0L);
        return menuTree;
    }
    /**
     * 构建菜单的父子菜单关系
     * <br>
     * 找到父menu对应的子menu ,然后将他们放到一个集合中,最后设置给children这个字段
     * @param menus 传入的方法
     * @param parentId 父菜单id
     * @return
     */
    private List<Menu> buildMenuTree(List<Menu> menus, Long parentId) {
        List<Menu> menuList = menus.stream()//通过这样筛选就可以得到第一层级的menu
                .filter(menu -> menu.getParentId().equals(parentId))
                /*
                传入的menus是得到了第一层的menus(相当于Tree中的root节点),然后需要设置他的子菜单(left 和 right)
                因为menus中有所有的菜单(父子都有), 所以我们在设置left和right时需要找到他们的子菜单
                所以就调用getChildren找到left或者right的子菜单,然后得到之后再设置给他们
                 */
                .map(menu -> menu.setChildren(getChildren(menu, menus)))
                .collect(Collectors.toList());
        return menuList;
    }
    /**
     * 获取传入参数的子menu的list集合
     *  在menus中找打当前传入的menu的子菜单
     * @param menu
     * @param menus
     */
    private List<Menu> getChildren(Menu menu, List<Menu> menus){
        List<Menu> children = menus.stream()
                .filter(menu1 -> menu1.getParentId().equals(menu.getId()))
                .map(menu1 -> menu1.setChildren(getChildren(menu1,menus)))  //如果有很多的子菜单,那么就可以用到这个递归
                .collect(Collectors.toList());
        return children;
    }
}


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
14天前
|
JSON Java 数据库
SpringBoot项目使用AOP及自定义注解保存操作日志
SpringBoot项目使用AOP及自定义注解保存操作日志
30 1
|
3月前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
130 1
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
|
3月前
|
开发框架 .NET Docker
【Azure 应用服务】App Service .NET Core项目在Program.cs中自定义添加的logger.LogInformation,部署到App Service上后日志不显示Log Stream中的问题
【Azure 应用服务】App Service .NET Core项目在Program.cs中自定义添加的logger.LogInformation,部署到App Service上后日志不显示Log Stream中的问题
|
3月前
|
XML Java Maven
logback在springBoot项目中的使用 springboot中使用日志进行持久化保存日志信息
这篇文章详细介绍了如何在Spring Boot项目中使用logback进行日志记录,包括Maven依赖配置、logback配置文件的编写,以及实现的日志持久化和控制台输出效果。
logback在springBoot项目中的使用 springboot中使用日志进行持久化保存日志信息
|
3月前
|
数据可视化 Java API
如何在项目中快速引入Logback日志并搭配ELK使用
如何在项目中快速引入Logback日志并搭配ELK使用
|
3月前
|
开发框架 .NET API
如何在 ASP.NET Core Web Api 项目中应用 NLog 写日志?
如何在 ASP.NET Core Web Api 项目中应用 NLog 写日志?
163 0
|
3月前
|
监控 程序员 数据库
分享一个 .NET Core Console 项目中应用 NLog 写日志的详细例子
分享一个 .NET Core Console 项目中应用 NLog 写日志的详细例子
|
9天前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
110 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
|
1月前
|
XML JSON Java
Logback 与 log4j2 性能对比:谁才是日志框架的性能王者?
【10月更文挑战第5天】在Java开发中,日志框架是不可或缺的工具,它们帮助我们记录系统运行时的信息、警告和错误,对于开发人员来说至关重要。在众多日志框架中,Logback和log4j2以其卓越的性能和丰富的功能脱颖而出,成为开发者们的首选。本文将深入探讨Logback与log4j2在性能方面的对比,通过详细的分析和实例,帮助大家理解两者之间的性能差异,以便在实际项目中做出更明智的选择。
210 3
|
1月前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1619 14