SpringBoot编写RESTful API
网易云课堂《用Spring Boot编写RESTful API》学习笔记
RESTful API
- REST是Representational State Transfer的缩写
- 所有的东西都是资源,所有的操作都通过对资源的增删改查(CRUD)实现
- 对资源的增删改查对应于URL的操作(POST, DELETE, PUT, GET)
- 无状态
无状态:客户端到服务器的每个请求都必须包含理解请求所需的所有信息,并且不能利用服务器上存储的任何上下文。因此,会话状态完全保留在客户端上。
无状态意味着不能使用Session在服务器端存储登陆状态等信息。(可以使用JWT做无状态的身份认证)
RESTful API URI示例
以一个对电视剧列表的管理作为示例:
- GET /tvseries 获取电视剧列表
- POST /tvseries 创建一个新电视剧
- GET /tvseries/101 获取编号为101的电视剧信息
- PUT /tvseries/101 修改编号为101的电视剧信息
- DELETE /tvseries/101 删除编号为101的电视剧
- GET /tvseries/101/characters 获取编号为101的电视剧的人物列表
URI命名:
- /资源名称
- /资源名称/{资源ID}
- /资源名称/{资源ID}/子资源名称
- /资源名称/{资源ID}/子资源名称/{子资源ID}
示例程序
创建SpringBoot项目
选择依赖时,勾选Rest Repositories,其他依赖根据自己需要勾选




