第三十八章 Spring之假如让你来写MVC——适配器篇

Spring源码阅读目录

第一部分——IOC篇

第一章 Spring之最熟悉的陌生人——IOC
第二章 Spring之假如让你来写IOC容器——加载资源篇
第三章 Spring之假如让你来写IOC容器——解析配置文件篇
第四章 Spring之假如让你来写IOC容器——XML配置文件篇
第五章 Spring之假如让你来写IOC容器——BeanFactory和FactoryBean
第六章 Spring之假如让你来写IOC容器——Scope和属性填充
第七章 Spring之假如让你来写IOC容器——属性填充特别篇:SpEL表达式
第八章 Spring之假如让你来写IOC容器——拓展篇
第九章 Spring之源码阅读——环境搭建篇
第十章 Spring之源码阅读——IOC篇

第二部分——AOP篇

第十一章 Spring之不太熟的熟人——AOP
第十二章 Spring之不得不了解的内容——概念篇
第十三章 Spring之假如让你来写AOP——AOP联盟篇
第十四章 Spring之假如让你来写AOP——雏形篇
第十五章 Spring之假如让你来写AOP——Joinpoint(连接点)篇
第十六章 Spring之假如让你来写AOP——Pointcut(切点)篇
第十七章 Spring之假如让你来写AOP——Advice(通知)上篇
第十八章 Spring之假如让你来写AOP——Advice(通知)下篇
第十九章 Spring之假如让你来写AOP——番外篇:Spring早期设计
第二十章 Spring之假如让你来写AOP——Aspect(切面)篇
第二十一章 Spring之假如让你来写AOP——Weaver(织入器)篇
第二十二章 Spring之假如让你来写AOP——Target Object(目标对象)篇
第二十三章 Spring之假如让你来写AOP——融入IOC容器篇
第二十四章 Spring之源码阅读——AOP篇

第三部分——事务篇

第二十五章 Spring之曾经的老朋友——事务
第二十六章 Spring之假如让你来写事务——初稿篇
第二十七章 Spring之假如让你来写事务——铁三角篇
第二十八章 Spring之假如让你来写事务——属性篇
第二十九章 Spring之假如让你来写事务——状态篇
第三十章 Spring之假如让你来写事务——管理篇
第三十一章 Spring之假如让你来写事务——融入IOC容器篇
第三十二章 Spring之源码阅读——事务篇

第四部分——MVC篇

第三十三章 Spring之梦开始的地方——MVC
第三十四章 Spring之假如让你来写MVC——草图篇
第三十五章 Spring之假如让你来写MVC——映射器篇
第三十六章 Spring之假如让你来写MVC——拦截器篇
第三十七章 Spring之假如让你来写MVC——控制器篇
第三十八章 Spring之假如让你来写MVC——适配器篇
第三十九章 Spring之假如让你来写MVC——番外篇:类型转换
第四十章 Spring之假如让你来写MVC——ModelAndView篇
第四十一章 Spring之假如让你来写MVC——番外篇:数据绑定
第四十二章 Spring之假如让你来写MVC——视图篇
第四十三章 Spring之假如让你来写MVC——上传文件篇
第四十四章 Spring之假如让你来写MVC——异常处理器篇
第四十五章 Spring之假如让你来写MVC——国际化篇
第四十六章 Spring之假如让你来写MVC——主题解析器篇
第四十七章 Spring之假如让你来写MVC——闪存管理器篇
第四十八章 Spring之假如让你来写MVC——请求映射视图篇
第四十九章 Spring之假如让你来写MVC——番外篇:属性操作
第五十章 Spring之假如让你来写MVC——融入IOC容器篇
第五十一章 Spring之源码阅读——MVC篇


文章目录

  • Spring源码阅读目录
    • 第一部分——IOC篇
    • 第二部分——AOP篇
    • 第三部分——事务篇
    • 第四部分——MVC篇
  • 前言
  • 尝试动手写IOC容器
      • 第三十四版 适配器
        • `request`参数
        • 参数类型转换
        • 注解适配器
        • 改造`DispatcherServlet`
        • 测试
  • 总结


