基于SpringBoot的合家云社区物业管理平台 - 社区资产模块

合家云社区物业管理平台

3.社区资产模块开发

3.1 功能分析

社区资产模块主要管理小区信息、楼栋信息、单元信息及房屋信息的数据更新与维护。该模块主要以下几个子模块:

  • 小区信息模块
  • 楼栋信息模块
  • 单元信息模块
  • 房屋信息模块

image.png

1)小区信息实现的功能包括:

image.png

2)楼栋信息实现的功能包括:

image.png

3)单元信息实现的功能包括:

image.png

4)单元信息实现的功能包括:

image.png

3.2 数据库表设计

社区资产模块包括的表有:

  • 小区表:hjy_community

    image.png

    行政区划代码是行政区划唯一的“身份号码”,行政区划代码从左到右的数字有着不同的含义,以清波街道330102001为例,33表示浙江省,01表示杭州市,02表示上城区,001表示清波街道。

  • 楼栋表:hjy_building

    image.png

  • 单元表:hjy_unit

    image.png

  • 房间表:hjy_room

    image.png

-- 查询表字段、类型、说明
select COLUMN_NAME 字段名,column_comment 字段说明,column_type 字段类型,
  column_key 约束 from information_schema.columns 
  where table_schema='数据库名' and table_name='表名';

表关系示例:

image.png

3.3 小区信息模块开发

3.3.1 数据封装对象介绍

3.3.1.1 Entity、VO、DTO解释

1)Entity:实体,与数据库的每一行数据打交道的,它的属性对应数据库每个字段

class User{
	private Long idCard;
	private String name;
	private Date birthday;
	......
}

对应数据库的id,name,birthday等等字段,在CRUD中都会频繁用到

2)VO(View Object):视图类对象,属性对应前端页面用到的变量,例如用户登录页面涉及账号和密码,通过VO的封装传到HTML页面

class UserVO {
	private String username;
	private String password;
}

3)DTO(Data Transform Object):数据传输对象,是用于在层与层之间传输数据的容器。

这个概念来源于 J2EE 的设计模式,原来的目的是为了 EJB 的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载。一般情况下DTO类比实体类具有更多或者更少的字段。

class UserDto{
	private Long idCard;
	private String name;
	private Date birthday;
	private Integer height;
	private Float weight;
}
3.3.1.2 创建包结构

com.msb.hjycommunity.community.domain包下,创建vo 和 dto包。

image.png

3.3.1.3 创建Entity类

HjyCommunity

/**
 * 小区对象 HjyCommunity
 * @author spikeCong
 * @date 2023/2/23
 **/
public class HjyCommunity extends BaseEntity {

    private static final long serialVersionUID = 1L;

    @TableId
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private Long communityId;

    /**
     * 小区名称
     */
    private String communityName;

    /**
     * 小区编码
     */
    private String communityCode;

    /**
     * 省区划码
     */
    private String communityProvinceCode;

    /**
     * 市区划码
     */
    private String communityCityCode;

    /**
     * 区县区划码
     */
    private String communityTownCode;

    /**
     * 详细地址
     */
    private String communityDetailedAddress;

    /**
     * 经度
     */
    private String communityLongitude;

    /**
     * 纬度
     */
    private String communityLatitude;

    /**
     * 物业id
     */
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private Long deptId;

    /**
     * 排序
     */
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private Long communitySort;
 
    //get、set省略。。。。。。
}
3.3.1.4 创建DTO类

2)HjyCommunityDto

public class HjyCommunityDto extends BaseEntity {

    /** 小区id */
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private Long communityId;

    /** 小区名称 */
    private String communityName;

    /** 小区编码 */
    private String communityCode;

    /** 省区划码 */
    private String communityProvinceCode;

    /** 市区划码 */
    private String communityCityCode;

    /** 区县区划码 */
    private String communityTownCode;


    /** 详细地址 */
    private String communityDetailedAddress;

    /** 经度 */
    private String communityLongitude;

    /** 纬度 */
    private String communityLatitude;

    /** 物业id */
    private Long deptId;

    /** 排序 */
    private Long communitySort;
 
    //get/set。。。。。。
}

3.3.2 功能1:多条件分页查询

3.3.2.1 需求分析

1)在小区信息表中,只有省市区的划码信息,需要连接sys_area表,获取划码对应的详细地址信息。 其他信息正常获取即可。

image.png

2)表关系分析

image.png

3)在HjyCommunityDto类中添加三个属性,分别对应省市区名称

private String communityProvenceName;

    private String communityCityName;

    private String communityTownName;
3.3.2.2 查看接口文档进行编码
1)dao层代码编写
  • 编写SQL
SELECT 
    *,
    s1.`name` AS communityProvinceName,
    s2.`name` AS communityCityName,
    s3.`name` AS communityTownName
FROM hjy_community hc 
    LEFT JOIN sys_area s1 ON hc.`community_province_code` = s1.`code`
    LEFT JOIN sys_area s2 ON hc.`community_city_code` = s2.`code`
    LEFT JOIN sys_area s3 ON hc.`community_town_code` = s3.`code`

WHERE hc.community_name LIKE CONCAT('%','宏福苑','%') AND hc.community_code = 'COMMUNITY_1675945745985';
  • 编写dao接口
public interface HjyCommunityMapper extends BaseMapper<HjyCommunity> {

    @Select("<script>SELECT \n" +
            "    *,\n" +
            "    s1.`name` AS communityProvinceName,\n" +
            "    s2.`name` AS communityCityName,\n" +
            "    s3.`name` AS communityTownName\n" +
            "FROM hjy_community hc \n" +
            "LEFT JOIN sys_area s1 ON hc.`community_province_code` = s1.`code`\n" +
            "LEFT JOIN sys_area s2 ON hc.`community_city_code` = s2.`code`\n" +
            "LEFT JOIN sys_area s3 ON hc.`community_town_code` = s3.`code`" +
            "<where>" +
            "<if test=\"communityName !=null and communityName != ''\">" +
            "hc.community_name like concat('%',#{communityName},'%')" +
            "</if> " +

            "<if test=\"communityCode !=null and communityCode != ''\">" +
            "and hc.community_code = #{communityCode}" +
            "</if> " +
            "</where>"+
            "</script>")
    List<HjyCommunityDto> queryList(HjyCommunity hjyCommunity);

}
2)service层代码编写
  • service接口
public interface HjyCommunityService {

    /**
     * 查询小区信息列表
     * @param hjyCommunity
     * @return: java.util.List<com.msb.hjycommunity.community.domain.dto.HjyCommunityDto>
     */
    List<HjyCommunityDto> selectHjyCommunityList(HjyCommunity hjyCommunity);
  
}
  • service实现
@Service
public class HjyCommunityServiceImpl implements HjyCommunityService {

    @Resource
    private HjyCommunityMapper communityMapper;

    @Override
    public List<HjyCommunityDto> selectHjyCommunityList(HjyCommunity hjyCommunity) {
        return communityMapper.queryList(hjyCommunity);
    }
}
3)web层代码编写
3.1)分页查询相关类
  • 分页数据对象
    创建分页数据对象PageDomain,用于接收前端请求中的 当前页每页显示条数,包路径 com.msb.hjycommunity.common.core.page
public class PageDomain {

    /** 当前记录起始索引 */
    private Integer pageNum;

    /** 每页显示记录数 */
    private Integer pageSize;

    public Integer getPageNum() {
        return pageNum;
    }

    public void setPageNum(Integer pageNum) {
        this.pageNum = pageNum;
    }

    public Integer getPageSize() {
        return pageSize;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }
}
  • 分页数据统一封装响应对象
    包路径:com.msb.hjycommunity.common.core.page.PageResult ,该类的作用是:分页查询结果集统一响应封装对象
/**
 * 分页查询统一响应封装类
 * @author spikeCong
 * @date 2023/2/28
 **/
public class PageResult implements Serializable {

    private static final long serialVersionUID = 1L;

    /** 总记录数 */
    private long total;

    /** 列表数据 */
    private List<?> rows;

    /** 消息状态码 */
    private int code;

    /** 消息内容 */
    private String msg;

    public PageResult() {
    }

    /**
     * 分页
     * @param total 列表数据
     * @param rows  总记录数
     * @return: null
     */
    public PageResult(long total, List<?> rows) {
        this.total = total;
        this.rows = rows;
    }

    public long getTotal() {
        return total;
    }

    public void setTotal(long total) {
        this.total = total;
    }

    public List<?> getRows() {
        return rows;
    }

    public void setRows(List<?> rows) {
        this.rows = rows;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}
3.2)导入状态码常量类
  • 所在包: com.msb.hjycommunity.common.constant.HttpStatus
/**
 * 返回状态码
 * @author spikeCong
 * @date 2023/2/28
 **/
public class HttpStatus {

    /**
     * 操作成功
     */
    public static final int SUCCESS = 200;

    /**
     * 对象创建成功
     */
    public static final int CREATED = 201;

    /**
     * 请求已经被接受
     */
    public static final int ACCEPTED = 202;

    /**
     * 操作已经执行成功,但是没有返回数据
     */
    public static final int NO_CONTENT = 204;

    /**
     * 资源已被移除
     */
    public static final int MOVED_PERM = 301;

    /**
     * 重定向
     */
    public static final int SEE_OTHER = 303;

    /**
     * 资源没有被修改
     */
    public static final int NOT_MODIFIED = 304;

    /**
     * 参数列表错误(缺少,格式不匹配)
     */
    public static final int BAD_REQUEST = 400;

    /**
     * 未授权
     */
    public static final int UNAUTHORIZED = 401;

    /**
     * 访问受限,授权过期
     */
    public static final int FORBIDDEN = 403;

    /**
     * 资源,服务未找到
     */
    public static final int NOT_FOUND = 404;

