盘点Spring Framework中的那些特殊符号

在使用spring framework过程中你会遇到形形色色的特殊符号:$#{ }[]?:,它们在不同的场合有不同的含义,笔者试着做一个集中对比,方便记忆。

0. 目录

  1. 配置参数引用符:${}
  2. Url Path 变量占位符:{}
  3. Expand Url Template:{}
  4. SQL查询占位符:?与:
  5. 属性访问表达式(Bean Manipulation Expression)
  6. SpEL中的表达式( Spring Expression Language)
  7. Slf4j Logger 消息占位符

1. 配置参数引用符:${}

1.1 property-placeholder

Spring framework的核心是对象工厂(BeanFactory ),用于按照图纸构造对象、装配依赖、注入参数。这份图纸,在以前是用xml(applicationContext.xml),现在习惯直接用注解来表示。

装配工厂是应当支持动态化配置的,那首先要解决的问题就是设置参数文件并引用其中的参数。Java编程中习惯使用properties做参数文件,spring同样支持读取参数文件,并且还能将参数用在对象构造过程。


  



  
  
  
   

jdbc.properties中大约是这样

jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root

如此一来,spring在构造dataSource时,会将真正的jdbc.urljdbc:hsqldb:hsql://production:9002填入类的setUrl方法中。

1.2 @Value

xml文件里参数替换的机制在注解配置下一脉相承:

@Configuration 
@ImportResource("classpath:/config/jdbc.properties") 
public class AppConfig { 

  @Value("${jdbc.url}") 
  private String url; 

  @Value("${jdbc.username}") 
  private String username; 

  @Value("${jdbc.password}") 
  private String password; 

  @Bean public DataSource dataSource() { 
    return new DriverManagerDataSource(url, username, password); 
  }
}

这个类在spring里构造时,String url的值会设置为参数文件中的值jdbc:hsqldb:hsql://production:9002。注意,如果不加${},则相当于传递一个字符串常量值:String url = "${jdbc.url}",不会报异常,但这样就没起到参数值注入的作用。

1.3 指定默认值

支持在引用参数名时给定一个默认值:

@Value("${jdbc.username:root}") 
private String username; 

上例中,冒号后面的root就是默认值,如果外部参数缺少了jdbc.url,那么默认值就会生效,默认值始终是字符串常量。

1.4 延伸阅读

  1. 参数引用符号中,前缀和后缀实际上是可以自定义的。可以参考
    class PropertySourcesPlaceholderConfigurer 的说明。

  2. ${}不仅仅是简单的字符串引用。spring在拿到参数的字符串值以后,还会根据目标的类型来做转换。这套转换机制的实现是:PropertyEditor、Converter、ConversionService。例如:

@Value("${jdbc.url}")
URL url;

Spring在拿到字符串值以后,会先做类型转换,这样url字段就能正确拿到URL类型的值。常规的类型、格式,spring都已经内建支持,例如:数字、字符、日期、货币等等。你也可以自己定义一个转换工具来转换自己需要的类型,在@Configuration中定义一个@Bean,继承Converterinterface即可。这样,我们就能实现下面的转换:

@Value("1,2")
Point point;
  1. 许多的编程语言(例如bash、groovy、javascript),也是采用${}作为变量值引用的。

2. Url Path 变量占位符{}

2.1 PathVariable

在web api中我们有时需要设计从path中提取变量值的情况,例如:

@GetMapping("/user/{id}")
public UserDto queryUserDetail(@PathVariable("id") String id);

如此声明的话,将会匹配类似 /user/1 的url,并且spring在调用方法时,会将1作为参数值传入方法中。

如果在引用时没有指定名字,则是按照顺序来提取。这个也在其他场合适用。

2.2 延伸阅读

Url 匹配机制的判断逻辑在org.springframework.web.servlet.mvc.method.RequestMappingInfo.getMatchingCondition(HttpServletRequest)

中url中提取参数值的逻辑在org.springframework.util.AntPathMatcher.extractUriTemplateVariables(String, String)

3. Expand Url Template: {}

在构造http url时,可以使用url expand feature:

restTemplate
  .getForObject( "https://example.com/hotels/{hotel}"
      , String.class, "golden");

实际产生的url会是: https://example.com/hotels/golden

也可以使用Map传名字和值:

//按照hash map key替换
Map vars = Collections.singletonMap("hotel", "42"); 
restTemplate.getForObject( 
   "https://example.com/hotels/{hotel}/rooms/{hotel}",
   String.class, vars);
//按照顺序替换
String result = restTemplate.getForObject( 
  "https://example.com/hotels/{hotel}/bookings/{booking}", 
  String.class, "42", "21");

这个设计背后使用的是UriComponentsBuilder类:

URI uri = UriComponentsBuilder
  .fromUriString("https://example.com/hotels/{hotel}")
  .queryParam("q", "{q}") 
  .encode()
  .buildAndExpand("Westin", "123") 
  .toUri();
//https://example.com/hotels/Westin?q=123

4. SQL查询占位符:“?”

4.1 顺序占位符“?”

熟悉SQL的朋友应该知道,SQL查询中有一类叫参数化查询(Prepared Statements)。使用参数化查询既能避免自行拼接SQL不当导致的注入漏洞,也能方便数据库去优化查询。SQL查询占位符采用的是问号的形式:

 jdbcTemplate.update( 
  "insert into t_actor (first_name, last_name) values (?, ?)", 
  "Leonor", "Watling");

