目录
1.静态资源映射
创建一个mvc框架的配置类:WebMvcConfig,继承WebMvcConfigurationSupport。重写addResourceHandlers方法。
/**
* 设置静态资源映射:springboot项目默认访问resource/static包下的静态页面,
* 通过下面的方法,可以改变静态资源的映射路径
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
//这样可以不用把backend包放在static包下,就可以被访问到
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
2.检查用户是否登录
如果用户未进行登录就访问其他页面时,对该操作进行拦截。
2.1过滤器
(1)创建自定义过滤器;(2)在启动类上加@ServletComponentScan,开启扫描;
(3)完善过滤器逻辑。
①获取本次请求的URI;②判断本次请求是否需要处理;③如果不需要,则直接放行;④如果需要,判断登录状态,如果已经登录,放行;⑤如果未登录则返回未登录结果。
@WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
//路径匹配器
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//获取本次请求的URI;
String requestURI = request.getRequestURI();
//不被拦截的路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//判断本次请求是否需要处理;
boolean check = check(urls, requestURI);
//如果不需要,则直接放行;
if (check){
filterChain.doFilter(request,response);
return;
}
//如果需要,判断登录状态,如果已经登录,放行。
Long employeeId = (Long) request.getSession().getAttribute("employeeId");
if (employeeId != null){
//使用ThreadLocal管理登录用户id,详情见第7小结
BaseContext.setCurrentId(employeeId);
filterChain.doFilter(request,response);
return;
}
//如果未登录则返回未登录结果,通过输出流方式向用户页面响应数据。
response.getWriter().write(JSON.toJSONString(Result.error("NOTLOGIN")));
return;
}
/**
* 检查本次请求是否需要放行
* @param urls 需要放行的路径数组
* @param requestURI 需要进行检查的路径
* @return true不需要被拦截,false需要被拦截
*/
public boolean check(String[] urls, String requestURI){
for (String url : urls){
//true 匹配上了,说明不需要被拦截,false需要被拦截
boolean match = PATH_MATCHER.match(url, requestURI);
if (match)
return match;
}
return false;
}
}
2.2拦截器
(1)创建一个登录配置类LoginConfig,实现WebMvcConfigurer接口
@Configuration
public class LoginConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册登录拦截器
InterceptorRegistration interceptor = registry.addInterceptor(new LoginInterceptor());
//添加拦击拦截的路径,所有都拦截
interceptor.addPathPatterns("/**");
//添加不被拦截的路径
interceptor.excludePathPatterns(
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
);
}
}
(2)创建登录拦截器类LoginInterceptor,实现HandlerInterceptor接口,并实现三个方法:preHandle、postHandle、afterCompletion。
public class LoginInterceptor implements HandlerInterceptor {
//在请求处理之前被调用(controller方法调用之前)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Long id = (Long) request.getSession().getAttribute("id");
if (id != null){
BaseContext.setCurrentId(id);
return true;
}
response.getWriter().write(JSON.toJSONString(Result.error("NOTLOGIN")));
return false;
}
//在请求处理之后被调用
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
//整个请求结束之后被调用
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
3.全局异常捕获
@Slf4j //拦截加了RestController和Controller注解的类
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
public class GlobalExceptionHandler{
//异常处理
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public Result<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if (ex.getMessage().contains("Duplicate entry"))
return Result.error("用户名已经存在,请修改");
return Result.error("未知错误");
}
}
4.配置MybatisPlus分页插件
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
5.获取请求参数的注解
注解一:@PathVariable。例如localhost:8080/user/2,使用该注解获取数据。
@GetMapping(/user/{id})
public void getUserById(@PathVariable(“id”) Integer id)
注解二:@RequestParam。例如localhost:8080/user?id=2,使用该注解获取数据。
@GetMapping("/user")
public void getUserById(@RequestParam(“id”) Integer id)
注解三:@RequestBody。该注解专门用于请求体,get请求无法使用。
@PostMapping("/login")
public void login(@RequestBody Employee employee)
6.精度丢失之消息转换器
vue处理超过16位数字精度时存在精度丢失问题,所以在进行数据传输的时候需要先将传输超过16位的数据转换为String,再以json的格式传输。
方法一:在存在精度丢失问题的实体类的字段上添加@JsonSerialize(using = ToStringSerializer.class)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
方法二:①提供对象转换器JacksonObjectMapper,基于Jackson进行java对象到json数据的转换;②在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此转化器中使用提供的对象转换器进行java对象到json数据的转换
/**
* 对象映射器:基于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_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(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用jackson将java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
}
7.通过ThreadLocal获取登录用户的id信息
在自定义元数据处理器之前需要注意,updateUser和createUser字段值,是根据登录账号用户的id进行赋值的,而在一开始,程序就将账户的id值放在了session中,因此我们可以获取sess ion中的数据,来对两个字段进行赋值。但是在我们自定义的元数据处理中,不能获得session对象。所以需要使用ThreadLocal来解决此问题。在客户端每次发送http请求时,对应的在服务器都会分配一个新的线程来处理。其中,在处理的过程中,①LoginCheckFilter的doFilter方法、②EmployeeController的insert或update方法、③MyMetaObjectHandler的insertFill或updateFill方法属于同一个进程。在客户观发送请求后,后端会对发送的请求先进行过滤,然后进行添加或修改,最后进行公共字段自动填充操作。
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。常用方法:
public void set(T value)//设置当前线程的线程局部变量的值
public T get()//返回当前线程所对应的线程局部变量的值
基于ThreadLocal编写BaseContext工具类,由于在许多修改和添加操作都涉及该问题,因此在过滤器获取登录用户的id,调用BaseContext工具类的setCurrentId方法,把id存放在Threadlocal中,最后在自定义的元数据处理器(MyMetaObjectHandler)中调用getCurrentId方法获取id值。
//基于ThreadLocal封装工具类,用于保存和获取登录用户的id
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id){
threadLocal.set(id);
}
public static Long getCurrentId(){
return threadLocal.get();
}
}
8.公共字段自动填充
在项目中每次添加数据都要设置创建时间、创建人、修改时间和修改人等字段信息。为了简化该操作,使用MybatisPlus的公共字段自动填充功能,也就是插入或者更新的时候为指定字段赋予指定的值。好处:统一对这些字段进行处理,避免重复代码。
①在需要被自动填充的属性上加入@TableField注解,给fill属性赋值,指定自动填充策略 。
@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;//在插入和更新时自动填充
②编写元数据对象处理器类,在此类中统一对公共填充字段进行赋值。
//元数据自定义处理器
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override//插入操作自动填充
public void insertFill(MetaObject metaObject) {
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) {
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
9.文件上传和下载
需要在yml文件中创建上传和下载地址
reggie:
path: D:\文件上传下载页面\
9.1文件上传
在前端文件上传时,对应的form表单有以下要求:提交方式必须是post、采用multipart格式上传文件、input的type类型必须是file。
<form action="" enctype="multipart/form-data" method="post">
<input type="file" name="myFile">
<input type="submit" value="提交">
</form>
在后端,Spring框架对文件上传功能进行了封装,只需要在Controller的方法中声明一个MultipartFile类型的参数即可上传文件。
@PostMapping("/upload")
public Result<String> upload(MultipartFile multipartFile){
//multipartFile必须与前端file中的name保持一致
System.out.println(multipartFile);
return null;
}
完整后端实现文件上传代码
@Value("${reggie.path}")//在yml文件中进行配置
private String basePath;
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws IOException {
//形参的file名,不能随便取,要和对应前端文件的name值对应
//file是一个临时文件,需要转存到指定位置,否者本次请求完成后临时文件就会删除
//获取原始的文件名
String filename = file.getOriginalFilename();
String suffix = filename.substring(filename.lastIndexOf("."));
//为了防止文件重名问题,使用UUID
String uuid = UUID.randomUUID().toString()+suffix;
//判断文件目录是否存在
File dir = new File(basePath);
if (!dir.exists()){
//目录不存在,创建目录
dir.mkdirs();
}
//将临时文件,转存到指定位置
file.transferTo(new File(basePath + uuid));
//返回文件名称,以便后续文件下载
return Result.success(uuid);
}
9.2文件下载
文件下载是指将文件从服务器传输到本地计算机的过程。通过浏览器进行文件下载通常有两种表现方式:①以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录。②直接在浏览器中打开:本质上是服务端将文件以流的形式写回浏览器的过程。
完整后端实现文件下载代码
@GetMapping("/download")
public void download(@RequestParam("name")String name, HttpServletResponse response) throws IOException {
//输入流,通过输入流读取文件内容
FileInputStream fis = new FileInputStream(basePath + name);
//输出流,通过输出流将文件写回浏览器,在浏览器上展示图片
ServletOutputStream os = response.getOutputStream();
response.setContentType("image/jpeg");
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1){
os.write(bytes,0,len);
os.flush();
}
//关闭资源
fis.close();
os.close();
}