    /**
     * 不允许的http方法
     */
    public static final int BAD_METHOD = 405;

    /**
     * 资源冲突,或者资源被锁
     */
    public static final int CONFLICT = 409;

    /**
     * 不支持的数据,媒体类型
     */
    public static final int UNSUPPORTED_TYPE = 415;

    /**
     * 系统内部错误
     */
    public static final int ERROR = 500;

    /**
     * 接口未实现
     */
    public static final int NOT_IMPLEMENTED = 501;
}
3.3)导入客户端工具类
  • **该工具类 直接从资料中导入即可,所在包 **com.msb.hjycommunity.common.utils.ServletUtils
  • 作用:这里我们首先用该工具获取request请求对象中的分页数据PageSize和PageNum
/**
 * 客户端工具类
 * @author spikeCong
 * @date 2023/2/28
 **/
public class ServletUtils {

    public static ServletRequestAttributes getRequestAttributes()
    {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        return (ServletRequestAttributes) attributes;
    }

    /**
     * 获取request
     */
    public static HttpServletRequest getRequest()
    {
        return getRequestAttributes().getRequest();
    }

    /**
     * 获取session
     */
    public static HttpSession getSession()
    {
        return getRequest().getSession();
    }

    /**
     * 获取response
     */
    public static HttpServletResponse getResponse()
    {
        return getRequestAttributes().getResponse();
    }

    /**
     * 获取String参数
     */
    public static String getParameter(String name)
    {
        return getRequest().getParameter(name);
    }


    /**
     * 获取Integer参数
     */
    public static Integer getParameterToInt(String name)
    {
        return Integer.parseInt(getRequest().getParameter(name));
    }

    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string)
    {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }

}
3.4)HjyCommunityController编写
@RestController
@RequestMapping("/community")
public class HjyCommunityController{

    @Resource
    private HjyCommunityService hjyCommunityService;

    /**
     * 查询小区
     * @param hjyCommunity
     * @return: com.msb.hjycommunity.common.core.page.PageResult
     */
    @GetMapping("/list")
    public PageResult list(HjyCommunity hjyCommunity){

        Integer pageNum = ServletUtils.getParameterToInt("pageNum");
        Integer pageSize = ServletUtils.getParameterToInt("pageSize");

        //使用PageHelper
        PageHelper.startPage(pageNum, pageSize);
        List<HjyCommunityDto> list = hjyCommunityService.selectHjyCommunityList(hjyCommunity);
        PageInfo<HjyCommunityDto> pageInfo = new PageInfo<>(list);

        //封装数据
        PageResult pageResult = new PageResult();
        pageResult.setCode(HttpStatus.SUCCESS);
        pageResult.setMsg("查询成功");
        pageResult.setRows(list);
        pageResult.setTotal(pageInfo.getTotal());

        //响应数据
        return pageResult;
    }
}
3.5)BaseController 基础控制器类
  • BaseController 作为所有Controller的基类,提供一些公共方法,比如每个模块都有分页查询的业务需求,那么我们就可以将分页查询的处理提取到 基类中, 所有继承自BaseController的子类 不用在进行分页处理。
public class BaseController {


    /**
     * 当前记录起始索引
     */
    public static final String PAGE_NUM = "pageNum";

    /**
     * 每页显示记录数
     */
    public static final String PAGE_SIZE = "pageSize";


    /**
     * 封装分页数据
     */
    public static PageDomain getPageDomain()
    {
        PageDomain pageDomain = new PageDomain();
        pageDomain.setPageNum(ServletUtils.getParameterToInt(PAGE_NUM));
        pageDomain.setPageSize(ServletUtils.getParameterToInt(PAGE_SIZE));
        return pageDomain;
    }


    /**
     * 调用PageHelper的startPage,设置分页参数
     */
    protected void startPage(){

        PageDomain pageDomain = getPageDomain();

        Integer pageNum = pageDomain.getPageNum();
        Integer pageSize = pageDomain.getPageSize();
        if (pageNum != null && pageSize != null) {
            PageHelper.startPage(pageNum, pageSize);
        }
    }

    /**
     * 响应分页数据
     * @param list
     * @return:
     */
    protected PageResult getData(List<?> list){
        PageResult pageResult = new PageResult();
        pageResult.setCode(HttpStatus.SUCCESS);
        pageResult.setMsg("查询成功");
        pageResult.setRows(list);
        pageResult.setTotal(new PageInfo(list).getTotal());

        return pageResult;
    }

}
3.6)修改HjyCommunityController
/**
 * 小区 Controller
 * @author spikeCong
 * @date 2023/2/28
 **/
@RestController
@RequestMapping("/community")
public class HjyCommunityController extends BaseController {

    @Resource
    private HjyCommunityService hjyCommunityService;

    @GetMapping("/list")
    public PageResult list(HjyCommunity hjyCommunity){
        startPage();
        List<HjyCommunityDto> list = hjyCommunityService.selectHjyCommunityList(hjyCommunity);

        //响应数据
        return getData(list);
    }
}

3.3.3 PostMan接口测试工具

3.3.3.1 PostMan介绍

Postman是一款功能强大的http接口测试工具,使用postman可以完成http各种请求的功能测试。

3.3.3.2 Postman使用

1)新建一个Postman窗口

image.png

2)创建目录,将请求分类

image.png

image.png

image.png

3)创建一个请求

image.png

image.png

image.png

3.3.3.3 接口测试

1)修改application.yml, 给项目的所有请求添加全局路径

image.png

servlet:
    # 应用的访问路径
    context-path: /hejiayun

2)查看接口文档,使用PostMan进行测试

image.png

3.3.4 功能2:新增小区信息

3.3.4.1 需求分析

1)表单信息包括:小区名称(必填) 、详细地址(必填)、所属划区(必填)、备注(非必填)。

image.png

2)所属划区的文字描述信息,还是需要从sys_area表中获取

image.png

3.3.4.2 区域信息接口开发

查看接口文档中的系统管理模块 -----> 获取省市区信息接口

1)Entity与Dto类编写
  • 区域信息domain,所在包 com.msb.hjycommunity.system.domain.SysArea
/**
 * 区域信息
 * @author spikeCong
 * @date 2023/3/2
 **/
public class SysArea implements Serializable {

    /**
     * 区划码
     */
    private Integer code;
    /**
     * 区划名称
     */
    private String name;
    /**
     * 上级区划码
     */
    private Integer parentCode;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getParentCode() {
        return parentCode;
    }

    public void setParentCode(Integer parentCode) {
        this.parentCode = parentCode;
    }
}
  • AreaDto 构建区域信息层级,包结构:com.msb.hjycommunity.system.domain.dto.AreaDto
/**
 * 区域层级模型 - 树结构
 * @author spikeCong
 * @date 2023/3/2
 **/
public class AreaDto implements Serializable {

    //区划码
    private Integer code;

    //区划名称
    private String name;

    //子区划
    private List<AreaDto> children;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<AreaDto> getChildren() {
        return children;
    }

    public void setChildren(List<AreaDto> children) {
        this.children = children;
    }
}
2)Dao层代码编写
  • 编写Dao接口,包结构:com.msb.hjycommunity.system.mapper.SysAreaMapper
public interface SysAreaMapper{

    List<SysArea> findAll();
}
  • 创建Mapper文件,资源目录结构:mapper/system/SysAreaMapper.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.msb.hjycommunity.system.mapper.SysAreaMapper">

    <select id="findAll" resultType="SysArea">

        SELECT code,name, parentId AS parentCode FROM sys_area;
  
    </select>

</mapper>
3)Service层代码编写
  • SysAreaService 接口, 包结构:com.msb.hjycommunity.system.service
public interface SysAreaService {

    /**
     * 获取区域数据的完整树
     * @param
     * @return: java.util.List<com.msb.hjycommunity.system.domain.dto.SysAreaDto>
     */
    List<SysAreaDto> findAreaAsTree();

}
  • 实现类 SysAreaServiceImpl,包结构:com.msb.hjycommunity.system.service.impl.SysAreaServiceImpl
@Service
public class SysAreaServiceImpl implements SysAreaService {

    @Resource
    private SysAreaMapper sysAreaMapper;


    @Override
    public List<SysAreaDto> findAreaAsTree() {

        //获取区域表数据
        List<SysArea> list = sysAreaMapper.findAll();

        return list.stream()    //把集合转换为流
                .filter(area -> area.getParentCode().equals(0))  //筛选pid为0的area根节点对象
                .map(area -> { //将area进行转换
                    SysAreaDto sysAreaDto = new SysAreaDto();
                    sysAreaDto.setCode(area.getCode());
                    sysAreaDto.setName(area.getName());
                    sysAreaDto.setChildren(getChildrenArea(sysAreaDto,list));
                    return sysAreaDto;
                }).collect(Collectors.toList());
    }

    /**
     * 递归设置区域信息
     * @param sysAreaDto 上级区域信息
     * @param list  所有区域信息
     * @return: java.util.List<com.msb.hjycommunity.system.domain.dto.SysAreaDto>
     */
    private List<SysAreaDto> getChildrenArea(SysAreaDto sysAreaDto, List<SysArea> list) {

        List<SysArea> subAreaList = list.stream().filter(area -> area.getParentCode().equals(sysAreaDto.getCode())) //获取当前父区域的子节点
                .collect(Collectors.toList());//把当前流转换为一个List集合

        if(subAreaList != null && subAreaList.size() != 0){
            return subAreaList.stream().map(area -> {
                SysAreaDto subAreaDto = new SysAreaDto();
                subAreaDto.setName(area.getName());
                subAreaDto.setCode(area.getCode());
                //设置子节点,递归调用直到获取到叶子结点为止
                subAreaDto.setChildren(getChildrenArea(subAreaDto,list));

                return subAreaDto;
            }).collect(Collectors.toList());
        }

        return null;
    }
}
4)Controller编写

