在使用spring framework过程中你会遇到形形色色的特殊符号:$
、 #
、 { }
、 []
、?
、:
,它们在不同的场合有不同的含义,笔者试着做一个集中对比,方便记忆。
0. 目录
- 配置参数引用符:${}
- Url Path 变量占位符:{}
- Expand Url Template:{}
- SQL查询占位符:?与:
- 属性访问表达式(Bean Manipulation Expression)
- SpEL中的表达式( Spring Expression Language)
- 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.url
值jdbc: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 延伸阅读
参数引用符号中,前缀和后缀实际上是可以自定义的。可以参考
class PropertySourcesPlaceholderConfigurer 的说明。${}
不仅仅是简单的字符串引用。spring在拿到参数的字符串值以后,还会根据目标的类型来做转换。这套转换机制的实现是:PropertyEditor、Converter、ConversionService。例如:
@Value("${jdbc.url}")
URL url;
Spring在拿到字符串值以后,会先做类型转换,这样url
字段就能正确拿到URL类型的值。常规的类型、格式,spring都已经内建支持,例如:数字、字符、日期、货币等等。你也可以自己定义一个转换工具来转换自己需要的类型,在@Configuration
中定义一个@Bean
,继承Converter
interface即可。这样,我们就能实现下面的转换:
@Value("1,2")
Point point;
- 许多的编程语言(例如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时,会按顺序替换成Leonor
、Watlling
。另外记得一点,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
机制。具体到类是:BeanWrapperImpl
或PropertyAccessorFactory
。例如:
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].name
、officers['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.MessageFormatter
:MessageFormatter.arrayFormat(message,argumentArray).getMessage();
,这个工具也可以拿来自己用。
参考资料
- Spring Framework Documentation ,Core
- Spring Framework Documentation,Data Access
- Spring Framework Documentation,Web
- Spring Framework Documentation,Integration
- Spring Data Documentation,Jpa Repository
- https://www.baeldung.com/spring-value-annotation