前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【小家Spring】Spring MVC好用工具介绍:UrlPathHelper、WebUtils、RequestContextUtils、WebApplicationContextUtils...

【小家Spring】Spring MVC好用工具介绍:UrlPathHelper、WebUtils、RequestContextUtils、WebApplicationContextUtils...

作者头像
YourBatman
发布2019-09-03 15:18:45
1.4K0
发布2019-09-03 15:18:45
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦
前言

随着struts2漏洞的出现,以及struts2使用的不方便,过重的设计。所以市面上MVC的实际标已经成了Spring MVC。

因此本文主要针对Spring MVC的web环境下,Spring-web提供的这个jar里的util包内的一些类,因为都是比较共用的一些web类,因此在这里做一些介绍~

实用类介绍(排名不分先后)
ContentCachingRequestWrapper、ContentCachingResponseWrapper

字面理解:把请求/响应内容缓存起来的代理类~

这两个类出现的版本比较晚(如下),之前我们一直实用的HttpServletRequestWrapper,现在有了它,使用起来就更加的方便了~

代码语言:javascript
复制
/**
* @since 4.1.3
*/
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {}

我们都知道request.getInputStream()请求body里面的内容只能被获取一次的。通过这个类(但其实有bug):

**它能够解决:解决HttpServletRequest inputStream只能读取一次的问题**

**它能够解决:解决HttpServletRequest inputStream只能读取一次的问题**

上篇博文 【小家Spring】从OncePerRequestFilter的源码解读去看看Spring里面的Filter有什么特别以及常用Filter使用介绍

最后讲到请求日志里看到,payload只有请求结束的时候才打印出来,为何呢?

我们看看源码:

代码语言:javascript
复制
private final ByteArrayOutputStream cachedContent;

它使用一个字段:cachedContent来缓存body体里面的内容。但最重要的是,什么时候才会向cachedCotent里面写内容呢?我们继续跟踪发现:大体有两个地方:

代码语言:javascript
复制
public String getParameter(String name);
public Map<String, String[]> getParameterMap();
public Enumeration<String> getParameterNames();
public String[] getParameterValues(String name)

//都有这个判断。必须没被写过,并且是表单形式的Post方式才会往里写内容(这是一种非常特殊的传值方式,使用较少)
if (this.cachedContent.size() == 0 && isFormPost()) {
	writeRequestParametersToCachedContent();
}

第二种方式才是重点:

代码语言:javascript
复制
	@Override
	public int read() throws IOException {
		int ch = this.is.read();
		if (ch != -1 && !this.overflow) {
			if (contentCacheLimit != null && cachedContent.size() == contentCacheLimit) {
				this.overflow = true;
				handleContentOverflow(contentCacheLimit);
			}
			else {
				cachedContent.write(ch);
			}
		}
		return ch;
	}

我们发现还是在read()方法里面,只有调用了请求流的read方法,才会把内容缓存起来。这个和Spring MVC的原理:@RequestBody注解的参数解析器(RequestResponseBodyMethodProcessor)就是调用了read()方法去获取到内容的。

到此我们其实能够很好的解释,上篇博文里为何请求开始没有payload,请求结束时有了

但是这么做,很多时候并不能满足我们的使用场景:

1、我们需要在filter中拿到requestBody数据进行处理(比如检查敏感词汇,过滤掉低俗的词汇等~)

2、在controller中注入@ReqeustBody读取rerquestBody数据

按照Spring目前的设计,这个request只要我们getInputStream()就不能再被controller接受了(·requestBody is missing…·),显然不是我们想要的。这个其实是Spring的一个bug,早在2014年就有人提出了,只是一直都没有被修复:this is a bug

在读取的时候应该先去缓存看看,有值就不要读流了,返回即可。这样缓存里面的数据是可以玩限次重复读的。在还没有这个类的时候,我写了一个wrapper,供以参考:

代码语言:javascript
复制
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper{
    
    private byte[] body;
    
    private BufferedReader reader;

    private ServletInputStream inputStream;

    public ContentCachingRequestWrapper(HttpServletRequest request) throws IOException{
        super(request);
        //读一次 然后缓存起来
        body = IOUtils.toByteArray(request.getInputStream());
        inputStream = new RequestCachingInputStream(body);
    }
    
    public byte[] getBody() {
        return body;
    }
    
    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (inputStream != null) {          
            return inputStream;
        }
        return super.getInputStream();
    }

    @Override
    public BufferedReader getReader() throws IOException {
        if (reader == null) {
            reader = new BufferedReader(new InputStreamReader(inputStream, getCharacterEncoding()));
        }
        return reader;
    }
    
    //代理一下ServletInputStream 里面真是内容为当前缓存的bytes
    private static class RequestCachingInputStream extends ServletInputStream {
        
        private final ByteArrayInputStream inputStream;

        public RequestCachingInputStream(byte[] bytes) {
            inputStream = new ByteArrayInputStream(bytes);
        }
        @Override
        public int read() throws IOException {
            return inputStream.read();
        }

        @Override
        public boolean isFinished() {
            return inputStream.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readlistener) {
        }

    }
    
}

因此,千万千万不要在Filter里提前去**getInputStream()**,否则**@RequestBody**将拿不到东西而报错的。使用此类的时候需要注意~

CookieGenerator

顾名思义,是生成Cookie的。使用起来也比较简单,不做介绍。一般在CAS单点登录里用得较多。现在都JWT了,就用得较少了

HtmlUtils

很多时候,由于特殊字符的原因,会造成用户输入的信息反馈到页面上时会显示成乱码,造成页面排版混乱;另外,黑客经常利用特殊字符对网站进行xss跨站攻击,所以我们需要对页面上提交的特殊字符进行html转码。

Spring提供的这个工具类,省去了我们写工具类对html中的特殊字符进行过滤的麻烦。

代码语言:javascript
复制
    public static void main(String[] args) {
        String specialStr = "#<table id=\"testid\"><tr>test1;test2</tr></table>";
        // 转义(用转义字符表示): #&lt;table id=&quot;testid&quot;&gt;&lt;tr&gt;test1;test2&lt;/tr&gt;&lt;/table&gt;
        String str1 = HtmlUtils.htmlEscape(specialStr);
        System.out.println(str1);

        // 转义(用数字字符表示): #&#60;table id=&#34;testid&#34;&#62;&#60;tr&#62;test1;test2&#60;/tr&#62;&#60;/table&#62;
        String str2 = HtmlUtils.htmlEscapeDecimal(specialStr);
        System.out.println(str2);

        // 转义(用16进制表示)
        String str3 = HtmlUtils.htmlEscapeHex(specialStr);
        System.out.println(str3);

        // 返转义,一个方法即可 结果见上面 specialStr
        System.out.println(HtmlUtils.htmlUnescape(str1));
        System.out.println(HtmlUtils.htmlUnescape(str2));
        System.out.println(HtmlUtils.htmlUnescape(str3));
    }

这样转义后是安全的。比如用编辑器编辑成良好格式的HTML串传过来。我们一般要过滤掉<script>这种标签,防止被黑

另外JavaScriptUtils可以js里面的一些特殊符号转义。如’ // 等等符号

TagUtils

jsp标签的工具支持类。可以说已废弃毕竟jsp技术已经过时好久了

UriComponentsBuilder

特别好用,推荐。

Spring MVC 提供了一种机制,可以构造和编码URI – 使用UriComponentsBuilder和UriComponents。这样我们不再自己去拼接了,提高了正确性。它可以细粒度的控制URI的各个要素,如构建、扩展模板变量以及编码

这两个类在Spring MVC controller内部原理中使用特别多,我们若有需要,也可以借助它来使用。

代码语言:javascript
复制
    public static void main(String[] args) {
        UriComponents uriComponents = UriComponentsBuilder.newInstance()
                .scheme("http").host("www.baidu.com").port("8080")
                .path("/junit-5").build();
        System.out.println(uriComponents.toUriString()); //http://www.baidu.com:8080/junit-5
        System.out.println(uriComponents.toString()); //http://www.baidu.com:8080/junit-5
    }

再看一例,单词之间有空格,也是能够支持的

代码语言:javascript
复制
    public static void main(String[] args) {
        UriComponents uriComponents = UriComponentsBuilder.newInstance()
                .scheme("http").host("www.baidu.com").port("8080")
                //此处单词之间有空格 RFC 3986规范是不允许的。这时候若需要,后面encode一下即可
                .path("/junit  abc").build().encode();
        System.out.println(uriComponents.toUriString()); //http://www.baidu.com:8080/junit%20%20abc
    }

注意,UriComponents是不可变的,expand()和encode()都是返回新的实例。

ServletUriComponentsBuilder

它继承自UriComponentsBuilder,相当于把路径和HttpServletRequest扯上关系了~~在原基础上增加了一些方法如:

代码语言:javascript
复制
// @since 3.1
public class ServletUriComponentsBuilder extends UriComponentsBuilder {
	public static ServletUriComponentsBuilder fromContextPath(HttpServletRequest request) { ... }
	fromServletMapping(HttpServletRequest request);
	fromRequestUri(HttpServletRequest request);
	fromRequest(HttpServletRequest request);
	fromCurrentContextPath();
	fromCurrentServletMapping(); 
	...
}
MvcUriComponentsBuilder

UriComponentsBuilder的基础上,从Spring4.0开始又提供了另外一个类:MvcUriComponentsBuilder。从名字就可知,它和MVC有关,也就是和我们的控制器有关。 指向的是Spring MVC上的@RequestMapping方法

它俩所在的包如下:org.springframework.web.util.UriComponentsBuilderorg.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder。前者所属jar为spring-web,后者所属jar为spring-webmvc

代码语言:javascript
复制
// @since 4.0
public class MvcUriComponentsBuilder {
	...
}

用法参考博文:Spring MVC 之 Build URIs

WebAppRootListener

这个listener的作用就是监听web.xml中的配置param-name为webAppRootKey的值:

代码语言:javascript
复制
<context-param>
        <param-name>webAppRootKey</param-name>
        <param-value>myroot</param-value>
</context-param>

然后配置这个监听器:

代码语言:javascript
复制
<listener>
    <listener-class>org.springframework.web.util.WebAppRootListener</listener-class>
</listener>

作用:它会在ServletContext上下文初始化的时候,首先获取根传递进来的servletContext得到物理路径,String path=servletContext.getRealPath("/");然后找到context-param的webAooRootKey对应的param-value,把param-value的值作为key,上面配置的是"myroot"。

接着执行System.setProperty("myroot",path)。这样在web中任意地方就可以使用System.getProperty("myroot")来获取系统的绝对路径。

如果只配置了监听器,没有配置webAppRootKey, 默认wenAppRootKey对应的param-value的值为webapp.root。 Spring提供次做法还是挺好用的,不然我们自己去实现把绝对路径放进去,差不多也是这个步骤。因此这样子我们只需要配上此监听器即可

WebUtils

该工具类主要用于Web应用程序,供各种框架使用。

ServletRequestUtils:若你需要获取参数调用request.getParameter()。那么就使用ServletRequestUtils吧,更方便一些~

setWebAppRootSystemProperty/removeWebAppRootSystemProperty:表示保存/移除系统根路径的key-value

getDefaultHtmlEscape:看web.xml中的defaultHtmlEscape的值是否设置为true

getTempDir:返回由当前servlet容器提供的 当前Web应用程序的临时目录

getRealPath:返回由servlet容器提供的,Web应用程序中给定路径的实际路径

getSessionId:

get/setSessionAttribute:

getNativeRequest:返回指定类型的合适的请求对象,如果可用,会unwrapping给定的request请求

getNativeResponse:

isIncludeRequest:判断请求是否是一个包含(Include)请求,即不是从外部进入的顶级Http请求。

getCookie:

getParametersStartingWith:返回包含具有给定前缀的所有参数的map。将单个值映射为String,多个值映射到String数组

RequestContextUtils

RequestContextUtils类是Spring提供的用于从HttpServletRequest上下文中获取特殊对象的工具类。

代码语言:javascript
复制
	// 这部分代码很简单:它先去request里面获取,然后层层递进的去找,使用方式基本同其余工具类。
	// 备注:此工具类在controller和service也都可以使用(但是它获取到的都会是web子容器,因此一定要注意)
	public static WebApplicationContext findWebApplicationContext(
			HttpServletRequest request, @Nullable ServletContext servletContext) {

		WebApplicationContext webApplicationContext = (WebApplicationContext) request.getAttribute(
				DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE);
		if (webApplicationContext == null) {
			if (servletContext != null) {
				webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
			}
			if (webApplicationContext == null) {
				webApplicationContext = ContextLoader.getCurrentWebApplicationContext();
			}
		}
		return webApplicationContext;
	}

下面是获取九大组件以及相关组件的相关方法:

代码语言:javascript
复制
public static LocaleResolver getLocaleResolver(HttpServletRequest request) {}
public static Locale getLocale(HttpServletRequest request) {}
public static TimeZone getTimeZone(HttpServletRequest request) {}
public static ThemeResolver getThemeResolver(HttpServletRequest request) {}
public static ThemeSource getThemeSource(HttpServletRequest request) {}
public static Theme getTheme(HttpServletRequest request) {}
public static Map<String, ?> getInputFlashMap(HttpServletRequest request) {}
public static FlashMap getOutputFlashMap(HttpServletRequest request) {}
public static FlashMapManager getFlashMapManager(HttpServletRequest request) {}
public static void saveOutputFlashMap(String location, HttpServletRequest request, HttpServletResponse response) {}
WebApplicationContextUtils