包结构:com.msb.hjycommunity.web.controller.system

@RestController
@RequestMapping("/system/area")
public class SysAreaController extends BaseController {

    @Resource
    private SysAreaService sysAreaService;

    @RequestMapping("/tree")
    public BaseResponse getAreaTree(){

        return BaseResponse.success(sysAreaService.findAreaAsTree());
    }

}
5)PostMan测试

根据接口文档进行测试

3.3.4.3 新增小区接口开发
1)自定义填充控制器
  • 前面我们说到mybatis plus的自动填充功能,除了要在字段上添加相关注解以外,自定义类还需要实现MetaObjectHandler接口,重写接口方法,实现公共字段自动写入。
  • 创建自定义填充控制器,包结构:com.msb.hjycommunity.common.handler
/**
 * 自定义填充控制器
 * @author spikeCong
 * @date 2023/2/28
 **/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    //insert时要填充的字段
    @Override
    public void insertFill(MetaObject metaObject) {
        //根据属性名称设置要填充的值
        this.strictInsertFill(metaObject,"createBy",String.class,"admin");
        this.strictInsertFill(metaObject,"updateBy",String.class,"admin");
        this.strictInsertFill(metaObject,"createTime", Date.class,new Date());
        this.strictInsertFill(metaObject,"updateTime", Date.class,new Date());

    }

    //update操作时要填充的字段
    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject,"updateBy",String.class,"admin");
        this.strictUpdateFill(metaObject,"updateTime", Date.class,new Date());
    }
}
2)service层代码编写
  • 接口
/**
     * 新增小区
     * @param hjyCommunity
     * @return: int
     */
    int insertHjyCommunity(HjyCommunity hjyCommunity);
  • 实现
@Service
public class HjyCommunityServiceImpl implements HjyCommunityService {

    @Resource
    private HjyCommunityMapper communityMapper;

    private static final String CODE_PREFIX = "COMMUNITY_";

    @Override
    public int insertHjyCommunity(HjyCommunity hjyCommunity) {
        //设置小区编码
        hjyCommunity.setCommunityCode(CODE_PREFIX + System.currentTimeMillis());
        return communityMapper.insert(hjyCommunity);
    }
  
}
3)Controller层代码编写
  • BaseController中添加一个 响应增删改操作是否成功的 方法
/**
     * 响应返回结果
     * @param rows 受影响行数
     * @return: com.msb.hjycommunity.common.core.domain.BaseResponse
     */
    protected BaseResponse toAjax(int rows){
        return rows > 0 ? BaseResponse.success(rows) : BaseResponse.fail("操作失败");
    }
  • HjyCommunityController
/**
     * 新增小区
     * @param hjyCommunity
     * @return: com.msb.hjycommunity.common.core.domain.BaseResponse
     */
    @PostMapping
    public BaseResponse add(@RequestBody HjyCommunity hjyCommunity){

        return toAjax( hjyCommunityService.insertHjyCommunity(hjyCommunity));
    }

3.3.5 功能3:修改小区信息

3.3.5.1 小区信息回显
1)service层代码编写
  • 接根据ID 获取小区详细信息,进行回显
/**
     * 获取小区详细信息
     * @param communityId
     * @return: com.msb.hjycommunity.community.domain.HjyCommunity
     */
    HjyCommunity selectHjyCommunityById(Long communityId);
  • 实现
@Override
    public HjyCommunity selectHjyCommunityById(Long communityId) {
        return communityMapper.selectById(communityId);
    }
2)Controller层代码编写
/**
     * 根据id查询小区信息
     * @param communityId 
     * @return: com.msb.hjycommunity.common.core.domain.BaseResponse
     */
    @GetMapping(value = "/{communityId}")
    public BaseResponse getInfo(@PathVariable("communityId") Long communityId){

        return BaseResponse.success(hjyCommunityService.selectHjyCommunityById(communityId));
    }
3)根据就接口文档,使用PostMan进行接口测试
3.3.5.1 小区信息修改
1)service层代码编写
  • 接根据ID 获取小区详细信息,进行回显
/**
     * 修改小区
     * @param hjyCommunity
     * @return: int
     */
    int updateHjyCommunity(HjyCommunity hjyCommunity);
  • 实现
@Override
    public int updateHjyCommunity(HjyCommunity hjyCommunity) {
        return communityMapper.updateById(hjyCommunity);
    }
2)Controller层代码编写
/**
     * 根据id查询小区信息
     * @param communityId 
     * @return: com.msb.hjycommunity.common.core.domain.BaseResponse
     */
    @GetMapping(value = "/{communityId}")
    public BaseResponse getInfo(@PathVariable("communityId") Long communityId){

        return BaseResponse.success(hjyCommunityService.selectHjyCommunityById(communityId));
    }
3)根据就接口文档,使用PostMan进行接口测试

3.3.6 功能4:删除小区

3.3.6.1 需求分析

1)单个删除: 点击某条数据后面的删除按钮,弹出提示框 提示是否删除,提示信息中包含小区ID

image.png

2)批量删除: 勾选多个选项,进行删除

image.png

3.3.6.2 删除小区信息编写
1)service层代码编写
  • 接根据ID 获取小区详细信息,进行回显
/**
     * 删除小区
     * @param communityIds
     * @return: int
     */
    int deleteHjyCommunityByIds(Long[] communityIds);
  • 实现
@Override
    public int deleteHjyCommunityByIds(Long[] communityIds) {
        return communityMapper.deleteBatchIds(Arrays.asList(communityIds));
    }
2)Controller层代码编写
/**
     * 删除小区
     * @param communityIds 
     * @return: com.msb.hjycommunity.common.core.domain.BaseResponse
     */
    @DeleteMapping("/{communityIds}")
    public BaseResponse remove(@PathVariable Long[] communityIds){
        return toAjax(hjyCommunityService.deleteHjyCommunityByIds(communityIds));
    }
3)根据就接口文档,使用PostMan进行接口测试

3.3.7 功能5:更换物业

3.3.7.1 需求分析

点击更换物业,显示物业公司信息,以及当前所在的物业公司。

image.png

3.3.7.2 物业信息查询
1)数据库表分析

image.png

2)实体类创建
/**
 * 部门表(SysDept)实体类
 *
 * @author spikecong
 * @since 2023-03-14 16:42:17
 */
public class SysDept extends BaseEntity {

    private static final long serialVersionUID = -53187399711230185L;

    /**
     * 部门id
     */
    private Long deptId;
    /**
     * 父部门id
     */
    private Long parentId;

    /**
     * 添加一个父部门名称
     */
    private String parentName;

    /**
     * 添加一个子部门属性
     */
    private List<SysDept> children = new ArrayList<>();

    /**
     * 祖级列表
     */
    private String ancestors;
    /**
     * 部门名称
     */
    private String deptName;
    /**
     * 显示顺序
     */
    private Integer orderNum;
    /**
     * 负责人
     */
    private String leader;
    /**
     * 联系电话
     */
    private String phone;
    /**
     * 邮箱
     */
    private String email;
    /**
     * 部门状态(0正常 1停用)
     */
    private String status;
    /**
     * 删除标志(0代表存在 2代表删除)
     */
    private String delFlag;
}
3)dao层代码编写
SELECT
    d.dept_id,
    d.parent_id,
    d.ancestors,
    d.dept_name,
    d.order_num,
    d.leader,
    d.phone,
    d.email,
    d.status,
    d.del_flag,
    d.create_by,
    d.create_time
 FROM sys_dept d WHERE d.del_flag = '0' ORDER BY d.parent_id, d.order_num
/**
 * 部门管理数据层
 * @author spikeCong
 * @date 2023/3/14
 **/
public interface SysDeptMapper extends BaseMapper<SysDept> {
    
    
    /**
     * 查询部门管理数据
     * @param sysDept 
     * @return: java.util.List<com.msb.hjycommunity.system.domain.SysDept>
     */
    public List<SysDept> selectDeptList(SysDept sysDept);
    
}
<select id="selectDeptList" parameterType="SysDept" resultMap="SysDeptMap">
    SELECT
        d.dept_id,
        d.parent_id,
        d.ancestors,
        d.dept_name,
        d.order_num,
        d.leader,
        d.phone,
        d.email,
        d.status,
        d.del_flag,
        d.create_by,
        d.create_time
    FROM sys_dept d WHERE d.del_flag = '0' ORDER BY d.parent_id, d.order_num
</select>
4)Service层代码编写
public interface ISysDeptService {

    /**
     * 查询部门管理数据
     *
     * @param dept 部门信息
     * @return 部门信息集合
     */
    public List<SysDept> selectDeptList(SysDept dept);
}
@Service
public class SysDeptServiceImpl implements ISysDeptService {

    @Resource
    private SysDeptMapper deptMapper;

    /**
     * 查询部门管理数据
     * @param dept
     * @return: java.util.List<com.msb.hjycommunity.system.domain.SysDept>
     */
    @Override
    public List<SysDept> selectDeptList(SysDept dept) {
        return deptMapper.selectDeptList(dept);
    }
}
5)Controller层代码编写
@RestController
@RequestMapping("/system/dept")
public class SysDeptController extends BaseController {

    @Resource
    private ISysDeptService deptService;

    
    /**
     * 获取部门列表
     * @param sysDept 
     * @return: com.msb.hjycommunity.common.core.domain.BaseResponse
     */
    @GetMapping("/list")
    public BaseResponse list(SysDept sysDept){

        List<SysDept> sysDepts = deptService.selectDeptList(sysDept);
        return BaseResponse.success(sysDepts);
    }

}
5)根据就接口文档,使用PostMan进行接口测试
3.3.7.3 更改物业
  • 直接调用修改小区信息接口即可,具体查看接口文档