前言

    对于Spring一直都是既熟悉又陌生,说对它熟悉吧,平时用用没啥问题,但面试的时候被问的一脸懵逼,就很尴尬,都不好意思在简历上写着熟悉Spring了
在这里插入图片描述

    所以决定花点时间研究研究Spring的源码。主要参考的书籍是:《Spring源码深度解析(第2版)》、《Spring揭秘》、《Spring技术内幕:深入解析Spring架构与设计原理(第2版)》


     书接上回,在上篇 第三十七章 Spring之假如让你来写MVC——控制器篇 中,A君 已经完成了 控制器 部分的功能了。接下来看看 A君 会有什么骚操作吧

尝试动手写IOC容器

    出场人物:A君(苦逼的开发)、老大(项目经理)

    背景:老大 要求 A君在一周内开发个简单的 IOC容器

    前情提要:A君 已经完成了 控制器 部分的功能了 。。。

第三十四版 适配器

    “A君 呐,你用if..else来判断调用哪个处理器就很灵性了。” 老大 感叹道

    “额。” A君 不知道怎么结果话茬,只能装傻充愣了

    “这种拓展性太低了,后边还有其他处理器的实现方式呢。这样子往后稍微发展下,你的代码就变成了传说中的 屎山代码。还有一点,就是关于请求参数的处理,要知道一个请求可以携带很多参数的,而你现在这个实现,用户只能去request里获取,这一点用户体验非常的糟糕。去把这两部部分内容处理下吧。” 说完,老大 就没再说什么了

    看到 老大 没有继续往下说的意思,看来是要自己发挥。A君 默默的退了出去,准备着手干活了

    对于这种类型适配器,A君 其实并不算陌生,早在之前实现 IOC容器 的时候,A君 就已经接触过,只不过当时是根据属性类型找到对应的转换器进行类型转换。而这次,只是改成根据不同的 控制器 找到对应的 适配器 而已,其本质本无实际上的区别。那么 适配器 的接口就好定义了,必然存在着两个方法,一个是能不能转换,另一个是进行转换。基本盘有了,接下来就要考虑特殊性了,由于HTTP中定义了Last-Modified。其规范如下:

在这里插入图片描述

这个属性跟缓存息息相关,如果没有超过对应时间,则服务器可以直接不处理,返回上次结果。那么整体的接口定义就出来了,A君 新增HandlerAdapter接口,代码如下:

/**
 * 控制器的适配器,找到对应的控制器
 */
public interface HandlerAdapter {
    /**
     * 是否可以转换
     *
     * @param handler
     * @return
     */
    boolean supports(Object handler);

    /**
     * 处理请求
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    Object handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

    /**
     * 如果没有默认实现,默认返回-1
     * 这个方法放在这里感觉并不合适,LastModified和适配器并有什么关系,违反了接口单一原则,并不符合Spring一贯的作风
     * 新版本已经弃用,这里考虑到老项目
     *
     * @param request
     * @param handler
     * @return
     */
    @Deprecated
    long getLastModified(HttpServletRequest request, Object handler);
}

Last-Modified的处理方法放在这里感觉并不合适,Last-Modified和适配器本身并没有什么关系,违反了接口单一原则。按理说,适配器只负责找到处理对应的控制器进行处理,并不会去关心Last-Modified这些东西,此处的设计不符合Spring一贯的作风,不知道当初是基于什么考虑才如此设计,这里为了贴近Spring,所以也加上getLastModified方法。在Spring5.3.9后续版本中,该方法已经标记成弃用

好了,接口出来后,先挑个简单的练练手,A君 盯上了ControllerServlet,原因也简单,实现了对应接口的,其实是最好处理的,只需要强转后调用该接口方法就行了。这里 A君 就以Controller适配器为例,新增SimpleControllerHandlerAdapter类,代码如下:

/**
 * Controller相关接口适配
 */
public class SimpleControllerHandlerAdapter implements HandlerAdapter {

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

    @Override
    public Object handle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        return ((Controller) handler).handleRequest(request, response);
    }

    @Override
    @SuppressWarnings("deprecation")
    public long getLastModified(HttpServletRequest request, Object handler) {
        if (handler instanceof LastModified) {
            return ((LastModified) handler).getLastModified(request);
        }
        return -1L;
    }
}

    简单过后,A君 就开始头疼了,简单之所以简单,是因为实现了对应接口,方法参数、返回值都是固定的。而基于注解的 控制器,这两个没有一个是能确定的,不能确定就意味着:需要框架去推算匹配,那么参数处理就会变得异常的麻烦。没办法,只能先易后难。A君 谨遵 老大 的教诲,对于有公共内容的,提取出来一个抽象类。于是,A君 新增AbstractHandlerMethodAdapter类,代码如下:

public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered {

    @Override
    public final boolean supports(Object handler) {
        return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
    }

    @Override
    public final Object handle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        return handleInternal(request, response, (HandlerMethod) handler);
    }

    protected abstract Object handleInternal(HttpServletRequest request,
                                             HttpServletResponse response, HandlerMethod handlerMethod) throws Exception;

    protected abstract boolean supportsInternal(HandlerMethod handlerMethod);

    @Override
    @SuppressWarnings("deprecation")
    public final long getLastModified(HttpServletRequest request, Object handler) {
        return getLastModifiedInternal(request, (HandlerMethod) handler);
    }

    @Deprecated
    protected abstract long getLastModifiedInternal(HttpServletRequest request, HandlerMethod handlerMethod);

}

简单的完成了,接下来就得考虑困难的事了,当务之急,就是要解决参数匹配的问题,不然 注解适配类 无从实现

request参数

    要想自动适配 控制器 参数,A君 得先知道哪些可以成为入参,也就是一个request请求中,哪些可以作为 控制器 的参数。A君 在翻阅 Servlet规范 时,看到这些东西,如下:

  1. 请求参数:
    在这里插入图片描述
  2. 属性:
    在这里插入图片描述

3.头:

在这里插入图片描述

那么正常情况下,request中:属性(Attributes)、请求头(Headers)、请求参数(Params) 都可以作为 控制器 的入参。明白参数从哪里之后,就可以先把这部分东西抽象化了。这里要注意的是,虽然 A君 一直以 Servlet规范 作为例子,实际上在Web环境下,不仅仅 Servlet 一种规范,还有很多不同的规范,像:WebSocket、 WebFlux、Portlet 等,所以这里定义接口,更是为了通用性,适配这些规范。A君 定义 RequestAttributes 接口,用来封装 属性(Attributes) 的相关操作。代码如下:

/**
 * request请求属性
 */
public interface RequestAttributes {

    int SCOPE_REQUEST = 0;

    int SCOPE_SESSION = 1;


    /**
     * request作用域
     */
    String REFERENCE_REQUEST = "request";
    /**
     * session作用域
     */
    String REFERENCE_SESSION = "session";

    Object getAttribute(String name, int scope);

    void setAttribute(String name, Object value, int scope);

    void removeAttribute(String name, int scope);

    String[] getAttributeNames(int scope);

    /**
     * 注册回调
     *
     * @param name
     * @param callback
     * @param scope
     */
    void registerDestructionCallback(String name, Runnable callback, int scope);

    /**
     * 处理引用类型
     *
     * @param key
     * @return
     */
    Object resolveReference(String key);

    /**
     * 获取SessionId
     *
     * @return
     */
    String getSessionId();

    /**
     * 获取Session同步锁
     *
     * @return
     */
    Object getSessionMutex();

}

