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,CommandLineRunner
的run
方法会在容器启动时自动运行。再运行项目就能看到输出中出现了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/static
或src/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的作用域,作用域识别可以通过@ComponentScan
的scopeResolver
和scopedProxy
属性配置。常用作用域有: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>
包装对象,否则注入表示null
的Optional<T>
对象。 - 如果有多个bean匹配,则带
@Primary
的会被注入
- 对于类型
- 通过在字段或设置器方法标记
@Resource(name="bean名")
注入bean,不指定name
则先尝试按字段名找再按类型找 - 通过在设置器方法标记
@Required
来指出字段必须在配置期设置(显式声明或自动连接)
BeanFactory
实例化bean会进行以下步骤:
- 调用
BeanNameAware
的setBeanName
方法 - 调用
BeanClassLoaderAware
的setBeanClassLoader
方法 - 调用
BeanFactoryAware
的setBeanFactory
方法 - 调用
EnvironmentAware
的setEnvironment
方法 - 调用
EmbeddedValueResolverAware
的setEmbeddedValueResolver
方法 - 调用
ResourceLoaderAware
的setResourceLoader
方法(如在应用程序上下文) - 调用
ApplicationEventPublisherAware
的setApplicationEventPublisher
方法(如在应用程序上下文) - 调用
MessageSourceAware
的setMessageSource
方法(如在应用程序上下文) - 调用
ApplicationContextAware
的setApplicationContext
方法(如在应用程序上下文) - 调用
ServletContextAware
的setServletContext
方法(如在web应用程序上下文) - 调用
BeanPostProcessor
的postProcessBeforeInitialization
方法 - 调用
InitializingBean
的afterPropertiesSet
方法 - 其它事情
- 调用
BeanPostProcessor
的postProcessAfterInitialization
方法
BeanFactory
销毁bean会进行以下步骤:
- 调用
DestructionAwareBeanPostProcessor
的postProcessBeforeDestruction
方法 - 调用
DisposableBean
的destroy
方法 - 其它事情
要获取一个bean对应的工厂,可以调用ApplicationContext
的getBean
方法,其中参数为前面加上&
后的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的方法调用语法 | 方法的返回值 |
表达式 关系运算符 表达式 |
比较结果,其中比较运算符有== 、< 、> 、<= 、>= 、!= 、lt 、gt 、le 、ge 、eq 、ne 、instanceof 、matches (匹配正则表达式),null 视为最小 |
表达式 逻辑运算符 表达式 |
逻辑运算结果,其中逻辑运算符有and 、or 、not |
表达式 算术运算符 表达式 |
算术运算结果,其中算术运算符有+ 、- 、* 、/ 、% 、^ 、div 、mod |
字段=表达式 |
用表达式值为参数调用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)
,其中location
以classpath:
开始的话表示来自类路径下、以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 (通过ResourceEditor 和Resource ,单向) |
|
LocaleEditor | Locale |
|
PatternEditor | java.util.regex.Pattern |
|
PropertiesEditor | Properties |
|
StringTrimmerEditor | String (但去除两边空白,空字符串可选地换成null ) |
否 |
URLEditor | URL |
如果需要注册其它编辑器,可以:
- 调用接口
ConfigurableBeanFactory
的registerCustomEditor
方法 - 实现配置类
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);
}
可以通过conversionService
bean进行显式类型转换和注册转换器。
Web MVC
分派
DispatcherServlet
分派请求的流程为:
- 搜索
WebApplicationContext
并绑定到请求的属性DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE
- 绑定
LocaleResolver
或LocaleContextResolver
以便获取客户的地区和时区来进行国际化。另外用LocaleChangeInterceptor
可以按请求参数(默认为locale
)更改地区。AcceptHeaderLocaleResolver
用HTTP头accept-language
CookieLocaleResolver
用cookie或在没有时accept-language
FixedLocaleResolver
总返回固定地区SessionLocaleResolver
用会话属性或在没有时accept-language
- 绑定
ThemeResolver
以便获取可用的个性化主题。另外可用ThemeChangeInterceptor
按请求参数更改主题。主题通常由类路径上的属性文件给出。FixedThemeResolver
总是返回同一主题(defaultThemeName
属性决定)SessionThemeResolver
使用会话属性CookieThemeResolver
用cookie
- 若指定了
MultipartResolver
则在发现multipart时把请求封装为MultipartHttpServletRequest
- 绑定
HandlerMapping
搜索合适的处理器,发现的话执行相应的链(预处理、控制器、后处理)以预备模型或渲染。对于注记的控制器,可能直接渲染响应而非返回视图(在HandlerAdapter
中)RequestMappingHandlerMapping
容许使用注记@Controller
或@RestController
(相当于@Controller
加上@ResponseBody
)注册控制器(但注意在配置启用组件扫描@Configuration@ComponentScan("包")
)。然后对方法用以下注记指定映射@RequestMapping
path
属性可指定路径,其中可以用?
匹配单个字符、用*
匹配路径段中的零个或以上字符、**
匹配零个或多个路径段、{varName:正则表达式}
。如果多个java.lang.String[] consumes
表示接受的媒体类型,用于用前置!
表示否定java.lang.String[] headers
表示请求头,形如键
、!键
或键=值
RequestMethod[] method
请求方法GET
、POST
、HEAD
、OPTIONS
、PUT
、PATCH
、DELETE
或TRACE
java.lang.String name
映射的名称java.lang.String[] params
请求参数,形如键
、!键
或键=值
java.lang.String[] path
请求路径,如对默认错误页是/error
,在默认情况下也匹配加上文件名后缀后的java.lang.String[] produces
表示产生的媒体类型,用于用前置!
表示否定
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
SimpleUrlHandlerMapping
管理显式注册映射规则
- 若返回了模型,将渲染视图。把视图名映射到渲染响应的视图用到
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
为视图URLUrlBasedViewResolver
直接把逻辑视图名解析为URLInternalResourceViewResolver
支持InternalResourceView
(实际上Servlet和JSP)FreeMarkerViewResolver
支持FreeMarkerView
ContentNegotiatingViewResolver
基于请求文件名或Accept头选择首个支持请求媒体类型的View
或DefaultViews
属性指定的
- 如上上述过程抛出了异常,则由
HandlerExceptionResolver
链把异常映射到处理器或HTML错误视图(返回ModelAndView
表示错误视图、返回空ModelAndView
表示异常已处理完、null
表示再尝试其它)。SimpleMappingExceptionResolver
把异常类名映射到错误视图DefaultHandlerExceptionResolver
把异常映射到HTTP状态码ResponseStatusExceptionResolver
按@ResponseStatus
注记指定的值映射到HTTP状态码ExceptionHandlerExceptionResolver
调用@Controller
或@ControllerAdvice
类的@ExceptionHandler
方法
以下是一些有用的过滤器:
ForwardedHeaderFilter
检测、提取和使用Forwarded
头或X-Forwarded-Host
、X-Forwarded-Port
、X-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 |
请求或响应,可能是ServletRequest 、HttpServletRequest 、MultipartRequest 或MultipartHttpServletRequest |
javax.servlet.http.HttpSession |
会话,总是非空。除非把RequestMappingHandlerAdapter 的synchronizeOnSession 设为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模板变量(可以指定name 或required ) |
@MatrixVariable |
URI路径段中形如/owners/42;q=11;r=12/pets/21;q=22;s=23 键值对(可以指定defaultValue 、pathVar 、name 或required ),通常类型为MultiValueMap<String, String> |
@RequestParam |
请求参数(可以指定defaultValue 、name 或required ) |
@RequestHeader |
请求头(可以指定defaultValue 、name 或required ),转换为指定类型 |
@CookieValue |
访问cookie(可以指定defaultValue 、name 或required ),转换为指定类型 |
@RequestBody |
HTTP请求体(可以指定required ),经HttpMessageConverter 转换为指定类型 |
HttpEntity<B> |
完整的请求头和体,体经HttpMessageConverter 转换为指定类型 |
@RequestPart |
multipart/form-data 请求的一部分(可以指定name 或required ),常用MultipartFile 类型 |
java.util.Map , org.springframework.ui.Model , org.springframework.ui.ModelMap |
暴露给模板的模型 |
RedirectAttributes |
重定向用的属性,如增加的查询字符串、flash属性 |
@ModelAttribute |
访问模型中属性(没有则实例化)(可以指定name 或binding ,后者可用于阻止绑定),涉及数据绑定和验证 |
Errors , BindingResult |
用于访问验证错误和数据绑定结果,必须紧接被验证的参数(@ModelAttribute 、@RequestBody 或@RequestPart |
SessionStatus |
可通过调用setComplete 方法清除类级@SessionAttributes (有属性names 和types )指定的参数 |
UriComponentsBuilder |
用于生成相当于当前请求的相对URL |
@SessionAttribute |
任意会话属性(可以指定name 或required ) |
@RequestAttribute |
请求属性(可以指定name 或required ) |
其它 | 把简单类型的视为@RequestParam 否则@ModelAttribute |
对于@RequestParam
、@RequestHeader
之类非必须的参数,可以用JDK 8的java.util.Optional
作为参数类型,相当于在注记属性required=false
。
返回值 | 用途 |
---|---|
@ResponseBody |
返回值通过HttpMessageConverters 转换后写到响应 |
HttpEntity<B> ,ResponseEntity<B> |
完整响应,包括HTTP头和体经HttpMessageConverters 转换后写到响应,可以包含缓存信息 |
HttpHeaders |
返回有头无体的响应 |
String |
视图名,模型来自@ModelAttribute 或Model |
View |
用于渲染的视图,模型来自@ModelAttribute 或Model |
java.util.Map , org.springframework.ui.Model |
加到模型中的属性,视图由RequestToViewNameTranslator 决定 |
@ModelAttribute |
加到模型中的属性,视图由RequestToViewNameTranslator 决定 |
ModelAndView |
视图和对象,还有可选的响应状态 |
void 或null |
当有ServletResponse 或OutputStream 或@ResponseStatus 参数,又或作出正面ETag 或lastModified 检查时视为已完成处理,否则对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
实例,把它作为参数传入。
类型转换可以在WebDataBinder
或FormattingConversionService
配置或注册。
视图
以下以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请求的方法,它使用同步(阻塞)APIWebClient
是提供函数式、流式的异步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
表示KeyGenerator
bean名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
表示KeyGenerator
bean名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
表示KeyGenerator
bean名
- 通过标注
@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
表示KeyGenerator
bean名
其中SpEL表达式中可访问变量有:
methodName
表示方法名method
表示方法target
表示目标对象targetClass
表示目标对象类型args
表示参数caches
表示缓存集合- 参数名对应参数值
result
表示返回值
测试
单元测试
对于POJO,单元测试可以如常进行。而对于涉及依赖注入的代码,可以使用模拟对象来测试以隔离问题和加快测试:
org.springframework.mock.env
包中的MockEnvironment
和MockPropertySource
可用于测试依赖环境属性的代码。org.springframework.mock.jndi
包中实现了JNDI SPI,可用于搭建简单的JNDI环境。org.springframework.mock.web
包包含Servlet API的各种模拟对象,可用于测试web上下文、控制器和过滤器。org.springframework.mock.http.server.reactive
包包含用于WebFlux程序的MockServerHttpResponse
和MockServerHttpResponse
模拟对象org.springframework.mock.web.server
包中的MockServerWebExchange
用于测试MockServerHttpRequest
和MockServerHttpResponse
另外有一些用于测试的工具:
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
注记的参数给出其它配置类。