3.3.8 功能6:导出Excel

3.3.8.1 需求分析
  • 需求分析: 点击导出按钮,导出数据到Excel

image.png

3.3.8.2 导入依赖
<!-- 引入easyPOI -->
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-spring-boot-starter</artifactId>
            <version>4.2.0</version>
        </dependency>
3.3.8.3 编写工具类
public class ExcelUtils {

    private static final Logger log = LoggerFactory.getLogger(ExcelUtils.class);

    /**
     * excel 导出
     *
     * @param list         数据列表
     * @param pojoClass    pojo类型
     * @param fileName     导出时的excel名称
     * @param response
     * @param exportParams 导出参数(标题、sheet名称、是否创建表头,表格类型)
     */
    public static void exportExcel(List<?> list, Class<?> pojoClass, String fileName, HttpServletResponse response, ExportParams exportParams){
        //把数据添加到excel表格中
        Workbook workbook = ExcelExportUtil.exportExcel(exportParams, pojoClass, list);
        downLoadExcel(fileName, response, workbook);
    }

    /**
     * excel下载
     *
     * @param fileName 下载时的文件名称
     * @param response
     * @param workbook excel数据
     */
    private static void downLoadExcel(String fileName, HttpServletResponse response, Workbook workbook) {

        ServletOutputStream outputStream = null;

        try {
            response.setCharacterEncoding("UTF-8");
            // 设置文件名,下载方式(弹框下载)
            response.setHeader("content-disposition","attachment;fileName="+ URLEncoder.encode("小区信息列表.xls","UTF-8"));

            outputStream = response.getOutputStream();
            workbook.write(outputStream);
        } catch (Exception e) {
            log.error("导出Excel异常{}",e.getMessage());
            throw new BaseException("500","导出Excel失败,请联系网站管理员!");
        }finally {
            try {
                outputStream.close();
                workbook.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

response.setHeader(“Content-Disposition”, “attachment;filename=” + URLEncoder.encode(fileName + “.xlsx”, “UTF-8”));

这段代码 Content-disposition其实可以控制用户请求所得的内容存为一个文件的时候提供一个默认的文件名,文件直接在浏览器上显示或者在访问时弹出文件下载对话框。

attachment:弹出对话框让用户下载:disposition-type是以什么方式下载,如attachment为以附件方式下载

3.3.8.4 用于导出DTO
@ExcelTarget("community")
@Data
public class HjyCommunityExcelDto  implements Serializable {


    /** 小区id */
    @Excel(name = "序号")
    private Long communityId;

    /** 小区名称 */
    @Excel(name = "小区名称")
    private String communityName;

    /** 小区编码 */
    @Excel(name = "小区编码")
    private String communityCode;

    /** 省 */
    @Excel(name = "省")
    private String communityProvinceName;

    /** 市 */
    @Excel(name = "市")
    private String communityCityName;

    /** 区 */
    @Excel(name = "区/县")
    private String communityTownName;

    /** 创建时间 */

    @Excel(name="创建时间",exportFormat = "yyyy年MM月dd日")
    private Date createTime;

    /** 备注 */
    @Excel(name = "备注")
    private String remark;
}
3.3.8.5 编写Controller

**所在包: **com.msb.hjycommunity.web.controller.common.ExportExcelController

@Controller
@RequestMapping("/exportExcel")
public class ExportExcelController extends BaseController {

    @Resource
    private HjyCommunityService hjyCommunityService;

    /**
     * 导出小区数据
     * @param hjyCommunity
     * @return: com.msb.hjycommunity.common.core.domain.BaseResponse
     */
    @GetMapping("/exportCommunityExcel")
    public BaseResponse exportExcel(HjyCommunity hjyCommunity, HttpServletResponse response) {
        startPage();
        List<HjyCommunityDto> list = hjyCommunityService.selectHjyCommunityList(hjyCommunity);

        List<HjyCommunityExcelDto> excelDtoList = list.stream().map(
                hjyCommunityDto -> {
                    HjyCommunityExcelDto excelDto = new HjyCommunityExcelDto();
                    excelDto.setCommunityId(hjyCommunityDto.getCommunityId());
                    excelDto.setCommunityName(hjyCommunityDto.getCommunityName());
                    excelDto.setCommunityCode(hjyCommunityDto.getCommunityCode());
                    excelDto.setCommunityProvinceName(hjyCommunityDto.getCommunityProvinceName());
                    excelDto.setCommunityCityName(hjyCommunityDto.getCommunityCityName());
                    excelDto.setCommunityTownName(hjyCommunityDto.getCommunityTownName());
                    excelDto.setCreateTime(hjyCommunityDto.getCreateTime());
                    excelDto.setRemark(hjyCommunityDto.getRemark());

                    return excelDto;
                }
        ).collect(Collectors.toList());

        ExcelUtils.exportExcel(excelDtoList, HjyCommunityExcelDto.class,"小区信息",
                response,new ExportParams("小区信息列表", "小区信息"));

        return BaseResponse.success("导出成功");
    }
}
3.3.8.6 测试导出功能

通过浏览器直接测试即可

http://localhost:9999/hejiayun/exportExcel/exportCommunityExcel?pageNum=1&pageSize=10

3.3.9 功能7: 小区下拉列表展示

3.3.3.9.1 需求分析
  • 在小区列表左侧有一个下拉列表,可以切换小区.

image.png

3.3.3.9.2 功能编写
1) 添加VO类
public class HjyCommunityVo{
 
     /** 小区id */
     @JsonFormat(shape = JsonFormat.Shape.STRING)
     private Long communityId;
 
     /** 小区名称 */
     private String communityName;
 
 //get...set...toString...
 }
2) 添加对象复制工具类

对象复制的类库工具有很多 Orika是目前性能最强,同时也最流行的对象映射工具,Orika底层采用了javassist类库生成Bean映射的字节码,之后直接加载执行生成的字节码文件,在速度上比使用反射进行赋值会快很多。

阿里巴巴开发手册上强制规定避免使用Apache BeanUtils

  • 原因在于 Apache BeanUtils底层源码为了追求完美,加了过多的包装,使用了很多反射,做了很多校验,所以导致性能较差

image.png

  • 添加依赖
<!-- 对象属性拷贝 -->
 <dependency>
     <groupId>ma.glasnost.orika</groupId>
     <artifactId>orika-core</artifactId>
     <version>1.5.4</version>
 </dependency>
  • 添加工具类, 直接从课程资料中导入即可
/**
  * 对象复制工具类
  * @author spikeCong
  * @date 2023/4/1
  **/
 public class OrikaUtils {
 
     //构造一个MapperFactory
     private static final MapperFactory FACTORY = new DefaultMapperFactory.Builder().build();
 
     /**
      * 缓存实例集合
      */
     private static final Map<String, MapperFacade> CACHE_MAPPER = new ConcurrentHashMap<>();
 
     private final MapperFacade mapper;
 
     public OrikaUtils(MapperFacade mapper) {
         this.mapper = mapper;
     }
     
     //省略......
 }
3) Service层编写
/**
      * 获取小区下拉列表
      * @param community
      * @return: com.msb.hjycommunity.community.domain.vo.HjyCommunityVo
      */
 List<HjyCommunityVo> queryPullDown(HjyCommunity community);
 
 
 
 @Override
 public List<HjyCommunityVo> queryPullDown(HjyCommunity community) {
 
     List<HjyCommunityDto> dtoList = hjyCommunityMapper.queryList(community);
 
     List<HjyCommunityVo> voList = dtoList.stream().map(dto -> {
         //对象拷贝
         HjyCommunityVo communityVo = OrikaUtils.convert(dto, HjyCommunityVo.class);
         return communityVo;
     }).collect(Collectors.toList());
 
 
     return voList;
 }
4) Controller层编写
/**
      * 小区下拉列表展示
      * @param hjyCommunity
      * @return: com.msb.hjycommunity.common.core.domain.BaseResponse
      */
 @GetMapping("/queryPullDown")
 public BaseResponse queryPullDown(HjyCommunity hjyCommunity){
 
     //打印入参日志
     log.info("log() called with parameters => [hjyCommunity = {}]",hjyCommunity);
 
     List<HjyCommunityVo> voList = null;
     try {
         voList = hjyCommunityService.queryPullDown(hjyCommunity);
     } catch (Exception e) {
         //e.printStackTrace();
         log.warn("获取小区下拉列表失败! !",e);
     }
 
     //打印返回结果
     log.info("log() returned: {}",voList);
     return BaseResponse.success(voList);
 }

Logback 为日志配置颜色搭配

<property name="log.pattern"
               value="%red(%date{yyyy-MM-dd HH:mm:ss}) %highlight(%-5level) %red([%thread]) %boldMagenta(%logger{50}) %cyan(%msg%n)"/>

3.4 EasyPOI实战

3.4 EasyPOI实战

Apache POI是Apache软件基金会的开源项目,POI提供API给Java程序对Microsoft Office格式档案读和写的功能。

Apache POI 代码实现复杂,学习成本较高。

Easypoi 功能如同名字easy,主打的功能就是容易,让一个没见接触过poi的人员 就可以方便的写出Excel导出,Excel模板导出,Excel导入,Word模板导出,通过简单的注解和模板 语言(熟悉的表达式语法),完成以前复杂的写法

EasyPOI官网:https://easypoi.mydoc.io/

3.4.2 环境搭建

1)搭建springboot项目

2)导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.8</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.mashibing</groupId>
    <artifactId>easy_poi</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>


        <!--引入mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <!--引入mysql-->
        <!-- Mysql驱动包 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.32</version>
        </dependency>

        <!--引入druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.19</version>
        </dependency>
        <!--引入thymelaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--引入easypoi-->
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-base</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-web</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-annotation</artifactId>
            <version>3.2.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3.4.3 EasyPOI相关注解