该例中出现两个问号,在实际执行SQL时,会按顺序替换成LeonorWatlling。另外记得一点,SQL占位符下标是从1开始的,在使用更底层的API时会遇到:preparedStatement.setString(1,"Leonor")

4.2 具名占位符:param

原生SQL仅支持问号占位符,spring在其基础上设计了具名占位符的机制。这个要使用 classNamedParameterJdbcTemplate

private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public int countOfActorsByFirstName(String firstName) { 
  String sql = "select count(*) from T_ACTOR where first_name = :first_name"; 
  SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName); 
  return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); 
}

4.3 JPA 查询占位符

JPA也同样支持参数占位符,但与SQL稍微不同,使用的是?1?2的形式:

try (EntityManager em = this.emf.createEntityManager()) { 
  Query query = em.createQuery("from Product as p where p.category = ?1"); 
  query.setParameter(1, category); 
  return query.getResultList(); 
}

4.4 @Query

在spring-data工具包中的@Query也是支持JPA查询占位符的:

public interface UserRepository extends JpaRepository { 

  @Query("select u from User u where u.emailAddress = ?1") 
  User findByEmailAddress(String emailAddress); 
}

%是sql中的like匹配, %?1相当于在 firstname的开头添加%来做like匹配, ?1%相当于在firstname的末尾添加%做like匹配。

public interface UserRepository extends JpaRepository { 

  @Query("select u from User u where u.firstname like %?1") 
  List findByFirstnameEndsWith(String firstname);
 }

同样支持具名参数:

public interface UserRepository extends JpaRepository {

  @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
  User findByLastnameOrFirstname(@Param("lastname") String lastname,
                                 @Param("firstname") String firstname);
}

4.5 Query With Spel

@Query中也支持嵌入spel表达式#{},其详细的语法在后面介绍,这里举一个例子:

public interface UserRepository extends JpaRepository {

  @Query("select u from #{#entityName} u where u.lastname = ?1")
  List findByLastname(String lastname);
}

#{#entityName} 就是一个SpEL,最后会替换成实体类名称,实体类名称默认应当是User,但也可以在@Entity注解中指定。

5. 属性访问表达式(Bean Wrap expression)

在spring中动态的访问一个POJO的属性值(Bean Manipulation)可以使用PropertyAccessor机制。具体到类是:BeanWrapperImplPropertyAccessorFactory。例如:

BeanWrapper user = new BeanWrapperImpl(new User());

// setting the company name.. 
user.setPropertyValue("username", "Alex"); 

user.getPropertyValue("roels[0]");

user.getPropertyValue("meta.dateCreated");

表达式 含义
name 访问get、set方法,getName() setName()
account.name getAccount().setName() or getAccount().getName() methods.
account[2] 访问Array, list中的元素
account[COMPANYNAME] 访问HashMap元素,没有引号

Spring 中的bean wrap 与其他工具中的 bean util(apache commons、jodd)实现的是类似的功能,而spring的优势在于值类型转换这一块是重用文章之前提到的conversion service,类型转换友好。

6. Spring EL

Spring 从3.0开始引入表达式(spring expression, SpEL for short)引擎,用于整个spring 生态体系。

6.1 #{}

SpEL 围绕 spring 生态的需求而设计,可以在各个层次集成。如下的例子所示,用来从系统属性中查询user.region值:

 
   
   

表达式之间的依赖关系也能处理:


    
  
 

 
   
   

示例中的变量引用是有依赖关系的,必须先计算randomNumber才能有numberGuess

6.2 @Value

@Value注解中也能直接使用SpEL,同样的#{}标记

@Value("#{ systemProperties['user.region'] }") 
private Locale defaultLocale;

6.3 延伸阅读

SpEL支持众多特性,典型的有:
运算:birthdate.year + 1900
取值:members[0].nameofficers['president'].placeOfBirth.city
构造List:{1,2,3,4}
构造Map:{name:'Nikola',dob:'10-July-1856'}
调用方法:'abc'.substring(1, 3)
自定义函数:#myFn(1)
引用Bean:@dataSource
安全访问:placeOfBirth?.city
更多的语法规则请看:spring el 的语法参考

7. Slf4j Logger 消息占位符

这个不算是spring的特性,但写代码经常用到。和其他的符号交织在一起容易混淆,因此值得拿出来比较。在slf4j里日志消息参数占位符是{}

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
static final Logger logger = LoggerFactory.getLogger(LoginController.class);

logger.info("user {} login success, time {}","张三",Instant.now())

设计采用顺序对应的匹配方法,占位符的次序和入参的顺序对应。最后可以在日志中看到:“user 张三 login success,time 2020-12-29T15:32:08.626Z

其中负责最终执行格式化的工具是:org.slf4j.helpers.MessageFormatterMessageFormatter.arrayFormat(message,argumentArray).getMessage();,这个工具也可以拿来自己用。

参考资料

  1. Spring Framework Documentation ,Core
  2. Spring Framework Documentation,Data Access
  3. Spring Framework Documentation,Web
  4. Spring Framework Documentation,Integration
  5. Spring Data Documentation,Jpa Repository
  6. https://www.baeldung.com/spring-value-annotation

你可能感兴趣的:(盘点Spring Framework中的那些特殊符号)