一、背景

公司的项目前段时间发版上线后,测试反馈用户的批量删除功能报错。正常情况下看起来应该是个小BUG,可怪就怪在上个版本正常,且此次发版未涉及用户功能的改动。因为这个看似小BUG我了解到不少未知的东西,在这里和你们分享下。

先声明下具体原因为了避免耽误找解决问题方法的小伙伴们的宝贵时间,因为项目重写了WebMvcConfigurationSupport,如果你的项目没有重写这个配置类,赶紧到别处找找,祝你很快找到解决BUG获取经验值升级。

二、问题描述

用户批量删除功能:前台传递用户ID数组,后台使用@RequestParam解析参数为list

错误提示:

Required List parameter 'ids[]' is not present

前台代码:

$.ajax({
url: "/users",
type: "delete",
data: {ids: ids},
success: function (response) {
if (response.code === 0) {
layer.msg("删除成功");
}
}
})

后台代码:

@DeleteMapping("/users")
@ResponseBody
public Result delete(@RequestParam(value = "ids[]") List<Long> ids) {
boolean status = sysUserService.deleteByIds(ids);
return Result.status(status);
}

知识点:

  1. 后台为什么使用@RequestParam解析?

    ajax如果不指定上传数据类型Content-Type,默认的是application/x-www-form-urlencoded,这种编码格式后台需要通过RequestParam来处理。

  2. 后台为什么参数名称是ids[]?

三、问题分析和猜想验证

1. 问题分析

前台确实传递了ids[],后台接收不到ids[],代码逻辑在上个版本是可行的,未对用户模块更新。思来想去得出的结论,此次的全局性的改动引发出来的问题。去其他页面功能点击批量删除,确实都不可用了。

想到全局性的改动,记得自己当时为了全局配置日期格式转换还有Long传值到前台精度丢失的问题重写了WebMvcConfigurationSupport,代码如下:

import com.alibaba.fastjson.serializer.SerializeConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.serializer.ToStringSerializer;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List; @Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport { @Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//1、定义一个convert转换消息的对象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
//2、添加FastJson的配置信息
FastJsonConfig fastJsonConfig = new FastJsonConfig(); //Long类型转String类型
SerializeConfig serializeConfig = SerializeConfig.globalInstance;
serializeConfig.put(BigInteger.class, ToStringSerializer.instance);
serializeConfig.put(Long.class, ToStringSerializer.instance);
// serializeConfig.put(Long.TYPE, ToStringSerializer.instance); //不转long值
fastJsonConfig.setSerializeConfig(serializeConfig); fastJsonConfig.setSerializerFeatures(
SerializerFeature.WriteMapNullValue, // 保留map空的字段
SerializerFeature.WriteNullStringAsEmpty, // 将String类型的null转成""
SerializerFeature.WriteNullNumberAsZero, // 将Number类型的null转成0
SerializerFeature.WriteNullListAsEmpty, // 将List类型的null转成[]
SerializerFeature.WriteNullBooleanAsFalse, // 将Boolean类型的null转成false
SerializerFeature.WriteDateUseDateFormat, //日期格式转换
SerializerFeature.DisableCircularReferenceDetect // 避免循环引用
);
//3、在convert中添加配置信息
fastConverter.setFastJsonConfig(fastJsonConfig);
//4、解决响应数据非json和中文响应乱码
List<MediaType> jsonMediaTypes = new ArrayList<>();
jsonMediaTypes.add(MediaType.APPLICATION_JSON);
fastConverter.setSupportedMediaTypes(jsonMediaTypes);
//5、将convert添加到converters中
converters.add(fastConverter);
//6、追加默认转换器
super.addDefaultHttpMessageConverters(converters);
} @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(
"classpath:/static/");
registry.addResourceHandler("swagger-ui.html").addResourceLocations(
"classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations(
"classpath:/META-INF/resources/webjars/");
super.addResourceHandlers(registry);
}
}

想到这,二话不说把@Configuration注释掉,让Spring启动不加载这个配置类,结果如猜想,可以传值了。

那么问题就出现在这个配置类中,毫无头绪的我只想找个背锅的。我第一篇有关项目问题的总结就是FastJSON,嘿嘿,然后就把消息转换器的代码configureMessageConverters注释了,然而并没有啥用。