这个接口只是规定了 属性(Attributes) 的相关操作,显然是远远不够的,还需要 请求头(Headers)、请求参数(Params) 的相关操作,于是,A君 对其进行拓展,定义WebRequest接口。代码如下:

public interface WebRequest extends RequestAttributes {

    /**
     * 获取请求头
     *
     * @param headerName
     * @return
     */
    String getHeader(String headerName);

    String[] getHeaderValues(String headerName);

    Iterator<String> getHeaderNames();

    String getParameter(String paramName);

    String[] getParameterValues(String paramName);

    Iterator<String> getParameterNames();

    Map<String, String[]> getParameterMap();

    Locale getLocale();

    String getContextPath();

    String getRemoteUser();

    Principal getUserPrincipal();

    boolean isUserInRole(String role);

    boolean isSecure();

    /**
     * 检查资源是否过期
     *
     * @param lastModifiedTimestamp
     * @return
     */
    boolean checkNotModified(long lastModifiedTimestamp);

    boolean checkNotModified(String etag);

    boolean checkNotModified(String etag, long lastModifiedTimestamp);

    String getDescription(boolean includeClientInfo);

}

前文提到,由于存在着多种规范,那么对于获取其真实的请求或者响应对象就很有必要了,A君 不可以预判到用户要用哪种协议,所以只能再定义个接口,用来获取真实的请求对象、响应对象。新增NativeWebRequest接口,代码如下:

public interface NativeWebRequest extends WebRequest {
    /**
     * 获取真实请求对象
     *
     * @return
     */
    Object getNativeRequest();

    /**
     * 获取真实响应对象
     *
     * @return
     */
    Object getNativeResponse();

    <T> T getNativeRequest(Class<T> requiredType);

    <T> T getNativeResponse(Class<T> requiredType);
}

接口定义完成之后,接下来就是实现了,虽然存在着多种规范,但 A君 并不太关心,起码现在不用关心,这里是 MVC 的主场。A君 只关注 Servlet 的实现,那问题就简单了,这些东西只需要从 Servlet容器 中获取就行了。A君 新增ServletWebRequest类,代码如下:

public class ServletWebRequest extends ServletRequestAttributes implements NativeWebRequest {
	
	@Override
    public String getParameter(String paramName) {
        return getRequest().getParameter(paramName);
    }
    @Override
    public String getHeader(String headerName) {
        return getRequest().getHeader(headerName);
    }
    //省略其他方法
}
参数类型转换

    解决了参数的来源问题,现在就可以开始进行参数的转换了。要知道request请求过来的参数可只有字符串类型或字节流,字节流现在不在考虑的范围之内,那还需要把字符串转成 控制器 对应的类型才行。所谓自动适配,无非就是把请求参数转成方法参数罢了。转换接口依旧是那两板斧,能处理吗?进行处理!玩不出什么花来。于是,A君 照猫画虎,定义HandlerMethodArgumentResolver接口,代码如下:

/**
 * 参数转换接口
 */
public interface HandlerMethodArgumentResolver {

    boolean supportsParameter(MethodParameter parameter);

    Object resolveArgument(MethodParameter parameter, NativeWebRequest webRequest) throws Exception;
}

接着就是实现了,类那么多,A君 还没颠到全部实现的地步,只要实现主流的类就行了,剩余的部分嘛?就看用户自己发挥了。A君 决定先从两个最重要的参数开始,那就是:requestresponse。两个都是类似的,这里就以request为例,A君 新增ServletRequestMethodArgumentResolver类,那么问题来了,要支持那些类型呢? 对于Web来说,request可谓是举足轻重,主要是从request可以拿到太多的东西了,比如:ServletRequestHttpSessionPrincipalInputStream等。那么能从request中获取的东西,就是ServletRequestMethodArgumentResolver支持的类型。代码如下:

public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {
	@Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> paramType = parameter.getParameterType();
        return (WebRequest.class.isAssignableFrom(paramType) ||
                ServletRequest.class.isAssignableFrom(paramType) ||
                HttpSession.class.isAssignableFrom(paramType) ||
                (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
                Principal.class.isAssignableFrom(paramType) ||
                InputStream.class.isAssignableFrom(paramType) ||
                Reader.class.isAssignableFrom(paramType) ||
                HttpMethod.class == paramType);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  NativeWebRequest webRequest) throws Exception {
        Class<?> paramType = parameter.getParameterType();
        if (WebRequest.class.isAssignableFrom(paramType)) {
            if (!paramType.isInstance(webRequest)) {
                throw new IllegalStateException("Current request is not of type [" + paramType.getName() + "]: " + webRequest);
            }
            return webRequest;
        }
        if (ServletRequest.class.isAssignableFrom(paramType)) {
            return resolveNativeRequest(webRequest, paramType);
        }
        return resolveArgument(paramType, resolveNativeRequest(webRequest, HttpServletRequest.class));
    }
}

这还是很简单的,一堆if...esle就行了,A君 感叹道。接着还需要支持下基本类型,首先要考虑的是如何从request中获取到参数,HTTP请求只有字符串或字节流,按照类型匹配显然是不合适的。现在 参数转换器 要做的事情,就是根据名称从request对应的值,转成 控制器 参数的类型:

解析
request参数
参数类型
是否匹配类型
成功转换
转换失败
控制器参数
结束

A君 先定义一个NamedValueInfo类,用以包装参数名相关信息。代码如下:

protected static class NamedValueInfo {

        private final String name;

        private final boolean required;


        private final String defaultValue;

        public NamedValueInfo(String name, boolean required, String defaultValue) {
            this.name = name;
            this.required = required;
            this.defaultValue = defaultValue;
        }
    }

接着,A君 思考了下:根据方法参数名(方法参数名的获取在AOP 相关章节中有过说明,这里就不在进行赘述了),去request中获取值,不论是 属性(Attributes)、请求头(Headers)、请求参数(Params) 都是从 request 中获取的,只是地方不一样罢了,那这就可以提取一个抽象类了。于是,A君 定义AbstractNamedValueMethodArgumentResolver类,代码如下:

public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {
	@Override
    public final Object resolveArgument(MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
    	/**
         * 获取目标参数名
         */
        NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
        MethodParameter nestedParameter = parameter.nestedIfOptional();

        Object resolvedName = namedValueInfo.name;
        if (resolvedName == null) {
            throw new IllegalArgumentException(
                    "Specified name must not resolve to null: [" + namedValueInfo.name + "]");
        }
		/**
         * 从request获取值,由子类实现
         */
        Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
        if (arg == null) {
            if (namedValueInfo.defaultValue != null) {
                arg = namedValueInfo.defaultValue;
            } else if (namedValueInfo.required && !nestedParameter.isOptional()) {
                handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
            }
            arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
        } else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
            arg = namedValueInfo.defaultValue;
        }

        handleResolvedValue(arg, namedValueInfo.name, parameter, webRequest);

        return arg;
    }
    //省略其他方法
}

好了,抽象类已经抽取完毕,那么 属性(Attributes)、请求头(Headers)、请求参数(Params) 都大同小异了,只需要去各自的区域取值就行了。A君 这里以 请求参数(Params) 为例,新增RequestParamMethodArgumentResolver类,代码如下:

public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
        implements UriComponentsContributor {

	@Override
    public boolean supportsParameter(MethodParameter parameter) {
        if (parameter.hasParameterAnnotation(RequestParam.class)) {
            /**
             * map类型
             */
            if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
                RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
                return (requestParam != null && StringUtils.hasText(requestParam.name()));
            } else {
                return true;
            }
        } else {
            /**
             * 是否是Optional
             */
            parameter = parameter.nestedIfOptional();
            if (this.useDefaultResolution) {
                return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
            } else {
                return false;
            }
        }
    }
	@Override
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
        HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
        Object arg = null;
        if (arg == null) {
            String[] paramValues = servletRequest.getParameterValues(name);
            if (paramValues != null) {
                arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
            }
        }
        return arg;
    }
    //省略其他方法
}