当 Web 应用集成 Spring 容器后,这个工具类就可以很方便的访问出一些web组件。比如`.

代码语言:javascript
复制
	@Nullable
	public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
		return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
	}

	//Required这个方法和上面唯一的区别是:它如果返回null 就直接抛出异常了,上面是返回null
	public static WebApplicationContext getRequiredWebApplicationContext(ServletContext sc) throws IllegalStateException {
		WebApplicationContext wac = getWebApplicationContext(sc);
		if (wac == null) {
			throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?");
		}
		return wac;
	}

	// 我在讲解源码的时候:创建容器失败的时候,也会吧异常放进来(我们调用者一般较少使用)
	public static WebApplicationContext getWebApplicationContext(ServletContext sc, String attrName) { ... }
	
	// 这个可以说是上面的加强版。当getWebApplicationContext(sc)没找到时,还会试从ServletContext的属性中查找唯一的一个WebApplicationContext
	// 如果找到的WebApplicationContext不唯一,则抛出异常声明该情况
	public static WebApplicationContext findWebApplicationContext(ServletContext sc) { ... }

	
	//向WebApplicationContext使用的BeanFactory注册web有关作用域对象 :
	public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory,
			@Nullable ServletContext sc) {

		beanFactory.registerScope(WebApplicationContext.SCOPE_REQUEST, new RequestScope());
		beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION, new SessionScope());
		if (sc != null) {
			ServletContextScope appScope = new ServletContextScope(sc);
			beanFactory.registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);
			// Register as ServletContext attribute, for ContextCleanupListener to detect it.
			// 此处,还吧ServletContext上下文的Scope,注册到上下文里,方便ContextCleanupListener 进行获取
			sc.setAttribute(ServletContextScope.class.getName(), appScope);
		}

		// 定义注入规则。当开发人员依赖注入ServletRequest对象时,注入的bean其实是这里的RequestObjectFactory工厂bean
		beanFactory.registerResolvableDependency(ServletRequest.class, new RequestObjectFactory());
		beanFactory.registerResolvableDependency(ServletResponse.class, new ResponseObjectFactory());
		beanFactory.registerResolvableDependency(HttpSession.class, new SessionObjectFactory());
		// ServletWebRequest 里面既有request,也有response
		beanFactory.registerResolvableDependency(WebRequest.class, new WebRequestObjectFactory());
		if (jsfPresent) {
			FacesDependencyRegistrar.registerFacesDependencies(beanFactory);
		}
	}

接下来看看这个方法registerEnvironmentBeans

代码语言:javascript
复制
	public static void registerEnvironmentBeans(ConfigurableListableBeanFactory bf,
			@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
		
		// 把servletContext和servletConfig以Bean的形式,注册到容器里面,这样我们就可以@Autowired了  如下面例子
		if (servletContext != null && !bf.containsBean(WebApplicationContext.SERVLET_CONTEXT_BEAN_NAME)) {
			bf.registerSingleton(WebApplicationContext.SERVLET_CONTEXT_BEAN_NAME, servletContext);
		}
        if (servletConfig != null && !bf.containsBean(ConfigurableWebApplicationContext.SERVLET_CONFIG_BEAN_NAME)) {
			bf.registerSingleton(ConfigurableWebApplicationContext.SERVLET_CONFIG_BEAN_NAME, servletConfig);
		}

		//这个特别特别重要:我们可以看到Spring在启动的时候,把ServletContext里面所有所有的InitParameter都拿出来了,存到一个Map里面
		// 最后把这个Bean注册到容器里面了,Bean名称为:contextParameters
		// 这就是为什么,后面我们可以非常非常方便拿到initParam的原因~~~
		if (!bf.containsBean(WebApplicationContext.CONTEXT_PARAMETERS_BEAN_NAME)) {
			Map<String, String> parameterMap = new HashMap<>();
			if (servletContext != null) {
				Enumeration<?> paramNameEnum = servletContext.getInitParameterNames();
				while (paramNameEnum.hasMoreElements()) {
					String paramName = (String) paramNameEnum.nextElement();
					parameterMap.put(paramName, servletContext.getInitParameter(paramName));
				}
			}
			if (servletConfig != null) {
				Enumeration<?> paramNameEnum = servletConfig.getInitParameterNames();
				while (paramNameEnum.hasMoreElements()) {
					String paramName = (String) paramNameEnum.nextElement();
					parameterMap.put(paramName, servletConfig.getInitParameter(paramName));
				}
			}
			bf.registerSingleton(WebApplicationContext.CONTEXT_PARAMETERS_BEAN_NAME,
					Collections.unmodifiableMap(parameterMap));
		}

		// 原理同上,这里吧ServletContext里面的contextAttributes,都以Bean的形式放进Bean容器里了
		if (!bf.containsBean(WebApplicationContext.CONTEXT_ATTRIBUTES_BEAN_NAME)) {
			Map<String, Object> attributeMap = new HashMap<>();
			if (servletContext != null) {
				Enumeration<?> attrNameEnum = servletContext.getAttributeNames();
				while (attrNameEnum.hasMoreElements()) {
					String attrName = (String) attrNameEnum.nextElement();
					attributeMap.put(attrName, servletContext.getAttribute(attrName));
				}
			}
			bf.registerSingleton(WebApplicationContext.CONTEXT_ATTRIBUTES_BEAN_NAME,
					Collections.unmodifiableMap(attributeMap));
		}
	}

因为可以看到上面已经注册了Bean,因此我们可以这么来使用,直接注入即可:

代码语言:javascript
复制
    @Autowired
    private ServletContext servletContext;
    @Autowired
    private ServletConfig servletConfig;

    @ResponseBody
    @GetMapping("/hello")
    public String helloGet() {
        System.out.println(servletContext); //org.apache.catalina.core.StandardWrapperFacade@1f3768f6
        System.out.println(servletConfig); //org.apache.catalina.core.StandardContext$NoPluggabilityServletContext@73ced3c2
        return "hello...Get";
    }
代码语言:javascript
复制
	//将 servletContext、servletConfig 添加到 propertySources
	public static void initServletPropertySources(MutablePropertySources sources,
			@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) { ... }
UrlPathHelper

这个帮助类非常的重要了,位于包org.springframework.web.util内。它帮助Spring MVC处理URL相关的各种难题,先介绍它的几个属性~~~

代码语言:javascript
复制
public class UrlPathHelper {

	// 是否总是按照全路径 默认是false(一般不改~) 
	private boolean alwaysUseFullPath = false;
	// 是否对url进行解码(默认都是需要解码的)  解码可以是request.getCharacterEncoding()。若没指定就是下面的defaultEncoding 
	private boolean urlDecode = true;
	// 设置是否应从请求URI中删除“;”(分号)
	private boolean removeSemicolonContent = true;
	// 默认编码是它:ISO-8859-1
	private String defaultEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING;

	...
}

它的API具体如图:

简单使用Demo如下:(我的请求地址为:http://localhost:8080/demo_war_war/api/v1/hello

代码语言:javascript
复制
    @ResponseBody
    @GetMapping("/hello")
    public String helloGet(HttpServletRequest request) throws Exception {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        System.out.println(urlPathHelper.getLookupPathForRequest(request)); // /api/v1/hello
        System.out.println(urlPathHelper.getOriginatingContextPath(request)); // /demo_war_war
        System.out.println(urlPathHelper.getContextPath(request)); // /demo_war_war
        System.out.println(urlPathHelper.getServletPath(request)); // /api/v1/hello
        System.out.println(urlPathHelper.getRequestUri(request)); // /demo_war_war/api/v1/hello
        return "hello...Get";
    }

总之它就是Spring用来处理URL的工具类,若你在你的项目中有类似的需要,也可以参考使用的。同时还有:UriUtilsUriComponentsUriComponentsBuilder等等

总结

Spring-web这个jar包内提供给我们还是有几个比较好用的类的,大家知悉即可。

特别是request的包装类,还是非常好用的~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年02月21日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 实用类介绍(排名不分先后)
    • ContentCachingRequestWrapper、ContentCachingResponseWrapper
      • CookieGenerator
        • HtmlUtils
          • TagUtils
            • UriComponentsBuilder
              • ServletUriComponentsBuilder
                • MvcUriComponentsBuilder
                  • WebAppRootListener
                    • WebUtils
                    • RequestContextUtils
                    • WebApplicationContextUtils
                    • UrlPathHelper
                    • 总结
                    相关产品与服务
                    容器服务
                    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档