easypoi 起因就是Excel的导入导出,最初的模板是实体和Excel的对应,model–row,filed–col 这样利用注解我们可以和容易做到excel到导入导出 经过一段时间发展,现在注解有5个类分别是

  • @Excel 作用到filed上面,是对Excel一列的一个描述
  • @ExcelCollection 表示一个集合,主要针对一对多的导出,比如一个老师对应多个科目,科目就可以用集合表示
  • @ExcelEntity 表示一个继续深入导出的实体,但他没有太多的实际意义,只是告诉系统这个对象里面同样有导出的字段
  • @ExcelIgnore 和名字一样表示这个字段被忽略跳过这个导导出
  • @ExcelTarget 这个是作用于最外层的对象,描述这个对象的id,以便支持一个对象可以针对不同导出做出不同处理
@ExcelTarget

1.说明

- 用在实体类上标识是一个可以通过EasyPOI导入导出的实体类
- 相关属性:
value:  [String][定义id唯一标识,不能重复]    `常用`
height: [Double][定义单元格高度]
fontSize:[short ][定义单元格字体大小]

2.使用

@ExcelTarget("users")
public class User implements Serializable {
 //..... 省略属性 相关GET,SET方法
}
@Excel
# 1.说明
- 用在filed(属性)上面,是对Excel一列的一个描述
- 常用属性: 
name :  [String][生成Excel表格中列名]
needMerge: [boolean][是否需要纵向合并单元格(用于含有list中,单个的单元格,合并list创建的多个row)]
orderNum :       [String][指定生成Excel中列的顺序,按照数字自然顺序排序]
savePath :       [String][指定导入Excel中图片的保存路径]
type :       [String][导出类型 1 是文本 2 是图片,3 是函数,10 是数字 默认是文本]
width    : [Double][指定导出Excel时列的宽度]
isImportField:   [boolean][是否是导入字段,如果没有说明是错误的Excel]
exportFormat:    [String][导出Excel的时间格式]
importFormat:    [String][导入Excel的时间格式]
format :       [String][相当于同时设置了exportFormat和importFormat]
imageType:   [int   ][导出类型 1 从file读取 2 是从数据库中读取 默认是文件 同样导入也是一样的]
suffix :       [String][文字后缀,如% 90 变成90%]

2.使用

public class User implements Serializable {

    @Excel(name="编号",orderNum="1",replace = {"xxx_1","nnn_2"})
    private String id;
    
    @Excel(name="姓名",orderNum="2")
    private String name;

    @Excel(name="年龄",orderNum="4",suffix = " $")
    private Integer age;

    @Excel(name="生日",orderNum = "3",width = 20.0,exportFormat = "yyyy年MM月dd日")
    private Date bir;
  //...省略GET、SET方法
}
@ExcelEntity

1.说明

- 标记是不是导出excel 标记为实体类,一遍是一个内部属性类,标记是否继续穿透
- 常用属性:
name: [String][定义唯一标识]

2.使用

@ExcelTarget("users")
public class User implements Serializable {
//... 省略GET SET和其他属性
    @ExcelEntity(name="身份信息")
    private Card card;
}

@ExcelTarget("card")
public class Card  implements Serializable {
    @Excel(name="身份证号",orderNum = "6")
    private String id;
    @Excel(name="家庭住址",orderNum = "7")
    private String address;
}
@ExcelCollection

1.说明

- 一对多的集合注解,用以标记集合是否被数据以及集合的整体排序
- 常用属性:
name: [String][定义集合列名]
orderNum:[int][用来指定导出excel集合内列的顺序]
type:     [Class\<?>][用来指定导出是创建对象类型]

2.使用

@ExcelTarget("users")
public class User implements Serializable {    
  //....省略GET SET其他属性
@ExcelCollection(name="订单",orderNum = "5")
    private List<Order> orders;
}

@ExcelTarget("orders")
public class Order implements Serializable {
//....省略GET SET方法
    @Excel(name = "订单编号")
    private String id;
    @Excel(name = "订单名称")
    private String name;
}
@ExcelIgnore

1.说明

  • 用在属性上,导出Excel时忽略这个属性

3.4.4 导出Excel

3.4.4.1 导出基本数据

注意:导出Excel的对象必须实现对象序列化接口

1)定义对象

@Data
@ExcelTarget("users")
public class User implements Serializable {

    @ExcelIgnore
    @Excel(name="编号",orderNum="1")
    private String id;

    @Excel(name="姓名",orderNum="2")
    private String name;

    @Excel(name="年龄",orderNum="3",suffix = " &")
    private Integer age;

    @Excel(name="生日",orderNum="4",width = 35.0,format = "yyyy-MM-dd HH:mm:ss")
    private Date birthday;

    @Excel(name ="状态" ,orderNum="5",replace = {"激活_1","未激活_0"})
    private String status;
}

2)定义测试数据

public List<User> getUsers(){

        List<User> users = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setId(String.valueOf(i));
            user.setName("向阳");
            user.setAge(16+i);
            user.setBirthday(new Date());
            user.setStatus(String.valueOf(i%2));
            users.add(user);
        }

        return users;
    }

3)导出Excel

@Test
    public void testExport() throws Exception {

        //1.配置对象 2.导出类型 3.导出数据集合
        Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams("用户列表", "测试"), User.class, getUsers());

        FileOutputStream outputStream = new FileOutputStream("C:\\Users\\86187\\Desktop\\user.xls");
        workbook.write(outputStream);

        outputStream.close();
        workbook.close();
    }

4)查看Excel

image.png

3.4.4.2 导出List集合

1)说明

- 往往有时候导出的对象中含有数组或者集合,需要导出这样的数据可以直接使用@Excel进行导出

2)使用

@Data
@ExcelTarget("users")
public class User implements Serializable {

    @Excel(name = "爱好",width=20.0,orderNum = "6")
    private List<String> hobby;
}

image.png

2) 改换格式

//    @Excel(name = "爱好",width=20.0,orderNum = "6")
    @ExcelIgnore
    private List<String> hobby;

    @Excel(name = "爱好",width=20.0,orderNum = "6")
    private String hobbystr;

    //自定义格式
    public String getHobbystr() {

        StringBuilder sb = new StringBuilder();
        this.hobby.forEach(s -> sb.append(s).append("、") );

        return sb.toString();
    }

image.png

3.4.4.3 导出对象中含有的对象

1) 说明

- 导出对象中含有对象的Excel
@Data
@ExcelTarget("users")
public class User implements Serializable {

    //定义对象
    @ExcelEntity(name="card")
    private Card card;
}

@ExcelTarget("card")
public class Card  implements Serializable {
    @Excel(name="身份证号",orderNum = "6")
    private String id;
    @Excel(name="家庭住址",orderNum = "7")
    private String address;
}

2) 为导出对象赋值

public List<User> getUsers(){
    List<User> users = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        User user = new User();
        user.setId(String.valueOf(i));
        user.setName("向阳");
        user.setAge(16+i);
        user.setBirthday(new Date());
        user.setStatus(String.valueOf(i%2));
        user.setHobby(Arrays.asList("抽烟","喝酒","烫头"));
        user.setCard(new Card("11000103422323212342","北京市朝阳区"));
        users.add(user);
    }
    return users;
}

3) 导出Excel

//1.配置对象 2.导出类型 3.导出数据集合
Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams("用户列表", "测试"), User.class, getUsers());
FileOutputStream outputStream = new FileOutputStream("C:\\Users\\86187\\Desktop\\user.xls");
workbook.write(outputStream);
outputStream.close();
workbook.close();

image.png

3.4.4.4 导出图片

1) 说明

- 往往随着业务不断变化,可能需要在导出excel时将图片信息也一并导出,如商品图标,用户头像信息等数据,这个时候easypoi该如何处理呢?
@ExcelTarget("users")
public class User implements Serializable {
    
    @Excel(name = "头像信息",type = 2,orderNum = "0",width = 12,height = 12)
    private String photo;//定义头像 直接写指定图片路径
}

2) 准备图片放入指定路径中,并在测试数据中进行赋值

public List<User> getUsers(){
    List<User> users = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        User user = new User();
        user.setId(String.valueOf(i));
        user.setName("向阳");
        user.setAge(16+i);
        user.setBirthday(new Date());
        user.setStatus(String.valueOf(i%2));
        user.setHobby(Arrays.asList("抽烟","喝酒","烫头"));
        user.setCard(new Card("11000103422323212342","北京市朝阳区"));
        user.setPhoto("C:\\Users\\86187\\Desktop\\桌面.png");
        users.add(user);
    }
    return users;
}

image.png

3) 导出Excel查看结果

image.png

3.4.5 导入Excel

3.4.5.1 导入基本数据

1) 准备导入的目标Excel

2) 定义导出数据基本对象

@Data
@ExcelTarget("emps")
public class Emp  implements Serializable {

    @Excel(name="编号")
    private String id;
    @Excel(name="姓名")
    private  String  name;
    @Excel(name="年龄")
    private Integer age;
    @Excel(name="生日",format = "yyyy-MM-dd HH:mm:ss")
    private Date bir;
    @Excel(name="状态",replace = {"激活_1","未激活_0"})
    private String status;

    @Excel(name="头像",type = 2,savePath = "I:\\MSB\\msb_hejiayun_lk\\easy_poi\\src\\main\\resources\\static")
    private String photo;
}

3) 导入excel中数据