好了,现在都准备好了,剩下的就是把这些参数处理器进行一下整合,方便别人使用。A君 在定义HandlerMethodArgumentResolverComposite类,这个不负责具体实现,而是调用别的类来实现转换,类似于一个管理者的角色。代码如下:

public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {

    private final List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();

    private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache =
            new ConcurrentHashMap<>(256);


	 @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return getArgumentResolver(parameter) != null;
    }

    private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
        HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
        if (result == null) {
            for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
                if (resolver.supportsParameter(parameter)) {
                    result = resolver;
                    this.argumentResolverCache.put(parameter, result);
                    break;
                }
            }
        }
        return result;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, NativeWebRequest webRequest) throws Exception {

        HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
        if (resolver == null) {
            throw new IllegalArgumentException("Unsupported parameter type [" +
                    parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
        }
        return resolver.resolveArgument(parameter, webRequest);
    }

好嘞,现在处理参数部分也完事了,饶了一大圈,总算把参数处理完了,现在可以继续前行了

注解适配器

    解决完最麻烦的参数处理之后,剩下的 注解适配器 就没有什么东西了,跟反射调用基本一样了。A君 新增RequestMappingHandlerAdapter类,代码如下:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
        implements BeanFactoryAware, InitializingBean {
	
	private HandlerMethodArgumentResolverComposite argumentResolvers;
	protected Object invokeHandlerMethod(HttpServletRequest request,
                                         HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

        ServletWebRequest webRequest = new ServletWebRequest(request, response);
        try {
            ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
            if (this.argumentResolvers != null) {
                invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
            }
            if (this.returnValueHandlers != null) {
                invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
            }
            invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);


            return invocableMethod.invokeAndHandle(webRequest);
        } finally {
            webRequest.requestCompleted();
        }
    }
    //省略其他代码
}

改造DispatcherServlet

    适配器 弄完之后,A君 想起还需要对DispatcherServlet进行改造,毕竟当初折腾 适配器 的目的,不就是为了替换DispatcherServlet中的if...else吗?改造如下:

  1. 新增 适配器 类:

在这里插入图片描述

  1. 替换doDispatch方法中的if...else

在这里插入图片描述

测试

    现在一切都准备好了,可以进入检验成果的时候了。A君 修改HelloController,新增一个参数。代码如下:

@Controller
public class HelloController {

    @RequestMapping("/hello")
    public String sayHello(HttpServletRequest req, HttpServletResponse resp, String str) {
        String key = "message";
        String message = (String) req.getAttribute(key);
        if (message == null) {
            message = "";
        }
        req.setAttribute(key, message + " V34 HandleMapping! " + str);
        return "hello";
    }
}

其他测试代码不需要变动,编写测试代码如下:

 @Test
    public void v34() throws Throwable {
        System.out.println("############# 第三十四版: 适配器篇 #############");
        Tomcat tomcat = new Tomcat();
        //设置端口
        tomcat.setPort(8082);
        //设置静态资源路径
        String webApp = new File("src/main/resources/v34").getAbsolutePath();
        Context context = tomcat.addWebapp("/test/", webApp);
        tomcat.start();
        //挂起
        tomcat.getServer().await();
    }

测试结果如下:

在这里插入图片描述

后台成功接收到参数并显示出来了,说明 A君 的努力并没有白费。OK!这下子总算弄好了,也可以向 老大 交差了

在这里插入图片描述


总结

    正所谓树欲静而风不止,欲知后事如何,请看下回分解(✪ω✪)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

了解 ASP.NET Core 中的中间件

