0%

ConcurrentHashMap

Collections.synchronizedMap

Collections.synchronizedMap 的实现方式较为简单:

通过新建一个包装类,在所有的 map 方法加上 synchronized 关键字即可。

[TOC]

Java为数据结构中的映射定义了一个接口java.util.Map,它有四个实现类,分别是HashMap、HashTable、LinkedHashMap和TreeMap。本节实例主要介绍这4中实例的用法和区别。

阅读全文 »

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 表达式

  1. 常量: 字符串:“ hello ” 字符:‘ h ’ 数字:除了像 java 的内置类型 int,long,float 和 double,Ognl 还有如例:10.01B,相当于 java.math.BigDecimal,使用’ b ’或者’ B ’后缀。 100000H,相当于 java.math.BigInteger,使用’ h ’ 或 ’ H ’ 后缀。
  2. 属性的引用 例如:user.name
  3. 变量的引用 例如:#name
  4. 静态变量的访问 使用 @class@field
  5. 静态方法的调用 使用 @class@method(args), 如果没有指定 class 那么默认就使用 java.lang.Math.
  6. 构造函数的调用 例如:new java.util.ArrayList();

其它的 Ognl 的表达式可以参考 Ognl 的语言手册。

mybatis 使用 OGNL 例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="select" parameterType="com.xingguo.springboot.model.User"  resultType="com.xingguo.springboot.model.User">
select username,password from t_user
where 1=1
<choose>
<!--调用静态方法 -->
<when test="@com.xingguo.springboot.model.TestConstant@checkStatus(state,0)">
<!--调用静态常量 -->
AND state = ${@com.xingguo.springboot.model.TestConstant@STATUS_0}
</when>
<otherwise>
AND state = ${@com.xingguo.springboot.model.TestConstant@STATUS_1}
</otherwise>
</choose>
</select>
1
2
3
4
5
6
7
8
9
10
11
12
package com.xingguo.springboot.model;

public class TestConstant {
//静态常量
public static final int STATUS_0 = 0;
public static final int STATUS_1 = 1;

//静态方法
public static Boolean checkStatus(int sourceStatus,int targetStatus){
return sourceStatus == targetStatus;
}
}

来源

OGNL 语言介绍与实践

相信很多人可能都遇到过下面这些异常:

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Object convertArgsToSqlCommandParam(Object[] args) {
final int paramCount = params.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasNamedParameters && paramCount == 1) {
return args[params.keySet().iterator().next()];
} else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : params.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// issue #71, add param names as param1, param2...but ensure backward compatibility
final String genericParamName = "param" + String.valueOf(i + 1);
if (!param.containsKey(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}123456789101112131415161718192021

在这里有个很关键的params,这个参数类型为Map<Integer, String>,他会根据接口方法按顺序记录下接口参数的定义的名字,如果使用@Param指定了名字,就会记录这个名字,如果没有记录,那么就会使用它的序号作为名字。

例如有如下接口:

1
List<User> select(@Param('sex')String sex,Integer age);1

那么他对应的params如下:

1
2
3
4
{
0:'sex',
1:'1'
}

继续看上面的convertArgsToSqlCommandParam方法,这里简要说明3种情况:

  1. 入参为null或没有时,参数转换为null
  2. 没有使用@Param注解并且只有一个参数时,返回这一个参数
  3. 使用了@Param注解或有多个参数时,将参数转换为Map1类型,并且还根据参数顺序存储了key为param1,param2的参数。

注意:从第3种情况来看,建议各位有多个入参的时候通过@Param指定参数名,方便后面(动态sql)的使用。

经过上面方法的处理后,在MapperMethod中会继续往下调用命名空间方式的方法:

1
2
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.<E>selectList(command.getName(), param);

从这之后开始按照统一的方式继续处理入参。

处理集合

不管是selectOne还是selectMap方法,归根结底都是通过selectList进行查询的,不管是delete还是insert方法,都是通过update方法操作的。在selectListupdate中所有参数的都进行了统一的处理。

DefaultSqlSession.java中的wrapCollection方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Object wrapCollection(final Object object) {
if (object instanceof Collection) {
StrictMap<Object> map = new StrictMap<Object>();
map.put("collection", object);
if (object instanceof List) {
map.put("list", object);
}
return map;
} else if (object != null && object.getClass().isArray()) {
StrictMap<Object> map = new StrictMap<Object>();
map.put("array", object);
return map;
}
return object;
}123456789101112131415

这里特别需要注意的一个地方是map.put(“collection”, object),这个设计是为了支持Set类型,需要等到MyBatis 3.3.0版本才能使用。

wrapCollection处理的是只有一个参数时,集合和数组的类型转换成Map2类型,并且有默认的Key,从这里你能大概看到为什么<foreach>中默认情况下写的arraylistMap类型没有默认值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
2
3
4
5
6
7
8
9
10
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}12345678910