@Test
    public void testImportExcel()throws Exception{

        ImportParams params = new ImportParams();
        params.setTitleRows(1); //标题列占几行
        params.setHeadRows(1); //列名占几行
        params.setNeedSave(true);
        params.setSaveUrl("I:\\msb_hejiayun\\easypoi_boot\\src\\main\\resources\\static");
  
        List<Emp> list = ExcelImportUtil.importExcel(new FileInputStream("C:\\Users\\86187\\Desktop\\emp.xls"), Emp.class, params);
        list.forEach(System.out::println);
    }

3.4.6 多Sheet页导入导出

3.4.6.1 技巧说明
- 读取指定的sheet
比如要读取上传得第二个sheet 那么需要把startSheetIndex = 1 就可以了

- 读取几个sheet 
比如读取前2个sheet,那么 sheetNum=2 就可以了

- 读取第二个到第五个sheet
设置 startSheetIndex = 1 然后sheetNum = 4

- 读取全部的sheet
sheetNum  设置大点就可以了

- 判断一个Excel是不是合法的Excel 
importFields 设置下值,就是表示表头必须至少包含的字段,如果缺一个就是不合法的excel,不导入
3.4.6.2 准备表格,创建对应实体
  • sheet

image.png

  • sheet2

image.png

  • ** LoginUser**
@Data
@ExcelTarget("loginUser")
public class LoginUser implements Serializable {

    @Excel(name = "用户ID",orderNum = "1")
    private String id;

    @Excel(name = "昵称",orderNum = "2")
    private String nickname;

    @Excel(name = "密码",orderNum = "3")
    private String password;

    @Excel(name="注册时间",orderNum="4",format = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    @Excel(name ="状态" ,orderNum="5",replace = {"VIP_1","普通用户_0"})
    private String status;

}
  • LoginUrl
@Data
@ExcelTarget("loginUrl")
public class LoginUrl implements Serializable {

    @Excel(name = "用户ID",orderNum = "1")
    private String userId;

    @Excel(name = "请求类型",orderNum = "2")
    private String type;

    @Excel(name = "访问地址",orderNum = "3")
    private String url;
}
3.4.6.3 多sheet导入方法
/**
     * 功能描述:根据接收的Excel文件来导入多个sheet,根据索引可返回一个集合
     * @param filePath   导入文件路径
     * @param sheetIndex  导入sheet索引
     * @param titleRows  表标题的行数
     * @param headerRows 表头行数
     * @param pojoClass  Excel实体类
     * @return
     */
    public static <T> List<T> importMultiSheet(String filePath,int sheetIndex,Integer titleRows, Integer headerRows, Class<T> pojoClass) {

        // 根据file得到Workbook,主要是要根据这个对象获取,传过来的excel有几个sheet页
        ImportParams params = new ImportParams();

        // 第几个sheet页
        params.setStartSheetIndex(sheetIndex);
        params.setTitleRows(titleRows);
        params.setHeadRows(headerRows);
        
        //是否保存本次上传的excel
        params.setNeedSave(false);

        //表示表头必须包含的字段,不包含 就报错.
        params.setImportFields(new String[]{"用户ID"});

        List<T> list = null;

        try {
            list = ExcelImportUtil.importExcel(new FileInputStream(filePath), pojoClass, params);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return list;
    }
//测试多sheet导入
    @Test
    public void testImportMultiSheet() throws Exception {

        String excelPath = "C:\\Users\\86187\\Desktop\\login.xls";

        List<LoginUser> loginUserList = this.importMultiSheet(excelPath, 0, 1, 1, LoginUser.class);
        loginUserList.forEach(System.out::println);

        List<LoginUrl> loginUrlList = this.importMultiSheet(excelPath, 1, 1, 1, LoginUrl.class);
        loginUrlList.forEach(System.out::println);

    }
3.4.6.4 多sheet导出方法
public void exportMultiSheet(Object... objects) throws Exception {

        //创建参数对象,用于设定Excel的sheet页内容等信息
        ExportParams loginUserExportParams = new ExportParams();
        //设置sheet的名称
        loginUserExportParams.setSheetName("登录用户");
        loginUserExportParams.setTitle("登录用户列表");

        //使用map创建sheet1
        HashMap<String, Object> sheet1Map = new HashMap<>();
        //设置title
        sheet1Map.put("title",loginUserExportParams);
        //设置导出的实体类型
        sheet1Map.put("entity",LoginUser.class);
        //sheet中要填充的数据
        sheet1Map.put("data",objects[0]);

        ///==================================

        //创建参数对象,用于设定Excel的sheet页内容等信息
        ExportParams loginUrlExportParams = new ExportParams();
        //设置sheet的名称
        loginUrlExportParams.setSheetName("URL路径");
        loginUrlExportParams.setTitle("URL路径");

        //使用map创建sheet2
        HashMap<String, Object> sheet2Map = new HashMap<>();
        //设置title
        sheet2Map.put("title",loginUrlExportParams);
        //设置导出的实体类型
        sheet2Map.put("entity",LoginUrl.class);
        //sheet中要填充的数据
        sheet2Map.put("data",objects[1]);

        //将sheet1和sheet2 进行包装
        List<Map<String,Object>> sheetList = new ArrayList<>();
        sheetList.add(sheet1Map);
        sheetList.add(sheet2Map);

        //执行方法
        Workbook workbook = ExcelExportUtil.exportExcel(sheetList, ExcelType.HSSF);

        FileOutputStream outputStream =
                new FileOutputStream("C:\\Users\\86187\\Desktop\\exportLogin.xls");
        workbook.write(outputStream);
        outputStream.close();
        workbook.close();
    }
//测试多sheet导入
    @Test
    public void testExportMultiSheet() throws Exception {


        List<LoginUser> sheet1 = new ArrayList<>();
        sheet1.add(new LoginUser("1001", "向阳", "123456", new Date(), "0"));
        sheet1.add(new LoginUser("1002", "文渊", "123456", new Date(), "1"));
        sheet1.add(new LoginUser("1003", "小李", "123456", new Date(), "0"));

        List<LoginUrl> sheet2 = new ArrayList<>();
        sheet2.add(new LoginUrl("1001", "get", "http://127.0.0.1:8080"));
        sheet2.add(new LoginUrl("1001", "post", "http://127.0.0.1:8080/logingout"));


        exportMultiSheet(sheet1,sheet2);
    }

3.4.6 集成web实现导入导出

3.4.6.1 环境搭建

1) 引入依赖

2) 创建包结构

image.png

3) 启动类

@SpringBootApplication
public class EasypoiApplication {

    public static void main(String[] args) {
        SpringApplication.run(EasypoiApplication.class,args);
    }
}

4) 编写配置文件

server.port=8088
spring.application.name=easypoi

spring.thymeleaf.cache=false

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url= jdbc:mysql://127.0.0.1:3306/easypoi?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

