陈颂光
全栈工程师,能够独立开发从解释器到网站和桌面/移动端应用的各类软件。
关注我的 GitHub

Spring框架初探

Spring已经从在Java EE做依赖注入的框架发展为包罗AOP、MVC、数据持久化、消息传递、安全的Spring全家桶生态,甚至已经把魔掌伸向命令行程序和手机应用开发。以下我们就来说明一下怎么用Spring做点简单的事情。

开始使用Spring

创建Spring项目

Web应用往往有一些令人厌烦的支架代码,幸运的是现在的IDE能自动生成它们。比如在启用了Spring Web MVC插件的Netbeans中,创建一个Maven类别下的Spring Boot basic project项目,则会在项目目录下创建以下样子的文件:

  • nbactions.xml是只供Netbeans用的配置文件,用于把Netbeans中的操作如运行、调试、调优对应到Maven操作。
  • pom.xml自然是Maven项目的配置文件。
  • src是源文件目录
    • main是产品源代码目录
      • java是Java源代码目录
        • com/github/chungkwong/toy/BasicApplication.java是程序入口文件
      • resources是其它源代码目录
        • application.properties是项目属性文件
    • test是测试源代码目录
      • java是Java源代码目录
        • com/github/chungkwong/toy/BasicApplicationTests.java是一个测试源文件

pom.xml形如:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.github.chungkwong</groupId>
    <artifactId>toy</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Toy</name>
    <description>A Toy Spring Boot application</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

程序入口文件src/main/java/com/github/chungkwong/toy/BasicApplication.java形如:

package com.github.chungkwong.toy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BasicApplication {

	public static void main(String[] args) {
		SpringApplication.run(BasicApplication.class, args);
	}
}

要注意是程序入口类加上了标注SpringBootApplication,同时main方法中用SpringApplication.run(BasicApplication.class, args);启动Spring。

测试文件src/test/java/com/github/chungkwong/toy/BasicApplicationTests.java形如:

package com.github.chungkwong.toy;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class BasicApplicationTests {

	@Test
	public void contextLoads() {
	}

}

在Netbeans中现在我们可以运行这个项目(或者mvn spring-boot:run),当然由于还没有指定什么事情让这个程序做,只能在输出中看到类似这样的结果:

cd /home/kwong/projects/springToy; SPRING_OUTPUT_ANSI_ENABLED=always JAVA_HOME=/usr/lib/jvm/default-java /home/kwong/netbeans-8.2/java/maven/bin/mvn "-Drun.jvmArguments=-noverify -XX:TieredStopAtLevel=1" -Drun.mainClass=com.example.BasicApplication spring-boot:run
Scanning for projects...
                                                                        
------------------------------------------------------------------------
Building basic 0.0.1-SNAPSHOT
------------------------------------------------------------------------

>>> spring-boot-maven-plugin:2.0.0.RELEASE:run (default-cli) @ basic >>>

--- maven-resources-plugin:3.0.1:resources (default-resources) @ basic ---
Using 'UTF-8' encoding to copy filtered resources.
Copying 1 resource
Copying 0 resource

--- maven-compiler-plugin:3.7.0:compile (default-compile) @ basic ---
Changes detected - recompiling the module!
Compiling 1 source file to /home/kwong/projects/springToy/target/classes

--- maven-resources-plugin:3.0.1:testResources (default-testResources) @ basic ---
Using 'UTF-8' encoding to copy filtered resources.
skip non existing resourceDirectory /home/kwong/projects/springToy/src/test/resources

--- maven-compiler-plugin:3.7.0:testCompile (default-testCompile) @ basic ---
Changes detected - recompiling the module!
Compiling 1 source file to /home/kwong/projects/springToy/target/test-classes

<<< spring-boot-maven-plugin:2.0.0.RELEASE:run (default-cli) @ basic <<<

--- spring-boot-maven-plugin:2.0.0.RELEASE:run (default-cli) @ basic ---
Attaching agents: []

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.0.RELEASE)

2018-04-23 13:22:07.552  INFO 6215 --- [           main] com.github.chungkwong.BasicApplication   : Starting BasicApplication on kwong with PID 6215 (/home/kwong/projects/springToy/target/classes started by kwong in /home/kwong/projects/springToy)
2018-04-23 13:22:07.555  INFO 6215 --- [           main] com.github.chungkwong.BasicApplication   : No active profile set, falling back to default profiles: default
2018-04-23 13:22:07.594  INFO 6215 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@57d5872c: startup date [Mon Apr 23 13:22:07 CST 2018]; root of context hierarchy
2018-04-23 13:22:07.975  INFO 6215 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-04-23 13:22:07.983  INFO 6215 --- [           main] com.github.chungkwong.BasicApplication   : Started BasicApplication in 0.672 seconds (JVM running for 1.006)
2018-04-23 13:22:07.985  INFO 6215 --- [       Thread-2] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@57d5872c: startup date [Mon Apr 23 13:22:07 CST 2018]; root of context hierarchy
2018-04-23 13:22:07.985  INFO 6215 --- [       Thread-2] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 3.810s
Finished at: Mon Apr 23 13:22:07 CST 2018
Final Memory: 25M/263M
------------------------------------------------------------------------

现在我们让它干点事,创建一个类com.github.chungkwong.toy.Starter

package com.github.chungkwong.toy;
import org.springframework.boot.*;
import org.springframework.stereotype.*;
@Component
public class Starter implements CommandLineRunner{
	@Override
	public void run(String... args) throws Exception{
		System.out.println("Hello world!");
	}
}

其中标注@Component让Spring自动发现这个类并注册一个bean,CommandLineRunnerrun方法会在容器启动时自动运行。再运行项目就能看到输出中出现了Hello world!,以下只摘录部分输出:

2018-04-23 13:41:01.989  INFO 7274 --- [           main] c.g.chungkwong.toy.BasicApplication      : Starting BasicApplication on kwong with PID 7274 (/home/kwong/projects/springToy/target/classes started by kwong in /home/kwong/projects/springToy)
2018-04-23 13:41:01.992  INFO 7274 --- [           main] c.g.chungkwong.toy.BasicApplication      : No active profile set, falling back to default profiles: default
2018-04-23 13:41:02.022  INFO 7274 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@45f45fa1: startup date [Mon Apr 23 13:41:02 CST 2018]; root of context hierarchy
2018-04-23 13:41:02.343  INFO 7274 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-04-23 13:41:02.351  INFO 7274 --- [           main] c.g.chungkwong.toy.BasicApplication      : Started BasicApplication in 0.566 seconds (JVM running for 0.808)
Hello world!
2018-04-23 13:41:02.353  INFO 7274 --- [       Thread-2] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@45f45fa1: startup date [Mon Apr 23 13:41:02 CST 2018]; root of context hierarchy
2018-04-23 13:41:02.354  INFO 7274 --- [       Thread-2] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown

打造一个Web应用

为了使用Spring web MVC,我们在pom.xml中加入以下依赖:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

然后我们新增一个控制器类com.github.chungkwong.toy.controller.GreetingController

package com.github.chungkwong.toy.controller;
import org.springframework.stereotype.*;
import org.springframework.web.bind.annotation.*;
@Controller
public class GreetingController{
	@RequestMapping(path="/greeting")
	@ResponseBody
	public String greet(){
		return "Hello world";
	}
	
}

其中@Controller标记这个类为控制器,@RequestMapping(path="/greeting")表示用方法处理对/greeting路径的请求,@ResponseBody表示用方法返回值作为响应的内容。再运行项目后在浏览器打开http://localhost:8080/greeting即可看到Hello world

当然,这样在控制器中直接生成输出不是好习惯。控制器应该只提供一个模型,然后通过把模型代入视图来生成输出。我们以Freemarker模板系统为例说明,先在pom.xml中增加依赖:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-freemarker</artifactId>
		</dependency>

现在编写模板如src/main/resources/templates/ask_for_name.ftl

<#assign localize=springMacroRequestContext.getMessage >
<!DOCTYPE html>
<html lang="${springMacroRequestContext.getLocale().toLanguageTag()}">
  <head>
    <meta charset='utf-8'>
    <meta http-equiv="X-UA-Compatible" content="chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>${localize("WHO_ARE_YOU")}</title>
  </head>
  <body>
    <form method="GET" action="/greeting">
      <label for="name">${localize("WHO_ARE_YOU")}</label><input type="text" name="name" id="name">
      <input type="submit">
    </form>
  </body>
</html>

还有src/main/resources/templates/hello.ftl

<#assign localize=springMacroRequestContext.getMessage >
<!DOCTYPE html>
<html lang="${springMacroRequestContext.getLocale().toLanguageTag()}">
  <head>
    <meta charset='utf-8'>
    <meta http-equiv="X-UA-Compatible" content="chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>${localize("HELLO",[name])}</title>
  </head>
  <body>
    ${localize("HELLO",[name])}
  </body>
</html>

接着我们给出国际化和本地化需要的资源,在src/main/resources/application.properties加入:

spring.messages.basename=message
spring.messages.encoding=UTF-8
spring.messages.fallback-to-system-locale=true
spring.messages.use-code-as-default-message=true

src/main/resources/message.properties给出默认资源:

HELLO=Hello, {0}
WHO_ARE_YOU=Who are you?

src/main/resources/message_zh_CN.properties给出中文资源:

HELLO={0},你好
WHO_ARE_YOU=怎样称呼你?

最后修改控制器类com.github.chungkwong.toy.controller.GreetingController

package com.github.chungkwong.toy.controller;
import org.springframework.stereotype.*;
import org.springframework.ui.*;
import org.springframework.web.bind.annotation.*;
@Controller
public class GreetingController{
	@RequestMapping(path="/greeting",params="!name")
	public String askForName(){
		return "ask_for_name";
	}
	@RequestMapping(path="/greeting",params="name")
	public String greet(@RequestParam String name,Model model){
		model.addAttribute("name",name);
		return "hello";
	}
}

再运行程序后在浏览器打开http://localhost:8080/greeting即可看到一个文本框问怎样称呼你?,比如你输入陈大文并提交,则下一页面会说陈大文,你好

另外,放到src/main/resources/staticsrc/main/resources/public目录下的文件也可以在浏览器中通过URLhttp://localhost:8080/路径访问到,其中路径相对于上述的两个目录之一,适合用于存放favicon.ico、CSS文件和静态图片等等。

登录

我们建立一个简单的登录系统,使用数据库保存用户信息,并容许通过验证邮箱注册。为了通过Spring使用数据库、邮箱和安全,我们在pom.xml中加入以下依赖:

		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<!-- 我们以MariaDB为例,使用其它DBMS的改为相应驱动 -->
		<dependency>
			<groupId>org.mariadb.jdbc</groupId>
			<artifactId>mariadb-java-client</artifactId>
			<version>2.2.3</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-mail</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

src/main/resources/application.properties加入:

spring.datasource.url=jdbc:mysql://localhost/数据库名称
spring.datasource.username=root
spring.datasource.password=密码
# 我们以MariaDB为例,使用其它DBMS的改为相应驱动
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver 
spring.jpa.hibernate.ddl-auto=update
spring.mail.default-encoding=UTF-8
# 我们以QQ邮件服务器发送验证邮件为例,使用其它邮件服务器的相应修改
spring.mail.host=smtp.qq.com
spring.mail.port=465
spring.mail.password=密码
spring.mail.protocol=smtp
spring.mail.username=用户名@qq.com
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.ssl.enable=true

关系式数据库的思维方式和面向对象的思维方式不一样,幸运的是利用JPA可以自动地进行关系-对象映射(ORM)。粗略来说,Java的类对应于数据库的表,Java的对象对应于数据库中表的一行。比如,我们希望用数据库记录用户信息,可以建立以下的类分别表示用户、角色和验证码:

package com.github.chungkwong.toy.model;
import java.util.*;
import javax.persistence.*;
@Entity
@Table(name="users")
public class User{
	private Long id;
	private String username;
	private String password;
	private String passwordConfirm;
	private String email;
	private Boolean enabled;
	private Set<Role> roles;
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	public Long getId(){
		return id;
	}
	public void setId(Long id){
		this.id=id;
	}
	public String getUsername(){
		return username;
	}
	public void setUsername(String username){
		this.username=username;
	}
	public String getPassword(){
		return password;
	}
	public void setPassword(String password){
		this.password=password;
	}
	@Transient
	public String getPasswordConfirm(){
		return passwordConfirm;
	}
	public void setPasswordConfirm(String passwordConfirm){
		this.passwordConfirm=passwordConfirm;
	}
	public String getEmail(){
		return email;
	}
	public void setEmail(String email){
		this.email=email;
	}
	public Boolean getEnabled(){
		return enabled;
	}
	public void setEnabled(Boolean enabled){
		this.enabled=enabled;
	}
	@ManyToMany
	@JoinTable(name="user_role",joinColumns=@JoinColumn(name="user_id"),inverseJoinColumns=@JoinColumn(name="role_id"))
	public Set<Role> getRoles(){
		return roles;
	}
	public void setRoles(Set<Role> roles){
		this.roles=roles;
	}
	@Override
	public String toString(){
		return username+":"+email;
	}
}
package com.github.chungkwong.toy.model;
import java.util.*;
import javax.persistence.*;
import org.springframework.security.core.*;
@Entity
@Table(name="roles")
public class Role implements GrantedAuthority{
	private Long id;
	private String name;
	private Set<User> users;
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	public Long getId(){
		return id;
	}
	public void setId(Long id){
		this.id=id;
	}
	public String getName(){
		return name;
	}
	public void setName(String name){
		this.name=name;
	}
	@ManyToMany(mappedBy="roles")
	public Set<User> getUsers(){
		return users;
	}
	public void setUsers(Set<User> users){
		this.users=users;
	}
	@Transient
	@Override
	public String getAuthority(){
		return name;
	}
}
package com.github.chungkwong.toy.model;
import java.util.*;
import javax.persistence.*;
@Entity
@Table(name="verification_tokens")
public class VerificationToken{
	private static final int EXPIRATION=60*24;
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private Long id;
	private String token;
	@OneToOne(targetEntity=User.class,fetch=FetchType.EAGER,cascade={CascadeType.REMOVE})
	@JoinColumn(nullable=false,name="user_id")
	private User user;
	private Date expiryDate;
	public VerificationToken(){
	}
	public VerificationToken(String token){
		this.token=token;
		this.expiryDate=calculateExpiryDate(EXPIRATION);
	}
	public VerificationToken(String token,User user){
		super();
		this.token=token;
		this.user=user;
		this.expiryDate=calculateExpiryDate(EXPIRATION);
	}
	public Long getId(){
		return id;
	}
	public String getToken(){
		return token;
	}
	public void setToken(String token){
		this.token=token;
	}
	public User getUser(){
		return user;
	}
	public void setUser(User user){
		this.user=user;
	}
	public Date getExpiryDate(){
		return expiryDate;
	}
	public void setExpiryDate(Date expiryDate){
		this.expiryDate=expiryDate;
	}
	public void update(){
		setExpiryDate(calculateExpiryDate(EXPIRATION));
	}
	private Date calculateExpiryDate(int expiryTimeInMinutes){
		final Calendar cal=Calendar.getInstance();
		cal.setTimeInMillis(new Date().getTime());
		cal.add(Calendar.MINUTE,expiryTimeInMinutes);
		return new Date(cal.getTime().getTime());
	}
}

然后我们分别为上述三个对象建立数据库接口:

package com.github.chungkwong.toy.repository;
import com.github.chungkwong.toy.model.*;
import org.springframework.data.jpa.repository.*;
public interface UserRepository extends JpaRepository<User,Long>{
	User findByUsername(String username);
	User findByEmail(String email);
}
package com.github.chungkwong.toy.repository;
import com.github.chungkwong.toy.model.*;
import org.springframework.data.jpa.repository.*;
public interface RoleRepository extends JpaRepository<Role,Long>{
}
package com.github.chungkwong.toy.repository;
import com.github.chungkwong.toy.model.*;
import java.util.*;
import org.springframework.data.jpa.repository.*;
public interface VerificationTokenRepository extends JpaRepository<VerificationToken,Long>{
	VerificationToken findByToken(String token);
	VerificationToken findByUser(User user);
	void deleteByExpiryDateLessThan(Date now);
}

关键的是我们不用实现这些类,框架就能给我们实现,我们直接用就是了。以下给出用户相关流程的接口和实现:

package com.github.chungkwong.toy.service;
import com.github.chungkwong.toy.model.*;
public interface UserService{
	void registerUser(User user);
	User findUserByUsername(String username);
	User findUserByEmail(String email);
	User getUserById(long id);
	void deleteUser(User user);
	String getVerificationCode(User user);
	User verify(String code);
}
package com.github.chungkwong.toy.service;
import com.github.chungkwong.toy.model.*;
import com.github.chungkwong.toy.repository.*;
import java.util.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.security.crypto.bcrypt.*;
import org.springframework.stereotype.*;
@Service
public class UserServiceImpl implements UserService{
	private BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
	@Autowired
	private UserRepository userRepository;
	@Autowired
	private VerificationTokenRepository tokenRepository;
	@Override
	public void registerUser(User user){
		user.setPassword(encoder.encode(user.getPassword()));
		user.setEnabled(Boolean.FALSE);
		userRepository.save(user);
	}
	@Override
	public User findUserByUsername(String username){
		return userRepository.findByUsername(username);
	}
	@Override
	public User findUserByEmail(String email){
		return userRepository.findByEmail(email);
	}
	@Override
	public User getUserById(long id){
		return userRepository.getOne(id);
	}
	@Override
	public void deleteUser(User user){
		userRepository.delete(user);
	}
	@Override
	public String getVerificationCode(User user){
		VerificationToken token=tokenRepository.findByUser(user);
		if(token!=null&&token.getExpiryDate().after(new Date())){
			token.update();
		}else{
			token=new VerificationToken(UUID.randomUUID().toString(),user);
			tokenRepository.save(token);
		}
		return token.getToken();
	}
	@Override
	public User verify(String code){
		VerificationToken token=tokenRepository.findByToken(code);
		if(token!=null&&token.getExpiryDate().after(new Date())){
			User user=token.getUser();
			user.setEnabled(Boolean.TRUE);
			userRepository.save(user);
			return user;
		}
		return null;
	}
}
package com.github.chungkwong.toy.service;
import java.util.stream.*;
import javax.transaction.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.security.core.userdetails.*;
@Service
public class UserDetailServiceImpl implements UserDetailsService{
	@Autowired
	private UserService userService;
	@Transactional
	@Override
	public UserDetails loadUserByUsername(String string) throws UsernameNotFoundException{
		com.github.chungkwong.toy.model.User user=userService.findUserByUsername(string);
		if(user!=null){
			return new org.springframework.security.core.userdetails.User(
					user.getUsername(),user.getPassword(),user.getEnabled(),true,true,true,
					user.getRoles().stream().collect(Collectors.toList()));
		}else{
			return null;
		}
	}
}
package com.github.chungkwong.toy.validator;
import com.github.chungkwong.toy.model.*;
import com.github.chungkwong.toy.repository.*;
import java.util.function.*;
import java.util.regex.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.stereotype.*;
import org.springframework.validation.*;
@Component
public class UserValidator implements Validator{
	@Autowired
	private UserRepository userRepository;
	private final Predicate<String> usernameNonPattern=Pattern.compile(
			"^.*@.*$|^[0-9]+$").asPredicate();
	private final Predicate<String> emailPattern=Pattern.compile(
			"^[-!#$%&'*+/=?^_`{|}~0-9a-zA-Z]+(\\.[-!#$%&'*+/=?^_`{|}~0-9a-zA-Z]+)*@[-!#$%&'*+/=?^_`{|}~0-9a-zA-Z]+(\\.[-!#$%&'*+/=?^_`{|}~0-9a-zA-Z]+)*$").asPredicate();
	@Override
	public boolean supports(Class<?> aClass){
		return User.class.equals(aClass);
	}
	@Override
	public void validate(Object o,Errors errors){
		User user=(User)o;
		if(user.getUsername()==null||user.getUsername().isEmpty()){
			errors.rejectValue("username","INVALID_USERNAME");
		}else if(user.getUsername().length()>=64||usernameNonPattern.test(user.getUsername())){
			errors.rejectValue("username","INVALID_USERNAME");
		}else if(userRepository.findByUsername(user.getUsername())!=null){
			errors.rejectValue("username","DUPLICATE_USERNAME");
		}
		if(!emailPattern.test(user.getEmail())){
			errors.rejectValue("email","INVALID_EMAIL");
		}else if(userRepository.findByEmail(user.getEmail())!=null){
			errors.rejectValue("email","DUPLICATE_EMAIL");
		}
		if(user.getPassword()==null||user.getPassword().isEmpty()){
			errors.rejectValue("password","INVALID_PASSWORD");
		}else if(user.getPassword().length()<8||user.getPassword().length()>256){
			errors.rejectValue("password","INVALID_PASSWORD");
		}else if(!user.getPasswordConfirm().equals(user.getPassword())){
			errors.rejectValue("passwordConfirm","MISMATCH_PASSWORD");
		}
	}
}

现在我们可以实现注册用的控制器:

package com.github.chungkwong.toy.controller;
import com.github.chungkwong.toy.model.*;
import com.github.chungkwong.toy.service.*;
import com.github.chungkwong.toy.validator.*;
import freemarker.template.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.core.env.*;
import org.springframework.mail.javamail.*;
import org.springframework.security.authentication.*;
import org.springframework.security.core.context.*;
import org.springframework.stereotype.*;
import org.springframework.ui.*;
import org.springframework.ui.freemarker.*;
import org.springframework.validation.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.*;
@Controller
public class UserController{
	@Autowired
	private JavaMailSender sender;
	@Autowired
	private Configuration freemarkerConfiguration;
	@Autowired
	private Environment env;
	@Autowired
	private UserValidator userValidator;
	@Autowired
	private UserService userService;
	@RequestMapping(value="/registration",method=RequestMethod.POST)
	public String registration(@ModelAttribute("user") User user,BindingResult bindingResult,Model model,HttpServletRequest request){
		userValidator.validate(user,bindingResult);
		if(bindingResult.hasErrors()){
			return "login";
		}
		userService.registerUser(user);
		String code=userService.getVerificationCode(user);
		HashMap<String,Object> mailModel=new HashMap<>();
		mailModel.put("url","http://localhost:8080/activate?code="+code);
		RequestContext context=new RequestContext(request);
		mailModel.put("springMacroRequestContext",context);
		sender.send((msg)->{
			msg.setRecipient(Message.RecipientType.TO,new InternetAddress(user.getEmail()));
			msg.setFrom(new InternetAddress(env.getProperty("spring.mail.username")));
			msg.setSubject(context.getMessage("ACTIVATE_YOUR_ACCOUNT"));
			msg.setSentDate(new Date());
			msg.setText(FreeMarkerTemplateUtils.processTemplateIntoString(
					freemarkerConfiguration.getTemplate("activate.ftl"),mailModel),"UTF-8","html");
			msg.saveChanges();
		});
		model.addAttribute("email",user.getEmail());
		model.addAttribute("title","VERIFY_EMAIL");
		return "verify";
	}
	@RequestMapping(value="/activate",params={"code"},method=RequestMethod.GET)
	public String activate(Model model,@RequestParam String code,ServletRequest request){
		User user=userService.verify(code);
		if(user!=null){
			UsernamePasswordAuthenticationToken detail=new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(),user.getRoles());
			SecurityContextHolder.getContext().setAuthentication(detail);
			return "redirect:/greeting";
		}
		return "redirect:/login";
	}
	@RequestMapping(value="/login",method=RequestMethod.GET)
	public String login(Model model,String error,String logout){
		if(error!=null){
			model.addAttribute("error","FAILED_LOGIN");
		}
		if(logout!=null){
			model.addAttribute("message","LOGED_OUT");
		}
		model.addAttribute("user",new User());
		model.addAttribute("title","LOGIN");
		return "login";
	}
}

还有相关视图:

src/main/resources/templates/login.ftl:

<#include "header.ftl">
<#if error??>
  <p class="error">${localize(error)}</p>
</#if>
<#if message??>
  <p class="success">${localize(message)}</p>
</#if>
<article>
<h3><@spring.message "LOGIN"/></h3>
  <form name='f' action='/login' method='POST'>
	<label for='username_old'><@spring.message "USERNAME"/></label><input type='text' name='username' value='' id='username_old'>
	<label for='password_old'><@spring.message "PASSWORD"/></label><input type='password' name='password' id='password_old'>
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
	<input type="submit" value="${localize('LOGIN')}">
</form>
</article>
<article>
  <h3><@spring.message "REGISTRATION"/></h3>
  <form method="POST" action="/registration">
    <label for='username'><@spring.message "USERNAME"/></label><@spring.formInput "user.username"/><@spring.showErrors "<br>"/>
    <label for='password'><@spring.message "PASSWORD"/></label><@spring.formInput "user.password",'','password'/><@spring.showErrors "<br>"/>
    <label for='password_confirm'><@spring.message "PASSWORD_CONFIRM"/></label><@spring.formInput "user.passwordConfirm",'','password'/><@spring.showErrors "<br>"/>
    <label for='email'><@spring.message "EMAIL"/></label><@spring.formInput "user.email",'','email'/><@spring.showErrors "<br>"/>
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
    <input type="submit" value="${localize('REGISTER')}"/>
  </form>
</article>
<#include "footer.ftl">

src/main/resources/templates/activate.ftl:

<#include "header.ftl">
    <@spring.message "CLICK_TO_ACTIVATE"/><a href="${url}">${url}</a>
<#include "footer.ftl">

src/main/resources/templates/verify.ftl:

<#include "header.ftl">
    <h1><@spring.message "VERIFY_EMAIL"/></h1>
    <@spring.messageArgs "CHECK_EMAIL",[email]/>
<#include "footer.ftl">

src/main/resources/templates/header.ftl:

<#import "/spring.ftl" as spring/>
<#assign localize=springMacroRequestContext.getMessage >
<!DOCTYPE html>
<html lang="${springMacroRequestContext.getLocale().toLanguageTag()}">
  <head>
    <meta charset='utf-8'>
    <meta http-equiv="X-UA-Compatible" content="chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title><#if title??><@spring.message title/>|</#if><@spring.message site!"EXAMPLE"/></title>
  </head>
  <body>
    <header>
      <h1>
        <a href="/"><@spring.message site!"EXAMPLE"/></a>
        <#if title??><@spring.message title/></#if>
      </h1>
    </header>

src/main/resources/templates/footer.ftl:

  </body>
</html>

最后再加上安全配置:

package com.github.chungkwong.toy;
import com.github.chungkwong.toy.service.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;
import org.springframework.security.crypto.bcrypt.*;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	@Autowired
	private UserDetailServiceImpl userDetailService;
	@Override
	protected void configure(HttpSecurity http) throws Exception{
		http
				.authorizeRequests()
				.antMatchers("/admin/**").hasRole("admin")
				.antMatchers("/registration").permitAll()
				.antMatchers("/activate").permitAll()
				.anyRequest().authenticated().and()
				.csrf().and()
				.httpBasic().and()
				.formLogin().loginPage("/login").permitAll();
	}
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception{
		auth.userDetailsService(userDetailService).passwordEncoder(bCryptPasswordEncoder());
	}
	@Bean
	public BCryptPasswordEncoder bCryptPasswordEncoder(){
		return new BCryptPasswordEncoder();
	}
}

