【手撕Spring源码】深度理解SpringMVC【下】

news2024/11/24 22:49:20

文章目录

  • 控制器方法执行流程
  • @ControllerAdvice 之 @ModelAttribute
  • 返回值处理器
  • MessageConverter
  • @ControllerAdvice 之 @ResponseBodyAdvice
  • BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter
  • RouterFunctionMapping 与 HandlerFunctionAdapter
  • SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter
  • WelcomePageHandlerMapping
  • 映射器与适配器小结
  • MVC异常处理
    • @ControllerAdvice 之 @ExceptionHandler
  • Tomcat 异常处理
    • 错误页处理
    • BasicErrorController
  • ViewResolver
  • MVC处理流程

控制器方法执行流程

我们知道RequestMappingHandlerAdapter一个重要的作用就是去执行控制器方法,而这个控制器方法就是下面的HandlerMethod

ServletInvocableHandlerMethod
+invokeAndHandle(ServletWebRequest,ModelAndViewContainer)
HandlerMethod
bean
method
WebDataBinderFactory
ParameterNameDiscoverer
HandlerMethodArgumentResolverComposite
HandlerMethodReturnValueHandlerComposite

HandlerMethod 需要

  • bean 即是哪个 Controller
  • method 即是 Controller 中的哪个方法

Controller有了,Controller中的方法有了,那么我们就可以使用反射进行调用了。

当然这远远不够,我们的方法上有参数,我们要进行一系列的处理,所以RequestMappingHandlerAdapter其实最后执行的是ServletInvocableHandlerMethod,它具有以下四个组件:

  • WebDataBinderFactory 负责对象绑定、类型转换
  • ParameterNameDiscoverer 负责参数名解析
  • HandlerMethodArgumentResolverComposite 负责解析参数
  • HandlerMethodReturnValueHandlerComposite 负责处理返回值

准备阶段

RequestMappingHandlerAdapter WebDataBinderFactory ModelFactory ModelAndViewContainer 准备 @InitBinder 准备 @ModelAttribute 添加Model数据 RequestMappingHandlerAdapter WebDataBinderFactory ModelFactory ModelAndViewContainer

根据这幅图我们可以看到:

  • RequestMappingHandlerAdapter首先会做一些准备工作
    • 创建WebDataBinderFactory,为以后的参数绑定和类型转换做铺垫
      • 这里涉及第一个拓展点:通过解析@InitBinder添加一些自定义的类型转换器等。
    • 创建ModelFactory工厂:用来产生模型数据
      • 这里也涉及一个拓展点:通过解析@ModelAttribute,将方法的返回值作为模型数据进行添加。添加的位置就是ModelAndViewContainer(后面会有详细说明)

调用阶段

RequestMappingHandlerAdapter ServletInvocableHandlerMethod ArgumentResolvers ReturnValueHandlers ModelAndViewContainer invokeAndHandle 获取 args 有的解析器涉及 RequestBodyAdvice 有的解析器涉及数据绑定生成模型数据 args method.invoke(bean,args) 得到 returnValue 处理 returnValue 有的处理器涉及 ResponseBodyAdvice 添加Model数据,处理视图名,是否渲染等 获取 ModelAndView RequestMappingHandlerAdapter ServletInvocableHandlerMethod ArgumentResolvers ReturnValueHandlers ModelAndViewContainer

这个时候万事俱备,就可以开始调用我们前面说的ServletInvocableHandlerMethod了,调用的时候会做三件事:

  • 准备参数
    • 通过HandlerMethodArgumentResolverComposite (组合参数解析器)获取参数
      • 在这个过程中有一个拓展点:有的解析器会根据RequestBodyAdvice对请求体进行拓展(后面也会详细介绍)
    • 某些参数解析器工作的过程中会涉及到数据绑定生成模型数据(也就是使用了@ModelAttribute的参数),它们也会被存储到ModelAndViewContainer中
    • 最后得到参数
  • 反射调用控制器方法:bean有了,方法有了,参数也有了,开始进行反射调用。最后得到返回结果。
  • 处理返回值
    • HandlerMethodReturnValueHandlerComposite处理返回值
      • 这里涉及拓展:有的返回值处理器会根据ResponseBodyAdvice对请求体进行拓展
    • 有的返回值代表了模型数据,他们会被添加到ModelAndViewContainer中去。
  • 返回值处理结束后ServletInvocableHandlerMethod会找ModelAndViewContainer获得ModelAndView对象(因为最终控制器的返回结果要统一成ModelAndViewu)。然后把ModelAndView返回给上层的调用者也就是DispatcherServlet。

接下来我们帮上述过程使用代码模拟一下:

public class A26 {

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(WebConfig.class);

        RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
        adapter.setApplicationContext(context);
        adapter.afterPropertiesSet();

        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setParameter("name", "张三");
        /*
            现在可以通过 ServletInvocableHandlerMethod 把这些整合在一起, 并完成控制器方法的调用, 如下
         */
        ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
                new Controller1(), Controller1.class.getMethod("foo", User.class));

        ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);

        handlerMethod.setDataBinderFactory(factory);
        handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
        handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));

        ModelAndViewContainer container = new ModelAndViewContainer();

        // 获取模型工厂方法
        Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
        getModelFactory.setAccessible(true);
        ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, factory);

        // 初始化模型数据
        modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);

        handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);

        System.out.println(container.getModel());

        context.close();


    }

@ControllerAdvice 之 @ModelAttribute

@ModelAttribute有两种用法:

在这里插入图片描述

  • @ModelAttribute注解可以在控制器方法的入参上使用,用于将请求参数绑定到该入参对应的对象中。并将该值添加到ModelAndViewContainer 中
  • @ModelAttribute注解也可以在控制器方法的返回值上使用,用于将返回值作为模型对象添加到ModelAndViewContainer 中

这个地方要注意:

  • 如果@ModelAttribute没有指定名称,那么会将类型的首字母小写当作名称存入ModelAndViewContainer 。
    在这里插入图片描述
  • 如果@ModelAttribute加在了方法上那么它的解析器就不是ServletModelAttributeMethodProcessor,而是RequestMappingHandlerAdapter。解析完了之后在我们去执行控制器方法之前,会有一个ModelFactory来调用标注了@ModelAttribute的方法,并把它们的返回结果作为模型数据存储在ModelAndViewContainer 中。

什么是ModelFactory?
ModelFactory是Spring MVC框架中的一个接口,用于创建和管理模型对象(模型对象是指用于在控制器方法和视图之间传递数据的Java对象,它可以让我们方便地将数据从控制器方法传递到视图中,从而实现动态生成页面的效果)。它的主要作用是创建模型对象,以及在控制器方法中共享模型对象。
具体来说,ModelFactory定义了两个方法:

  • createModel(): 用于创建一个新的模型对象。该方法会在每次请求到达控制器方法时被调用,用于创建一个新的模型对象。
  • mergeAttributes(): 用于将模型对象中的属性添加到ModelAndView对象中。该方法会在控制器方法执行完毕后被调用,用于将模型对象中的属性添加到ModelAndView对象中,以便在视图中使用。

在Spring MVC框架中,默认使用的是ExtendedModelMap类来实现ModelFactory接口。ExtendedModelMap类继承了LinkedHashMap类,因此它可以存储键值对,并且可以保持键值对的顺序。

还要注意:
在这里插入图片描述

  • RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @ModelAttribute 方法
  • RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @ModelAttribute 方法
  • 以上两种 @ModelAttribute 的解析结果都会缓存来避免重复解析
  • 控制器方法调用时,会综合利用本类的 @ModelAttribute 方法和 @ControllerAdvice 中的 @ModelAttribute 方法创建模型工厂

返回值处理器

常见的返回值处理器:
在这里插入图片描述

  • 返回值类型为ModelAndView时,分别获取其模型和视图名,放入 ModelAndViewContainer
  • 返回值类型为 String 时,把它当做视图名,放入 ModelAndViewContainer
  • 返回值添加了 @ModelAttribute 注解时,将返回值作为模型,放入 ModelAndViewContainer
    • 我们这个时候缺少视图,默认的处理是会把@RequestMapping中的路径名当作视图的名字
  • 返回值省略 @ModelAttribute 注解且返回非简单类型时,将返回值作为模型,放入 ModelAndViewContainer
    • 我们这个时候缺少视图,默认的处理是会把@RequestMapping中的路径名当作视图的名字
  • 返回值类型为 ResponseEntity 时
    • 此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true
  • 返回值类型为 HttpHeaders 时
    • 会设置 ModelAndViewContainer.requestHandled 为 true
  • 返回值添加了 @ResponseBody 注解时
    • 此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true
    • 注意:
      • @ResponseBody 是返回值处理器解析的
      • 但具体转换工作是 MessageConverter 做的

前4种方法都要走视图渲染流程:
在这里插入图片描述
而后三种直接响应就行,不用走视图的解析、渲染流程,ModelAndViewContainer中是空的。这是因为他们的处理器中把ModelAndViewContainer.requestHandled 设为 true
在这里插入图片描述

MessageConverter

MessageConverter是一个接口,它的主要作用是:将请求信息(如JSON、XML等)转换为对象,或将对象转换为响应信息。

当客户端发送HTTP请求时,请求中的数据可能以不同的格式进行传输,比如JSON、XML、文本等。而服务器端需要将这些数据转换成Java对象,以便后续的处理。此时,MessageConverter就可以派上用场了。它可以根据请求中的Content-Type头信息来自动选择相应的转换器,将请求中的数据转换成Java对象。

当服务器端需要将Java对象转换成HTTP响应时,同样可以使用MessageConverter来实现。比如,我们可以将Java对象转换成JSON格式的数据,然后将其作为HTTP响应返回给客户端。在这个过程中,MessageConverter会根据响应中的Content-Type头信息来自动选择相应的转换器,将Java对象转换成对应的格式。

Spring MVC中内置了许多实现,例如:

  • MappingJackson2HttpMessageConverter: 将JSON转换为对象或将对象转换为JSON
  • Jaxb2RootElementHttpMessageConverter: 将XML转换为对象或将对象转换为XML
  • StringHttpMessageConverter: 将String类型的请求/响应体转换为对象或将对象转换为String
  • 等等

我们也可以自定义MessageConverter实现,以满足特定的需求。

接下来我们使用代码模拟一下消息转换器的工作过程:

    //写入json
	public static void test1() throws IOException {
        MockHttpOutputMessage message = new MockHttpOutputMessage();
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        if (converter.canWrite(User.class, MediaType.APPLICATION_JSON)) {
            converter.write(new User("张三", 18), MediaType.APPLICATION_JSON, message);
            System.out.println(message.getBodyAsString());
        }
    }
    //写入xml
    private static void test2() throws IOException {
        MockHttpOutputMessage message = new MockHttpOutputMessage();
        MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter();
        if (converter.canWrite(User.class, MediaType.APPLICATION_XML)) {
            converter.write(new User("李四", 20), MediaType.APPLICATION_XML, message);
            System.out.println(message.getBodyAsString());
        }
    }
    //读入json
    private static void test3() throws IOException {
        MockHttpInputMessage message = new MockHttpInputMessage("""
                {
                    "name":"李四",
                    "age":20
                }
                """.getBytes(StandardCharsets.UTF_8));
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        if (converter.canRead(User.class, MediaType.APPLICATION_JSON)) {
            Object read = converter.read(User.class, message);
            System.out.println(read);
        }
    }
    

