项目简介
本项目是一款针对餐饮用户商家设计的外卖点单系统,主要使用的技术栈用SpringBoot和MySQL。产品分为后台管理端和移动用户端。后台管理端主要用于维护菜品相关信息,例如填删菜品、查找菜品、填删套餐、查找套餐等等。移动用户端主要供用户浏览菜品、挑选菜品进购物车以及下单等。
场景问题与解决办法
静态资源映射
SpringBoot内置了Servlet容器,拥有默认的静态资源路径,对于不一样的路径,我们需要添加静态资源映射。
首先需要实现一个自定义配置类,继承WebMvcConfigurationSupport类,WebMvcConfigurationSupport是Spring MVC的核心配置。
WebMvcConfigurationSupport类的功能:主要用于执行WebMvc的各种配置,告诉Spring用于处理请求的有关工具,这些工具用于处理映射关系,解析参数,返回消息等等。一个完整的web请求和响应过程中需要执行的动作所使用的配置(程序、配置)基本都可以在WMS中进行管理。
- 注册处理器映射,以便把请求导向具体的控制器、控制器方法、视图、资源。注意,这些映射器的目的是做一个关系匹配
- 注册处理器适配器,为具体的功能选择具体的处理程序。例如参数解析,消息转换等
- 注册异常处理器
- 注册路径映射解析有关类
WMS提供了通过Java代码配置WebMvc的方法,Spring拥有一个默认的子类WebMvcAutoConfiguration继承WMS来实现一些默认的MVC配置。如果想要自定义配置,可以通过以下解决方式解决:
- 对于一个使用@Configuration注解的配置类,添加@EnabledWebMvc注解,即可导入自定义的Mvc的Java配置。
- 继承WMS类,同时添加@Configuration注解,并重写对应的方法(对于重写有@Bean注解的方法同样需要添加@Bean注解)。
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry){
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
//addResourceHandler(String url)添加一个资源处理器ResourceHandlerRegistration,当遇到访问路径为url的请求时,映射到其他访问路径,
//通过ResourceHandlerRegistration.addResourceLocations(url)为资源处理器中的访问路径添加映射路径url
}
}
JS的id精度丢失
对于Long类型和Big Integer类型的数据(例如id),二进制中拥有64位的大小,JS内置有32位整数,而number类型的安全整数是53位,因此id等类型的数据传到前端时会发生精度丢失的问题。因此我们在把数据传给前端时,需要进行消息转换,把Long类型和BigInteger类型的数据转换成String类型的数据。可以通过配置Mvc来实现该消息转换。
首先是添加消息转换器,通过重写extendMessageConverters方法来向消息转换器列表converters中添加新的消息转换器对象:
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用jackson将java对象转成json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到MVC框架的转换器集合中
converters.add(0,messageConverter);
}
}
消息转换器对象的实现是通过继承Jackson提供的ObjectMapper类完成的:
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_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(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
异常处理
项目中处理业务时会遇到各种异常,为了方便代码,减少耦合性,需要对异常进行统一的处理。
SpringBoot提供了全局异常处理来对业务中产生的各种异常进行统一的处理,此外也可以考虑使用Spring AOP来对业务异常进行处理。这里我们使用的是SpringBoot提供的全局异常处理。
@ControllerAdvice注解是Spring MVC提供的功能,在SpringBoot中可以直接使用,可以看作@Controller的加强版往往用于处理全局数据。例如全局异常处理、全局数据绑定、全局数据预处理等。相应的,需要和@ExceptionHandler、@ModelAttribute、@InitBinder搭配使用,完成各项功能。
- 全局异常处理,处理业务中的各项异常,和@ExceptionHandler搭配使用
- 全局数据绑定,可以用来做一些初始化的数据操作,用 @ModelAttribute 注解标记该方法的返回数据是一个全局数据,默认情况下,这个全局数据的 key 就是返回的变量名,value 就是方法返回值
- 全局数据预处理,@ModelAttribute和@InitBinder搭配使用,实现对前端传来的数据的预处理,例如在Controller类的方法中使用@ModelAttribute(“a”)对方法的形参进行声明,会自动调用在 @ControllerAdvice 标记的类中使用@InitBinder(“a”)注解声明的方法来对该参数进行预处理。
首先自定义业务异常类,在业务中按需抛出业务异常
public class CustomException extends RuntimeException {
public CustomException(String msg){
super(msg);
}
}
SpringBoot的全局异常处理
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
//异常处理方法
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException exception){
log.error(exception.getMessage());
if(exception.getMessage().contains("Duplicate entry")){
String[] exs = exception.getMessage().split(" ");
String msg = exs[2] + "已存在!";
return R.error(msg);
}
return R.error("失败!");
}
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException exception){
log.error(exception.getMessage());
return R.error(exception.getMessage());
}
}
自动填充
在处理涉及到插入数据和更新数据的业务时,一个表的属性中往往有很多逻辑上相同的公共字段,对于它们的赋值与更新往往使用的是重复的代码,为了简化开发,可以利用MybatisPlus提供的自动填充功能。
MetaObjectHandler接口是MybatisPlus为我们提供的的一个扩展接口,我们可以利用这个接口在我们插入或者更新数据的时候,为一些字段指定默认值。(实现这个需求的方法不止一种,在sql层面也可以做到,在建表的时候也可以指定默认值。)
编写一个类实现该接口,同时重写insertFill()和updateFill()方法来实现公共字段的填充,使用@Component注解将该类交给Spring容器。
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充,Insert");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
@Override
public void updateFill(MetaObject metaObject) {
long id = Thread.currentThread().getId();
// log.info("线程id={}", id);
// log.info("公共字段自动填充,Update");
log.info(metaObject.toString());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
在实体类中通过@TableField注解设置自动填充的公共字段
@Data
public class Employee implements Serializable {
//... 其他属性
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
分页查询
MybatisPlus提供了分页插件,可以把分页查询的结果封装到Page类里。通过设置一个分页插件的配置类(使用@Configuration注解),实现分页查询的功能。
@Configuration
public class MybatisPlusConfig {
@Bean //告诉Spring容器下面这个方法可以得到一个Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
事务管理
当一个业务涉及到多个表的写操作时,需要将业务看作一整个事务来保证事务性原则。使用Spring提供的@Transactional注解可以实现对相应业务函数的事务的声明。同时在SpringBoot的启动类中需要使用@EnableTransactionManagement注解开启事务管理。
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//保存菜品到dish表中
this.save(dishDto);
Long dishId = dishDto.getId();
List<DishFlavor> dishFlavors = dishDto.getFlavors();
dishFlavors = dishFlavors.stream().map(item -> {
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
//保存菜品口味数据到口味表
dishFlavorService.saveBatch(dishFlavors);
}
过滤器
在一般的业务逻辑中,对于未登录的用户,只能访问登录页面,而不能访问主页。对此,可以使用过滤器或者拦截器实现。这里我们使用过滤器。
@ServletComponentScan是SpringBoot提供的注解之一,对启动类使用该注解后,Servlet、Filter、Listener可以直接通过@WebServlet、@WebFilter、@WebListener注解自动注册,无需其他代码。
使用@WebFilter注解实现了Filter接口的自定义过滤器类,重写doFilter方法,实现对请求路径的过滤:
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
//...
@Override
public void doFilter{
//...
}
}
文件上传下载
在处理一些业务时,浏览器经常会传递给服务器一些图片,或者请求一些图片,涉及到文件的上传与下载。我们可以把文件的上传与下载封装到一个公共的Controller中,不涉及到具体的实体。上传文件时,通过MultipartFile类封装从前端传来的对象,之后通过Stream流将文件存储到服务器中。下载文件时,通过Stream流将文件写到response中
文件上传
public R<String> upload(MultipartFile file){
//file是一个临时文件,需要转存到指定位置
//获得原始文件名
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf('.'));
//使用UUID重新生成文件名,防止文件名重复造成文件覆盖
String fileName = UUID.randomUUID() + suffix;
File dir = new File(basePath);
if(!dir.exists()) dir.mkdirs();
try{
file.transferTo(new File(basePath + fileName));
}catch (IOException e){
e.printStackTrace();
}
return R.success(fileName);
}
文件下载
public void download(String name, HttpServletResponse response){
try {
//输入流,读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
//输出流,写回浏览器
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("image/jpeg"); //设置回复的格式
int len = 0;
byte[] bytes = new byte[1024];
while((len = fileInputStream.read(bytes)) != -1){
outputStream.write(bytes,0, len);
outputStream.flush();
}
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}