mybatis.mapper-locations=classpath:com/mashibing/mapper/*.xml
mybatis.type-aliases-package=com.mashibing.entity

5) html页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>导入excel的主页面</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
<div class="container-fluid">
    <div class="row">
        <div class="col-md-12">
            <h1>选择Excel文件导入到数据中</h1>
            <form th:action="@{/course/importExcel}" method="post" enctype="multipart/form-data" class="form-inline">
                <div class="form-group">
                    <input class="form-control" type="file" name="excelFile">
                    <input type="submit" class="btn btn-danger" value="导入数据">
                </div>
            </form>
        </div>
        <div class="col-md-12">
            <h1>显示导入数据列表</h1>
            <table class="table table-bordered" >
                <tr>
                    <th>ID编号</th>
                    <th>订单编号</th>
                    <th>课程名称</th>
                    <th>课程简介</th>
                    <th>课程价格</th>
                </tr>
                <tr th:each="course : ${courses}">
                    <td th:text="${course.cid}"></td>
                    <td th:text="${course.orderno}"></td>
                    <td th:text="${course.cname}"></td>
                    <td th:text="${course.brief}"></td>
                    <td th:text="${course.price}"></td>
                </tr>
            </table>

            <hr>
            <a th:href="@{/course/exportExcel}" class="btn btn-info" >导出excel</a>

        </div>

    </div>
</div>
</body>
</html>
3.4.6.2 查询所有

1)准备数据Excel

CREATE TABLE `course` (
  `cid` bigint(20) NOT NULL AUTO_INCREMENT,
  `orderno` bigint(20) DEFAULT NULL,
  `cname` varchar(50) DEFAULT NULL,
  `brief` varchar(50) DEFAULT NULL,
  `price` double DEFAULT NULL,
  PRIMARY KEY (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8

2) 创建实体类

@Data
@ExcelTarget("courses")
public class Course implements Serializable {

    @Excel(name = "编号")
    private String cid;

    @Excel(name = "订单编号")
    private String orderno;

    @Excel(name = "课程名称")
    private String cname;

    @Excel(name = "简介")
    private String brief;

    @Excel(name = "价格")
    private double price;
}

2) 创建Dao接口

@Mapper
public interface CourseDao {

    //查询所有
    List<Course> findAll();
}
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mashibing.mapper.CourseDao">
    <!--查询所有-->
    <select id="findAll" resultType="Course">
        SELECT cid,orderno,cname,brief,price FROM course;
    </select>
</mapper>

3) 创建Service

public interface CourseService  {

    //查询所有
    List<Course> findAll();
}


@Service
public class CourseServiceImpl implements CourseService {

    @Autowired
    private CourseDao courseDao;

    @Override
    public List<Course> findAll() {
        return courseDao.findAll();
    }
}

4) 创建Controller

@Controller
@RequestMapping("/course")
public class CourseController {

    @Autowired
    private CourseService courseService;

    @RequestMapping("/findAll")
    public String findAll(Model model){
        List<Course> courses = courseService.findAll();
        System.out.println(courses);
        model.addAttribute("courses",courses);
        return "index";
    }
}

5) 访问: http://localhost:8088/course/findAll

image.png

向数据库添加一条数据,测试一下 即可.

3.4.6.3 导入数据

1) Dao接口

@Mapper
public interface CourseDao {

    //查询所有
    List<Course> findAll();

    //插入记录
    void save(Course course);
}
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mashibing.mapper.CourseDao">
    <!--查询所有-->
    <select id="findAll" resultType="Course">
        SELECT cid,orderno,cname,brief,price FROM course;
    </select>

    <!-- 插入记录 -->
    <insert id="save" parameterType="Course" >
        INSERT INTO course VALUES (#{cid},#{orderno},#{cname},#{brief},#{price})
    </insert>
</mapper>

2) 创建Service

public interface CourseService  {

    //查询所有
    List<Course> findAll();

    //插入记录
    void save(List<Course> courses);
}

@Service
public class CourseServiceImpl implements CourseService {

    @Autowired
    private CourseDao courseDao;

    @Override
    public List<Course> findAll() {
        return courseDao.findAll();
    }

    @Override
    public void save(List<Course> courses) {
        courses.forEach(course -> {
            course.setCid(null); //自动生成ID 不使用Excel中的编号
            courseDao.save(course);
        });
    }
}

3) 创建Controller

@Controller
@RequestMapping("/course")
public class CourseController {

    @Autowired
    private CourseService courseService;

    @RequestMapping("/findAll")
    public String findAll(Model model){
        List<Course> courses = courseService.findAll();
        System.out.println(courses);
        model.addAttribute("courses",courses);
        return "index";
    }


    @RequestMapping("/importExcel")
    public String importExcel(MultipartFile excelFile) throws Exception {
        ImportParams params = new ImportParams();
        params.setTitleRows(1); //设置1级标题行为一行
        params.setHeadRows(1); //设置header标题行为一行

        List<Course> courseList = ExcelImportUtil.importExcel(excelFile.getInputStream(), Course.class, params);
        courseService.save(courseList);
        return "redirect:/course/findAll";
    }

}

4) html页面

<div class="col-md-12">
    <h1>选择Excel文件导入到数据中</h1>
    <form th:action="@{/course/importExcel}" method="post" enctype="multipart/form-data" class="form-inline">
        <div class="form-group">
            <input class="form-control" type="file" name="excelFile">
            <input type="submit" class="btn btn-danger" value="导入数据">
        </div>
    </form>
</div>

5) 启动项目导入Excel数据

image.png

3.4.6.3 导出数据

1) Controller开发

//导出Excel
    @RequestMapping("/exportExcel")
    public void exportExcel(HttpServletResponse response)throws Exception{
        //查询数据库的所有数据
        List<Course> courseList = courseService.findAll();

        //生成Excel
        Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams("课程信息列表", "课程信息"),
                Course.class, courseList);
        response.setHeader("content-disposition","attachment;fileName="+ URLEncoder.encode("课程信息列表.xls","UTF-8"));
        ServletOutputStream outputStream = response.getOutputStream();
        workbook.write(outputStream);

        outputStream.close();
        workbook.close();
    }

2) HTML页面

<a th:href="@{/course/exportExcel}" class="btn btn-info" >导出excel</a>

3) 启动项目测试

image.png

3.5 SpringBoot 日志实战

3.5.1 日志框架分类与选择

3.5.1.1 日志框架的分类
日志门面 (日志抽象)日志实现
JCL(Jakarta Commons Logging) SLF4J(Simple Logging Facade for Java)Jul(Java Util Logging) , Log4j , Log4j2 , Logback

记录型日志框架

  1. Jul (Java Util Logging):JDK中的日志记录工具,也常称为JDKLog、jdk-logging,自Java1.4以来的官方日志实现。
  2. Log4j:Apache Log4j是一个基于Java的日志记录工具。它是由Ceki Gülcü首创的,现在则是Apache软件基金会的一个项目。 Log4j是几种Java日志框架之一。
  3. Log4j2:一个具体的日志实现框架,是Log4j 1的下一个版本,与Log4j 1发生了很大的变化,Log4j 2不兼容Log4j 1
  4. Logback:一个具体的日志实现框架,和Slf4j是同一个作者,但其性能更好(推荐使用)。

门面型日志框架

  1. JCL:Apache基金会所属的项目,是一套Java日志接口,之前叫Jakarta Commons Logging,后更名为Commons Logging
  2. SLF4J:是一套简易Java日志门面,本身并无日志的实现。(Simple Logging Facade for Java,缩写Slf4j)
3.5.1.2 日志框架的选择

选择日志框架的方式,就是先选择一个门面(抽象层) ,然后再选择一个实现

  • **日志门面: SLFJ **
  • 日志实现: LogBack

Spring默认使用JCL

SpringBoot默认选用的是SLF4J和 Logback.

Slf4j的设计思想比较简洁,使用了Facade设计模式,Slf4j本身只提供了一个slf4j-api-version.jar包,这个jar中主要是日志的抽象接口,jar中本身并没有对抽象出来的接口做实现。

对于不同的日志实现方案(例如Logback,Log4j…),封装出不同的桥接组件(例如logback-classic-version.jar,slf4j-log4j12-version.jar),这样使用过程中可以灵活的选取自己项目里的日志实现。

这里提到的门面模式和桥接模式,如果想要了解的同学, 可以去看本人录制的 <<23种设计模式精讲>>

3.5.2 SLF4J日志框架

3.5.2.1 如何使用SLF4j

日志方法的调用,不要直接调用日志的实现类, 而是调用日志抽象层里的方法 (面向抽象编程).

阿里的开发手册上有一条关于日志的规范:

  • **强制:应用中不可直接使用日志系统(log4j、logback)中的 API ,而应依赖使用日志框架 SLF4J 中的 API 。使用门面模式的日志框架,有利于维护和各个类的日志处理方式的统一。 **

image.png

日志框架之间的关系

  • Slf4j的设计思想比较简洁,使用了Facade设计模式,Slf4j本身只提供了一个slf4j-api-version.jar包,这个jar中主要是日志的抽象接口,jar中本身并没有对抽象出来的接口做实现。

image.png

代码示例

import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class HelloWorld {
 
   public static void main(String[] args) {
     Logger logger = LoggerFactory.getLogger(HelloWorld.class);
     logger.info("Hello World");
   }
 }

配置文件

  • **每一个日志实现框架都有自己的配置文件,使用slf4j之后,**配置文件还是选用具体日志实现框架的配置文件.
3.5.2.2 其他日志框架转换为slf4j
  • Slf4j的设计思想比较简洁,使用了Facade设计模式,Slf4j本身只提供了一个slf4j-api-version.jar包,这个jar中主要是日志的抽象接口,jar中本身并没有对抽象出来的接口做实现。
  • 对于不同的日志实现方案(例如Log4j…),封装出不同的桥接组件(例如: slf4j-log4j12-version.jar),这样使用过程中可以灵活的选取自己项目里的日志实现。

SLF4j与其它日志组件集成

image.png

应用调了sl4j-api,日志门面接口。日志门面接口本身通常并没有实际的日志输出能力,它底层还是需要去调用具体的日志框架API的,也就是实际上它需要跟具体的日志框架结合使用。由于具体日志框架比较多,而且互相也大都不兼容,日志门面接口要想实现与任意日志框架结合可能需要对应的桥接器,上图红框中的组件即是对应的各种桥接器!

3.5.3 SpringBoot结合logback输出日志

3.5.3.1 SpringBoot日志关系

在SpringBoot中,底层是Spring框架,Spring框架默认使用JCL,而****SpringBoot默认集成的日志框架使用的是SLF4j+Logback组合

**因为 **spring-boot-starter-logging 是Logback的日志实现,而Spring Boot启动项spring-boot-starter又依赖了spring-boot-starter-logging,所以Spring Boot就默认集成了Logback。

image.png

SpringBoot默认集成了Logback,可以开箱即用,非常方便。在基于SpringBoot实现的系统中,使用SLF4j方法如下:

日志记录方法的调用,不应该来直接调用日志的实现类,而是应该调用日志抽象层的方法,即直接使用SLF4j日志门面。

在代码中使用的方式如下:

@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class LogDemo {

    private final Logger logger = LoggerFactory.getLogger(LogDemo.class);


    @Test
    public void testLog(){
        Logger logger = LoggerFactory.getLogger(LogDemo.class);
        logger.info("Hello LogBack!");
    }
}
 
 //打印结果
 15:16:26.759 [main] INFO  c.m.h.log.LogDemo - [main,15] - Hello LOG !

image.png

3.5.3.2 logback配置文件

在resources文件夹下创建 logback.xml,logback会自动在该目录下加载该配置文件,在该文件中对打印日志的级别、形式、保存路径等进行配置。

https://www.springcloud.cc/spring-boot.html#boot-features-logging-format

image.png

如果可能,我们建议您使用 -spring变体进行日志记录配置(例如,logback-spring.xml而不是 logback.xml)。如果使用标准配置位置,Spring无法完全控制日志初始化。

<configuration  scan="true" scanPeriod="60 seconds" debug="false">
3.5.3.2.1 configuration 根节点

configuration 根节点包含的属性:

  • scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
  • scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。
  • debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
<configuration> 的子节点有contextName、property、appender、logger、root等,其中contextName和property是属性节点,appender、root、logger是
3.5.3.2.2 property节点

property标签可用于自定义属性,比如定义一个property name=“log.pattern”,然后使用${log.pattern}去引用它。

<!-- 日志存放路径 -->
 <property name="log.path" value="/home/hejiayun/logs" />
 <!-- 日志输出格式 -->
 <property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />

日志输出格式说明

%d表示时间
 
 %thread表示线程名
 
 %-5level 表示日志级别,允许以五个字符长度输出
 
 %logger{20}表示具体的日志输出者,比如类名,括号内表示长度
 
 %method:表示方法名字
 
 %line:表示第几行
 
 %msg表示具体的日志消息,就是logger.info("xxx")中的xxx
 
 %n表示换行
3.5.3.2.3 appender节点

是负责写日志的组件,在这里可以理解为一个日志的渲染器,比如console日志选择器、文件渲染器。有两个必要属性name和class:

  • name指定的名称,表示该渲染器的名字。
  • class指定的全限定名,表示使用的输出策略,常见的有控制台输出策略和文件输出策略。

appender日志输出方式实现类:ConsoleAppender、FileAppender、RollingFileAppender、SocketAppender、SMTPAppender、DBAppender、SyslogAppender、SiftingAppender等.

平时主要使用的是 ConsoleAppenderRollingFileAppender,其中 ConsoleAppender是往控制台打印日志,RollingFileAppender是往磁盘文件追加日志,而且可以按照一定的设置方式动态分割日志

1) 控制台输出–ConsoleAppender

<!-- 控制台输出 -->
 <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
     <encoder>
         <pattern>${log.pattern}</pattern>
     </encoder>
 </appender>

encoder表示输出格式

2) 文件输入RollingFileAppender

文件输出主要包括配置:以指定格式将日志输出到指定文件夹下的文件中,可以配置该文件的名称、最大大小、保存时间等。

<!-- 系统日志输出 -->
 <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
     <file>${log.path}/sys-info.log</file>
     <!-- 循环政策:基于时间创建日志文件 -->
     <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
         <!-- 日志文件名格式 -->
         <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
         <!-- 日志最大的历史 60天 -->
         <maxHistory>60</maxHistory>
     </rollingPolicy>
     <encoder>
         <pattern>${log.pattern}</pattern>
     </encoder>
     <filter class="ch.qos.logback.classic.filter.LevelFilter">
         <!-- 过滤的级别 -->
         <level>INFO</level>
         <!-- 匹配时的操作:接收(记录) -->
         <onMatch>ACCEPT</onMatch>
         <!-- 不匹配时的操作:拒绝(不记录) -->
         <onMismatch>DENY</onMismatch>
     </filter>
 </appender>
  1. <rollingPolicy>:当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名(重命名文件)。<rollingPolicy>的作用是当发生滚动时,定义RollingFileAppender的行为,其中上面的TimeBasedRollingPolicy是最常用的滚动策略,它根据时间指定滚动策略,既负责滚动也负责触发滚动,有以下节点:
  2. <fileNamePattern>,必要节点,包含文件名及"%d"转换符,"%d"可以包含一个Java.text.SimpleDateFormat指定的时间格式,如%d{yyyy-MM},如果直接使用%d那么格式为yyyy-MM-dd。RollingFileAppender的file子节点可有可无,通过设置file可以为活动文件和归档文件指定不同的位置。
  3. <maxHistory>,可选节点,控制保留的归档文件的最大数量,如果超出数量就删除旧文件,假设设置每个月滚动且 <maxHistory>是6,则只保存最近6个月的文件。
  4. <filter> logbcak允许给日志记录器appender配置一个或多个Filter(或者给整体配置一个或多个TurboFilter),来控制:当满足过滤器指定的条件时,才记录日志(或不满足条件时,拒绝记录日志)
    只记录ERROR级别的日志,其他级别的日志拒绝记录
     <filter class="ch.qos.logback.classic.filter.LevelFilter">
         <!-- 过滤的级别 -->
         <level>ERROR</level>
         <!-- 匹配时的操作:接收(记录) -->
         <onMatch>ACCEPT</onMatch>
         <!-- 不匹配时的操作:拒绝(不记录) -->
         <onMismatch>DENY</onMismatch>
     </filter>
    
3.5.2.3.3 logger节点

<logger>用来设置某一个包或者具体某一个类的日志打印级别、以及指定 <appender><logger>可以包含零个或者多个 <appender-ref>元素,标识这个appender将会添加到这个logger。<logger>仅有一个name属性、一个可选的level属性和一个可选的additivity属性:

<!-- 系统模块日志级别控制  -->
 <logger name="com.msb" level="info" />
 
 <!-- Spring日志级别控制  -->
 <logger name="org.springframework" level="warn" />
  • name:用来指定受此logger约束的某一个包或者具体的某一个类
  • level:用来设置打印级别,五个常用打印级别从低至高依次为TRACE、DEBUG、INFO、WARN、ERROR,如果未设置此级别,那么当前logger会继承上级的级别
  • additivity:是否向上级logger传递打印信息,默认为true。
3.5.2.3.4 root节点

root节点实际上是配置启用哪种appender,可以添加多个appender。<root>也是 <logger>元素,但是它是根logger,只有一个level属性,因为它的name就是ROOT。 如果没有指定logger,则那么所有的logger都会继承根logger的level。

<root level="info">
     <appender-ref ref="console" />
 </root>
 
 <!--系统操作日志-->
 <root level="info">
     <appender-ref ref="file_info" />
     <appender-ref ref="file_error" />
 </root>

appender-ref : 表示level为info级别,启用渲染器`CONSOLE


3.5.3.3 如何进行日志打印
3.5.3.3.1 日志打印级别
TRACE < DEBUG < INFO <  WARN < ERROR
  • TRACE : 级别最小,打印最详细,使用较少
  • DEBUG:主要用于调试阶段输出,此日志应尽可能详尽。DEBUG日志应该以调试人员满足调试需求为主,生产环境不应输出debug级别日志。
  • INFO:主要用来反馈系统当前状态的输出,应视为系统产品最终呈现给用户的一部分,因此不可滥用。INFO级别通常用来进行日常运维以及错误回溯时查看上下文场景。通过INFO日志可以了解系统的运行情况,对系统进行监控。
  • WARN:主要针对可预知,有解决预案的错误,如下单数量超过该用户所持总量的情况。WARN级别打印关键信息即可,事后用于分析错误原因。
  • ERROR:主要针对不可预知的信息,比如异常和错误。ERROR级别通常指一些较为严重的异常发生,所以需要尽可能多的打印出详细信息,如方法的入参,执行过程产生数据等。

image.png

详解:

  • 项目中设置的日志级别为:TRACE,包括 TRACE / DEBUG / INFO / WARN / ERROR 的日志级别都打印
  • 项目中设置的日志级别为:DEBUG ,包括 DEBUG / INFO / WARN / ERROR 的日志级别都打印
  • 项目中设置的日志级别为:INFO ,包括 INFO / WARN / ERROR 的日志级别都打印
  • 项目中设置的日志级别为:WARN,包括 WARN / ERROR 的日志级别都打印
  • 项目中设置的日志级别为:ERROR ,只打印 ERROR 的日志级别都打印
3.5.3.3.2 日志打印方式
@RunWith(SpringRunner.class)
 @SpringBootTest
 @Slf4j
 public class LogDemo {
 
     private final Logger logger = LoggerFactory.getLogger(LogDemo.class);
 
     /**
      * 传统方式实现日志
      */
     @Test
     public void test1(){
         logger.error("发生了严重错误,程序阻断了,需要立即处理,发送警报");
         logger.warn("这个错误很少见,不影响程序继续运行,酌情处理");
         logger.info("没有什么问题,单纯想打印个日志");
         logger.debug("你经常写BUG,测试的时候,多打点日志");
         logger.trace("这个级别很少用,为了追踪");
     }
 
     /**
      * Slf4j注解方式实现日志
      * 每次写新的类,就需要重新写logger,麻烦,可以使用@Slf4j注解简化:
      */
     @Test
     public void test2(){
         log.error("发生了严重错误,程序阻断了,需要立即处理,发送警报");
         log.warn("这个错误很少见,不影响程序继续运行,酌情处理");
         log.info("没有什么问题,单纯想打印个日志");
         log.debug("你经常写BUG,测试的时候,多打点日志");
         log.trace("这个级别很少用,为了追踪");
     }
 }