结果:
在这里插入图片描述

那么这么多MessageConverter是如何决定由谁来解析的呢?

  • 首先看 @RequestMapping 上有没有指定
    在这里插入图片描述

    相当于response.setContentType(“application/json”);

  • 其次看 request 的 Accept 头有没有指定

  • 最后按 MessageConverter 的顺序, 谁能谁先转换

@ControllerAdvice 之 @ResponseBodyAdvice

@ControllerAdvice与@ResponseBodyAdvice、@RequestBodyAdvice可以配合,来实现对请求体/响应体的增强拓展,当然其本质上是对消息转换的拓展。

这里我们就以@ResponseBodyAdvice为例,ResponseBodyAdvice 增强 在整个 HandlerAdapter 调用过程中所处的位置如下:
在这里插入图片描述

它的主要作用是:对@ResponseBody或ResponseEntity方法返回的内容进行后处理。

@ResponseBodyAdvice支持的后处理操作包括:

  1. 对Object类型的body进行包装或解包装
  2. 基于内容类型改变body对象
  3. 添加或修改响应头
  4. 等等

@ResponseBodyAdvice最常用的功能就是返回响应体前进行自定义的包装。其目的就是保持响应体的统一。

我们来看一个例子:

{"name":"王五","age":18}响应结果转换成 {"code":xx, "msg":xx, data: {"name":"王五","age":18} }

@Configuration
public class WebConfig {

    @ControllerAdvice
    static class MyControllerAdvice implements ResponseBodyAdvice<Object> {
        // 满足条件才转换
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            if (returnType.getMethodAnnotation(ResponseBody.class) != null ||
                AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null) {
//                returnType.getContainingClass().isAnnotationPresent(ResponseBody.class)) {
                return true;
            }
            return false;
        }

        // 将 User 或其它类型统一为 Result 类型
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            if (body instanceof Result) {
                return body;
            }
            return Result.ok(body);
        }
    }

    // @Controller
    // @ResponseBody
    @RestController
    public static class MyController {
        public User user() {
            return new User("王五", 18);
        }
    }
}

注意:

  • 这里我们使用了AnnotationUtils.findAnnotation方法,对注解进行一个深度寻找,例如@RestController中包含@ResponseBody的情况。

BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter

这两者配套使用,就像RequestMappingHandlerMapping和RequestMappingHandlerAdapter,一个做路径映射,另一个做控制器方法调用。不过这一组更加的简单,他们属于SpringMVC更为早期的实现。

我们来看一下使用代码:

@Configuration
public class WebConfig {
    @Bean // ⬅️内嵌 web 容器工厂
    public TomcatServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory(8080);
    }

    @Bean // ⬅️创建 DispatcherServlet
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }

    @Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
    public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
    }

    // /c1  -->  /c1
    // /c2  -->  /c2
    @Bean
    public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {
        return new BeanNameUrlHandlerMapping();
    }

    @Bean
    public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
        return new SimpleControllerHandlerAdapter();
    }

    @Component("/c1")
    public static class Controller1 implements Controller {
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            response.getWriter().print("this is c1");
            return null;
        }
    }

    @Component("/c2")
    public static class Controller2 implements Controller {
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            response.getWriter().print("this is c2");
            return null;
        }
    }

    @Bean("/c3")
    public Controller controller3() {
        return (request, response) -> {
            response.getWriter().print("this is c3");
            return null;
        };
    }
}

代码解读:

  • BeanNameUrlHandlerMapping是通过bean的名字进行路径映射。比如说请求是/c1,那么它也会去容器里面寻找一个名叫/c1的bean。当然前提是这个bean的名字必须以/开头,才会被当作处理请求的bean。
  • SimpleControllerHandlerAdapter也是用来做控制器方法调用。只不过调用规则不同:作为控制器的类必须实现同一个接口Controller(不是注解Controller)。
    在这里插入图片描述

接下来我们自己通过代码来实现一个:

@Configuration
public class WebConfig_1 {
    @Bean // ⬅️内嵌 web 容器工厂
    public TomcatServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory(8080);
    }

    @Bean // ⬅️创建 DispatcherServlet
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }

    @Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
    public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
    }

    // /c1  -->  /c1
    // /c2  -->  /c2
    @Component
    static class MyHandlerMapping implements HandlerMapping {
        @Override
        public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
            String key = request.getRequestURI();
            Controller controller = collect.get(key);
            if (controller == null) {
                return null;
            }
            return new HandlerExecutionChain(controller);
        }

        @Autowired
        private ApplicationContext context;
        private Map<String, Controller> collect;

        @PostConstruct
        public void init() {
            collect = context.getBeansOfType(Controller.class).entrySet()
                    .stream().filter(e -> e.getKey().startsWith("/"))
                    .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
            System.out.println(collect);
        }
    }

    @Component
    static class MyHandlerAdapter implements HandlerAdapter {

        @Override
        public boolean supports(Object handler) {
            return handler instanceof Controller;
        }

        @Override
        public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            if (handler instanceof Controller controller) {
                controller.handleRequest(request, response);
            }
            return null;
        }

        @Override
        public long getLastModified(HttpServletRequest request, Object handler) {
            return -1;
        }
    }

    @Component("/c1")
    public static class Controller1 implements Controller {
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            response.getWriter().print("this is c1");
            return null;
        }
    }

    @Component("c2")
    public static class Controller2 implements Controller {
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            response.getWriter().print("this is c2");
            return null;
        }
    }

    @Bean("/c3")
    public Controller controller3() {
        return (request, response) -> {
            response.getWriter().print("this is c3");
            return null;
        };
    }
}