创建实体类
TvSeriesDto.java:
package com.example.rest;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;
public class TvSeriesDto {
private Integer id;
private String name;
private int seasonCount; // 总共多少季
private List tvCharacters; // 角色表
// 设置日期格式化为Json格式
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
private Date originRelease; // 发行日期
public TvSeriesDto(){
}
public TvSeriesDto(int id, String name, int seasonCount, Date originRelease) {
this.id = id;
this.name = name;
this.seasonCount = seasonCount;
this.originRelease = originRelease;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSeasonCount() {
return seasonCount;
}
public void setSeasonCount(int seasonCount) {
this.seasonCount = seasonCount;
}
public Date getOriginRelease() {
return originRelease;
}
public void setOriginRelease(Date originRelease) {
this.originRelease = originRelease;
}
public List getTvCharacters() {
return tvCharacters;
}
public void setTvCharacters(List tvCharacters) {
this.tvCharacters = tvCharacters;
}
@Override
public String toString() {
return "TvSeriesDto{" +
"id=" + id +
", name='" + name + '\'' +
", seasonCount=" + seasonCount +
", originRelease=" + originRelease +
'}';
}
}
这里配置了日期的Json格式化,除了注解方式,还可以在配置文件中配置。这里我们采用.yml配置,将application.properties修改为application.yml,然后配置日期的Json格式化:
spring:
jackson:
date-format: yyyy-MM-dd #如果使用字符串型表示,用这行设置格式
timezone: GMT+8
serialization:
write-dates-as-timestamps: true #使用数值时间戳表示日期
如果两种配置方式同时使用的话,注解优先。
TvCharacterDto.java:
public class TvCharacterDto {
private Integer id;
private int tvSeriesId;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public int getTvSeriesId() {
return tvSeriesId;
}
public void setTvSeriesId(int tvSeriesId) {
this.tvSeriesId = tvSeriesId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
创建Controller
TvSeriesController.java:
@RestController
@RequestMapping("/tvseries")
public class TvSeriesController {
@GetMapping
public List getAll() {
// 示例:http://localhost:8080/tvseries
List list = new ArrayList<>();
list.add(createWestWorld());
list.add(createPoi());
return list;
}
// 两个辅助方法,产生两个电视剧实例
private TvSeriesDto createPoi() {
Calendar c = Calendar.getInstance();
c.set(2011, Calendar.SEPTEMBER, 22, 0, 0);
return new TvSeriesDto(102, "Person of Interest", 5, c.getTime());
}
private TvSeriesDto createWestWorld() {
Calendar c = Calendar.getInstance();
c.set(2016, Calendar.OCTOBER, 2, 0, 0);
return new TvSeriesDto(101, "West World", 1, c.getTime());
}
}
然后启动SpringBoot,在浏览器地址输入:http://localhost:8080/tvseries,可以看到返回的Json数据:
[{"id":101,"name":"West World","seasonCount":1,"originRelease":"2016-10-02"},{"id":102,"name":"Person of Interest","seasonCount":5,"originRelease":"2011-09-22"}]
配置日志
在application.yml中加入:
logging:
file: target/app.log
level:
ROOT: WARN
com.example: TRACE
其中file是日志的保存位置,level用于配置日志的级别。
日志级别:TARCE < DEBUG < INFO < WARN < ERROR < FATAL
如果日志级别设置为INFO,那么级别低于INFO的(TRACE, DEBUG)都不会输出。
ROOT表示项目的所有日志级别。我们还可以配置指定包下的日志级别,比如com.example
在项目中使用日志
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@RestController
@RequestMapping("/tvseries")
public class TvSeriesController {
// 日志
private static final Log log = LogFactory.getLog(TvSeriesController.class);
@GetMapping
public List getAll() {
// 示例:http://localhost:8080/tvseries
if (log.isTraceEnabled()) {
log.trace("getAll()被调用了");
}
List list = new ArrayList<>();
list.add(createWestWorld());
list.add(createPoi());
return list;
}
}
SpringBoot热部署
首先在pom.xml中加入devtools:
org.springframework.boot
spring-boot-devtools
然后在Idea中打开自动编译

使用快捷键Command+Shift+A,查找Registry,找到并勾选compiler.automake.allow.when.app.running

利用CURL命令或者PostMan工具进行调试
CURL:http://blog.ishavanti.top/2019/04/23/%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7CURL/
PostMan:https://www.getpostman.com/
实现tvseries的API
根据id获取某个电视剧的信息
GET /tvseries/{id}
@GetMapping("/{id}")
public TvSeriesDto getOne(@PathVariable int id) {
// 示例:http://localhost:8080/tvseries/101
if (log.isTraceEnabled()) {
log.trace("getOne " + id);
}
// TODO: 查找数据库
if (id == 101) {
return createWestWorld();
} else if (id == 102) {
return createPoi();
} else {
throw new ResourceNotFoundException();
}
}
新增一个电视剧
POST /tvseries
@PostMapping
public TvSeriesDto insertOne(@RequestBody TvSeriesDto tvSeriesDto) {
// Post请求
if (log.isTraceEnabled()) {
log.trace("这里应该写新增到数据库的代码,传递进来的参数是:" + tvSeriesDto);
}
tvSeriesDto.setId(9999);
return tvSeriesDto;
}
根据id修改一个电视剧的信息
PUT /tvseries/{id}
@PutMapping("/{id}")
public TvSeriesDto updateOne(@PathVariable int id, @RequestBody TvSeriesDto tvSeriesDto) {
if (log.isTraceEnabled()) {
log.trace("updateOne " + tvSeriesDto);
}
if (id == 101 || id == 102) {
//TODO: 根据tvSeriesDto的内容更新数据库,更新后返回新内容
return createPoi();
} else {
throw new ResourceNotFoundException();
}
}
根据id删除一个电视剧
DELETE /tvseries/{id}
@DeleteMapping("/{id}")
public Map deleteOne(@PathVariable int id, HttpServletRequest request, @RequestParam(value = "delete_reason", required = false)String deleteReason) throws Exception {
if (log.isTraceEnabled()) {
log.trace("deleteOne " + id);
}
Map result = new HashMap<>();
if (id == 101) {
//TODO: 执行删除代码
result.put("message", "#101被" + request.getRemoteAddr() + "删除(原因:" + deleteReason + ")");
} else if (id == 102) {
throw new RuntimeException("#102不能被删除");
} else {
throw new ResourceNotFoundException();
}
return result;
}
文件上传示例
POST /tvseries/{id}/photos
import org.apache.commons.io.IOUtils;
...
@PostMapping(value = "/{id}/photos", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void addPhoto(@PathVariable int id, @RequestParam("photo")MultipartFile imgFile) throws IOException {
if (log.isTraceEnabled()) {
log.trace("接收到文件 " + id + ", 收到文件: " + imgFile.getOriginalFilename());
}
// 保存文件
FileOutputStream fos = new FileOutputStream("target/" + imgFile.getOriginalFilename());
IOUtils.copy(imgFile.getInputStream(), fos);
fos.close();
}
注意consumes = MediaType.MULTIPART_FORM_DATA_VALUE
IOUtils需要引入commons-io依赖:
pom.xml
commons-io
commons-io
2.6
文件下载示例
GET /tvseries/{id}/icon
@GetMapping(value = "/{id}/icon", produces = MediaType.IMAGE_JPEG_VALUE)
public byte[] getIcon(@PathVariable int id) throws Exception {
if (log.isTraceEnabled()) {
log.trace("getIcon(" + id + ")");
}
String iconFile = "target/test.jpg";
InputStream is = new FileInputStream(iconFile);
return IOUtils.toByteArray(is);
}
注意:produces = MediaType.IMAGE_JPEG_VALUE
返回byte数组。
在RestController中获取各种相关信息的方法


对客户端传入的数据进行校验
JSR303
JSR是java的一个规范,JSR303是对JavaBean进行验证的规范(Bean Validation)
JSR303只是一个规范,没有具体的实现
Hibernate Validator是对JSR303的一个实现
Bean Validation注解
常用注解:
- @Null 验证对象是否为空
- @NotNull 验证对象是否为非空
- @Min 验证Number和String对象是否大于等于指定的值
- @Max 验证Number和String对象是否小于等于指定的值
- @Size 验证对象(Array, Collection, Map, String)长度是否在给定的范围之内
- @Past 验证Date和Calendar对象是否在当前时间之前
- @Future 验证Date和Calendar对象是否在当前时间之后
- @AssertTrue 验证Boolean对象是否为True
- @AssertFalse 验证Boolean对象是否为False
- @Valid 级联验证注解
代码示例
TvSeriesDto.java:
public class TvSeriesDto {
@Null private Integer id; // 表示客户端可以不传id,id必须为空
@NotNull private String name; // 表示客户端必须传名称
@DecimalMin("1") private int seasonCount; // 总共多少季
@Valid @NotNull @Size(min = 2) // @Valid表示要级联校验;@Size(min = 2)表示这个列表至少要有2项内容,否则不通过校验
private List tvCharacters;
// 设置日期格式化为Json格式
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
// @Past表示只接受过去的时间,比当前时间晚的被认为不合格
@Past private Date originRelease; // 发行日期
}
TvCharacterDto.java:
public class TvCharacterDto {
private Integer id;
private int tvSeriesId;
@NotNull private String name;
}
最重要的是在Controller函数中,要对需要验证的参数使用@Valid注解开启验证:
/**
* @Valid 注解表示需要验证传入的参数TvSeriesDto,需要验证的field在TvSeriesDto内通过注解定义
* @param tvSeriesDto
* @return
*/
@PostMapping
public TvSeriesDto insertOne(@Valid @RequestBody TvSeriesDto tvSeriesDto) {
// Post请求
if (log.isTraceEnabled()) {
log.trace("这里应该写新增到数据库的代码,传递进来的参数是:" + tvSeriesDto);
}
tvSeriesDto.setId(9999);
return tvSeriesDto;
}
用PostMan测试

出现两个错误:
- Field error in object ‘tvSeriesDto’ on field ‘id’: rejected value [0];…;default message [必须为null]
原因是TvSeriesDto类中,我把id设置为int类型,又加上了@Null注解,而int类型默认值为0,不会为空,所以无法通过验证。应该把int类型改为Integer类型:
@Null private Integer id; // id必须为空
- Field error in object ‘tvSeriesDto’ on field ‘tvCharacters’: rejected value [null];…;default message [不能为null]
原因是TvSeriesDto类中的字段tvCharacters忘了写getter和setter方法 - -!!!
前面的代码已纠正
进阶

注解的位置:
- Field level 成员变量
- Property level get(is) 方法上
- Class level 类上
示例:
// images不可为null;每个元素(Image类型)需要级联验证
@NotNull
private List<@Valid Image> images;
// images长度至少为1,不可为null;每个元素(String类型)不可为null,最短长度为10
@NotNull
@Size(min = 1)
private List<@Size(min = 10) @NotNull String> images;
约束规则对子类有效
复杂验证情况
groups参数:
- 每个约束用注解都有一个groups参数
- 可接收多个class类型(必须是接口)
- 不声明groups参数是默认组javax.validation.groups.Default
- 声明了groups参数的会从Default组移除,如需加入Default组需要显示声明,例如
@Null(groups={Default.class, Step1.class})


手动验证
在Controller层,使用了@Validated注解,Spring会自动完成校验工作。但是有时在其他地方也需要进行校验,比如在Service层,就需要手动进行验证。

Bean Validation 2.0 中的约束用注解


后端程序的逻辑层次

数据对象



整合Mybatis
基本步骤
- 修改pom.xml,添加Mybatis支持
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.1
mysql
mysql-connector-java
- 修改application.yml,添加数据库连接
spring:
datasource:
dbcp2:
validation-query: select 1
# driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/rest
username: root
password: ******
mybatis:
configuration:
map-underscore-to-camel-case: true
map-underscore-to-camel-case: true是为了实现驼峰命名的转换
driver-class-name: com.mysql.jdbc.Driver不需要手动导入:The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
- 修改启动类,增加@MapperScan(“package-of-mapping”)注解
@SpringBootApplication
@MapperScan("com.example.rest.mybatis.dao")
public class RestApplication {
public static void main(String[] args) {
SpringApplication.run(RestApplication.class, args);
}
}
- 添加Mybatis Mapping接口
- 添加Mapping对应的XML(可选)
准备数据库
创建数据库:
create database if not exists rest default charset utf8mb4 collate utf8mb4_unicode_ci;
创建数据表:
create table tv_series(
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
season_count INT NOT NULL,
origin_release DATE,
delete_reason VARCHAR(100),
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
插入一条数据:
insert into tv_series(name, season_count, origin_release) values ('西部世界', 1, '2016/10/02');
创建层次结构
还是以电视剧列表管理作为例子。首先新建一个pojo目录,直接将之前的TvSeriesDto.java和TvCharacter.java拷贝过来重命名后作为我们的POJO。
Dao层
新建dao目录:
TvSeriesDao.java
@Repository
public interface TvSeriesDao {
@Select("select * from tv_series")
List getAll();
}
这里的查询语句是直接写在注解里的,如果查询比较复杂的话可以写在XML文件中
Service层
新建service目录:
TvSeriesService.java
@Service
public class TvSeriesService {
@Autowired
TvSeriesDao tvSeriesDao;
public List getAllTvSeries() {
return tvSeriesDao.getAll();
}
}
Controller层
新建controller目录,把之前的TvSeriesController拷贝过来(之前的controller需要把注解删掉或者注释掉,否则两个controller会冲突),修改数据操作逻辑:
@RestController
@RequestMapping("/tvseries")
public class TvSeriesController {
// 日志
private static final Log log = LogFactory.getLog(TvSeriesController.class);
@Autowired
TvSeriesService tvSeriesService;
@GetMapping
public List getAll() {
// 示例:http://localhost:8080/tvseries
if (log.isTraceEnabled()) {
log.trace("getAll()被调用了");
}
List list = tvSeriesService.getAllTvSeries();
if (log.isTraceEnabled()) {
log.trace("查询到" + list.size() + "条记录");
}
return list;
}
}
其他
MyBatis可以用TypeHandler处理枚举、数组和JSON类型
单元测试
JUnit

mockito
pom.xml
org.mockito
mockito-core
test
mockito是Java中使用最广泛的mock框架,mock就是创建一个虚假的类对象,在测试环境中替换掉真实对象。使用mockito可以避免测试时与数据库的依赖。
测试业务逻辑层
TvSeriesServiceTests.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class TvSeriesServiceTests {
@MockBean
TvSeriesDao tvSeriesDao;
@Autowired
TvSeriesService tvSeriesService;
@Test
public void testGetAllWithMockito() {
List list = tvSeriesService.getAllTvSeries();
// 这里的测试结果依赖连接数据库内的记录,很难写一个判断是否成功的条件,甚至无法执行
// 下面的testGetAll()方法,使用了mock出来的dao作为桩模块,避免了这一情形
Assert.assertTrue(list.size() >= 0);
}
@Test
public void testGetAll() {
// 设置一个TvSeries list
List list = new ArrayList<>();
TvSeries ts = new TvSeries();
ts.setName("POI");
list.add(ts);
// 下面这个语句是告诉mock出来的tvSeriesDao当执行getAll方法时,返回上面创建的list
Mockito.when(tvSeriesDao.getAll()).thenReturn(list);
// 测试tvSeriesService的getAllTvSeries()方法,获得返回值
List result = tvSeriesService.getAllTvSeries();
// 判断返回值是否和最初做的那个list相同,应该是相同的
Assert.assertTrue(result.size() == list.size());
Assert.assertTrue("POI".equals(result.get(0).getName()));
}
@Test
public void testGetOne() {
// 根据不同的传入参数,被mock的bean返回不同的数据的例子
String newName = "Person of Interest";
BitSet mockExecuted = new BitSet();
Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
Object[] args = invocationOnMock.getArguments();
TvSeries bean = (TvSeries)args[0];
// 传入的值,应该和下面调用tvSeriesService.updateTvSeries()方法时的参数中的值相同
Assert.assertEquals(newName, bean.getName());
mockExecuted.set(0);
return 1;
}
});
TvSeries ts = new TvSeries();
ts.setName(newName);
ts.setId(111);
tvSeriesService.updateTvSeries(ts);
Assert.assertTrue(mockExecuted.get(0));
}
}
测试web控制层
AppTests.java
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class AppTests {
/**
* @MockBean 可给当前的spring context装载一个假的bean上去替代原有的同名bean
* mock了dao的所有bean后,数据访问层就别接管了,从而实现测试不受具体数据库内数据值影响的效果
*/
@MockBean
TvSeriesDao tvSeriesDao;
@MockBean
TvCharacterDao tvCharacterDao;
@Autowired
MockMvc mockMvc;
@Autowired
private TvSeriesController tvSeriesController;
@Test
public void testGetAll() throws Exception {
List list = new ArrayList<>();
TvSeries ts = new TvSeries();
ts.setName("POI");
list.add(ts);
Mockito.when(tvSeriesDao.getAll()).thenReturn(list);
// 下面这个是相当于在启动项目后,执行 GET /tvseries , 被测模块是web控制层,因为web控制层会调用业务逻辑层,所以业务逻辑层也会被测试
// 业务逻辑层调用了被mock出来的数据访问层桩模块
// 如果想仅仅测试web控制层,(例如业务逻辑层尚未编码完毕),可以mock一个业务逻辑层的桩模块
mockMvc.perform(MockMvcRequestBuilders.get("/tvseries")).andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk()).andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("POI")));
// 上面这几句和字面意思一致,期望状态是200,返回值包含POI三个字母,桩模块返回的一个电视剧名字是POI,如果测试正确是包含这三个字母的。
}
}
SpringBoot事务Transactional


