[TOC]
ConcurrentHashMap.md
[TOC]
同步Map的多种方式
4个map实例的总结.md
[TOC]
Java为数据结构中的映射定义了一个接口java.util.Map,它有四个实现类,分别是HashMap、HashTable、LinkedHashMap和TreeMap。本节实例主要介绍这4中实例的用法和区别。
OGNL 语言介绍与实践(转)
OGNL 的基本语法
OGNL 表达式一般都很简单。虽然 OGNL 语言本身已经变得更加丰富了也更强大了,但是一般来说那些比较复杂的语言特性并未影响到 OGNL 的简洁:简单的部分还是依然那么简单。比如要获取一个对象的 name 属性,OGNL 表达式就是 name, 要获取一个对象的 headline 属性的 text 属性,OGNL 表达式就是 headline.text 。 OGNL 表达式的基本单位是“导航链”,往往简称为“链”。最简单的链包含如下部分:
表达式组成部分 | 示例 |
---|---|
属性名称 | 如上述示例中的 name 和 headline.text |
方法调用 | hashCode() 返回当前对象的哈希码。 |
数组元素 | listeners[0] 返回当前对象的监听器列表中的第一个元素。 |
所有的 OGNL 表达式都基于当前对象的上下文来完成求值运算,链的前面部分的结果将作为后面求值的上下文。你的链可以写得很长,例如:
name.toCharArray()[0].numericValue.toString()
上面的表达式的求值步骤:
- 提取根 (root) 对象的 name 属性。
- 调用上一步返回的结果字符串的 toCharArray() 方法。
- 提取返回的结果数组的第一个字符。
- 获取字符的 numericValue 属性,该字符是一个 Character 对象,Character 类有一个 getNumericValue() 方法。
- 调用结果 Integer 对象的 toString() 方法。
上面的例子只是用来得到一个对象的值,OGNL 也可以用来去设置对象的值。当把上面的表达式传入 Ognl.setValue() 方法将导致 InappropriateExpressionException,因为链的最后的部分(toString()
)既不是一个属性的名字也不是数组的某个元素。 了解了上面的语法基本上可以完成绝大部分工作了。
OGNL 表达式
- 常量: 字符串:“ hello ” 字符:‘ h ’ 数字:除了像 java 的内置类型 int,long,float 和 double,Ognl 还有如例:10.01B,相当于 java.math.BigDecimal,使用’ b ’或者’ B ’后缀。 100000H,相当于 java.math.BigInteger,使用’ h ’ 或 ’ H ’ 后缀。
- 属性的引用 例如:user.name
- 变量的引用 例如:#name
- 静态变量的访问 使用 @class@field
- 静态方法的调用 使用 @class@method(args), 如果没有指定 class 那么默认就使用 java.lang.Math.
- 构造函数的调用 例如:new java.util.ArrayList();
其它的 Ognl 的表达式可以参考 Ognl 的语言手册。
mybatis 使用 OGNL 例子
1 | <select id="select" parameterType="com.xingguo.springboot.model.User" resultType="com.xingguo.springboot.model.User"> |
1 | package com.xingguo.springboot.model; |
来源
深入了解MyBatis参数(转)
相信很多人可能都遇到过下面这些异常:
- Parameter ‘xxx’ not found. Available parameters are […]
- Could not get property ‘xxx’ from xxxClass. Cause:
- The expression ‘xxx’ evaluated to a null value.
- Error evaluating expression ‘xxx’. Return value (xxxxx) was not iterable.
不只是上面提到的这几个,我认为有很多的错误都产生在和参数有关的地方。
想要避免参数引起的错误,我们需要深入了解参数。
想了解参数,我们首先看MyBatis处理参数和使用参数的全部过程。
本篇由于为了便于理解和深入,使用了大量的源码,因此篇幅较长,需要一定的耐心看完,本文一定会对你起到很大的帮助。
参数处理过程
处理接口形式的入参
在使用MyBatis时,有两种使用方法。一种是使用的接口形式,另一种是通过SqlSession
调用命名空间。这两种方式在传递参数时是不一样的,命名空间的方式更直接,但是多个参数时需要我们自己创建Map
作为入参。相比而言,使用接口形式更简单。
接口形式的参数是由MyBatis自己处理的。如果使用接口调用,入参需要经过额外的步骤处理入参,之后就和命名空间方式一样了。
在MapperMethod.java会首先经过下面方法来转换参数:
1 | public Object convertArgsToSqlCommandParam(Object[] args) { |
在这里有个很关键的params
,这个参数类型为Map<Integer, String>
,他会根据接口方法按顺序记录下接口参数的定义的名字,如果使用@Param
指定了名字,就会记录这个名字,如果没有记录,那么就会使用它的序号作为名字。
例如有如下接口:
1 | List<User> select(@Param('sex')String sex,Integer age);1 |
那么他对应的params
如下:
1 | { |
继续看上面的convertArgsToSqlCommandParam
方法,这里简要说明3种情况:
- 入参为
null
或没有时,参数转换为null
- 没有使用
@Param
注解并且只有一个参数时,返回这一个参数 - 使用了
@Param
注解或有多个参数时,将参数转换为Map
1类型,并且还根据参数顺序存储了key为param1,param2的参数。
注意:从第3种情况来看,建议各位有多个入参的时候通过@Param指定参数名,方便后面(动态sql)的使用。
经过上面方法的处理后,在MapperMethod
中会继续往下调用命名空间方式的方法:
1 | Object param = method.convertArgsToSqlCommandParam(args); |
从这之后开始按照统一的方式继续处理入参。
处理集合
不管是selectOne
还是selectMap
方法,归根结底都是通过selectList
进行查询的,不管是delete
还是insert
方法,都是通过update
方法操作的。在selectList
和update
中所有参数的都进行了统一的处理。
在DefaultSqlSession.java中的wrapCollection
方法:
1 | private Object wrapCollection(final Object object) { |
这里特别需要注意的一个地方是map.put(“collection”, object),这个设计是为了支持Set类型,需要等到MyBatis 3.3.0版本才能使用。
wrapCollection
处理的是只有一个参数时,集合和数组的类型转换成Map
2类型,并且有默认的Key,从这里你能大概看到为什么<foreach>
中默认情况下写的array和list(Map
类型没有默认值map)。
参数的使用
参数的使用分为两部分:
- 第一种就是常见
#{username}
或者${username}
。 - 第二种就是在动态SQL中作为条件,例如
<if test="username!=null and username !=''">
。
下面对这两种进行详细讲解,为了方便理解,先讲解第二种情况。
在动态SQL条件中使用参数
关于动态SQL的基础内容可以查看官方文档。
动态SQL为什么会处理参数呢?
主要是因为动态SQL中的<if>,<bind>,<foreache>
都会用到表达式,表达式中会用到属性名,属性名对应的属性值如何获取呢?获取方式就在这关键的一步。不知道多少人遇到Could not get property xxx from xxxClass或: Parameter ‘xxx’ not found. Available parameters are[…],都是不懂这里引起的。
在DynamicContext.java中,从构造方法看起:
1 | public DynamicContext(Configuration configuration, Object parameterObject) { |
这里的Object parameterObject
就是我们经过前面两步处理后的参数。这个参数经过前面两步处理后,到这里的时候,他只有下面三种情况:
null
,如果没有入参或者入参是null
,到这里也是null
。Map
类型,除了null
之外,前面两步主要是封装成Map
类型。- 数组、集合和
Map
以外的Object
类型,可以是基本类型或者实体类。
看上面构造方法,如果参数是1,2情况时,执行代码bindings = new ContextMap(null);
参数是3情况时执行if
中的代码。我们看看ContextMap
类,这是一个内部静态类,代码如下:
1 | static class ContextMap extends HashMap<String, Object> { |
我们先继续看DynamicContext
的构造方法,在if/else
之后还有两行:
1 | bindings.put(PARAMETER_OBJECT_KEY, parameterObject); |
其中两个Key分别为:
1 | public static final String PARAMETER_OBJECT_KEY = "_parameter"; |
也就是说1,2两种情况的时候,参数值只存在于"_parameter"
的键值中。3情况的时候,参数值存在于"_parameter"
的键值中,也存在于bindings
本身。
当动态SQL取值的时候会通过OGNL从bindings
中获取值。MyBatis在OGNL中注册了ContextMap
:
1 | static { |
当从ContextMap
取值的时候,会执行ContextAccessor
中的如下方法:
1 | @Override |
参数中的target
就是ContextMap
类型的,所以可以直接强转为Map
类型。
参数中的name
就是我们写在动态SQL中的属性名。
下面举例说明这三种情况:
null的时候:
不管name
是什么(name="_databaseId"
除外,可能会有值),此时Object result = map.get(name);
得到的result=null
。
在Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
中parameterObject=null
,因此最后返回的结果是null
。
在这种情况下,不管写什么样的属性,值都会是null,并且不管属性是否存在,都不会出错。Map
类型:
此时Object result = map.get(name);
一般也不会有值,因为参数值只存在于"_parameter"
的键值中。
然后到Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
,此时获取到我们的参数值。
在从参数值((Map)parameterObject).get(name)
根据name
来获取属性值。
在这一步的时候,如果name
属性不存在,就会报错:1
throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());1
name
属性是什么呢,有什么可选值呢?这就是处理接口形式的入参和处理集合处理后所拥有的Key。
如果你遇到过类似异常,相信看到这儿就明白原因了。数组、集合和
Map
以外的Object
类型:
这种类型经过了下面的处理:1
2MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);12MetaObject
是MyBatis的一个反射类,可以很方便的通过getValue
方法获取对象的各种属性(支持集合数组和Map,可以多级属性点.
访问,如user.username
,user.roles[1].rolename
)。
现在分析这种情况。
首先通过name
获取属性时Object result = map.get(name);
,根据上面ContextMap
类中的get
方法:1
2
3
4
5
6
7
8
9
10public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
}
if (parameterMetaObject != null) {
return parameterMetaObject.getValue(strKey);
}
return null;
}12345678910可以看到这里会优先从
Map
中取该属性的值,如果不存在,那么一定会执行到下面这行代码:1
return parameterMetaObject.getValue(strKey)1
如果
name
刚好是对象的一个属性值,那么通过MetaObject
反射可以获取该属性值。如果该对象不包含name
属性的值,就会报错:1
throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ". Cause: " + t.toString(), t);1
理解这三种情况后,使用动态SQL应该不会有参数名方面的问题了。
在SQL语句中使用参数
SQL中的两种形式#{username}
或者${username}
,虽然看着差不多,但是实际处理过程差别很大,而且很容易出现莫名其妙的错误。
${username}
的使用方式为OGNL方式获取值,和上面的动态SQL一样,这里先说这种情况。
${propertyName}参数
在TextSqlNode.java中有一个内部的静态类BindingTokenParser
,现在只看其中的handleToken
方法:
1 | @Override |
从put("value"
这个地方可以看出来,MyBatis会创建一个默认为"value"
的值,也就是说,在xml中的SQL中可以直接使用${value}
,从else if
可以看出来,只有是简单类型的时候,才会有值。
关于这点,举个简单例子,如果接口为List<User> selectOrderby(String column)
,如果xml内容为:
1 | <select id="selectOrderby" resultType="User"> |
这种情况下,虽然没有指定一个value属性,但是MyBatis会自动把参数column赋值进去。
再往下的代码:
1 | Object value = OgnlCache.getValue(content, context.getBindings()); |
这里和动态SQL就一样了,通过OGNL方式来获取值。
看到这里使用OGNL这种方式时,你有没有别的想法?
特殊用法:你是否在SQL查询中使用过某些固定的码值?一旦码值改变的时候需要改动很多地方,但是你又不想把码值作为参数传进来,怎么解决呢?你可能已经明白了。
就是通过OGNL的方式,例如有如下一个码值类:
1 | package com.abel533.mybatis; |
如果在xml,可以这么使用:
1 | <select id="selectUser" resultType="User"> |
除了码值之外,你可以使用OGNL支持的各种方法,如调用静态方法。
#{propertyName}参数
这种方式比较简单,复杂属性的时候使用的MyBatis的MetaObject。
在DefaultParameterHandler.java中:
1 | public void setParameters(PreparedStatement ps) throws SQLException { |
上面这段代码就是从参数中取#{propertyName}
值的方法,这段代码的主要逻辑就是if/else
判断的地方,单独拿出来分析:
1 | if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params |
- 首先看第一个
if
,当使用<foreach>
的时候,MyBatis会自动生成额外的动态参数,如果propertyName
是动态参数,就会从动态参数中取值。 - 第二个
if
,如果参数是null
,不管属性名是什么,都会返回null
。 - 第三个
if
,如果参数是一个简单类型,或者是一个注册了typeHandler
的对象类型,就会直接使用该参数作为返回值,和属性名无关。 - 最后一个
else
,这种情况下是复杂对象或者Map
类型,通过反射方便的取值。
下面我们说明上面四种情况下的参数名注意事项。
动态参数,这里的参数名和值都由
MyBatis
动态生成的,因此我们没法直接接触,也不需要管这儿的命名。但是我们可以了解一下这儿的命名规则,当以后错误信息看到的时候,我们可以确定出错的地方。
在ForEachSqlNode.java中:1
2
3private static String itemizeItem(String item, int i) {
return new StringBuilder(ITEM_PREFIX).append(item).append("_").append(i).toString();
}123其中
ITEM_PRFIX
为public static final String ITEM_PREFIX = "__frch_";
。
如果在<foreach>
中的collection="userList" item="user"
,那么对userList
循环产生的动态参数名就是:frch_user_0,frch_user_1,__frch_user_2…
如果访问动态参数的属性,如
user.username
会被处理成__frch_user_0.username
,这种参数值的处理过程在更早之前解析SQL的时候就已经获取了对应的参数值。具体内容看下面有关<foreach>
的详细内容。参数为
null
,由于这里的判断和参数名无关,因此入参null
的时候,在xml中写的#{name}
不管name
写什么,都不会出错,值都是null
。可以直接使用
typeHandler
处理的类型。最常见的就是基本类型,例如有这样一个接口方法User selectById(@Param("id")Integer id)
,在xml中使用id
的时候,我们可以随便使用属性名,不管用什么样的属性名,值都是id
。复杂对象或者
Map
类型一般都是我们需要注意的地方,这种情况下,就必须保证入参包含这些属性,如果没有就会报错。这一点和可以参考上面有关MetaObject
的地方。
详解
所有动态SQL类型中,<foreach>
似乎是遇到问题最多的一个。
例如有下面的方法:
1 | <insert id="insertUserList"> |
对应的接口:
1 | int insertUserList(@Param("userList")List<User> list);1 |
我们通过foreach
源码,看看MyBatis如何处理上面这个例子。
在ForEachSqlNode.java中的apply
方法中的前两行:
1 | Map<String, Object> bindings = context.getBindings(); |
这里的bindings
参数熟悉吗?上面提到过很多。经过一系列的参数处理后,这儿的bindings如下:
1 | { |
collectionExpression
就是collection="userList"
的值userList
。
我们看看evaluator.evaluateIterable
如何处理这个参数,在ExpressionEvaluator.java中的evaluateIterable
方法:
1 | public Iterable<?> evaluateIterable(String expression, Object parameterObject) { |
首先通过看第一行代码:
1 | Object value = OgnlCache.getValue(expression, parameterObject);1 |
这里通过OGNL获取到了userList
的值。获取userList
值的时候可能出现异常,具体可以参考上面动态SQL部分的内容。
userList
的值分四种情况。
value == null
,这种情况直接抛出异常BuilderException
。value instanceof Iterable
,实现Iterable
接口的直接返回,如Collection
的所有子类,通常是List
。value.getClass().isArray()
数组的情况,这种情况会转换为List
返回。value instanceof Map
如果是Map
,通过((Map) value).entrySet()
返回一个Set
类型的参数。
通过上面处理后,返回的值,是一个Iterable
类型的值,这个值可以使用for (Object o : iterable)
这种形式循环。
在ForEachSqlNode
中对iterable
循环的时候,有一段需要关注的代码:
1 | if (o instanceof Map.Entry) { |
如果是通过((Map) value).entrySet()
返回的Set
,那么循环取得的子元素都是Map.Entry
类型,这个时候会将mapEntry.getKey()
存储到index
中,mapEntry.getValue()
存储到item
中。
如果是List
,那么会将序号i
存到index
中,mapEntry.getValue()
存储到item
中。
常见错误补充
当collection="userList"
的值userList
中的User
是一个继承自Map
的类型时,你需要保证<foreach>
循环中用到的所有对象的属性必须存在,Map
类型存在的问题通常是,如果某个值是null
,一般是不存在相应的key
,这种情况会导致<foreach>
出错,会报找不到__frch_user_x
参数。所以这种情况下,就是值是null
,你也需要map.put(key,null)
。
Request 和 response
主要的与请求和接口相关的类及接口
方 法 | 说 明 |
---|---|
ServletInputStream | Servlet的输入流 |
ServletOutputStream | Servlet的输出流 |
ServletRequest | 代表Servlet请求的一个接口 |
ServletResponse | 代表Servlet响应的一个接口 |
ServletRequestWrapper | 该类实现ServletRequest接口 |
ServletResponseWrapper | 该类实现ServletResponse接口 |
HttpServletRequest | 继承了ServletRequest接口,表示HTTP请求 |
HttpServletResponse | 继承了ServletResponse接口,表示HTTP请求 |
HttpServletRequestWrapper | HttpServletRequest的实现 |
HttpServletResponseWrapper | HttpServletResponse的实现 |
在上面给出的类和接口中,最主要的是HttpServletRequest和HttpServletResponse接口
1. HttpServletRequest
HttpServletRequest接口最常用的方法就是获得请求中的参数,这些参数一般是客户端表单中的数据。同时,HttpServletRequest接口可以获取由客户端传送的名称,也可以获取产生请求并且接收请求的服务器端主机名及IP地址,还可以获取客户端正在使用的通信协议等信息。
接口HttpServletRequest的常用方法:
方 法 | 说 明 |
---|---|
getAttributeNames() | 返回当前请求的所有属性的名字集合 |
getAttribute(String name) | 返回name指定的属性值 |
getCookies() | 返回客户端发送的Cookie |
getsession() | 返回和客户端相关的session,如果没有给客户端分配session,则返回null |
getsession(boolean create) | 返回和客户端相关的session,如果没有给客户端分配session,则创建一个session并返回 |
getParameter(String name) | 获取请求中的参数,该参数是由name指定的 |
getParameterValues(String name) | 返回请求中的参数值,该参数值是由name指定的 |
getCharacterEncoding() | 返回请求的字符编码方式 |
getContentLength() | 返回请求体的有效长度 |
getInputStream() | 获取请求的输入流中的数据 |
getMethod() | 获取发送请求的方式,如get、post |
getParameterNames() | 获取请求中所有参数的名字 |
getProtocol() | 获取请求所使用的协议名称 |
getReader() | 获取请求体的数据流 |
getRemoteAddr() | 获取客户端的IP地址 |
getRemoteHost() | 获取客户端的名字 |
getServerName() | 返回接受请求的服务器的名字 |
getServerPath() | 获取请求的文件的路径 |
2. HttpServletResponse
在Servlet中,当服务器响应客户端的一个请求时,就要用到HttpServletResponse接口。设置响应的类型可以使用setContentType()方法。发送字符数据,可以使用getWriter()返回一个对象。
接口HttpServletResponse的常用方法
方 法 | 说 明 |
---|---|
addCookie(Cookie cookie) | 将指定的Cookie加入到当前的响应中 |
addHeader(String name,String value) | 将指定的名字和值加入到响应的头信息中 |
containsHeader(String name) | 返回一个布尔值,判断响应的头部是否被设置 |
encodeURL(String url) | 编码指定的URL |
sendError(int sc) | 使用指定状态码发送一个错误到客户端 |
sendRedirect(String location) | 发送一个临时的响应到客户端 |
setDateHeader(String name,long date) | 将给出的名字和日期设置响应的头部 |
setHeader(String name,String value) | 将给出的名字和值设置响应的头部 |
setStatus(int sc) | 给当前响应设置状态码 |
setContentType(String ContentType) | 设置响应的MIME类型 |
其他-时间表示方式
1、各种时间定义
1.1 UNIX时间
或称UNIX时间戳(UNIX TIMESTAMP)、POSIX时间(POSIX TIME),是一种UNIX或类UNIX系统使用的时间表示方式,定义为从协调世界时(有些文档为格林威治时间)
1970年01月01日00时00分00秒起至现在的总秒数,不包括润秒。
1.2 GMT时间
格林威治标准时间(中国大陆翻译:格林尼治平均时间或格林尼治标准时间,台、港、澳翻译:格林威治标准时间;英语:Greenwich Mean Time,GMT)
指位于英国伦敦郊区的皇家格林威治天文台的标准时间,因为本初子午线被定义在通过那里的经线。
自1924年2月5日开始,格林威治天文台每隔一小时会向全世界发放调时信息。
理论上来说,格林威治标准时间的正午是指当太阳横穿格林威治子午线时(也就是在格林威治上空最高点时)的时间。由于地球在它的椭圆轨道里的运动速度不均匀,这个时刻可能与实际的太阳时有误差,最大误差达16分钟。
由于地球每天的自转是有些不规则的,而且正在缓慢减速,因此格林威治时间已经不再被作为标准时间使用。1972年1月1日,UTC(协调世界时)成为新的世界标准时间。现在的标准时间,是由原子钟报时的协调世界时(UTC)。
1.3 UTC时间
协调世界时,又称世界标准时间或世界协调时间,简称UTC(从英文“Coordinated Universal Time”/法文“Temps Universel Cordonné”而来),
最主要的世界时间标准,其以原子时秒长为基础,在时刻上尽量接近于格林尼治平时。
1.4 GMT和UTC的区别
1972 年1 月1日,Universal Time Coordinated(协调世界时)成为新的世界标准时间。为了方便,通常记成UTC。同样为了方便,在不需要精确到秒的情况下,通常也将GMT和UTC视作等同。
1.5 时区和时差
时区(Time Zone) 是地球上的区域使用同一个时间定义。以前,人们通过观察太阳的位置(时角)决定时间,这就使得不同经度的地方的时间有所不同(地方时)。1884年在华盛顿召开国际经度会议时,为了克服时间上的混乱,规定将全球划分为24个时区。
世界各个国家位于地球不同位置上,因此不同国家的日出、日落时间必定有所偏差。这些偏差就是所谓的时差。
在计算机控制面板的日期和时间设置中有一个时区设置,时区以[UTC+-时差]表示,如果本地时间比UTC时间快,例如中国大陆、香港、澳门、台湾、蒙古国、新加坡、马来西亚、菲律宾、澳大利亚西部的时间比UTC快8小时,就会写作UTC+8,俗称东8区。相反,如果本地时间比UTC时间慢,例如夏威夷的时间比UTC时间慢10小时,就会写作UTC-10,俗称西10区。
2、如何确认UNIX时间
任一时间求UNIX时间,需要知道时间所处时区时差,加减时区时差转换为相应的UTC时间,而后减去UTC时间1970年01月01日00时00分00秒,得到以秒为单位的时间差,即为UNIX时间。
资源路径
一、application配置文件加载顺序
spring应用会根据特定的顺序加载配置文件:
1 | 1. file:./config/ |
二、资源表示方式
- “file:” 开头表示读取文件系统中的文件地址
- “classpath” 表示读取jar包中的文件地址
三、资源获取方式
- 获取文件路径
1 | File f = ResourceUtils.getFile("classpath:sqlscript/eventLogDataMigration.sql"); |
- 模式匹配文件
1 | Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader).getResources |
3 ResourceLoader
https://smarterco.de/java-load-file-from-classpath-in-spring-boot/
静态资源处理
静态资源处理
Spring Boot 默认为我们提供了静态资源处理,使用 WebMvcAutoConfiguration
中的配置各种属性。
建议大家使用Spring Boot的默认配置方式,如果需要特殊处理的再通过配置进行修改。
如果想要自己完全控制WebMVC,就需要在@Configuration注解的配置类上增加@EnableWebMvc
,增加该注解以后WebMvcAutoConfiguration中
配置就不会生效,你需要自己来配置需要的每一项。这种情况下的配置还是要多看一下WebMvcAutoConfiguration
类。
我们既然是快速使用Spring Boot,并不想过多的自己再重新配置。本文还是主要针对Spring Boot的默认处理方式,部分配置在application 配置文件中(.properties 或 .yml)
配置资源映射
其中默认配置的 /** 映射到 /static (或/public、/resources、/META-INF/resources)
其中默认配置的 /webjars/** 映射到 classpath:/META-INF/resources/webjars/
PS:上面的 static、public、resources 等目录都在 classpath: 下面(如 src/main/resources/static)。
优先级顺序为:META/resources > resources > static > public
*自定义 *- 继承WebMvcConfigurerAdapter
如果你想增加如/mystatic/**
映射到classpath:/mystatic/
,你可以让你的配置类继承WebMvcConfigurerAdapter
,然后重写如下方法:
1 | @Configuration |
这种方式会在默认的基础上增加/mystatic/**
映射到classpath:/mystatic/
,不会影响默认的方式,可以同时使用。
*自定义 *- 配置文件
静态资源映射还有一个配置选项,为了简单这里用.properties
方式书写:
1 | # 默认值为 /** |
这个配置会影响默认的/**
,例如修改为/static/**
后,只能映射如/static/js/sample.js
这样的请求(修改前是/js/sample.js
)。这个配置只能写一个值,不像大多数可以配置多个用逗号隔开的。
使用注意
例如有如下目录结构:
1 | └─resources |
在index.ftl
中该如何引用上面的静态资源呢?
如下写法:
1 | <link rel="stylesheet" type="text/css" href="/css/index.css"> |
注意:默认配置的/**
映射到/static
(或/public
,/resources
,/META-INF/resources
)
当请求/css/index.css
的时候,Spring MVC 会在/static/
目录下面找到。
如果配置为/static/css/index.css
,那么上面配置的几个目录下面都没有/static
目录,因此会找不到资源文件!
所以写静态资源位置的时候,不要带上映射的目录名(如/static/,/public/ ,/resources/,/META-INF/resources/)!
使用webjars
先说一下什么是webjars?我们在Web开发中,前端页面中用了越来越多的JS或CSS,如jQuery等等,平时我们是将这些Web资源拷贝到Java的目录下,这种通过人工方式拷贝可能会产生版本误差,拷贝版本错误,前端页面就无法正确展示。
WebJars 就是为了解决这种问题衍生的,将这些Web前端资源打包成Java的Jar包,然后借助Maven这些依赖库的管理,保证这些Web资源版本唯一性。
WebJars 就是将js, css 等资源文件放到 classpath:/META-INF/resources/webjars/ 中,然后打包成jar 发布到maven仓库中。
简单应用
以jQuery为例,文件存放结构为:
1 | META-INF/resources/webjars/jquery/2.1.4/jquery.js |
Spring Boot 默认将 /webjars/** 映射到 classpath:/META-INF/resources/webjars/ ,结合我们上面讲到的访问资源的规则,便可以得知我们在JSP页面中引入jquery.js的方法为:
1 | <script type="text/javascript" |
想实现这样,我们只需要在pom.xml 文件中添加jquery的webjars 依赖即可,如下:
1 | <dependency> |
静态资源版本管理
Spring MVC 提供了静态资源版本映射的功能。
用途:当我们资源内容发生变化时,由于浏览器缓存,用户本地的静态资源还是旧的资源,为了防止这种情况导致的问题,我们可能会手动在请求url的时候加个版本号或者其他方式。
版本号如:
1 | <script type="text/javascript" src="/js/sample.js?v=1.0.1"></script>1 |
Spring MVC 提供的功能可以很容易的帮助我们解决类似问题。
Spring MVC 有两种解决方式。
注意:下面的配置方式针对freemarker模板方式,其他的配置方式可以参考。
资源名-md5 方式
例如:
1 | <link rel="stylesheet" type="text/css" href="/css/index-2b371326aa93ce4b611853a309b69b29.css"> |
Spring 会自动读取资源md5,然后添加到index.css
的名字后面,因此当资源内容发生变化的时候,文件名发生变化,就会更新本地资源。
配置方式:
在application.properties
中做如下配置:
1 | spring.resources.chain.strategy.content.enabled=true |
这样配置后,所有/**
请求的静态资源都会被处理为上面例子的样子。
到这儿还没完,我们在写资源url的时候还要特殊处理。
首先增加如下配置:
1 | @ControllerAdvice |
然后在页面写的时候用下面的写法:
1 | <link rel="stylesheet" type="text/css" href="${urls.getForLookupPath('/css/index.css')}"> |
使用urls.getForLookupPath('/css/index.css')
来得到处理后的资源名。
版本号 方式
在application.properties
中做如下配置:
1 | spring.resources.chain.strategy.fixed.enabled=true |
这里配置需要特别注意,将version
的值配置在paths
中。原因我们在讲Spring MVC 处理逻辑的时候说。
在页面写的时候,写法如下:
1 | <script type="text/javascript" src="${urls.getForLookupPath('/js/index.js')}"></script>1 |
注意,这里仍然使用了urls.getForLookupPath
,urls
配置方式见上一种方式。
在请求的实际页面中,会显示为:
1 | <script type="text/javascript" src="/v1.0.0/js/index.js"></script>1 |
可以看到这里的地址是/v1.0.0/js/index.js
。
静态资源版本管理 处理过程
在Freemarker模板首先会调用urls.getForLookupPath
方法,返回一个/v1.0.0/js/index.js
或/css/index-2b371326aa93ce4b611853a309b69b29.css
。
这时页面上的内容就是处理后的资源地址。
这之后浏览器发起请求。
这里分开说。
第一种md5方式
请求/css/index-2b371326aa93ce4b611853a309b69b29.css
,我们md5配置的paths=/**
,所以Spring MVC 会尝试url中是否包含-
,如果包含会去掉后面这部分,然后去映射的目录(如/static/
)查找/css/index.css
文件,如果能找到就返回。
第二种版本方式
请求/v1.0.0/js/index.js
。
如果我们paths
中没有配置/v1.0.0
,那么上面这个请求地址就不会按版本方式来处理,因此会找不到上面的资源。
如果配置了/v1.0.0
,Spring 就会将/v1.0.0
去掉再去找/js/index.js
,最终会在/static/
下面找到。