苍穹外卖02

今天继续苍穹外卖和无纸办公平台的技术对比,主要关注以下方面:

  1. 学习优雅的抓异常
  2. 插入时,当前用户的id怎么获取的
  3. 不同pojo对象属性拷贝
  4. 分页pagehelper对比
  5. 日期格式转换

1.插入user时,由于数据库用户名唯一,重复会报SQLIntegrityConstraintViolationException异常,由于无纸办公是我独立开发的,异常方面比较简陋,通常时直接抛出去完事了(哭)。我们可以看到苍穹外卖还是比较优雅的,也许这就是企业级开发应有的素质!

首先来看`GlobalExceptionHandler`类的定义:
```java
/**
 * 全局异常处理器,处理项目中抛出的业务异常
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕获业务异常
     * @param ex
     * @return
     */
    @ExceptionHandler
    public Result exceptionHandler(BaseException ex){
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }

    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        // Duplicate entry '13' for key 'idx_username'
        String message = ex.getMessage();
        if(message.contains("Duplicate entry")){
            String[] split = message.split(" ");
            String username = split[2];
            String msg = username + "已存在";
            return Result.error(msg);
        }else{
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }

}
```

我们可以看到有两个注解,分别时方法上的@ExceptionHandler和类上的@RestControllerAdvice

@RestControllerAdvice 是 Spring Boot 提供的增强 @ControllerAdvice 的注解,它的作用是对所有 @RestController 生效,即该类中的异常处理方法会影响整个项目中的 REST 接口。
@ExceptionHandler 作用于方法,告诉 Spring 这个方法是一个异常处理器。

也就是说,通过两个注解的配合,轻松的实现了抓取并定制化Controller中的异常。
目前以及定义了两个异常,一个是继承RuntimeExceptionBaseException异常,通常抛出未知异常。第二个就是数据库的主键唯一异常SQLIntegrityConstraintViolationException

2. 插入时,当前用户的id怎么获取的。在插入user的时候,需要知道当前操控的用户是谁,也就是创建人和修改人,苍穹外卖的实现方式是使用JWT解析出当前操作的用户,但是由于JWT工作于拦截器,无法通过Controller传递给Service进行业务。项目使用了ThreadLocal传递参数。

这里需要知道一些前置知识:

前端的每一个请求,tomcat都会开辟(分配)一条新的线程,也就是说,每一个请求作用的拦截器,Controller,Service,dao的线程是独立的。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

也就是说,我们可以在拦截器解析当前用户,然后向ThreadLocal插入当前的用户,之后的Controller,Service等都可以获取到这个值。

苍穹外卖官方实现:

public class BaseContext {
    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }
}

官方调用:

`BaseContext.setCurrentId(empId);`

但是这个实现有一个问题是只在ThreadLocal存了一个值,且只可以存一个值,我感觉这有点浪费这个特性了。我们可以将其存储Map对象来达到存储多个值的目的。

public class BaseContext {
    private static final ThreadLocal<Map<String, Object>> threadLocal = ThreadLocal.withInitial(HashMap::new);

    public static void set(String key, Object value) {
        threadLocal.get().put(key, value);
    }

    public static Object get(String key) {
        return threadLocal.get().get(key);
    }

    public static void remove(String key) {
        threadLocal.get().remove(key);
    }

    public static void clear() {
        threadLocal.remove();
    }
}

无纸办公平台实现获取操作用户ID

(叠甲)由于项目做的时候学习的比较拉,从0到1的开发,边学边做,用的MVC(前端jsp)、前后端分离(MVC+jQuery)混合架构,这里用的还是JSP(捂脸)。
前端JSP通过SpringSecurity获得的用户名,如下:

<div class="col-md-2 title">申请人</div>
<div class="col-md-4 data">
      <h5 id="apply_people"><security:authentication property="principal.username"/></h5>
</div>

就这样先渲染到前端,然后传回后端,后端再做校验:

@RequestMapping("/save.do")
@ResponseBody
public ResultInfo save(@RequestBody ApplyUse applyUse,
                       @RequestParam(name = "submit", required = false) String submit) {
    // 获取当前登录用户
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String username = authentication.getName();  // 当前登录用户

    // **1. 校验申请人是否为空**
    if (applyUse.getApply_people() == null || applyUse.getApply_people().trim().isEmpty()) {
        return new ResultInfo(false, "申请人不能为空!");
    }

    // **2. 校验申请人是否与当前登录用户一致**
    if (!applyUse.getApply_people().equals(username)) {
        return new ResultInfo(false, "申请人信息错误,禁止提交!");
    }
    // 业务逻辑
}

3. 不同pojo对象属性拷贝,前端传过来的DTO通常没有后端需要存的创建时间等,因此需要在insert之前需要转换entity对象(包含时间等字段)。

苍穹外卖使用的是新建entity对象,将DTO拷贝,创建时间等单独设置。

public void save(CategoryDTO categoryDTO) {
        Category category = new Category();
        //属性拷贝
        BeanUtils.copyProperties(categoryDTO, category);

        //分类状态默认为禁用状态0
        category.setStatus(StatusConstant.DISABLE);

        //设置创建时间、修改时间、创建人、修改人
        category.setCreateTime(LocalDateTime.now());
        category.setUpdateTime(LocalDateTime.now());
        category.setCreateUser(BaseContext.getCurrentId());
        category.setUpdateUser(BaseContext.getCurrentId());

        categoryMapper.insert(category);
    }

我的路子比较野(吗?)直接entity对象包含DTO对象,通过构造函数传值。

public StockInHistoryVO(Stock stock) {
        this.stock = stock;
        this.date = new Date();
    }

4. 分页pagehelper对比,在进行分页查询的时候,我发现有一些小区别。

苍穹外卖通过dao(Mapper)返回的直接是Page

    <select id="pageQuery" resultType="com.sky.entity.Employee">
        select * from employee
        <where>
            <if test="name != null and name != ''">
                and name like concat('%',#{name},'%')
            </if>
        </where>
        order by create_time desc
    </select>

Mapper 接口

Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

在这种方式下,PageHelper 会拦截 pageQuery 方法的 SQL 查询,并在执行时自动添加 LIMIT 语句。

而我返回的是List

@Select("select * from user")
List<UserInfo> findAll();

之后在上层包装

PageInfo<UserInfo> pageInfo = new PageInfo<>(userInfoList);

我们可以看到构造函数如下:

public PageInfo(List<T> list, int navigatePages) {
        this.isFirstPage = false;
        this.isLastPage = false;
        this.hasPreviousPage = false;
        this.hasNextPage = false;
        if (list instanceof Page) {
            Page page = (Page)list;
            this.pageNum = page.getPageNum();
            this.pageSize = page.getPageSize();
            this.pages = page.getPages();
            this.list = page;
            this.size = page.size();
            this.total = page.getTotal();
            if (this.size == 0) {
                this.startRow = 0;
                this.endRow = 0;
            } else {
                this.startRow = page.getStartRow() + 1;
                this.endRow = this.startRow - 1 + this.size;
            }
        } else if (list instanceof Collection) {
            this.pageNum = 1;
            this.pageSize = list.size();
            this.pages = this.pageSize > 0 ? 1 : 0;
            this.list = list;
            this.size = list.size();
            this.total = (long)list.size();
            this.startRow = 0;
            this.endRow = list.size() > 0 ? list.size() - 1 : 0;
        }

        if (list instanceof Collection) {
            this.navigatePages = navigatePages;
            this.calcNavigatepageNums();
            this.calcPage();
            this.judgePageBoudary();
        }

    }

如果 list 是 Page 类型,PageInfo 会直接从 Page 里获取分页信息,无需额外计算。
如果 list 只是普通 List,PageInfo 会自己计算分页信息(默认 pageNum=1,pageSize=list.size())。

5.日期格式转换

在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理

	/**
     * 扩展Spring MVC框架的消息转化器
     * @param converters
     */
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转化器加入容器中
        converters.add(0,converter);
    }

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    //public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}
posted @   子画12  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示