基本概念

Spring框架的基本想法就是尽量推迟决策,这是也是模块化的基本原则。

依赖注入

在Java开发中,经常需要用到许多对象。在经典的Java开发中,我们需要显式地创建对象,然后在所有需要它的地方都通过参数传入。这样,不同类之间存在直接引用,造成较高程度的耦合,灵活性相应降低。为了缓和这个问题,出现了工厂方法等设计模式,当把工厂方法这个设计模式发挥到极致,就是所有我们关心的对象(称为bean)都由同一个工厂方法返回。在Spring容器中这个通用的工厂方法就是org.springframework.context.ApplicationContext接口的T getBean(String name, Class<T> requiredType)方法,它的实现类负责自动实例化、配置和组合bean。

为了可以通过ApplicationContext获取bean,首先需要向ApplicationContext注册这个bean。在Spring中,通常有以下途径注册:

  • Spring能自动检测并注册@Component组件类(如果指定了@SpringBootApplication或者在一个@Configuration类中注记了@ComponentScan,可用其属性配置检测行为),@Repository@Service@Controller都是它的特例,但更强调用途:持久化、服务和表现层,从而更方便AOP中设置切点,例如@Repository已经支持对异常运行转换。这些注记都可以指定bean名字为属性,否则bean名字会自动生成(通常是首字母小写化后的类名,但可以在@ComponentScan的属性nameGenerator中指定)。组件,
    • 通过在方法标记@PostConstruct,则在创建这bean类的实例后会自动调用方法(假如CommonAnnotationBeanPostProcessor已经在ApplicationContext注册的话且对于非Web应用程序需要调用过registerShutdownHook()方法)
    • 通过在方法标记@PreDestroy,则在销毁这bean类的实例前会自动调用方法(假如CommonAnnotationBeanPostProcessor已经在ApplicationContext注册的话)
  • 在一个@Configuration类中的方法中标记@Bean,则该方法返回的对象会注册为bean。另外可以在标注中使用属性:
    • Autowire autowire控制是否通过按名称或类型注入依赖
    • java.lang.String destroyMethod控制销毁这bean实例前会自动调用的方法名
    • java.lang.String initMethod控制创建bean实例后会自动调用的方法名
    • java.lang.String[] name控制bean的名称,否则bean名字会自动生成
  • 在XML文件中注册,这是Spring最初的方法,但我们更提倡用Java注记,因为内聚性更强。

不论是@Bean方法还是@Component类,都可以额外加上以下注记:

  • 通过标记@Scope("作用域")可指定bean的作用域,作用域识别可以通过@ComponentScanscopeResolverscopedProxy属性配置。常用作用域有:
    • singleton(默认)是指在整个容器内有惟一的实例
    • prototype是指可以有任意多个实例
    • request是指每个HTTP请求有自己的一个实例
    • session是指每个HTTP会话有自己的一个实例
    • application是指每个ServletContext有自己的一个实例
    • websocket是指每个WebSocket有自己的一个实例
  • 通过标记@Qualifier("修饰")可指定修饰,注入时可指定匹配某修饰
  • 通过标记@Primary可指定在找到多个匹配的bean时优先注入这个
  • 通过标记@Lazy,则容器只在首次需要时才实例化bean,否则通常容器会尽早实例化bean
  • 通过标记@DependsOn({bean名,...})可保证在实例化这bean前先实例化指定的bean
  • 通过标记@Profile('轮廓')来指出只有特定轮廓活跃(如在application.properties的属性spring.profiles.active)时才注册,也可用于配置类

另外对于bean所属的类,可以使用如下注记:

  • 通过在字段或其设置器方法标记@Autowired则容器会自动把相应类型的bean注入到字段,通过在构造器或其它方法标记@Autowired则它们被容器调用时容器会自动把相应类型的bean注入到各参数。如果带@Qualifier("修饰")还要求与被注入bean的@Qualifier("修饰")一致。
    • 对于类型Map<String,T>,会注入一个映射表把bean名映射到各个有指定类型的bean
    • 对于类型Optional<T>,会在有指定类型bean时注入Optional<T>包装对象,否则注入表示nullOptional<T>对象。
    • 如果有多个bean匹配,则带@Primary的会被注入
  • 通过在字段或设置器方法标记@Resource(name="bean名")注入bean,不指定name则先尝试按字段名找再按类型找
  • 通过在设置器方法标记@Required来指出字段必须在配置期设置(显式声明或自动连接)

BeanFactory实例化bean会进行以下步骤:

  1. 调用BeanNameAwaresetBeanName方法
  2. 调用BeanClassLoaderAwaresetBeanClassLoader方法
  3. 调用BeanFactoryAwaresetBeanFactory方法
  4. 调用EnvironmentAwaresetEnvironment方法
  5. 调用EmbeddedValueResolverAwaresetEmbeddedValueResolver方法
  6. 调用ResourceLoaderAwaresetResourceLoader方法(如在应用程序上下文)
  7. 调用ApplicationEventPublisherAwaresetApplicationEventPublisher 方法(如在应用程序上下文)
  8. 调用MessageSourceAwaresetMessageSource方法(如在应用程序上下文)
  9. 调用ApplicationContextAwaresetApplicationContext方法(如在应用程序上下文)
  10. 调用ServletContextAwaresetServletContext方法(如在web应用程序上下文)
  11. 调用BeanPostProcessorpostProcessBeforeInitialization方法
  12. 调用InitializingBeanafterPropertiesSet方法
  13. 其它事情
  14. 调用BeanPostProcessorpostProcessAfterInitialization方法

BeanFactory销毁bean会进行以下步骤:

  1. 调用DestructionAwareBeanPostProcessorpostProcessBeforeDestruction方法
  2. 调用DisposableBeandestroy方法
  3. 其它事情

要获取一个bean对应的工厂,可以调用ApplicationContextgetBean方法,其中参数为前面加上&后的bean名。org.springframework.beans.factory.FactoryBean有以下方法: - Object getObject()返回对象实例 - boolean isSingleton()返回是否单例 - Class getObjectType()返回getObject()方法会返回的类型,不确定则返回null

面向方面

除了对象,有时我们还想注入代码。面向方面作为一种技术在方法论上可以作为面向对象的补充,其中一个用武之地是简化中间件的实现。以下是面向方面编程的基本概念:

  • 方面是跨越多个类的关注点,例如事务。Spring中要创建一个方面,在一个类中加上注记@Aspect
  • 整合点是程序的一个执行时刻。Spring中整合点表示方法的执行。
  • 建议是在一个特定整合点采取的行动。Spring把建议实现为拦截器,每个整合点维护一个拦截器链。不同类型的建议包括:
    • 前置建议在整合点前执行,但除非抛出异常否则不能阻止执行流继续。@Before注记可创建前置建议并设置切点表达式。
    • 返回后建议在整合点正常完成后执行。@AfterReturning注记可创建返回后建议并设置切点表达式,另外可用属性returning指定一个参数名,这样建议方法可通过这参数获取返回值并限制返回值类型。
    • 抛出后建议在在方法因异常退出后执行。@AfterThrowing注记可创建异常后建议并设置切点表达式,另外可用属性throwing指定一个参数名,这样建议方法可通过这参数获取异常并限制异常类型。
    • 后置建议在整合点退出后执行(不论正常与否)。@After注记可创建后置建议并设置切点表达式,通常用于释放资源。
    • 环绕建议包围整合点,不仅可以在方法调用前后做不同事,还可以决定是否调用方法和改变返回值或异常。@Around注记可创建后置建议并设置切点表达式,建议方法的首个参数必须为类型ProceedingJoinPoint,调用它的proceed()方法会运行实际方法,也可传入Object[]参数以修改调用方法的参数。
  • 切点是一个匹配整合点的谓词。每个建议与一个切点表达式关联并在匹配它的整合点执行。Spring默认使用AspectJ切点表达式语言。要创建一个切点,在一个方法中加上注记@Pointcut("谓词")。其中可用的谓词有:
    • execution(可选的修饰符模式 返回类型模式 可选的声明类型模式和句点 名称模式(参数模式) 可选的异常模式)匹配方法,最为常用,其中可用*通配一层、用..通配多个。
    • within(类型)匹配指定类型中的方法
    • this(类型)匹配给定类型的实例
    • target(类型)匹配目标对象有给定类型的方法
    • args(类型)匹配实参有给定类型的方法
    • @target(注记类)匹配目标对象运行期类型有给定注记的方法
    • @args(注记类)匹配实参运行期类型有给定注记的方法
    • @within(注记类)匹配有给定注记的类型中的方法
    • @annotation(注记类)匹配有给定注记的方法
    • bean(名称)匹配有指定名称的Spring bean
  • 引入是指以一个类型的身份声明额外字段或方法。Spring AOP中可以引入接口和对应实现,如用于检测修改与否。
  • 目标对象是被一个或多个方面建议的对象。
  • AOP代理是由AOP框架生成的对象,用于实现方面契约。Spring中AOP代理为JDK动态代理(默认)或CGLIB代理(代理类而非接口)。也就是说,在Spring AOP情况下,我们直接操作的是代理对象而不是目标对象,这种实现方式的一个缺陷是目标对象调用自己的方法时Spring拦截不了。
  • 编排是指把方面和其它类型或对象链接起来创建一个带建议的对象。Spring在运行时编排而不是像AspectJ那样在编译时。