在 .NET Core 中&#xff0c;中间件&#xff08;Middleware&#xff09; 是处理 HTTP 请求和响应的核心组件。它们被组织成一个请求处理管道&#xff0c;每个中间件都可以在请求到达最终处理程序之前或之后执行操作。中间件可以用于实现各种功能&#xff0c;如身份验证、路由、…

kalilinux - 目录扫描之dirsearch

情景导入 先简单介绍一下dirsearch有啥用。 假如你现在访问一个网站&#xff0c;例如https://www.example.com/ 它是一个电商平台或者其他功能性质的平台。 站在开发者的角度上思考&#xff0c;我们只指导https://www.example.com/ 但不知道它下面有什么文件&#xff0c;文…

DHCP、MSTP+VRRP总结实验

R1即使服务器&#xff08;给予dhcp的地址的&#xff09; [LSW1]int Eth-Trunk 12 [LSW1-Eth-Trunk12]mode manual load-balance //配置链路聚合模式为手工负载分担模式 [LSW1-Eth-Trunk12]load-balance src-dst-mac //配置基于源目IP的负载分担模式[LSW1-Eth-Trunk12]trunk p…

【爬虫】单个网站链接爬取文献数据:标题、摘要、作者等信息

源码链接&#xff1a; https://github.com/Niceeggplant/Single—Site-Crawler.git 一、项目概述 从指定网页中提取文章关键信息的工具。通过输入文章的 URL&#xff0c;程序将自动抓取网页内容 二、技术选型与原理 requests 库&#xff1a;这是 Python 中用于发送 HTTP 请求…

RabbitMQ故障全解析:消费、消息及日常报错处理与集群修复

文章目录 前言&#xff1a;1 消费慢2 消息丢失3 消息重复消费4 日常报错及解决4.1 报错“error in config file “/etc/rabbitmq/rabbitmq.config” (none): no ending found”4.2 生产者发送消息报错4.3 浏览器打开IP地址&#xff0c;无法访问 RabbitMQ&#xff08;白屏没有结…

C#格式化输出

C#提供了多个格式化输出的方法&#xff0c;使得我们在灵活且可读的方法构建字符串&#xff1b;主要的格式化方法包括&#xff1a;String.Format方法、字符串插值&#xff0c;以及使用符合格式字符串与Console.WriteLint或Console.Write函数。 String.Format方法 string.Format…

记一次学习skynet中的C/Lua接口编程解析protobuf过程

1.引言 最近在学习skynet过程中发现在网络收发数据的过程中数据都是裸奔&#xff0c;就想加入一种数据序列化方式&#xff0c;json、xml简单好用&#xff0c;但我就是不想用&#xff0c;于是就想到了protobuf&#xff0c;对于protobuf C/C的使用个人感觉有点重&#xff0c;正好…

vue2修改表单只提交被修改的数据的字段传给后端接口

效果&#xff1a; 步骤一、 vue2修改表单提交的时候&#xff0c;只将修改的数据的字段传给后端接口&#xff0c;没有修改得数据不传参给接口。 在 data 对象中添加一个新的属性&#xff0c;用于存储初始表单数据的副本&#xff0c;与当前表单数据进行比较&#xff0c;找出哪些…

大数据运维管理体系的搭建

[〇]关于本文 本文将介绍一种大型集群的运维管理体系 【大型集群的管理大于解决问题】意在大规模数据集群的运维过程中&#xff0c;系统化、规范化的管理措施比单纯的故障处理更为关键。通过有效的管理&#xff0c;可以预防问题的发生、提升系统的稳定性和性能&#xff0c;从而…

如何使用开源图床程序EasyImage搭建一个私有图库并实现远程传图

前言&#xff1a;在输出内容时&#xff0c;一张高质量的图片往往能够瞬间吸引读者的眼球&#xff0c;提升内容的整体价值。然而&#xff0c;对于许多博主、站长和自媒体人来说&#xff0c;找到一个稳定且免费的图床服务却成了头疼的问题。很多图床要么不稳定&#xff0c;导致图…

Java Web开发进阶——错误处理与日志管理