其实主要问题在于对SpringMVC读取请求参数的流程不清楚,如果把流程梳理清楚了,应该就知道参数在哪丢了?!

附:SpringMVC请求处理流程(可略过)

声明这里单纯的因为自己对Spring请求处理的流程不熟悉,可能和下文引出的问题产生原因并无直接关联,东西有点多不感兴趣的童鞋可以直接跳过。后面我会单独整理篇有关SpringMVC请求处理流程,这里就问题案例来进行的流程分析。

接下来在源码的角度层面来认识SpringMVC处理请求的过程。

SpringMVC处理请求流程从DispatcherServlet#doService方法作为入口,请求处理核心部分委托给doDispatch方法。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
... try {
try {
ModelAndView mv = null;
Object dispatchException = null; try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 获取 HandlerExecutionChain处理器执行链,由handler处理器和interceptor拦截器组成
mappedHandler = this.getHandler(processedRequest);
... // 根据handler获取对应的handlerAdapter去执行这个handler(Controller的方法)
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
...
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
} this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
...
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
}
... }

根据请求信息的请求路径和方法类型(get/post/put/delete)从HandlerMapping映射集合获取

HandlerExecutionChain处理器执行链(包含handler和interceptor)。

通过获得的handler类型去适配handlerAdapter执行对应的逻辑。那怎么去找适配器呢?首先你至少知道你的handler是什么类型吧。在此之前,引入一个概念HandlerMethod,简单点说就是你控制器Controller里用来处理请求的方法的信息、还有方法参数信息等。

调试时发现这里使用的是AbstractHandlerMethodAdapter,看下内部用来做适配的supports方法。handler instanceof HandlerMethod这个判断点明了一切。

public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered {

    public final boolean supports(Object handler) {
return handler instanceof HandlerMethod && this.supportsInternal((HandlerMethod)handler);
} protected abstract boolean supportsInternal(HandlerMethod var1); @Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return this.handleInternal(request, response, (HandlerMethod)handler);
} @Nullable
protected abstract ModelAndView handleInternal(HttpServletRequest var1, HttpServletResponse var2, HandlerMethod var3) throws Exception;
}

找到适配器后,执行其handle方法,调用内部方法handleInternal,交由其子类RequestMappingHandlerAdapter实现,我们平时开发最常用的也就是这个适配器了。来看下RequestMappingHandlerAdapter#handleInternal方法。

protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
...
// 调用RequestMappingHandlerAdapter#invokeHandlerMethod方法
mav = this.invokeHandlerMethod(request, response, handlerMethod);
...
return mav;
}

调用内部方法RequestMappingHandlerAdapter#invokeHandlerMethod,继续走。

protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
...
Object result;
try {
...
// 生成一个可调用的方法invocableMethod
ServletInvocableHandlerMethod invocableMethod = this.createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
// 绑定参数解析器
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
} if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
} invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
...
// 核心 通过调用ServletInvocableHandlerMethod的invokeAndHandle方法执行Controller里处理请求的方法
invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
...
}
return (ModelAndView)result;
}

将HandlerMethod转化成ServletInvocableHandlerMethod,可以说这个ServletInvocableHandlerMethod是SpringMVC最最核心的部分了。至于为什么这么说?

  1. 绑定了HandlerMethodArgumentResolver参数解析器
  2. 绑定了HandlerMethodReturnValueHandler返回值处理器
  3. 核心方法invokeAndHandle囊括了从请求到响应几乎整个SpringMVC生命周期
 public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// 调用请求
Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
...
try {
// 处理返回值
this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);
}
...
}

本篇的BUG也就在于处理请求阶段的问题,所以我们来看下ServletInvocableHandlerMethod#invokeForRequest方法。

public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
...
return this.doInvoke(args);
}

方法内部通过调用父类InvocableHandlerMethod#getMethodArgumentValues方法获取请求参数。

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
MethodParameter[] parameters = this.getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
} else {
Object[] args = new Object[parameters.length]; for(int i = 0; i < parameters.length; ++i) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] == null) {
...
try {
// 调用HandlerMethodArgumentResolverComposite的resolveArgument解析参数获取返回值
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
...
}
} return args;
}
}

this.resolvers是HandlerMethodArgumentResolverComposite(相当于组合模式的变种),同时实现了HandlerMethodArgumentResolver接口,内部又包含所有参数解析器的列表。

