苍穹外卖02
今天继续苍穹外卖和无纸办公平台的技术对比,主要关注以下方面:
- 学习优雅的抓异常
- 插入时,当前用户的id怎么获取的
- 不同pojo对象属性拷贝
- 分页pagehelper对比
- 日期格式转换
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中的异常。
目前以及定义了两个异常,一个是继承RuntimeException
的BaseException
异常,通常抛出未知异常。第二个就是数据库的主键唯一异常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
如果 list 只是普通 List
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);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通