这里的Object parameterObject就是我们经过前面两步处理后的参数。这个参数经过前面两步处理后,到这里的时候,他只有下面三种情况:

  1. null,如果没有入参或者入参是null,到这里也是null
  2. Map类型,除了null之外,前面两步主要是封装成Map类型。
  3. 数组、集合和Map以外的Object类型,可以是基本类型或者实体类。

看上面构造方法,如果参数是1,2情况时,执行代码bindings = new ContextMap(null);参数是3情况时执行if中的代码。我们看看ContextMap类,这是一个内部静态类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static class ContextMap extends HashMap<String, Object> {
private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
}
public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
}
if (parameterMetaObject != null) {
// issue #61 do not modify the context when reading
return parameterMetaObject.getValue(strKey);
}
return null;
}
}1234567891011121314151617

我们先继续看DynamicContext的构造方法,在if/else之后还有两行:

1
2
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());12

其中两个Key分别为:

1
2
public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";12

也就是说1,2两种情况的时候,参数值只存在于"_parameter"的键值中。3情况的时候,参数值存在于"_parameter"的键值中,也存在于bindings本身。

当动态SQL取值的时候会通过OGNL从bindings中获取值。MyBatis在OGNL中注册了ContextMap:

1
2
3
static {
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}123

当从ContextMap取值的时候,会执行ContextAccessor中的如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public Object getProperty(Map context, Object target, Object name)
throws OgnlException {
Map map = (Map) target;

Object result = map.get(name);
if (map.containsKey(name) || result != null) {
return result;
}

Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
if (parameterObject instanceof Map) {
return ((Map)parameterObject).get(name);
}

return null;
}1234567891011121314151617

参数中的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
    2
    MetaObject metaObject = configuration.newMetaObject(parameterObject);
    bindings = new ContextMap(metaObject);12

    MetaObject是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
    10
    public 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
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public String handleToken(String content) {
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
checkInjection(srtValue);
return srtValue;
}12345678910111213

put("value"这个地方可以看出来,MyBatis会创建一个默认为"value"的值,也就是说,在xml中的SQL中可以直接使用${value},从else if可以看出来,只有是简单类型的时候,才会有值。

关于这点,举个简单例子,如果接口为List<User> selectOrderby(String column),如果xml内容为:

1
2
3
<select id="selectOrderby" resultType="User">
select * from user order by ${value}
</select>123

这种情况下,虽然没有指定一个value属性,但是MyBatis会自动把参数column赋值进去。

再往下的代码:

1
2
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value));12

这里和动态SQL就一样了,通过OGNL方式来获取值。

看到这里使用OGNL这种方式时,你有没有别的想法?
特殊用法:你是否在SQL查询中使用过某些固定的码值?一旦码值改变的时候需要改动很多地方,但是你又不想把码值作为参数传进来,怎么解决呢?你可能已经明白了。
就是通过OGNL的方式,例如有如下一个码值类:

1
2
3
4
5
package com.abel533.mybatis;
public interface Code{
public static final String ENABLE = "1";
public static final String DISABLE = "0";
}12345

如果在xml,可以这么使用:

1
2
3
<select id="selectUser" resultType="User">
select * from user where enable = ${@com.abel533.mybatis.Code@ENABLE}
</select>123

除了码值之外,你可以使用OGNL支持的各种方法,如调用静态方法。

#{propertyName}参数

这种方式比较简单,复杂属性的时候使用的MyBatis的MetaObject。

DefaultParameterHandler.java中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void setParameters(PreparedStatement ps) throws SQLException {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}1234567891011121314151617181920212223242526272829