另外,要启用面向方面,需要在@Configuration类再加上注记@EnableAspectJAutoProxy

辅助工具

表达式语言

对于字段、方法或参数,可以使用@Value注记用表达式语言指定默认值:

public static class FieldValueTestBean
    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }
    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}
public static class PropertyValueTestBean
    private String defaultLocale;
    @Value("#{ systemProperties['user.region'] }")
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }
    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}
public class SimpleMovieLister {
    private MovieFinder movieFinder;
    private String defaultLocale;
    @Autowired
    public void configure(MovieFinder movieFinder,
            @Value("#{ systemProperties['user.region'] }") String defaultLocale) {
        this.movieFinder = movieFinder;
        this.defaultLocale = defaultLocale;
    }
}

另外也可以对表达式语言进行求值:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'");
String message = exp.getValue(String.class);
表达式
字面值 常数、布尔值、null或用单引号包围的字符串(用两个单引号表示单引号)
(表达式) 子表达式的值
对象.属性 属性值,属性的首个字母不区分大小写
列表、数组或映射[键] 对应值
{元素,...} 列表
{键:值,...} 映射,键不一定要引用
同Java的数组创建语法 数组,目前不能初始化多维数组
同Java的方法调用语法 方法的返回值
表达式 关系运算符 表达式 比较结果,其中比较运算符有==<><=>=!=ltgtlegeeqneinstanceofmatches(匹配正则表达式),null视为最小
表达式 逻辑运算符 表达式 逻辑运算结果,其中逻辑运算符有andornot
表达式 算术运算符 表达式 算术运算结果,其中算术运算符有+-*/%^divmod
字段=表达式 用表达式值为参数调用set字段方法
T(类) 指定的java.lang.Class对象,可在其上调用静态方法
new 类名(参数,...) 创建的对象
#变量 EvaluationContext中的变量值,对Method对象还可以调用,特别地#this引用当前求值对象,#root引用根求值对象
@bean 指定的bean
&bean 指定的bean工厂
表达式?表达式:表达式 按条件决定取值
表达式?:表达式 首个表达式值非null时以它为值否则以后一表达式的值为值
表达式?.字段或方法调用 对表达式的值获取字段值或方法返回值,在表达式值为null时为null
表达式.?[选择表达式] 由首表达式表示的集合中满足选择表达式的元素(Map的元素为Map.Entry)组成的子集合
表达式.^[选择表达式] 首表达式表示的集合中首个满足选择表达式的元素
表达式.$[选择表达式] 首表达式表示的集合中最后一个满足选择表达式的元素
表达式.![投影表达式] 由首表达式表示的集合中各元素对应的投影表达式值组成的子集合
#{ 表达式 } 表达式的值,可用于字符串中

表达式语言在语法和语义与Java有不少差异,务必小心。另外,Spring的表达式语言与Java EE的官方表达式语言也不同。出现这些混乱的原因大概是Java早年并不能很好地作为脚本语言执行,否则用Java本身就好了。

资源

public interface Resource extends InputStreamSource {
    boolean exists();
    boolean isOpen();
    URL getURL() throws IOException;
    File getFile() throws IOException;
    Resource createRelative(String relativePath) throws IOException;
    String getFilename();
    String getDescription();
}
public interface InputStreamSource {
    InputStream getInputStream() throws IOException;
}

获取资源可通过调用ApplicationContext(实现了接口ResourceLoader)的方法Resource getResource(String location),其中locationclasspath:开始的话表示来自类路径下、以file:开始的话表示来自文件系统、以http:开始的话表示来自HTTP协议。类型为Resource的bean属性也可以通过XML注入,如<property name="属性名" value="location"/>

验证

对了验证一个对象是否有效,可以实现org.springframework.validation.Validator接口,它的方法有:

  • boolean supports(Class)返回这验证器能否验证给定的类
  • void validate(Object,org.springframework.validation.Errors)在对象不合法时把错误信息记录到Errors对象,通常是用类org.springframework.validation.ValidationUtils的静态方法:
    • rejectIfEmpty(Errors errors, java.lang.String field, java.lang.String errorCode)
    • rejectIfEmpty(Errors errors, java.lang.String field, java.lang.String errorCode, java.lang.Object[] errorArgs)
    • rejectIfEmpty(Errors errors, java.lang.String field, java.lang.String errorCode, java.lang.Object[] errorArgs, java.lang.String defaultMessage)
    • rejectIfEmpty(Errors errors, java.lang.String field, java.lang.String errorCode, java.lang.String defaultMessage)
    • rejectIfEmptyOrWhitespace(Errors errors, java.lang.String field, java.lang.String errorCode)
    • rejectIfEmptyOrWhitespace(Errors errors, java.lang.String field, java.lang.String errorCode, java.lang.Object[] errorArgs)
    • rejectIfEmptyOrWhitespace(Errors errors, java.lang.String field, java.lang.String errorCode, java.lang.Object[] errorArgs, java.lang.String defaultMessage)
    • rejectIfEmptyOrWhitespace(Errors errors, java.lang.String field, java.lang.String errorCode, java.lang.String defaultMessage)

属性编辑器

PropertyEditor在字符串和对象间进行转换以便程序员进行配置:

内置编辑器 用途 默认注册
ByteArrayPropertyEditor 把字符串对应于其字节表示  
ClassEditor Class  
CustomBooleanEditor Boolean  
CustomCollectionEditor Collection类型  
CustomDateEditor java.util.Date
CustomNumberEditor Number的子类  
FileEditor java.io.File  
InputStreamEditor InputStream(通过ResourceEditorResource,单向)  
LocaleEditor Locale  
PatternEditor java.util.regex.Pattern  
PropertiesEditor Properties  
StringTrimmerEditor String(但去除两边空白,空字符串可选地换成null
URLEditor URL  

如果需要注册其它编辑器,可以:

  • 调用接口ConfigurableBeanFactoryregisterCustomEditor方法
  • 实现配置类CustomEditorConfigurer

格式化器

以下是与用户数据格式相关的接口:

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}
public interface Printer<T> {
    String print(T fieldValue, Locale locale);
}
import java.text.ParseException;
public interface Parser<T> {
    T parse(String clientValue, Locale locale) throws ParseException;
}
package org.springframework.format;
public interface AnnotationFormatterFactory<A extends Annotation> {
    Set<Class<?>> getFieldTypes();
    Printer<?> getPrinter(A annotation, Class<?> fieldType);
    Parser<?> getParser(A annotation, Class<?> fieldType);
}
package org.springframework.format;
public interface FormatterRegistry extends ConverterRegistry {
    void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
    void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
    void addFormatterForFieldType(Formatter<?> formatter);
    void addFormatterForAnnotation(AnnotationFormatterFactory<?, ?> factory);
}
package org.springframework.format;
public interface FormatterRegistrar {
    void registerFormatters(FormatterRegistry registry);
}

类型转换

以下是相关接口:

package org.springframework.core.convert.converter;
public interface Converter<S, T> {
    T convert(S source);
}
package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R> {
    <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
package org.springframework.core.convert.converter;
public interface GenericConverter {
    public Set<ConvertiblePair> getConvertibleTypes();
    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
package org.springframework.core.convert;
public interface ConversionService {
    boolean canConvert(Class<?> sourceType, Class<?> targetType);
    <T> T convert(Object source, Class<T> targetType);
    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

可以通过conversionServicebean进行显式类型转换和注册转换器。

Web MVC

分派

DispatcherServlet分派请求的流程为:

  1. 搜索WebApplicationContext并绑定到请求的属性DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE
  2. 绑定LocaleResolverLocaleContextResolver以便获取客户的地区和时区来进行国际化。另外用LocaleChangeInterceptor可以按请求参数(默认为locale)更改地区。
    • AcceptHeaderLocaleResolver用HTTP头accept-language
    • CookieLocaleResolver用cookie或在没有时accept-language
    • FixedLocaleResolver总返回固定地区
    • SessionLocaleResolver用会话属性或在没有时accept-language
  3. 绑定ThemeResolver以便获取可用的个性化主题。另外可用ThemeChangeInterceptor按请求参数更改主题。主题通常由类路径上的属性文件给出。
    • FixedThemeResolver总是返回同一主题(defaultThemeName属性决定)
    • SessionThemeResolver使用会话属性
    • CookieThemeResolver用cookie
  4. 若指定了MultipartResolver则在发现multipart时把请求封装为MultipartHttpServletRequest
  5. 绑定HandlerMapping搜索合适的处理器,发现的话执行相应的链(预处理、控制器、后处理)以预备模型或渲染。对于注记的控制器,可能直接渲染响应而非返回视图(在HandlerAdapter中)
    • RequestMappingHandlerMapping容许使用注记@Controller@RestController(相当于@Controller加上@ResponseBody)注册控制器(但注意在配置启用组件扫描@Configuration@ComponentScan("包"))。然后对方法用以下注记指定映射
      • @RequestMapping
        • path属性可指定路径,其中可以用?匹配单个字符、用*匹配路径段中的零个或以上字符、**匹配零个或多个路径段、{varName:正则表达式}。如果多个
        • java.lang.String[] consumes表示接受的媒体类型,用于用前置!表示否定
        • java.lang.String[] headers表示请求头,形如!键键=值
        • RequestMethod[] method请求方法GETPOSTHEADOPTIONSPUTPATCHDELETETRACE
        • java.lang.String name映射的名称
        • java.lang.String[] params请求参数,形如!键键=值
        • java.lang.String[] path请求路径,如对默认错误页是/error,在默认情况下也匹配加上文件名后缀后的
        • java.lang.String[] produces表示产生的媒体类型,用于用前置!表示否定
      • @GetMapping
      • @PostMapping
      • @PutMapping
      • @DeleteMapping
      • @PatchMapping
    • SimpleUrlHandlerMapping管理显式注册映射规则
  6. 若返回了模型,将渲染视图。把视图名映射到渲染响应的视图用到ViewResolver链,特别地redirect:后接URL表示重定向,forward:后接URL表示转到指定资源。
    • AbstractCachingViewResolver缓存视图实例可以通过把cache属性设为false阻止缓存,也可以用removeFromCache(String viewName, Locale loc)方法使缓存失效
    • XmlViewResolver接受XML文件,默认是/WEB-INF/views.xml
    • ResourceBundleViewResolver用一个ResourceBundle中的定义,[viewname].(class)属性决定类,[viewname].url为视图URL
    • UrlBasedViewResolver直接把逻辑视图名解析为URL
    • InternalResourceViewResolver支持InternalResourceView(实际上Servlet和JSP)
    • FreeMarkerViewResolver支持FreeMarkerView
    • ContentNegotiatingViewResolver基于请求文件名或Accept头选择首个支持请求媒体类型的ViewDefaultViews属性指定的
  7. 如上上述过程抛出了异常,则由HandlerExceptionResolver链把异常映射到处理器或HTML错误视图(返回ModelAndView表示错误视图、返回空ModelAndView表示异常已处理完、null表示再尝试其它)。
    • SimpleMappingExceptionResolver把异常类名映射到错误视图
    • DefaultHandlerExceptionResolver把异常映射到HTTP状态码
    • ResponseStatusExceptionResolver@ResponseStatus注记指定的值映射到HTTP状态码
    • ExceptionHandlerExceptionResolver调用@Controller@ControllerAdvice类的@ExceptionHandler方法

以下是一些有用的过滤器:

  • ForwardedHeaderFilter检测、提取和使用Forwarded头或X-Forwarded-HostX-Forwarded-PortX-Forwarded-Proto头”
  • ShallowEtagHeaderFilter计算ETag
  • CorsFilter应在Spring Security的过滤器之上

拦截器通常实现org.springframework.web.servlet.HandlerInterceptor接口:

  • void afterCompletion(HttpServletRequest request, HttpServletResponse response, java.lang.Object handler, java.lang.Exception ex)
  • void postHandle(HttpServletRequest request, HttpServletResponse response, java.lang.Object handler, ModelAndView modelAndView)
  • boolean preHandle(HttpServletRequest request, HttpServletResponse response, java.lang.Object handler)返回false则不再继续处理

通过在@Configuration类实现WebMvcConfigurer接口,可以定制Web MVC,比如下面我们注册一个LocaleChangeInterceptor

package com.github.chungkwong.toy;
import org.springframework.context.annotation.*;
import org.springframework.web.servlet.*;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.i18n.*;
@Configuration
public class WebConfig implements WebMvcConfigurer{
	@Override
	public void addInterceptors(InterceptorRegistry registry){
		WebMvcConfigurer.super.addInterceptors(registry);
		registry.addInterceptor(localeChangeInterceptor());
	}
	@Bean
	public LocaleChangeInterceptor localeChangeInterceptor(){
		LocaleChangeInterceptor localeChangeInterceptor=new LocaleChangeInterceptor();
		localeChangeInterceptor.setParamName("locale");
		return localeChangeInterceptor;
	}
	@Bean
	public LocaleResolver getLocaleResolver(){
		return new CookieLocaleResolver();
	}
}

控制器

控制器方法可以接受如下参数:

参数
WebRequest, NativeWebRequest 可用于访问请求参数、请求和会话属性
javax.servlet.ServletRequest, javax.servlet.ServletResponse 请求或响应,可能是ServletRequestHttpServletRequestMultipartRequestMultipartHttpServletRequest
javax.servlet.http.HttpSession 会话,总是非空。除非把RequestMappingHandlerAdaptersynchronizeOnSession设为true,否则会话一般不是线程安全的
javax.servlet.http.PushBuilder 用于HTTP/2的推,可能是null(如果不支持HTTP/2)
java.security.Principal或其子类 当前的认证用户
HttpMethod HTTP方法
java.util.Locale 当前地区
java.util.TimeZone, java.time.ZoneId 请求的时区,由LocaleContextResolver决定
java.io.InputStream, java.io.Reader 用于读取请求内容的流
java.io.OutputStream, java.io.Writer 用于写入响应内容的流
@PathVariable URI模板变量(可以指定namerequired
@MatrixVariable URI路径段中形如/owners/42;q=11;r=12/pets/21;q=22;s=23键值对(可以指定defaultValuepathVarnamerequired),通常类型为MultiValueMap<String, String>
@RequestParam 请求参数(可以指定defaultValuenamerequired
@RequestHeader 请求头(可以指定defaultValuenamerequired),转换为指定类型
@CookieValue 访问cookie(可以指定defaultValuenamerequired),转换为指定类型
@RequestBody HTTP请求体(可以指定required),经HttpMessageConverter转换为指定类型
HttpEntity<B> 完整的请求头和体,体经HttpMessageConverter转换为指定类型
@RequestPart multipart/form-data请求的一部分(可以指定namerequired),常用MultipartFile类型
java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap 暴露给模板的模型
RedirectAttributes 重定向用的属性,如增加的查询字符串、flash属性
@ModelAttribute 访问模型中属性(没有则实例化)(可以指定namebinding,后者可用于阻止绑定),涉及数据绑定和验证
Errors, BindingResult 用于访问验证错误和数据绑定结果,必须紧接被验证的参数(@ModelAttribute@RequestBody@RequestPart
SessionStatus 可通过调用setComplete方法清除类级@SessionAttributes(有属性namestypes)指定的参数
UriComponentsBuilder 用于生成相当于当前请求的相对URL
@SessionAttribute 任意会话属性(可以指定namerequired
@RequestAttribute 请求属性(可以指定namerequired
其它 把简单类型的视为@RequestParam否则@ModelAttribute

对于@RequestParam@RequestHeader之类非必须的参数,可以用JDK 8的java.util.Optional作为参数类型,相当于在注记属性required=false

返回值 用途
@ResponseBody 返回值通过HttpMessageConverters转换后写到响应
HttpEntity<B>,ResponseEntity<B> 完整响应,包括HTTP头和体经HttpMessageConverters转换后写到响应,可以包含缓存信息
HttpHeaders 返回有头无体的响应
String 视图名,模型来自@ModelAttributeModel
View 用于渲染的视图,模型来自@ModelAttributeModel
java.util.Map, org.springframework.ui.Model 加到模型中的属性,视图由RequestToViewNameTranslator决定
@ModelAttribute 加到模型中的属性,视图由RequestToViewNameTranslator决定
ModelAndView 视图和对象,还有可选的响应状态
voidnull 当有ServletResponseOutputStream@ResponseStatus参数,又或作出正面ETaglastModified检查时视为已完成处理,否则对REST控制器表示无内容而对HTML控制器表示默认视图名选取
DeferredResult<V> 通过任意线程异步地产生上述的返回值
Callable<V> 通过Spring MVC管理的线程线程异步地产生上述的返回值
ListenableFuture<V>, java.util.concurrent.CompletionStage<V>, java.util.concurrent.CompletableFuture<V> DeferredResult<V>类似
ResponseBodyEmitter, SseEmitter 异步地产生对象流通过HttpMessageConverter写到响应
StreamingResponseBody 异步地写到响应
响应式类型—— Reactor, RxJava, 或借助ReactiveAdapterRegistry的 带多值流DeferredResult的替代品
其它 对于String是视图名、void则通过RequestToViewNameTranslator决定视图,其它非简单类型表示模型属性,否则仍然待定

如果需要支持跨源请求,可以在类或方法加上注记@CrossOrigin,其中可以有属性

  • String allowCredentials给出Access-Control-Allow-Credentials
  • java.lang.String[] allowedHeaders给出容许的头,*表示所有
  • java.lang.String[] exposedHeaders给出Access-Control-Expose-Headers
  • long maxAge表示缓存有效期,默认1800秒
  • RequestMethod[] methods表示容许的HTTP方法
  • java.lang.String[] origins表示容许的源

可以在控制器类中加入@ExceptionHandler(可加上异常类数组)方法处理错误,它的参数和返回值含义与普通@RequestMapping方法类似,但另外可用异常类型作参数取得异常和用HandlerMethod类作参数来取得导致异常的方法。

另外可以用@InitBinder方法来初始化WebDataBinder实例,把它作为参数传入。

类型转换可以在WebDataBinderFormattingConversionService配置或注册。

视图

以下以Freemarker为例给出用于HTML的的视图。Spring给出一些有用的宏,但记住要先导入它们才能用:

<#import "/spring.ftl" as spring/>
用途
<@spring.message code/> 显示对应于指定代码的信息
<@spring.messageText code, text/> 显示对应于指定代码的信息,没有则退回指定文本
<@spring.url relativeUrl/> 把程序上下文根加到相对URL前
<@spring.formInput path, attributes, fieldType/> 一个输入框
<@spring.formHiddenInput path, attributes/> 一个隐藏的字段
<@spring.formPasswordInput path, attributes/> 一个密码框
<@spring.formTextarea path, attributes/> 一个多行文本框
<@spring.formSingleSelect path, options, attributes/> 一个单选列表
<@spring.formMultiSelect path, options, attributes/> 一个可以多选的列表
<@spring.formRadioButtons path, options, separator, attributes/> 一组单选框
<@spring.formCheckboxes path, options, separator, attributes/> 一组多选框
<@spring.formCheckbox path, attributes/> 多选框
<@spring.showErrors separator, classOrStyle/> 显示绑定字段的验证错误

其中,

  • path表示把字段绑定到的名称
  • options表示一个映射,把值映射到用户看到的名字
  • separator表示不同元素的分隔符如"<br>"
  • attributes表示一组HTML属性,会直接复制进标签
  • classOrStyle表示CSS类,没有则用<b></b>包围错误

然而,还有许多模板引擎,如

  • Thymeleaf
  • Groovy Markup
  • Script Template支持基于JVM脚本语言的各种模板系统,如基于Nashorn的Handlebars、Mustache、React、EJS,基于 JRuby的ERB,基于Jython的String templates,基于Kotlin的Kotlin Script templating
  • JSP和JSTL
  • Tiles
  • RSS和Atom
  • PDF和Excel
  • JSON
  • XML,可经过XSLT

RESTful客户端

  • RestTemplate提供了进行HTTP请求的方法,它使用同步(阻塞)API
  • WebClient是提供函数式、流式的异步API,适合高并发情况

WebSocket

Spring MVC提供了对WebSocket及其子协议STOMP的支持,并可以在需要后退回SockJS。如果需要客户端与服务器端需要高频低延迟地双向通信(比如即时通信服务),有可能用到它。

整合

事务

通过对类、接口或方法标记@org.springframework.transaction.annotation.Transactional可使设置方法调用的事务性(假设对象由Spring管理):

  • Isolation isolation,可以是:
    • DEFAULT(同数据源)
    • READ_COMMITTED(不容许肮读,但容许不可重复读和幻影)
    • READ_UNCOMMITTED(容许肮读、不可重复读和幻影)
    • REPEATABLE_READ(不容许肮读和不可重复读,但容许幻影)
    • SERIALIZABLE(不容许肮读、不可重复读和幻影)
  • java.lang.Class<? extends java.lang.Throwable>[] noRollbackFor表示不会导致回滾的异常类型
  • java.lang.String[] noRollbackForClassName表示不会导致回滾的异常类型名
  • Propagation propagation表示事务传播方式:
    • MANDATORY(事务地执行,没有事务则抛出异常)
    • NESTED(在已有事务时在嵌套事务执行)
    • NEVER(非事务地执行,有事务则抛出异常)
    • NOT_SUPPORTED(非事务地执行,有事务则中断它)
    • REQUIRED(事务地执行,没有事务则创建,默认)
    • REQUIRES_NEW(事务地执行,已有事务则中断它)
    • SUPPORTS(当且仅当已有事务时事务地执行)
  • boolean readOnly表示事务是否只读的
  • java.lang.Class<? extends java.lang.Throwable>[] rollbackFor表示会导致回滾的异常类型
  • java.lang.String[] rollbackForClassName表示会导致回滾的异常类型名
  • int timeout表示事务的时限
  • java.lang.String transactionManager表示事务管理器

持久化

可以用JdbcTemplate直接与关系数据库打交道,但由于关系式数据库的思维方式和面向对象的思维方式不一样,利用JPA自动地进行关系-对象映射(ORM)更为方便。

只用声明一个扩展JpaRepository<实例类型,主键类型>的接口,Spring即可给我们实现它,其中还可以声明方法名如:

  • find可选的字段By字段用于寻找
  • count可选的字段By字段
  • delete可选的字段By字段removeBy字段

实际上上述字段可以用连词合成更复杂的形式:

连词 例子 对应的JPQL
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is,Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull findByAgeIsNull … where x.age is null
IsNotNull,NotNull findByAgeIsNotNull,findByAgeNotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (参数后加上%)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (参数后加上%)
Containing findByFirstnameContaining … where x.firstname like ?1 (参数两边加上%)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstame) = UPPER(?1)

这些方法的参数可以为:

  • Sort表示排序方法
  • Pageable表示分页方法
  • 其它参数会视为查询参数,可用@Param("参数名")指定参数名,否则作位置参数
返回值类型 返回值
void
基本类型 同下
基本类型的包装类型 同下
T 惟一的实体,没有则返回null,不惟一则抛出IncorrectResultSizeDataAccessException
Iterator<T> 可迭代结果实体的迭代器
Collection<T> 结果实体组成的集合
List<T> 结果实体组成的列表
Optional<T> 把惟一的实体或null包装起来,不惟一则抛出IncorrectResultSizeDataAccessException
Stream<T> 结果实体组成的流
Future<T> 表示未来的异步执行结果,方法应标记@Async
CompletableFuture<T> 表示未来的异步执行结果,方法应标记@Async
ListenableFuture 表示未来的异步执行结果,方法应标记@Async
Slice 一组大小受限的数据,要求Pageable参数
Page<T> 一组大小受限的数据和额外信息如结果总数,要求Pageable参数
GeoResult<T> 一项结果和额外信息如到参考点距离
GeoResults<T> 一列GeoResult<T>与额外信息如平均距离
GeoPage<T> 一页GeoResult<T>与额外信息如平均距离

另外可以用以下标记:

  • @NonNullApi用到包上可声明包中所有参数和返回值默认能否是null
  • @NonNull可标记参数或返回值不能为null
  • @Nullable可标记参数或返回值能为null
  • @Query("SQL语句")可指定方法用给定查询语句而不是自动生成

邮件

org.springframework.mail.javamail.JavaMailSender接口提供了一个发送邮件的客户端。

消息

要启用Java消息服务(JMS),首先在一个@Configuration类中标记@EnableJms

通过在@Component类中的方法标记@JmsListener(destination="目的地"),在指定的javax.jms.Destination收到信息时就会调用方法,并可通过以下类型的参数(通过DefaultMessageHandlerMethodFactory可定制)取得消息:

  • javax.jms.Message或子类
  • javax.jms.Session
  • org.springframework.messaging.Message
  • @Header参数可取得特定头的值或全部头(若类型为java.util.Map或子类)
  • 其它参数指为负载,可以通过@Valid验证

至于返回值类型可以是:

  • JmsResponse表示响应,可以在运行时计算目的地
  • Message表示响应,目的地为原消息的JMSReplyTo头或默认目的地,除非对方法标记@SendTo("回复地")
  • 其它非void类型表示响应内容,目的地为原消息的JMSReplyTo头或默认目的地,除非对方法标记@SendTo("回复地")
  • void

要发送消息,可以使用The package org.springframework.jms.core.JmsTemplate类。

任务调度

要启用任务调度,在一个@Configuration中标记@EnableAsync@EnableScheduling

对于需要定时执行的方法,标记@Scheduled,可用以下属性:

  • java.lang.String cron是类似UNIX的cron表达式(但支持秒),形如"0 * * * * MON-FRI",其中由空白分隔的部分分别表示秒、分钟、小时、日、月、周天,每个部分形如
    • *表示所有
    • 表示一个值
    • 数-数表示范围(包含边界)
    • 数-数/数表示从首个数开始每隔最后一个数直到第二个数
    • ,分隔的上述三种形式,表示求并
  • long fixedDelay指定在完成一次调用后等多少毫秒才开始下一次调用
  • java.lang.String fixedDelayString同上但用字符串
  • long fixedRate指定在开始一次调用后等多少毫秒才开始下一次调用
  • java.lang.String fixedRateString同上但用字符串
  • long initialDelay在首次调用前等多少毫秒
  • java.lang.String initialDelayString同上但用字符串
  • java.lang.String zone用于cron表达式的时区(默认为本地时区)

通过把方法标记为@Async(另外可在属性value指定Executor)可让它异步执行,void但返回值类型只能为Future。另外,可以通过实现AsyncConfigurer配置AsyncUncaughtExceptionHandler,以便捕获异步执行期间发生的异常(对返回void的方法特别有用)。

缓存

对于函数式的方法,即对相同参数总返回相同值的方法,通过缓存结果可以节省计算时间。要启用缓存,首先要在一个@Configuration类再标记@EnableCaching。然后如下控制缓存:

  • 通过标注@org.springframework.cache.annotation.Cacheable告诉一个方法或类中所有方法的返回值可以缓存,下次再调用时如果在缓存找到结果就直接返回它,它的属性有:
    • java.lang.String cacheManager表示CacheManager的bean名(不指定则用默认的)
    • java.lang.String[] cacheNames表示缓存的名称
    • java.lang.String cacheResolver表示CacheResolver的bean名
    • java.lang.String condition是用于开启缓存的SpEL表达式
    • java.lang.String key是用于计算键的SpEL表达式
    • java.lang.String keyGenerator表示KeyGeneratorbean名
    • boolean sync表示是否在多线程企图加载同一键对应缓存值时同步
    • java.lang.String unless是用于禁止缓存的SpEL表达式
  • 通过标注@org.springframework.cache.annotation.CachePut指出应当在运行一个方法或类中任何方法后更新缓存,它的属性有:
    • java.lang.String cacheManager表示CacheManager的bean名(不指定则用默认的)
    • java.lang.String[] cacheNames表示缓存的名称
    • java.lang.String cacheResolver表示CacheResolver的bean名
    • java.lang.String condition是用于开启缓存的SpEL表达式
    • java.lang.String key是用于计算键的SpEL表达式
    • java.lang.String keyGenerator表示KeyGeneratorbean名
    • java.lang.String unless是用于禁止缓存的SpEL表达式
  • 通过标注@org.springframework.cache.annotation.CacheEvict指出应当清除缓存,它的属性有:
    • boolean allEntries表示是否清除缓存的所有条目
    • boolean beforeInvocation表示是否在调用方法前消除缓存
    • java.lang.String cacheManager表示CacheManager的bean名(不指定则用默认的)
    • java.lang.String[] cacheNames表示缓存的名称
    • java.lang.String cacheResolver表示CacheResolver的bean名
    • java.lang.String condition是用于开启缓存的SpEL表达式
    • java.lang.String key是用于计算键的SpEL表达式
    • java.lang.String keyGenerator表示KeyGeneratorbean名
  • 通过标注@org.springframework.cache.annotation.Caching可把多个缓存相关标注组合在一起,它的属性有:
    • Cacheable[] cacheable
    • CacheEvict[] evict
    • CachePut[] put
  • 通过标注@org.springframework.cache.annotation.CacheEvict指出类中各方法默认缓存属性:
    • java.lang.String cacheManager表示CacheManager的bean名(不指定则用默认的)
    • java.lang.String[] cacheNames表示缓存的名称
    • java.lang.String cacheResolver表示CacheResolver的bean名
    • java.lang.String keyGenerator表示KeyGeneratorbean名

其中SpEL表达式中可访问变量有:

  • methodName表示方法名
  • method表示方法
  • target表示目标对象
  • targetClass表示目标对象类型
  • args表示参数
  • caches表示缓存集合
  • 参数名对应参数值
  • result表示返回值

测试

单元测试

对于POJO,单元测试可以如常进行。而对于涉及依赖注入的代码,可以使用模拟对象来测试以隔离问题和加快测试:

  • org.springframework.mock.env包中的MockEnvironmentMockPropertySource可用于测试依赖环境属性的代码。
  • org.springframework.mock.jndi包中实现了JNDI SPI,可用于搭建简单的JNDI环境。
  • org.springframework.mock.web包包含Servlet API的各种模拟对象,可用于测试web上下文、控制器和过滤器。
  • org.springframework.mock.http.server.reactive包包含用于WebFlux程序的MockServerHttpResponseMockServerHttpResponse模拟对象
  • org.springframework.mock.web.server包中的MockServerWebExchange用于测试MockServerHttpRequestMockServerHttpResponse

另外有一些用于测试的工具:

  • org.springframework.test.util.ReflectionTestUtils提供一些基于反射的工具方法,用于改变常量值、设置非公开字段、调用非公开方法。对ORM框架、依赖注入机制和生命周期回调的测试可能有用。
  • org.springframework.test.util.AopTestUtils提供一些AOP相关的工具方法,用于获取隐藏在Spring代理后的目标对象。
  • org.springframework.test.web.ModelAndViewAssert提供一些用于测试ModelAndView对象的断言
  • org.springframework.test.web.reactive.server.WebTestClient用于测试基于WebFlux的程序或其它基于HTTP端到端应用的集成测试,它的非阻塞特性使它适合异步和流场景
  • org.springframework.test.web.client.MockRestServiceServer可用作模拟服务器来测试使用RestTemplate的客户端代码

自动配置

只要在主方法所在的类(应当在一个包中且你的其它类都在它的子包中)注记@SpringBootApplication,并把主方法设为SpringApplication.run(类名.class,args);,Spring Boot就会自动根据依赖的JAR自动配置。在Maven组org.springframework.boot下有一些方便的依赖:

依赖 用途
spring-boot-starter 核心,包括自动配置支持、日志和YAML
spring-boot-starter-activemq 使用Apache ActiveMQ作JMS消息传递
spring-boot-starter-amqp Spring AMQP和Rabbit MQ
spring-boot-starter-aop Spring AOP和AspectJ面向方面编程支持
spring-boot-starter-artemis 使用Apache Artemis作JMS消息传递JMS messaging using
spring-boot-starter-batch 使用Spring Batch
spring-boot-starter-cache 使用Spring框架的缓存支持
spring-boot-starter-cloud-connectors 使用Spring Cloud Connectors简化连接Cloud Foundry和Heroku上的云服务
spring-boot-starter-data-cassandra 使用Cassandra分布式数据库和Spring Data Cassandra
spring-boot-starter-data-cassandra-reactive 使用Cassandra分布式数据库和Spring Data Cassandra Reactive
spring-boot-starter-data-couchbase 使用Couchbase文档数据库和Spring Data Couchbase
spring-boot-starter-data-couchbase-reactive 使用Couchbase文档数据库和Spring Data Couchbase Reactive
spring-boot-starter-data-elasticsearch 使用Elasticsearch搜索和分析引擎和Spring Data Elasticsearch
spring-boot-starter-data-jpa 使用Spring Data JPA和
spring-boot-starter-data-ldap 使用Spring Data LDAP
spring-boot-starter-data-mongodb 使用MongoDB文档数据库和Spring Data MongoDB
spring-boot-starter-data-mongodb-reactive 使用MongoDB文档数据库和Spring Data MongoDB Reactive
spring-boot-starter-data-neo4j 使用Neo4j图数据库和Spring Data Neo4j
spring-boot-starter-data-redis 使用Redis键值数据库和Spring Data Redis与Lettuce客户端
spring-boot-starter-data-redis-reactive 使用Redis键值数据库和Spring Data Redis reactive与Lettuce客户端
spring-boot-starter-data-rest 使用Spring Data REST通过REST暴露Spring Data仓库
spring-boot-starter-data-solr 使用Apache Solr搜索平台和Spring Data Solr
spring-boot-starter-freemarker 使用FreeMarker视图
spring-boot-starter-groovy-templates 使用Groovy Templates视图
spring-boot-starter-hateoas 用Spring MVC和Spring HATEOAS搭建基于超媒体的RESTful web程序
spring-boot-starter-integration 使用Spring集成
spring-boot-starter-jdbc 通过HikariCP连接池使用JDBC
spring-boot-starter-jersey 用JAX-RS和Jersey搭建RESTful web应用
spring-boot-starter-jooq 使用jOOQ访问SQL数据库
spring-boot-starter-json 读写JSON
spring-boot-starter-jta-atomikos 使用JTA事务(Atomikos)
spring-boot-starter-jta-bitronix 使用JTA事务(Bitronix)
spring-boot-starter-jta-narayana 使用JTA事务(Narayana)
spring-boot-starter-mail 支持Java Mail和Spring框架的邮件发送
spring-boot-starter-mustache 使用Mustache视图
spring-boot-starter-quartz 使用Quartz调度器
spring-boot-starter-security 使用Spring安全
spring-boot-starter-test 使用JUnit、Hamcrest和Mockito测试Spring Boot应用
spring-boot-starter-thymeleaf 使用Thymeleaf视图
spring-boot-starter-validation 支持在Hibernate使用Java Bean验证器
spring-boot-starter-web Spring MVC支持,以Tomcat为默认的嵌入容器
spring-boot-starter-web-services Spring Web服务
spring-boot-starter-webflux Spring框架的响应式Web支持(WebFlux)
spring-boot-starter-websocket Spring框架的WebSocket支持
spring-boot-starter-actuator 使用Spring Boot Actuator提供产品级特性来监视和管理应用程序
spring-boot-starter-jetty 使用Jetty为内嵌的servlet容器
spring-boot-starter-log4j2 使用Log4j2作日志
spring-boot-starter-logging 使用Logback作日志
spring-boot-starter-reactor-netty 使用Reactor Netty为内嵌的servlet容器
spring-boot-starter-tomcat 使用Tomcat为内嵌的servlet容器
spring-boot-starter-undertow 使用Undertow为内嵌的servlet容器

有时候我们想用手动的配置取代部分自动配置,这时可把一个类(通常是带主方法的)加上注记@Configuration,并用@Import注记的参数给出其它配置类。

关键词 java web