public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
// SpringMVC参数解析器的集合列表
private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList();
...
public boolean supportsParameter(MethodParameter parameter) {
return this.getArgumentResolver(parameter) != null;
} @Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
} else {
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
} @Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
if (result == null) {
Iterator var3 = this.argumentResolvers.iterator(); // 遍历寻找适配的参数解析器
while(var3.hasNext()) {
HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, resolver);
break;
}
}
} return result;
} }

MethodParameter是处理器方法(HandlerMethod)的一个HandlerMethodParameter处理器方法参数信息,这里面其中就包含了描述方法参数的注解信息(eg:@RequestParam)。然后需要根据参数信息从参数解析器列表查找适配的参数解析器。

终于,在27个参数解析器中找到了RequestParamMapMethodArgumentResolver解析器,那我们去看下这个解析器做的适配方法supportsParameter。

public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
if (!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
return true;
} else {
RequestParam requestParam = (RequestParam)parameter.getParameterAnnotation(RequestParam.class);
return requestParam != null && StringUtils.hasText(requestParam.name());
}
} else if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
} else {
parameter = parameter.nestedIfOptional();
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
} else {
return this.useDefaultResolution ? BeanUtils.isSimpleProperty(parameter.getNestedParameterType()) : false;
}
}
}

可以看到RequestParamMapMethodArgumentResolver支持被注解@RequestParam、@RequestPart修饰的方法参数。

在确定了参数解析器后,使用解析器的resolveArgument方法解析参数。RequestParamMapMethodArgumentResolver自身没有resolveArgument方法,而是使用父类AbstractNamedValueMethodArgumentResolver的resolveArgument的方法。

public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 提取注解的属性键值(eg: 注解RequestParam的name、required、defaultValue)
AbstractNamedValueMethodArgumentResolver.NamedValueInfo namedValueInfo = this.getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
// 获取处理器方法参数名
Object resolvedName = this.resolveStringValue(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");
} else {
// 根据参数名从request请求对象获取值
Object arg = this.resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = this.resolveStringValue(namedValueInfo.defaultValue);
} else if (namedValueInfo.required && !nestedParameter.isOptional()) {
this.handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
} arg = this.handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
} else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = this.resolveStringValue(namedValueInfo.defaultValue);
} if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, (Object)null, namedValueInfo.name); try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
} catch (ConversionNotSupportedException var11) {
throw new MethodArgumentConversionNotSupportedException(arg, var11.getRequiredType(), namedValueInfo.name, parameter, var11.getCause());
} catch (TypeMismatchException var12) {
throw new MethodArgumentTypeMismatchException(arg, var12.getRequiredType(), namedValueInfo.name, parameter, var12.getCause());
}
} this.handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
}

这样一个一个的解析处理器方法参数,直到把方法所有的参数都从request拿到对应的值之后,返回args。对应逻辑在ServletInvocableHandlerMethod#invokeForRequest,最后返回参数数组args。这样整个参数解析完成之后执行后面的逻辑this.doInvoke(args)。

至此,SpringMVC请求处理流程就结束了。

总结下整个SpringMVC请求处理的流程:

  1. 请求由DispatcherServlet拦截处理。
  2. 根据request信息从HandlerMapping映射关系获取对应的HandlerExecutionChain执行链(包含了处理器handler和interceptor)。
  3. 从HandlerExecutionChain获取处理器handler,根据handler类型去匹配对应的适配器。我们平时最常写的Controller用来处理请求的方法对应的handler类型是HandlerMethod,匹配的适配器是RequestMappingHandlerAdapter。
  4. 请求处理委托给RequestMappingHandlerAdapter处理,适配器将拿到的handler转换成ServletInvocableHandlerMethod,其内部绑定了参数解析器HandlerMethodArgumentResolver和返回值处理器HandlerMethodReturnValueHandler。
  5. 执行ServletInvocableHandlerMethod的invokeAndHandle方法。整个方法包含了请求调用和响应处理,请求中包含了参数的解析过程。

2. 猜想验证

其实上面扯了这么多,还没说到关键点为什么重写了WebMvcConfigurationSupport会导致后台接收不了FormData?按照之前的分析我们需要的FormData数据可能在哪个阶段丢了。前台传过来的数据肯定会存在request对象中,既然这样,笨办法是不是可以想比较下没有重写和重写的情景,看看两次的request对象是否有差异不就行了。