3.5.3.3.2 日志打印实操

日志打印的规范介绍

  • 输出Exceptions的全部Throwable信息
  • 出于日志文件储存和性能的考虑,建议为可能会大量出现的低级别日志添加判断
  • 尽量使用占位符而不是拼接来打印日志,这样的可读性更高,而且只有打印时才会处理函数,提升效率
@Test
     public void logNorm(){
 
         //使用{}作为占位符,而不是字符串拼接
         String name = "我是大佬";
         log.info("hello {}",name);
         log.debug("hello " + name);
 
         String userId = "10010";
         String orderId = "3242343253253535";
         log.debug("order is paying with userId:[{}] and orderId : [{}]",userId, orderId);
 
         // e.printStackTrace();不使用这种,打印堆栈日志与业务日志混合
         try {
             int i = 1 / 0;
         } catch (Exception e) {
 //            e.printStackTrace();
             log.error("/ by zero",e);
         }
 
         //先拼接字符串“hello”和“name”。然后执行debug方法,判断日志级别。
         log.debug("hello" + name);
 
         //不提前拼接,先判断日志级别, 然后选择是否执行debug方法,拼接字符串“hello”和“world”
         //isDebugEnabled() 可以避免无用的字符串操作,提高性能
         if(log.isDebugEnabled()){
             log.debug("hello" + name);
         }
     }

日志打印的注意事项

  • 尽量不要在日志中调用方法获取值,否则会因为日志而报出空指针异常。(日志不能打断业务逻辑)
  • 上线后除了进行常规的测试外,应可以通过对日志进行观察来判断新功能是否工作正常。
  • 对不同类型的日志进行分类输出,比如ERROR日志单独输出,防止日志数量过大时不利于分析错误信息。
  • 日志文件一般保留周期为15天,防止以周为单位的错误出现,应设置定时任务删除过期日志(如15天前)。
  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Add小兵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值