RouterFunctionMapping 与 HandlerFunctionAdapter

这一组较新,在Spring5.2才引入

在这里插入图片描述

使用代码:

@Configuration
public class WebConfig {
    @Bean // ⬅️内嵌 web 容器工厂
    public TomcatServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory(8080);
    }

    @Bean // ⬅️创建 DispatcherServlet
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }

    @Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
    public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
    }

    @Bean
    public RouterFunctionMapping routerFunctionMapping() {
        return new RouterFunctionMapping();
    }

    @Bean
    public HandlerFunctionAdapter handlerFunctionAdapter() {
        return new HandlerFunctionAdapter();
    }

    @Bean
    public RouterFunction<ServerResponse> r1() {
        return route(GET("/r1"), request -> ok().body("this is r1"));
    }

    @Bean
    public RouterFunction<ServerResponse> r2() {
        return route(GET("/r2"), request -> ok().body("this is r2"));
    }

}

r1简化之前的代码:
在这里插入图片描述

总结:

        函数式控制器
            a. RouterFunctionMapping, 通过 RequestPredicate 映射
            b. handler 要实现 HandlerFunction 接口
            c. HandlerFunctionAdapter, 调用 handler

        对比
            a. RequestMappingHandlerMapping, 以 @RequestMapping 作为映射路径
            b. 控制器的具体方法会被当作 handler
            c. RequestMappingHandlerAdapter, 调用 handler

函数式控制器就是在匹配方式上,调用方式上做了些改动。新是新但是功能并没有RequestMapping那套体系强大,毕竟对参数解析,返回值处理并没有那么多丰富的逻辑。好处就是十分的简洁,适合一些业务逻辑不复杂的场景。

SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter

又是一组,不过这一组一般是用来处理静态资源的。

使用代码:

@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ApplicationContext context) {
    SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
    Map<String, ResourceHttpRequestHandler> map 
        = context.getBeansOfType(ResourceHttpRequestHandler.class);
    handlerMapping.setUrlMap(map);
    return handlerMapping;
}

@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
    return new HttpRequestHandlerAdapter();
}

@Bean("/**")
public ResourceHttpRequestHandler handler1() {
    ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
    handler.setLocations(List.of(new ClassPathResource("static/")));
    return handler;
}

@Bean("/img/**")
public ResourceHttpRequestHandler handler2() {
    ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
    handler.setLocations(List.of(new ClassPathResource("images/")));
    return handler;
}

目录结构:
在这里插入图片描述

  1. SimpleUrlHandlerMapping 不会在初始化时收集映射信息,需要手动收集
  2. SimpleUrlHandlerMapping 映射路径,同样也是bean的名字作为匹配的路径
  3. ResourceHttpRequestHandler 作为静态资源 handler
  4. HttpRequestHandlerAdapter, 调用此 handler

这个handler其实还可以进行优化。我们来看看ResourceHttpRequestHandler 的初始化方法afterPropertiesSet(一位它实现了InitializingBean接口),在这个方法中我们可以看到他设置了一个资源解析器:

在这里插入图片描述
也就是说本质上他寻找那些静态资源是依靠的这个PathResourceResolver这个资源解析器。Spring内部还有其他几种资源解析器,可以增强它的功能。

  • PathResourceResolver:就是在磁盘上都对应的资源
  • EncodedResourceResolver:可以读该资源所对应的压缩资源
  • CachingResourceResolver:在读取资源的时候加入缓存功能

增强代码:

@Bean("/**")
public ResourceHttpRequestHandler handler1() {
    ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
    handler.setLocations(List.of(new ClassPathResource("static/")));
    handler.setResourceResolvers(List.of(
        	// ⬇️缓存优化
            new CachingResourceResolver(new ConcurrentMapCache("cache1")),
        	// ⬇️压缩优化
            new EncodedResourceResolver(),
        	// ⬇️原始资源解析
            new PathResourceResolver()
    ));
    return handler;
}

注意:

  1. 责任链模式体现
  2. 压缩文件需要手动生成
  3. 注意此处资源解析器的顺序,优先走缓存

WelcomePageHandlerMapping

又叫欢迎页映射器,其作用是把访问根目录的请求映射到一个欢迎页上去。另外这个欢迎页映射器是SpringBoot提供的,传统的SpringMVC没有:
在这里插入图片描述

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context) {
    Resource resource = context.getResource("classpath:static/index.html");
    return new WelcomePageHandlerMapping(null, context, resource, "/**");
}

@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
    return new SimpleControllerHandlerAdapter();
}

注意💡:

  1. 欢迎页支持静态欢迎页与动态欢迎页
  2. WelcomePageHandlerMapping 映射欢迎页(即只映射 ‘/’)
    • 其内部会生成一个实现了Controller接口的处理器ParameterizableViewController (所以后来使用SimpleControllerHandlerAdapter进行handler的调用)
    • ParameterizableViewController 作用是不执行逻辑,仅根据视图名找视图
    • 视图名固定为 forward:index.html
  3. SimpleControllerHandlerAdapter, 调用 handler
    • 转发至 /index.html
    • 处理 /index.html 又会走上面的静态资源处理流程