我们把断点打在InvocableHandlerMethod#getMethodArgumentValues方法中,因为这里是从request对象中提取出参数的方法。我们要做的只需观察两次的request对象的差异即可。

果不其然,重写过WebMvcConfigurationSupport后,少了formParams这个属性,而formParams包含了我们想要的参数ids[]。

至于为什么重写WebMvcConfigurationSupport会丢失formParams?是不是毫无头绪?别急,我们先看下这个formParams是什么。

从上图可以看得到formParams是FormContentFilter中静态内部类FomContentRequestWrapper的一个属性。猜想formParams应该是使用FormContentFilter过滤器从request对象提取出来的,那现在少了formParams应该是过滤器FormContentFilter没有加载。

重写配置类之前没有配置过FormContentFilter过滤器,所以这个过滤器应该是SpringBoot自动配置并加载的。来看下SpringBoot的WebMvc自动配置类WebMvcAutoConfiguration。这个类配置在spring.factories里,SpringBoot启动时自动加载配置在里面的类,是SpringBoot的扩展机制,类似java的SPI。

FormContentFilter如我们所料在SpringBoot的WebMvc自动配置类中,随着SpringBoot启动自动装配。那至于为什么重写了WebMvcConfigurationSupport就会导致自动配置失效了呢?再看下WebMvcAutoConfiguration的头部注解描述。

@ConditionalOnMissingBean({WebMvcConfigurationSupport.class}),意思就是Spring容器中没有WebMvcConfigurationSupport类相关bean自动配置才会生效。而我这里重写WebMvcConfigurationSupport并加载到Spring容器中,显然导致SpringBoot自动配置不能生效,最终表现出来的现象是后台接收不到前台FromData传值。

四、解决方案

  1. 既然自动配置失效,手动配置吧
    @Bean
public FormContentFilter formContentFilter(){
FormContentFilter formContentFilter=new OrderedFormContentFilter();
return formContentFilter;
}

这种方案问题在于还是重写了WebMvcConfigurationSupport,除了本篇说到的问题还有其他自动配置同样会失效,感觉终究还是不是太好的方案。

  1. 继承实现WebMvcConfigurer接口,会在支持原有默认配置的情况下新增配置(推荐)。
  2. 其他方案,要是你知道的话欢迎下方留言。

五、总结

其实就是项目中批量删除失败看似一个很小的BUG,引出SpringMVC有关请求参数处理原理和SpringBoot自动装配原理。因为时间和篇幅的原因,SpringMVC和SpringBoot原理后面会开专栏细说。最后说一下老生常谈的问题,最好的学习方式是结合工作项目中学,个人完全赞同这种说法。而我们大多数人终究逃不了那循环CRUD的命,但千万别浪费时间去抱怨,有时候所处的环境无法改变时,要相信自己可以创造环境。哪怕重复的CRUD、哪怕再小的BUG,我们也可以做到独具慧眼看到深藏在它们背后的东西,而我们要的就是这些藏在这背后的东西。废话了很多,所有都得看自己,有帮助的话希望点个关注❤