上面这段代码就是从参数中取#{propertyName}值的方法,这段代码的主要逻辑就是if/else判断的地方,单独拿出来分析:

1
2
3
4
5
6
7
8
9
10
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}12345678910
  • 首先看第一个if,当使用<foreach>的时候,MyBatis会自动生成额外的动态参数,如果propertyName是动态参数,就会从动态参数中取值。
  • 第二个if,如果参数是null,不管属性名是什么,都会返回null
  • 第三个if,如果参数是一个简单类型,或者是一个注册了typeHandler的对象类型,就会直接使用该参数作为返回值,和属性名无关。
  • 最后一个else,这种情况下是复杂对象或者Map类型,通过反射方便的取值。

下面我们说明上面四种情况下的参数名注意事项。

  1. 动态参数,这里的参数名和值都由MyBatis动态生成的,因此我们没法直接接触,也不需要管这儿的命名。但是我们可以了解一下这儿的命名规则,当以后错误信息看到的时候,我们可以确定出错的地方。
    ForEachSqlNode.java中:

    1
    2
    3
    private static String itemizeItem(String item, int i) {
    return new StringBuilder(ITEM_PREFIX).append(item).append("_").append(i).toString();
    }123

    其中ITEM_PRFIXpublic 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>的详细内容。

  2. 参数为null,由于这里的判断和参数名无关,因此入参null的时候,在xml中写的#{name}不管name写什么,都不会出错,值都是null

  3. 可以直接使用typeHandler处理的类型。最常见的就是基本类型,例如有这样一个接口方法User selectById(@Param("id")Integer id),在xml中使用id的时候,我们可以随便使用属性名,不管用什么样的属性名,值都是id

  4. 复杂对象或者Map类型一般都是我们需要注意的地方,这种情况下,就必须保证入参包含这些属性,如果没有就会报错。这一点和可以参考上面有关MetaObject的地方。

详解

所有动态SQL类型中,<foreach>似乎是遇到问题最多的一个。

例如有下面的方法:

1
2
3
4
5
6
7
<insert id="insertUserList">
INSERT INTO user(username,password)
VALUES
<foreach collection="userList" item="user" separator=",">
(#{user.username},#{user.password})
</foreach>
</insert>1234567

对应的接口:

1
int insertUserList(@Param("userList")List<User> list);1

我们通过foreach源码,看看MyBatis如何处理上面这个例子。

ForEachSqlNode.java中的apply方法中的前两行:

1
2
Map<String, Object> bindings = context.getBindings();
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);12

这里的bindings参数熟悉吗?上面提到过很多。经过一系列的参数处理后,这儿的bindings如下:

1
2
3
4
5
6
7
{
"_parameter":{
"param1":list,
"userList":list
},
"_databaseId":null,
}1234567

collectionExpression就是collection="userList"的值userList

我们看看evaluator.evaluateIterable如何处理这个参数,在ExpressionEvaluator.java中的evaluateIterable方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value == null) {
throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
}
if (value instanceof Iterable) {
return (Iterable<?>) value;
}
if (value.getClass().isArray()) {
int size = Array.getLength(value);
List<Object> answer = new ArrayList<Object>();
for (int i = 0; i < size; i++) {
Object o = Array.get(value, i);
answer.add(o);
}
return answer;
}
if (value instanceof Map) {
return ((Map) value).entrySet();
}
throw new BuilderException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable.");
}12345678910111213141516171819202122

首先通过看第一行代码:

1
Object value = OgnlCache.getValue(expression, parameterObject);1

这里通过OGNL获取到了userList的值。获取userList值的时候可能出现异常,具体可以参考上面动态SQL部分的内容。

userList的值分四种情况。

  1. value == null,这种情况直接抛出异常BuilderException
  2. value instanceof Iterable,实现Iterable接口的直接返回,如Collection的所有子类,通常是List
  3. value.getClass().isArray()数组的情况,这种情况会转换为List返回。
  4. value instanceof Map如果是Map,通过((Map) value).entrySet()返回一个Set类型的参数。

通过上面处理后,返回的值,是一个Iterable类型的值,这个值可以使用for (Object o : iterable)这种形式循环。