映射器与适配器小结

  1. HandlerMapping 负责建立请求与控制器之间的映射关系
    • RequestMappingHandlerMapping (与 @RequestMapping 匹配)
    • WelcomePageHandlerMapping (/)
    • BeanNameUrlHandlerMapping (与 bean 的名字匹配 以 / 开头)
    • RouterFunctionMapping (函数式 RequestPredicate, HandlerFunction)
    • SimpleUrlHandlerMapping (静态资源 通配符 /** /img/**)
    • 之间也会有顺序问题, boot 中默认顺序如上
  2. HandlerAdapter 负责实现对各种各样的 handler 的适配调用
    • RequestMappingHandlerAdapter 处理:@RequestMapping 方法
      • 参数解析器、返回值处理器体现了组合模式
    • SimpleControllerHandlerAdapter 处理:Controller 接口
    • HandlerFunctionAdapter 处理:HandlerFunction 函数式接口
    • HttpRequestHandlerAdapter 处理:HttpRequestHandler 接口 (静态资源处理)
    • 这也是典型适配器模式体现

MVC异常处理

我们进入DispatcherServlet中遇到异常只是把异常捉住,赋值给一个Exception变量并没有马上处理,而是在接下来的processDispatchResult中去处理异常:

在这里插入图片描述
我们进入这个方法发现里面的逻辑大致为:

  • 如果Exception非空走异常处理逻辑
  • 如果Exception为空则走视图渲染的逻辑

在这里插入图片描述
我们可以看到异常处理的核心方法是processHandlerException,我们继续进入这个方法,看看里面干了什么:

在这里插入图片描述

  • 在DispatcherServlet初始化的时候收集一些异常处理器
  • 然后遍历异常处理器对异常进行处理

所有的异常处理器都继承了HandlerExceptionResolver接口,这里我们说一个其中比较重要的异常处理器ExceptionHandlerExceptionResolver。

  1. ExceptionHandlerExceptionResolver 初始化时会解析 @ControllerAdvice 中的 @ExceptionHandler 方法
  2. ExceptionHandlerExceptionResolver 会以类为单位,在该类首次处理异常时,解析此类的 @ExceptionHandler 方法
  3. 以上两种 @ExceptionHandler 的解析结果都会缓存来避免重复解析

我们来通过代码看看过程:

public class A30 {
    public static void main(String[] args) throws NoSuchMethodException {
        ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
        resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
        resolver.afterPropertiesSet();

        MockHttpServletRequest request = new MockHttpServletRequest();
        MockHttpServletResponse response = new MockHttpServletResponse();
        // 1.测试 json
        HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
        Exception e = new ArithmeticException("被零除");        resolver.resolveException(request, response, handlerMethod, e);
        System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
    }
}

控制器:

在这里插入图片描述

结果:
在这里插入图片描述

  • 处理异常的本质就是执行异常处理方法,而这个方法也有参数、返回值,所以也需要参数解析器、返回值处理器。这里我们使用了@ResponseBody注解,所以需要一个消息转换器。
  • ExceptionHandlerExceptionResolver也实现了InitializingBean接口,所以也有afterPropertiesSet方法,在这个方法中为其添加了几个参数解析器以及返回值处理器
    在这里插入图片描述
  • 调用resolveException处理异常的时候:根据HandlerMethod就会知道将来这个异常是在哪个类中被触发的,然后进入这个类中检查,看是否有标注为@ExceptionHandler的方法,找到之后它会把方法的参数异常类型与实际发生的异常类型进行一个异常匹配,如果能匹配上则说明这个方法就是进行异常处理的,然后反射调用这个方法,调用的时候参数该解析的解析,返回值该处理的处理。

接下来我们测试一下嵌套异常:

public class A30 {
    public static void main(String[] args) throws NoSuchMethodException {
        ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
        resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
        resolver.afterPropertiesSet();

        MockHttpServletRequest request = new MockHttpServletRequest();
        MockHttpServletResponse response = new MockHttpServletResponse();
                // 3.测试嵌套异常
        HandlerMethod handlerMethod = new HandlerMethod(new Controller3(), Controller3.class.getMethod("foo"));
        Exception e = new Exception("e1", new RuntimeException("e2", new IOException("e3")));
        resolver.resolveException(request, response, handlerMethod, e);
        System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
    }
}

控制器:
在这里插入图片描述

结果:
在这里插入图片描述
之所以可以可以进行嵌套循环是因为:
在这里插入图片描述
之后会拿这个异常的数组去进行匹配。

最后我们小总结一下:

关于ExceptionHandlerExceptionResolver:

  1. 它能够重用参数解析器、返回值处理器,实现组件重用
  2. 它能够支持嵌套异常

@ControllerAdvice 之 @ExceptionHandler

@ControllerAdvice + @ExceptionHandler是一个全局异常处理方案

我们来看一段示例代码:

在这里插入图片描述

public class A31 {
    public static void main(String[] args) throws NoSuchMethodException {
        MockHttpServletRequest request = new MockHttpServletRequest();
        MockHttpServletResponse response = new MockHttpServletResponse();

//        ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
//        resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
//        resolver.afterPropertiesSet();

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
        ExceptionHandlerExceptionResolver resolver = context.getBean(ExceptionHandlerExceptionResolver.class);

        HandlerMethod handlerMethod = new HandlerMethod(new Controller5(), Controller5.class.getMethod("foo"));
        Exception e = new Exception("e1");
        resolver.resolveException(request, response, handlerMethod, e);
        System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
    }

    static class Controller5 {
        public void foo() {

        }
    }
}

结果:
在这里插入图片描述

那么我们的异常处理器是在什么时机找到的全局异常处理方法呢?

还是在内置初始化的时候:
在这里插入图片描述

我们进入这个方法看看:

在这里插入图片描述

总结一下:

  1. ExceptionHandlerExceptionResolver 初始化时会解析 @ControllerAdvice 中的 @ExceptionHandler 方法
  2. ExceptionHandlerExceptionResolver 会以类为单位,在该类首次处理异常时,解析此类的 @ExceptionHandler 方法
  3. 以上两种 @ExceptionHandler 的解析结果都会缓存来避免重复解析

Tomcat 异常处理

错误页处理

我们上面所涉及的异常处理其实能力是有限的,我们知道 @ExceptionHandler 只能处理发生在 mvc 流程中的异常,例如控制器内、拦截器内,那么如果是 Filter 出现了异常,如何进行处理呢?,这个时候我们需要一个更上层的异常处理者,也就是Tomcat异常处理。

我们可以看到Tomcat的错误页面是这样的:
在这里插入图片描述
那么我们能对其进行定制吗?

我们只需要加两个bean就可以完成这个需求:

@Bean // ⬅️修改了 Tomcat 服务器默认错误地址, 出错时使用请求转发方式跳转
public ErrorPageRegistrar errorPageRegistrar() {
    return webServerFactory -> webServerFactory.addErrorPages(new ErrorPage("/error"));
}

@Bean // ⬅️TomcatServletWebServerFactory 初始化前用它增强, 注册所有 ErrorPageRegistrar
public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor() {
    return new ErrorPageRegistrarBeanPostProcessor();
}
  • ErrorPageRegistrar :错误的处理路径,也就是定义Tomcat默认错误页面的地址
  • ErrorPageRegistrarBeanPostProcessor :简单来说就是让你定义的错误页面生效
  • 如果出现了错误会使用请求转发forward的方法跳转到错误地址
  • Tomcat只要出现异常就会把它存在request作用域中
    在这里插入图片描述

BasicErrorController

  • 在 Spring Boot 中 Tomcat的异常处理,是这么实现的:
  • 因为内嵌了 Tomcat 容器,因此可以配置 Tomcat 的错误页面,Filter 与 错误页面之间是通过请求转发跳转的,可以在这里做手脚
  • 先通过 ErrorPageRegistrarBeanPostProcessor 这个后处理器配置错误页面地址,默认为 /error 也可以通过 ${server.error.path} 进行配置
  • 当 Filter 发生异常时,不会走 Spring 流程,但会走 Tomcat 的错误处理,于是就希望转发至 /error 这个地址
    • 当然,如果没有 @ExceptionHandler,那么最终也会走到 Tomcat 的错误处理
  • Spring Boot 又提供了一个 BasicErrorController,它就是一个标准 @Controller,@RequestMapping 配置为 /error,所以处理异常的职责就又回到了 Spring
  • 异常信息由于会被 Tomcat 放入 request 作用域,因此 BasicErrorController 里也能获取到
  • 具体异常信息会由 DefaultErrorAttributes 封装好
  • BasicErrorController 通过 Accept 头判断需要生成哪种 MediaType 的响应
    • 如果要的不是 text/html,走 MessageConverter 流程
    • 如果需要 text/html,走 mvc 流程,此时又分两种情况
      • 配置了 ErrorViewResolver,根据状态码去找 View
      • 没配置或没找到,用 BeanNameViewResolver 根据一个固定为 error 的名字找到 View,即所谓的 WhitelabelErrorView

ViewResolver

ViewResolver 是一个接口,它的主要作用是:将逻辑视图名解析为实际的 View 对象

在 Spring MVC 中,我们通常使用逻辑视图名来指定视图,例如:

return "user/list";

然后,ViewResolver 会根据这个逻辑视图名,解析出对应的 View 对象:

View view = viewResolver.resolveViewName("user/list", locale);

常见的 View 对象有:

  • InternalResourceView:渲染 JSP 视图
  • FreeMarkerView:渲染 FreeMarker 模板
  • RedirectView:重定向到另一个 URL
  • 等等

Spring MVC 中默认配置了若干 ViewResolver,例如:

  • InternalResourceViewResolver:将逻辑视图名解析为 JSP 视图
  • FreeMarkerViewResolver:将逻辑视图名解析为 FreeMarker 模板
  • RedirectViewResolver:用于重定向视图

当我们返回一个逻辑视图名时,Spring MVC 会根据注册的 ViewResolver 解析成实际的 View 对象,然后由视图渲染工具(ViewRenderer)来渲染视图,生成客户端响应。

所以,ViewResolver 的主要特征是:

  1. 将逻辑视图名解析为 View 对象实例
  2. 不同的 ViewResolver 支持不同类型的 View(JSP、FreeMarker 等)
  3. 我们可以注册多个 ViewResolver,他们之间并不冲突
  4. 当渲染视图时,会根据注册的 ViewResolver 解析出对应的 View
  5. 默认的 ViewResolver 有 InternalResourceViewResolver、FreeMarkerViewResolver 等
  6. 自定义 ViewResolver 可以选择支持任意类型的 View

除此之外,ViewResolver 还有如下优点:

  1. 解耦视图技术与 Spring MVC,我们可以自由选择视图技术
  2. 可扩展性强,自定义 ViewResolver 可以支持任意视图类型
  3. 通过在配置中调整 ViewResolver 顺序,可以灵活选择优先视图技术
  4. 对视图技术的支持可以动态添加或删除
  5. 使用逻辑视图名,屏蔽了视图技术的差异,提高可移植性

MVC处理流程

当浏览器发送一个请求 http://localhost:8080/hello 后,请求到达服务器,其处理流程是:

  1. 服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术

    • 路径:默认映射路径为 /,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器
      • jsp 不会匹配到 DispatcherServlet
      • 其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet
    • 创建:在 Boot 中,由 DispatcherServletAutoConfiguration 这个自动配置类提供 DispatcherServlet 的 bean
    • 初始化:DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量
      • HandlerMapping,初始化时记录映射关系
      • HandlerAdapter,初始化时准备参数解析器、返回值处理器、消息转换器
      • HandlerExceptionResolver,初始化时准备参数解析器、返回值处理器、消息转换器
      • ViewResolver:视图解析器
  2. DispatcherServlet 会利用 RequestMappingHandlerMapping 查找控制器方法

    • 例如根据 /hello 路径找到 @RequestMapping(“/hello”) 对应的控制器方法

    • 控制器方法会被封装为 HandlerMethod 对象,并结合匹配到的拦截器一起返回给 DispatcherServlet

    • HandlerMethod 和拦截器合在一起称为 HandlerExecutionChain(调用链)对象

  3. DispatcherServlet 接下来会:

    1. 调用拦截器的 preHandle 方法
    2. RequestMappingHandlerAdapter 调用 handle 方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod
      • @ControllerAdvice 全局增强点1️⃣:补充模型数据
      • @ControllerAdvice 全局增强点2️⃣:补充自定义类型转换器
      • 使用 HandlerMethodArgumentResolver 准备参数
        • @ControllerAdvice 全局增强点3️⃣:RequestBody 增强
      • 调用 ServletInvocableHandlerMethod
      • 使用 HandlerMethodReturnValueHandler 处理返回值
        • @ControllerAdvice 全局增强点4️⃣:ResponseBody 增强
      • 根据 ModelAndViewContainer 获取 ModelAndView
        • 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程
          • 例如,有的返回值处理器调用了 HttpMessageConverter 来将结果转换为 JSON,这时 ModelAndView 就为 null
        • 如果返回的 ModelAndView 不为 null,会在第 4 步走视图解析及渲染流程
    3. 调用拦截器的 postHandle 方法
    4. 处理异常或视图渲染
      • 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
        • @ControllerAdvice 全局增强点5️⃣:@ExceptionHandler 异常处理
      • 正常,走视图解析及渲染流程
    5. 调用拦截器的 afterCompletion 方法

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/608972.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Elasticsearch:节点角色 - node roles

你可能已经知道 Elasticsearch 集群由一个或多个节点组成。 每个节点将数据存储在分片上&#xff0c;每个分片存储在一个节点上。 到目前为止&#xff0c;你看到的每个节点都至少存储了一个分片&#xff0c;但值得注意的是&#xff0c;节点并不总是必须存储分片。 这是因为每个…

【Unity3D】运动模糊特效

1 运动模糊原理 开启混合&#xff08;Blend&#xff09;后&#xff0c;通过 Alpha 通道控制当前屏幕纹理与历史屏幕纹理进行混合&#xff0c;当有物体运动时&#xff0c;就会将当前位置的物体影像与历史位置的物体影像进行混合&#xff0c;从而实现运动模糊效果&#xff0c;即模…

javascript基础二十二:举例说明你对尾递归的理解,有哪些应用场景

一、递归 递归&#xff08;英语&#xff1a;Recursion&#xff09; 在数学与计算机科学中&#xff0c;是指在函数的定义中使用函数自身的方法 在函数内部&#xff0c;可以调用其他函数。如果一个函数在内部调用自身本身&#xff0c;这个函数就是递归函数 其核心思想是把一个大型…

Redis高级数据结构之GEO

GEO的介绍 Redis3.2版本提供了GEO地址位置信息定位的功能。支持存储地理位置信息来实现诸如摇一摇&#xff0c;附近位置这类地理位置信息的功能。 Redis也是使用业界比较通用的地理位置距离排序算法GeoHash算法。将二维的经纬度坐标数据映射到一维的整数&#xff0c;将所有元素…

1.项目环境部署操作

第一步 将资料中提供虚拟机压缩包, 解压到一个没有中文没有空格, 以及磁盘空间相对充足的磁盘中(大于100GB) 第二步 修改VMware的网卡设置: 统一修改为 88网段, 网关为192.168.88.2 vm虚拟机 windows系统 第三步 将两台项目虚拟机挂载到VMware上

Kubernetes学习笔记-开发应用的最佳实践(2)20230604

三、确保所有的客户端请求都得到了妥善处理 如何在pod启动的时候&#xff0c;确保所有的连接都被妥善处理了 1、在pod启动时避免客户端连接断开 当个pod启动的时候&#xff0c;他以服务端点的方式提供给所有的服务&#xff0c;这些服务的标签选择器和pod的标签匹配。pod需要…

【简单实用框架】【读Excel表】【可移植】

☀️博客主页&#xff1a;CSDN博客主页 &#x1f4a8;本文由 我是小狼君 原创&#xff0c;首发于 CSDN&#x1f4a2; &#x1f525;学习专栏推荐&#xff1a;面试汇总 ❗️游戏框架专栏推荐&#xff1a;游戏实用框架专栏 ⛅️点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd;&…

【算法思维】-- 贪心算法

OJ须知&#xff1a; 一般而言&#xff0c;OJ在1s内能接受的算法时间复杂度&#xff1a;10e8 ~ 10e9之间&#xff08;中值5*10e8&#xff09;。在竞赛中&#xff0c;一般认为计算机1秒能执行 5*10e8 次计算。 时间复杂度取值范围o(log2n)大的离谱O(n)10e8O(nlog(n))10e6O(nsqrt(…

LinuxC编程——标准IO

目录 标准I/O一、概念二、特点⭐⭐⭐三、缓冲区⭐⭐⭐3.1 全缓冲3.1 行缓冲3.3 不缓冲 四、函数接口⭐⭐⭐⭐4.1 打开4.1.1 fopen4.1.2 freopen4.1.2 容错机制perror 4.2 关闭4.2.1 fclose4.3 读写操作4.3.1 字符I/O4.3.2 行I/O4.3.3 块I/O 4.4 定位操作4.5 文件结束和错误 脑图…

Redis高级数据结构之HyperLogLog

HyperLogLog的介绍 这并不是一种全新的数据结构、实际类型是一种字符串类型。通过一种基数&#xff08;不重复的元素数量就是基数&#xff09;算法&#xff0c;便可以使用很小的内存空间完成独立总数的统计。数据集可以是IP、Email、ID等官方给出的统计误差是0.81%&#xff0c…

python文本注释数学表达式设置|python绘图中的数学表达式设置

本篇文章将介绍如何在Matplotlib中设置文本、注释和数学表达式&#xff0c;以便更好地呈现数据&#xff0c;提高可视化效果。 文章目录 一、Matplotlib中的文本设置1.1 纯文本设置1.2 含箭头的文本设置 二、Matplotlib中的数学表达式设置三、Matplotlib中的字体设置 一、Matplo…

Vue项目中通过插件pxtorem实现大屏响应式

一、原理 rem单位代表的是根节点的font-size大小&#xff0c;所以当我们在页面上使用rem去替代px的时候&#xff0c;就可以通过修改根节点font-size的值&#xff0c;动态地让页面上的元素根据不同浏览器宽高下去实现变化。 二、工具 1.postcss-pxtorem 作用&#xff1a;在编…

Spring Boot Application.properties和yaml配置文件

文章目录 一、全局配置文件概述二、Application.properties配置文件&#xff08;一&#xff09;创建Spring Boot的Web项目PropertiesDemo&#xff08;二&#xff09;在应用属性文件里添加相关配置1、配置服务器端口号和web虚拟路径2、对象类型的配置与使用&#xff08;1&#x…

微应用如何实现自动更新提示

首先, 先讲一下本次文章所讲的场景, 经过调研, 公司内部使用后台, 当有需求功能迭代的时候, 通常使用者会没有感知, 使用者只会在浏览器内一直打开这个页面, 当需要使用的时候, 再切换这个tab来使用. 这就导致使用者一直不知道系统更新了, 一直没有访问最新的页面(由于最新页面…

日志框架——Log4j2

日志框架——Log4j2 日志框架Log4j21. 概述2. Log4j2主要由几个重要的组件构成:3.项目中使用3.1 引入相关依赖pom.xml3.2 加入日志配置文件src/main/resources/log4j2.xml3.3 测试 日志框架Log4j2 1. 概述 Apache Log4j2是一个开源的日志记录组件&#xff0c;使用非常的广泛。…

【Protobuf速成指南】什么是Protobuf?

文章目录 一、序列化和反序列化1.1 概念1.2 场景1.3 如何序列化 二、Protobuf介绍1. 自身特点2.使用特点 一、序列化和反序列化 1.1 概念 &#x1f3af;[总结]: 序列化&#xff1a;把对象转换为字节序列的过程称为对象的序列化。反序列化&#xff1a;把字节序列恢复为对象的过…

MySQL数据库 8.DML操作

目录 ​编辑 &#x1f914;前言&#xff1a; &#x1f914;DML介绍&#xff1a; &#x1f914;语法详情&#xff1a; &#x1f600;1.插入数据&#xff1a; 特点&#xff1a; 1.给指定字段添加数据&#xff1a; 代码示例: 运行结果&#xff1a; 2.给所有的字段添加数据&…

好的用户体验和性能:现代前端的双赢之路

部分数据来源&#xff1a;ChatGPT 引言 随着 Web 应用程序的复杂度和重要性逐渐上升&#xff0c;前端开发人员已经开始更加注重应用程序的可用性和性能。在这个快速变化的时代&#xff0c;前端开发是日益增长的一个领域。 在当前的前端领域&#xff0c;用户体验和性能是前端开…

解决在谷歌浏览器下载时文件名包含逗号导致页面显示网页不可用问题

项目场景&#xff1a; 自己开发的文件服务项目在使用时测试反馈在下载文件时&#xff0c;文件名包含逗号时下载失败&#xff0c;无法跳转到下载链接页面。 项目使用springboot开发&#xff0c;文件的上传基于SpringMVC的表单文件上传。但是下载时由于需要下载原文件名&#xf…

【IC设计】基于Verilog的8层矩阵乘法设计

文章目录 项目要求基本要求截断要求低位截断高位饱和 参考结果 项目实现实现思路实现代码matrix_multiplier_16.vtb_mm_mlp.v VCS&Verdi综合前仿真dc综合VCS&Verdi综合后仿真不足之处 项目要求 基本要求 输入有9个矩阵&#xff0c;权重矩阵有8个&#xff0c;分别是We…