小BUG大原理:重写WebMvcConfigurationSupport后SpringBoot自动配置失效的更多相关文章

  1. 小BUG大原理 | 第一篇:重写WebMvcConfigurationSupport后SpringBoot自动配置失效

    一.背景 公司的项目前段时间发版上线后,测试反馈用户的批量删除功能报错.正常情况下看起来应该是个小BUG,可怪就怪在上个版本正常,且此次发版未涉及用户功能的改动.因为这个看似小BUG我了解到不少未知的 ...

  2. 小BUG大原理:FastJSON实体转换首字母小写的尴尬事件

    问题描述 因为项目连接的Oracle数据库,字段名映射方便使用大写,但是通过接口调用返回到前端的字段名首字母为小写,这样带来的问题前端显示的字段就需要写这种很尴尬的格式. 原因分析 开发环境使用的是S ...

  3. 这样讲 SpringBoot 自动配置原理,你应该能明白了吧

    https://juejin.im/post/5ce5effb6fb9a07f0b039a14 前言 小伙伴们是否想起曾经被 SSM 整合支配的恐惧?相信很多小伙伴都是有过这样的经历的,一大堆配置问题 ...

  4. SpringBoot实战之SpringBoot自动配置原理

    SpringBoot 自动配置主要通过 @EnableAutoConfiguration, @Conditional, @EnableConfigurationProperties 或者 @Confi ...

  5. SpringBoot自动配置原理学习

    介绍 构建Springboot项目时我们会创建一个启动类 @SpringBootApplication public class DemoApplication { public static voi ...

  6. springboot自动配置原理以及手动实现配置类

    springboot自动配置原理以及手动实现配置类 1.原理 spring有一个思想是"约定大于配置". 配置类自动配置可以帮助开发人员更加专注于业务逻辑开发,springboot ...

  7. 浅谈springboot自动配置原理

    前言 springboot自动配置关键在于@SpringBootApplication注解,启动类之所以作为项目启动的入口,也是因为该注解,下面浅谈下这个注解的作用和实现原理 @SpringBootA ...

  8. 全网最深分析SpringBoot MVC自动配置失效的原因

    前言 本来没有计划这一篇文章的,只是在看完SpringBoot核心原理后,突然想到之前开发中遇到的MVC自动失效的问题,虽然网上有很多文章以及官方文档都说明了原因,但还是想亲自看一看,本以为很简单的事 ...

  9. SpringBoot自动配置源码调试

    之前对SpringBoot的自动配置原理进行了较为详细的介绍(https://www.cnblogs.com/stm32stm32/p/10560933.html),接下来就对自动配置进行源码调试,探 ...

随机推荐

  1. WebStorm 2019 3.3 安装及破解教程附汉化教程

    WebStorm2019 3.3 安装及破解教程附加汉化教程 安装包及破解补丁 链接: https://pan.baidu.com/s/19ATTAW3Tsm0huIJSqYChTw 提取码:1ei7 ...

  2. AntDesignPro的权限控制和动态路由

    最近看了AntDesignPro关于权限控制的官方文档以及自己框架里权限控制的实现,总结一下. 先贴一下官网上关于权限控制的图有利于理解 步骤如下: 判断是否有 AccessToken 如果没有则跳转 ...

  3. struts2入门教学

    我的博客地址:https://blog.csdn.net/qq_41907991 首先介绍一下struts2使用的基本步骤: 1.导入相关的 jar 文件 2.需要在 web.xml 文件中配置一个 ...

  4. 使用Proteus模拟操作HDG12864F-1液晶屏

    在Proteus中模拟了89C52操作HDG12864F-1液晶屏,原理图如下: 一.HDG12864F-1官网信息 该液晶屏是Hantronix的产品,官网上搜索出这个型号是系列型号中的一种,各种型 ...

  5. MySQL 入门(3):事务隔离

    摘要 在这一篇内容中,我将从事务是什么开始,聊一聊事务的必要性. 然后,介绍一下在InnoDB中,四种不同级别的事务隔离,能解决什么问题,以及会带来什么问题. 最后,我会介绍一下InnoDB解决高并发 ...

  6. 【Scala】新手入门,基础语法概览

    目录 变量.常量和数据类型 var val 数据类型 条件表达式 块表达式 to循环 for循环 for推导式 scala中的方法和函数 方法的定义 函数的定义 函数和方法的区别 变量.常量和数据类型 ...

  7. FOC 算法基础之欧拉公式

    文章目录 欧拉公式 几何意义 复数平面 动态过程 加法 FOC电压矢量的推导 总结 参考 FOC中电压矢量合成的推导,对于欧拉公式的几何意义做了一个全面的回顾. 欧拉公式 欧拉是一个天才,欧拉公式甚至 ...

  8. hex文件格式总结

    hex文件格式总结 文章目录 hex文件格式总结 什么是hex文件? 文件格式 指令类型(Record type) 校验和 :04 02B0 00 92020008 AE :04 0000 05 08 ...

  9. @RequestParam和@RequestBody和@PathVariable用法小结

    @RequestParam 使用@RequestParam接收前段参数比较方便,前端传参的URL: url = "${ctx}/main/mm/am/edit?Id=${Id}&na ...

  10. NetCore项目实战篇06---服务注册与发现之consul

    至此,我们的解决方案中新建了三个项目,网关(Zhengwei.Gateway).认证中心(Zhengwei.Identity)和用户资源API(Zhengwei.Use.Api).当要访问用户API的 ...