让thymeleaf ${param.xx}只返回第一个同名参数的值

在使用thymeleaf时,想要获取链接中的参数,通常有两种方法,一种是 ${param.xx},另一种是${#request.getParameter('xx')},具体请见 springmvcaccessdata

通过${param.xx}时获取时,如果 xx 不存在,返回一个空字符串,否则返回一个List<String>,最终通过toString()方法来输出,具体请见 WebEngineContext$RequestParameterValues,因此当链接中参数名称唯一时,可以正常获取,但当链接中存在多个相同名称的参数时,例如请求/?xx=1&xx=2

<span th:text="${param.xx}"></span>

输出的是[1,2],为了避免这个问题,可以通过

<span th:text="${param.xx[0]}" th:if="${param.xx != null}"></span>

这种写法。

但实际上,连接中带多个相同名称参数的情况很少(至少对我来说),而${param.xx}相对于${#request.getParameter('xx')}而言肯定是更方便的写法,但当连接中存在多个相同参数时,前者便不能正常工作了,而th:text="${param.xx != null ? param.xx[0] : ''}"这种方法一下子让我多打了这么多字,肯定心有不甘,那有没有办法让${param.xx}只返回第一个同名参数的值呢?

首先看下WebEngineContext的源码:

WebEngineContext中,param其实对应的是一个Map:RequestParametersMap,看了下源码可以发现

private static final class RequestParametersMap extends NoOpMapImpl

不仅是private,还是final,继承它可以死心了。

既然继承不了,那能不能替换 param 属性?发现来软的也不行:

private final Map<String,Object> requestParametersVariablesMap;

public WebEngineContext(...) {
    this.requestParametersVariablesMap = new RequestParametersMap(this.request);
}

看来在WebEngineContext内部作文章似乎不大可行,那就只有在它外部做文章了,首先看了下它本身不是final的,关键方法Object getVariable(Object key)也不是final的,那么继承并替换它可行,再看下WebEngineContext的来源:

TemplateEngine->IEngineContextFactory->createEngineContext

IEngineContextFactory可以在创建TemplateEngine时替换,那么这样我们就有了一条曲折的路来解决这个问题。

  1. 继承WebEngineContext
final class ThymeleafWebEngineContext extends WebEngineContext

@Override
public Object getVariable(String key) {
    if ("param".equals(key)) {
        if (paramMap == null) {
            paramMap = new ParamMap(getRequest());
        }
        return paramMap;
    }
    return super.getVariable(key);
}
private final class ParamMap implements Map<String, List<String>> {

  private final HttpServletRequest request;
  private Map<String, List<String>> map;

  ParamMap(final HttpServletRequest request) {
      super();
      this.request = request;
  }

  @Override
  public int size() {
      return request.getParameterMap().size();
  }

  @Override
  public boolean isEmpty() {
      return this.request.getParameterMap().isEmpty();
  }

  @Override
  public boolean containsKey(Object key) {
      return true;
  }

  @Override
  public boolean containsValue(Object value) {
      throw new UnsupportedOperationException();
  }

  @Override
  public List<String> get(Object key) {
      final String[] values = this.request.getParameterValues(key == null ? null : key.toString());
      if (values == null) {
          return null;
      }
      return new Param(values);
  }

  @Override
  public List<String> put(String key, List<String> value) {
      throw new UnsupportedOperationException();
  }

  @Override
  public List<String> remove(Object key) {
      throw new UnsupportedOperationException();
  }

  @Override
  public void putAll(Map<? extends String, ? extends List<String>> m) {
      throw new UnsupportedOperationException();
  }

  @Override
  public void clear() {
      throw new UnsupportedOperationException();
  }

  @Override
  public Set<String> keySet() {
      return this.request.getParameterMap().keySet();
  }

  @Override
  public Collection<List<String>> values() {
      setMap();
      return this.map.values();
  }

  @Override
  public Set<Map.Entry<String, List<String>>> entrySet() {
      setMap();
      return this.map.entrySet();
  }

  private void setMap() {
      if (this.map == null) {
          this.map = new HashMap<>();
          for (Map.Entry<String, String[]> it : this.request.getParameterMap().entrySet()) {
              this.map.put(it.getKey(), List.of(it.getValue()));
          }
      }
  }
  }

  private final class Param extends ArrayList<String> {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private final String[] values;

    public Param(String[] values) {
        super();
        this.values = values;
    }

    @Override
    public int size() {
        return values.length;
    }

    @Override
    public boolean isEmpty() {
        return values.length == 0;
    }

    @Override
    public String get(int index) {
        return values[index];
    }

    @Override
    public String toString() {
        if (values.length == 0) {
            return "";
        }
        return values[0];
    }

    @Override
    public Iterator<String> iterator() {
        return Arrays.stream(values).iterator();
    }
  }
  1. 替换ThymeleafEngine

在Spring Boot中

@Bean
SpringTemplateEngine templateEngine(ThymeleafProperties properties,
        ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
    SpringTemplateEngine engine = new SpringTemplateEngine();
    engine.setEngineContextFactory(new ThymeleafEngineContextFactory());
    engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
    engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
    templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
    dialects.orderedStream().forEach(engine::addDialect);
    return engine;
}

参考:ThymeleafAutoConfiguration$ThymeleafDefaultConfiguration

【Spring Boot】谁转发了我的错误?
Spring Boot MaxUploadSizeExceededException获取最大允许文件上传大小