错误处理和日志管理是任何生产环境中不可或缺的一部分。在 Spring Boot 中&#xff0c;合理的错误处理机制不仅能够提升用户体验&#xff0c;还能帮助开发者快速定位问题&#xff1b;而有效的日志管理能够帮助团队监控应用运行状态&#xff0c;及时发现和解决问题。 1. 常见错误…

二分查找算法——山脉数组的峰顶索引

一.题目描述 852. 山脉数组的峰顶索引 - 力扣&#xff08;LeetCode&#xff09; 二.题目解析 题目给了我们一个山脉数组&#xff0c;山脉数组的值分布就如下面的样子&#xff1a; 然后我们只需要返回数组的峰值元素的下标即可。 三.算法原理 1.暴力解法 因为题目明确说明…

2. Doris数据导入与导出

一. Doris数据导入 导入方式使用场景支持的文件格式导入模式Stream Load导入本地文件或者应用程序写入csv、json、parquet、orc同步Broker Load从对象存储、HDFS等导入csv、json、parquet、orc异步Routine Load从kakfa实时导入csv、json异步 1. Stream Load 基本原理 在使用…

30_Redis哨兵模式

在Redis主从复制模式中,因为系统不具备自动恢复的功能,所以当主服务器(master)宕机后,需要手动把一台从服务器(slave)切换为主服务器。在这个过程中,不仅需要人为干预,而且还会造成一段时间内服务器处于不可用状态,同时数据安全性也得不到保障,因此主从模式的可用性…

把PX4及子仓库添加到自己的gitee

导入主仓库 此处以导入PX4为例 先用gitee导入仓库然后clone gitee仓库先checkout到v1.11&#xff0c;git submodule update --init --recursive&#xff0c;确保可以make之后再新建branchgit checkout -b my1.11.0按照提示连接到origin改代码然后三件套就行了git add ./*git …

解决:ubuntu22.04中IsaacGymEnv保存视频报错的问题

1. IsaacGymEnvs项目介绍 IsaacGymEnvs&#xff1a;基于NVIDIA Isaac Gym的高效机器人训练环境 IsaacGymEnvs 是一个基于 NVIDIA Isaac Gym 的开源 Python 环境库&#xff0c;专为机器人训练提供高效的仿真环境。Isaac Gym 是由 NVIDIA 开发的一个高性能物理仿真引擎&#xf…

ELK日志分析实战宝典之ElasticSearch从入门到服务器部署与应用

目录 ELK工作原理展示图 一、ElasticSearch介绍&#xff08;数据搜索和分析&#xff09; 1.1、特点 1.2、数据组织方式 1.3、特点和优势 1.3.1、分布式架构 1.3.2、强大的搜索功能 1.3.3、数据处理与分析 1.3.4、多数据类型支持 1.3.5、易用性与生态系统 1.3.6、高性…

android 自定义SwitchCompat,Radiobutton,SeekBar样式

纯代码的笔记记录。 自定义SwitchCompat按钮的样式 先自定义中间的圆球switch_thumb_bg.xml <?xml version"1.0" encoding"utf-8"?> <shape xmlns:android"http://schemas.android.com/apk/res/android"android:shape"oval&q…

【学习路线】Python自动化运维 详细知识点学习路径(附学习资源)

学习本路线内容之前&#xff0c;请先学习Python的基础知识 其他路线&#xff1a; Python基础 >> Python进阶 >> Python爬虫 >> Python数据分析&#xff08;数据科学&#xff09; >> Python 算法&#xff08;人工智能&#xff09; >> Pyth…

【URDF和SDF区别】

URDF&#xff08;Unified Robot Description Format&#xff0c;统一机器人描述格式&#xff09;和SDF&#xff08;Simulation Description Format&#xff0c;仿真描述格式&#xff09;是两种常用的机器人和仿真环境建模格式。虽然它们在许多方面有相似之处&#xff0c;但也存…