ForEachSqlNode中对iterable循环的时候,有一段需要关注的代码:

1
2
3
4
5
6
7
8
9
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}123456789

如果是通过((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)

主要的与请求和接口相关的类及接口

方 法 说 明
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
2
3
4
1. file:./config/
2. file:./
3. classpath:/config/
4. classpath:/

二、资源表示方式

  • “file:” 开头表示读取文件系统中的文件地址
  • “classpath” 表示读取jar包中的文件地址

三、资源获取方式

  1. 获取文件路径
1
File f =  ResourceUtils.getFile("classpath:sqlscript/eventLogDataMigration.sql");
  1. 模式匹配文件
1
2
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader).getResources
("classpath:static/yaml/*.yaml");

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
2
3
4
5
6
7
8
9
10
@Configuration
public class MyWebAppConfigurer
extends WebMvcConfigurerAdapter {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/mystatic/**")
.addResourceLocations("classpath:/mystatic/");
}
}

这种方式会在默认的基础上增加/mystatic/**映射到classpath:/mystatic/,不会影响默认的方式,可以同时使用。

*自定义 *- 配置文件

静态资源映射还有一个配置选项,为了简单这里用.properties方式书写:

1
2
3
4
# 默认值为 /**
spring.mvc.static-path-pattern=
# 默认值为 classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
spring.resources.static-locations=这里设置要指向的路径,多个使用英文逗号隔开,

这个配置会影响默认的/**,例如修改为/static/**后,只能映射如/static/js/sample.js这样的请求(修改前是/js/sample.js)。这个配置只能写一个值,不像大多数可以配置多个用逗号隔开的。

使用注意

例如有如下目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
└─resources
│ application.yml

├─static
│ ├─css
│ │ index.css
│ │
│ └─js
│ index.js

└─templates
index.ftl123456789101112

index.ftl中该如何引用上面的静态资源呢?
如下写法:

1
2
<link rel="stylesheet" type="text/css" href="/css/index.css">
<script type="text/javascript" src="/js/index.js"></script>12

注意:默认配置的/**映射到/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
2
3
4
META-INF/resources/webjars/jquery/2.1.4/jquery.js
META-INF/resources/webjars/jquery/2.1.4/jquery.min.js
META-INF/resources/webjars/jquery/2.1.4/jquery.min.map
META-INF/resources/webjars/jquery/2.1.4/webjars-requirejs.js1234

Spring Boot 默认将 /webjars/** 映射到 classpath:/META-INF/resources/webjars/ ,结合我们上面讲到的访问资源的规则,便可以得知我们在JSP页面中引入jquery.js的方法为:

1
2
<script type="text/javascript" 
src="${pageContext.request.contextPath }/webjars/jquery/2.1.4/jquery.js"></script>

想实现这样,我们只需要在pom.xml 文件中添加jquery的webjars 依赖即可,如下:

1
2
3
4
5
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>2.1.4</version>
</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
2
spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**

这样配置后,所有/**请求的静态资源都会被处理为上面例子的样子。

到这儿还没完,我们在写资源url的时候还要特殊处理。

首先增加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
@ControllerAdvice
public class ControllerConfig {

@Autowired
ResourceUrlProvider resourceUrlProvider;

@ModelAttribute("urls")
public ResourceUrlProvider urls() {
return this.resourceUrlProvider;
}

}

然后在页面写的时候用下面的写法:

1
<link rel="stylesheet" type="text/css" href="${urls.getForLookupPath('/css/index.css')}">

使用urls.getForLookupPath('/css/index.css')来得到处理后的资源名。

版本号 方式

application.properties中做如下配置:

1
2
3
spring.resources.chain.strategy.fixed.enabled=true
spring.resources.chain.strategy.fixed.paths=/js/**,/v1.0.0/**
spring.resources.chain.strategy.fixed.version=v1.0.0

这里配置需要特别注意,将version的值配置在paths中。原因我们在讲Spring MVC 处理逻辑的时候说。

在页面写的时候,写法如下:

1
<script type="text/javascript" src="${urls.getForLookupPath('/js/index.js')}"></script>1

注意,这里仍然使用了urls.getForLookupPathurls配置方式见上一种方式。

在请求的实际页面中,会显示为:

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/下面找到。