propagation参数




isolation参数
事务隔离

几种数据异常



几种隔离级别的比较

锁与隔离的区别

示例

timeout参数

默认值-1表示永远不会超时
Spring Security
依赖:
org.springframework.boot
spring-boot-starter-security
配置

- 关闭Session
- 所有请求都要通过授权验证
- 访问以“/manager”开始的url都必须具备admin这个角色
- 其他的url都可以直接访问
注解

- prePostEnabled: 控制蓝色区域的注解是否可用
- securedEnabled: 控制黄色区域的注解是否可用
- jsr250Enabled: 控制绿色区域的注解是否可用
Pre Post:
- @PreAuthorize: 在方法执行前,验证PreAuthorize里面的Spring表达式,True则继续执行,False则返回403
- @PostAuthorize: 在方法执行后,验证方法执行的结果是否满足表达式,True则继续,False返回403
- @PreFilter: 在方法执行前对传入参数进行过滤
- @PostFilter: 在方法执行后对返回结果进行过滤
常用表达式

- hasRole: 同时具有参数中设置的角色
- hasAnyRole: 具有参数中任意一个角色即可
- hasAuthority: 同时具有参数中的权限
- hasAnyAuthority: 具有任意一个权限
- permitAll: 所有的都允许
- denyAll: 所有都不允许
角色和权限的区别

Controller中获取当前用户

在其他地方,比如Service层,可以用上图最后一条语句获取当前用户
示例
加入Spring Security的依赖后,访问http://localhost:8080/tvseries就会自动跳转到http://localhost:8080/login

然后在控制台我们可以看到Spring Security自动生成了密码:

我们输入用户名user,和控制台生成的密码就可以通过验证,查看到返回结果
需要注意的是,需要将之前的日志配置中,ROOT的日志级别设置为INFO,才能看到生成的密码
如果要实现更复杂的验证,就需要对Spring Security进行配置
配置
WebSecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = false)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final static Log log = LogFactory.getLog(WebSecurityConfig.class);
@Override
protected void configure(HttpSecurity http) throws Exception {
if (log.isTraceEnabled()) {
log.trace("configure httpSecurity ...");
}
// 默认的spring-security配置会让所有的请求都必须在已登陆的情况下访问;下面这段代码禁止了这种操作
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().anyRequest().permitAll();
}
}
Token管理
TokenService.java
对Token的生成,查询和存储
@Service
public class TokenService {
// 一般是把token和用户对应关系放在数据库或高速缓存(例如readis/memcache等),放在一个单例类的成员变量里仅适合很小规模的情形
private Map tokenMap = new HashMap<>();
/**
* 登陆,成功返回token
* @param username
* @param password
* @return
*/
public String login(String username, String password) {
UserDetails ud = null;
// 此例中支持三个用户: author/reader/admin 三个用户的密码都是password; author具有author角色;reader具有reader角色;admin则2个角色都有
if ("author".equals(username) && "password".equals(password)) {
ud = createUser(username, password, new String[] {"author"});
} else if ("reader".equals(username) && "password".equals(password)) {
ud = createUser(username, password, new String[] {"reader"});
} else if ("admin".equals(username) && "password".equals(password)){
ud = createUser(username, password, new String[]{"author", "reader"});
}
if (ud != null) {
String token = UUID.randomUUID().toString();
tokenMap.put(token, ud);
return token;
} else {
return null;
}
}
/**
* 退出,移除token
* @param token
*/
public void Logout(String token) {
tokenMap.remove(token);
}
/**
* 这个方法在每次访问都会被调用;为了提示效率应该使用@Cacheable注解缓存
* @param token
* @return
*/
public UserDetails getUserFromToken(String token) {
if (token == null) {
return null;
}
return tokenMap.get(token);
}
private UserDetails createUser(String username, String password, String[] roles) {
return new UserDetails() {
@Override
public Collection extends GrantedAuthority> getAuthorities() {
Collection authorities = new ArrayList<>();
//这是增加了一种名为query的权限,可以使用 @hasAuthority("query") 来判断
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("query");
authorities.add(authority);
//这是增加xxx角色,可以用hasRole("xxx")来判断;需要注意所有的角色在这里增加时必须以ROLE_前缀,使用时则没有ROLES_前缀
for (String role : roles) {
SimpleGrantedAuthority sga = new SimpleGrantedAuthority("ROLES_" + role);
authorities.add(sga);
}
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
};
}
}
TokenController.java
@RestController
@RequestMapping("/token")
public class TokenController {
private final static Log log = LogFactory.getLog(TokenController.class);
@Autowired
TokenService tokenService;
@PostMapping
public ResponseEntity
AuthenticationTokenFilter.java
对请求过滤时,将Token对应的用户信息取出放置到Spring上下文中
/**
* 从request中获取token,并把token转换成用户,放置到当前的spring context内
* 这个类必须写一个@Service注解,否则Spring不会把它加载作为filter
*/
@Service
public class AuthenticationTokenFilter extends OncePerRequestFilter {
private final static Log log = LogFactory.getLog(AuthenticationTokenFilter.class);
@Autowired
TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String authToken = null;
// 下面的代码从Http Header的Authorization中获取token,也可以从其他header, cookie等中获取,看客户端怎么传递token
String requestHeader = httpServletRequest.getHeader("Authorization");
if (requestHeader != null && requestHeader.startsWith("bearer")) {
authToken = requestHeader.substring(7);
}
if (log.isTraceEnabled()) {
log.trace("token is " + authToken);
}
if (authToken != null) {
UserDetails user = null;
user = tokenService.getUserFromToken(authToken);
if (user != null) {
// 把user设置到SecurityContextHolder内,以便Spring使用
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
示例
SampleService.java
@Service
public class SampleService {
private final static Log log = LogFactory.getLog(SampleService.class);
/**
* PreAuthorize/PostAuthorize注解也可以写在Service的public方法上
*/
@PreAuthorize("#id % 2 == 0")
// @PostAuthorize("returnObject.get('id') <= 10")
public Map getOneById(int id) {
if (log.isTraceEnabled()) {
log.trace("getOneById: " + id);
}
HashMap map = new HashMap<>();
map.put("id", id);
return map;
}
}
SampleController.java
@RestController
@RequestMapping("/")
public class SampleController {
private final static Log log = LogFactory.getLog(SampleController.class);
@Autowired
SampleService sampleService;
/**
* 使用注解@PreAuthorize来保护方法只能被特定用户组的用户访问
* 测试时:
* curl http://localhost:8080/ 会返回403禁止访问
* curl -H "Authorization: Bearer xxxx" http://localhost:8080/ 则会正常返回内容,得到一个长度为2的JSON数组(xxxx是token)
* @return
*/
@GetMapping
@PreAuthorize("hasRole('author')")
public List
OAuth2.0


OAuth2.0有四种授权模式,其中授权码模式、密码模式比较常用,简化模式、客户端模式较少使用。
Refresh Token不是OAuth的授权模式,但是在SpringBoot中可以设置,用来在Token到期前刷新Token,得到有效期更长的Token。
授权码模式


服务器和浏览器之间通过token进行交互,但是服务器并不知道token对应的是谁,如果需要知道对应的用户信息,则需要向OAuth服务器进行查询。
将token与用户关系存入缓存可以缓解这种问题,但是OAuth端可以随时让token失效,缓存中无法即时更新token。
另外的解决办法就是跳过向OAuth服务器查询的步骤,让token自己证明自己。JWT就是这样一种token。
JWT

注意Playload部分没有加密,不要存放敏感信息。
依赖:
com.auth0
java-jwt
3.3.0
Spring异步 @Async

@EnableAsync注解只能加在有@SpringBootApplication或@Configuration注解的类上,其他类上无作用。
@EnableAsync启用异步后,在方法中加上@Async注解后就可以异步执行了。
只有从外部调用才能异步执行:

- 1是异步执行的
- 2不是异步执行的,因为
doC方法没有@Async注解 - 3,4不是异步执行的,因为不是从外部调用
@Async注解的方法的返回值:
- void
- Future\
- 其他类型一律返回null;遇到int/double/float/boolean基本类型,执行时会抛出异常:AopInvocationException
Spring Scheduling 定时任务
- @EnableScheduling 注解启用Scheduling(@SpringBootApplication类或者@Configuration类上)
- 方法上加@Scheduled注解,方法会按照参数定期执行
参数


, : 用于设置多个非连续值,比如在小时处使用 6, 8, 12 就表示6点,8点,12点三个时间点
- : 用于设置连续的区间,比如在小时处使用 6-12 就表示6点到12点期间
* : 表示任意
? : 只能写在日和星期几,当日期和星期几冲突时,表示任意
/ : 用来指定数值的增量,比如在子表达式(分钟)里的“0/15”表示从第0分钟开始,每15分钟
L : 表示Last,最后一个;在日期那里表示一个月的最后一天;在星期那里表示一周的最后一天;“6L”表示这个月的倒数第6天
W : workday;表示为最近工作日,如“15W”放在每月(day-of-month)字段上表示为“到本月15日最近的工作日”
# : 是用来指定每月第n个星期几,例如在每周(day-of-week)这个字段中内容为”6#3” or “FRI#3” 则表示“每月第三个星期五”
示例:

默认只有一个线程执行@Scheduled任务
配置多个线程执行:

缓存
Spring Cache
@EnableCaching 启用缓存(@SpringBootApplication类或@Configuration类)
@Cacheable:声明方法可缓存。查询缓存中是否有值,有则直接取用;没有则执行相应的操作,并且会将结果保存到缓存中
- @CacheEvict:删除。额外的参数allEntries表示是否需要清除缓存中的所有元素,默认为false,表示不需要;当指定了allEntries为true时,Spring Cache将忽略指定的key。
- @CachePut:更新
- @CacheConfig:有时候一个类中可能会有多个缓存操作,而这些缓存操作可能是重复的。这个时候可以使用@CacheConfig。它是类级别的注解,允许共享缓存的名称、KeyGenerator、CacheManager 和CacheResolver。该操作会被覆盖。



key

默认即“cacheName::key”
@CacheEvict

示例

设置过期

Redis

@Autowired加载规则



定制Spring Rest的错误返回信息

