Spring Boot

Spring Boot

0526

第一个项目

  • 选择spring intelizer 之后
  • 选择构建方式要选择Gradle 默认是Maven
  • 最后要选择本地的gradle 不然会一直去远程下载网速慢的话就卡死了

项目构建

1
2
3
4
5
6
7
8
9
10
11
12
13
buildscript {
repositories { jcenter() }
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.3.0.BUILD-SNAPSHOT")
}
}
apply plugin: 'java'
apply plugin: 'spring-boot'
repositories { jcenter() }
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
testCompile("org.springframework.boot:spring-boot-starter-test")
}

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
com
+- example
+- myproject
+- Application.java
|
+- domain
| +- Customer.java
| +- CustomerRepository.java
|
+- service
| +- CustomerService.java
|
+- web
+- CustomerController.java

Spring Boot属性配置文件详解

自定义属性与加载

  • 我们在使用Spring Boot的时候,通常也需要定义一些自己使用的属性,我们可以如下方式直接定义:
1
2
com.OKjava.blog.name=benny
com.OKjava.blog.title=Spring Boot学习笔记
  • 然后通过@Value("${属性名}")注解来加载对应的配置属性,具体如下:
1
2
3
4
5
6
7
8
9
@Component
public class BlogProperties {
@Value("${com.didispace.blog.name}")
private String name;
@Value("${com.didispace.blog.title}")
private String title;
// 省略getter和setter
}
  • 通过单元测试来验证BlogProperties中的属性是否已经根据配置文件加载了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
public class ApplicationTests {
@Autowired
private BlogProperties blogProperties;
@Test
public void getHello() throws Exception {
Assert.assertEquals(blogProperties.getName(), "程序猿DD");
Assert.assertEquals(blogProperties.getTitle(), "Spring Boot教程");
}
}

参数间的引用

  • application.properties中的各个参数之间也可以直接引用来使用
1
2
3
com.OKjava.blog.name=benny
com.OKjava.blog.title=Spring Boot学习笔记
com.OKjava.blog.desc=${com.OKjava.blog.name}正在努力写《${com.OKjava.blog.title}》

使用随机数

  • 在一些情况下,有些参数我们需要希望它不是一个固定的值,比如密钥、服务端口等。Spring Boot的属性配置文件中可以通过${random}来产生int值、long值或者string字符串,来支持属性的随机值。
1
2
3
4
5
6
7
8
9
10
# 随机字符串
com.OKjava.blog.value=${random.value}
# 随机int
com.OKjava.blog.number=${random.int}
# 随机long
com.OKjava.blog.bignumber=${random.long}
# 10以内的随机数
com.OKjava.blog.test1=${random.int(10)}
# 10-20的随机数
com.OKjava.blog.test2=${random.int[10,20]}

通过命令行设置属性值

  • 相信使用过一段时间Spring Boot的用户,一定知道这条命令:java -jar xxx.jar --server.port=8888,通过使用--server.port属性来设置xxx.jar应用的端口为8888。

  • 在命令行运行时,连续的两个减号--就是对 application.properties 中的属性值进行赋值的标识。所以,java -jar xxx.jar --server.port=8888命令,等价于我们在 application.properties 中添加属性 server.port=8888 ,该设置在样例工程中可见,读者可通过删除该值或使用命令行来设置该值来验证。

  • 通过命令行来修改属性值固然提供了不错的便利性,但是通过命令行就能更改应用运行的参数,那岂不是很不安全?是的,所以 Spring Boot 提供了屏蔽命令行访问属性的设置

  • Spring Boot 屏蔽命令行访问属性的设置
1
SpringApplication.setAddCommandLineProperties(false)。

多环境配置

我们在开发Spring Boot应用时,通常同一套程序会被应用和安装到几个不同的环境,比如:开发、测试、生产等。其中每个环境的数据库地址、服务器端口等等配置都会不同,如果在为不同环境打包时都要频繁修改配置文件的话,那必将是个非常繁琐且容易发生错误的事。

在Spring Boot中多环境配置文件名需要满足 application - {profile}.properties 的格式,其中{profile}对应你的环境标识,比如:

1
2
3
application-dev.properties:开发环境
application-test.properties:测试环境
application-prod.properties:生产环境
  • 至于哪个具体的配置文件会被加载,需要在application.properties文件中通过spring.profiles.active属性来设置,其值对应{profile}值。

  • 如:spring.profiles.active=test就会加载application-test.properties配置文件内容

  • 针对各环境新建不同的配置文件 application-dev.properties application-test.properties application-prod.properties

  • 在这三个文件均都设置不同的server.port属性,如:dev环境设置为1111,test环境设置为2222,prod环境设置为3333

  • application.properties 中设置spring.profiles.active=dev,就是说默认以dev环境设置

  • 测试不同配置的加载

  • 执行java -jar xxx.jar,可以观察到服务端口被设置为1111,也就是默认的开发环境(dev)

  • 执行java -jar xxx.jar –spring.profiles.active=test,可以观察到服务端口被设置为2222,也就是测试环境的配置(test)

  • 执行java -jar xxx.jar –spring.profiles.active=prod,可以观察到服务端口被设置为3333,也就是生产环境的配置(prod)

按照上面的实验,可以如下总结多环境的配置思路:

application.properties 中配置通用内容,并设spring.profiles.active=dev,以开发环境为默认配置
application-{profile}.properties 中配置各个环境不同的内容通过命令行方式去激活不同环境的配置

Spring boot 应用的入口

  • Application.java
    • Application.java文件将声明main 方法,还有基本的 @Configuration
1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

构建web项目应用模板

提别注意:

  • @RestController@Controller 的区别

    • 页面渲染时,使用 @Controller
    • Json数据交互时,使用 @RestController
  • Thymeleaf [taɪm'liːf] 模板配置:applicattion.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#是否开启模板缓存,默认是开启,开发时关闭
spring.thymeleaf.cache=false
# Check that the templates location exists.
spring.thymeleaf.check-template-location=true
#模板类型
spring.thymeleaf.content-type=text/html
# Enable MVC Thymeleaf view resolution.
spring.thymeleaf.enabled=true
#模板的编码设置,默认为UTF—8
spring.thymeleaf.encoding=UTF-8
#模板模式设置默认为HTML5
spring.thymeleaf.mode=HTML5
# Prefix that gets prepended to view names when building a URL.
spring.thymeleaf.prefix=classpath:/templates/
#模板的后缀设置
spring.thymeleaf.suffix=.html

spring boot 配置Tomcat

在application.java中配置, 配置文件配置tomcat

1
2
3
4
5
6
server.port= #配置程序端口,默认为8080
server.session-timeout= #用户会话session过期时间,以秒为单位
server.context-path= #配置访问路径
server.tomcat.uri-encoding= # 配置Tomcat编码,默认为UTF-8
server.tomcat.compression= # Tomcat是否开启压缩,默认为关闭

代码配置tomcat

  • 注册一个实现EmbeddedServletContainerCustomizer、接口的Bean
  • 若先要配置Tomcat则可以直接定义TomcatEmbeddedServletContainerFactory

spring boot 整合 thymeleaf 热部署

  1. spring.thymeleaf.cache=false
  2. 修改完代码后,Ctrl + F9,重新make一下

使用Swagger2构建强大的RESTful API文档

  • Swagger2依赖
1
2
compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.5.0'
compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.5.0'
  • Swagger2配置类
    • Application.java同级目录下创建Swagger2的配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.ttc.myproject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author by benny on 2016/8/13.
* @version 1.0
* @description
*/
@Configuration
@EnableSwagger2
public class swagger2 {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//指定扫描那个包下的注解
.apis(RequestHandlerSelectors.basePackage("com.ttc.web"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Spring Boot中使用Swagger2构建RESTful APIs")
.description("更多Spring Boot相关文章请关注:http://www.OKjava.com/")
.termsOfServiceUrl("http://www.OKjava.com/").contact(new Contact("benny","http://www.OKjava.com/",""))
.version("1.0")
.build();
}
}
  • 通过@Configuration注解,让Spring来加载该类配置。
  • 再通过@EnableSwagger2注解来启用Swagger2
  • 再通过createRestApi函数创建DocketBean之后,
  • apiInfo()用来创建该Api的基本信息(这些基本信息会展现在文档页面中)。
  • select()函数返回一个ApiSelectorBuilder实例用来控制哪些接口暴露给Swagger来展现
  • 本例采用指定扫描的包路径来定义,Swagger会扫描该包下所有Controller定义的API,并产生文档内容(除了被@ApiIgnore指定的请求)。

添加文档内容

在完成了上述配置后,其实已经可以生产文档内容,但是这样的文档主要针对请求本身,而描述主要来源于函数等命名产生,对用户并不友好,我们通常需要自己增加一些说明来丰富文档内容。

  • 我们通过@ApiOperation注解来给API增加说明
  • 通过@ApiImplicitParams、@ApiImplicitParam注解来给参数增加说明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequestMapping(value="/users") // 通过这里配置使下面的映射都在/users下,可去除
public class UserController {
static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>());
@ApiOperation(value="获取用户列表", notes="")
@RequestMapping(value={""}, method=RequestMethod.GET)
public List<User> getUserList() {
List<User> r = new ArrayList<User>(users.values());
return r;
}
@ApiOperation(value="创建用户", notes="根据User对象创建用户")
@ApiImplicitParam(name = "user", value = "用户详细实体user", required = true,paramType = "path" , dataType = "User")
@RequestMapping(value="", method=RequestMethod.POST)
public String postUser(@RequestBody User user) {
users.put(user.getId(), user);
return "success";
}
}

### 常见swagger注解一览与使用

APIs
@Api
@ApiClass
@ApiError
@ApiErrors
@ApiOperation
@ApiParam
@ApiParamImplicit
@ApiParamsImplicit
@ApiProperty
@ApiResponse
@ApiResponses
@ApiModel
注解 说明
@Api 用在类上,说明该类的作用
@ApiOperation 用在方法上,说明方法的作用
@ApiImplicitParams 用在方法上包含一组参数说明
@ApiImplicitParam 用在@ApiImplicitParams注解中,指定一个请求参数的各个方面
paramType 参数放在哪个地方
header 请求参数的获取:@RequestHeader
query 请求参数的获取:@RequestParam
path(用于restful接口)–> 请求参数的获取:@PathVariable
body(不常用)
form(不常用)
name 参数名
dataType 参数类型
required 参数是否必须传
value 参数的意思
defaultValue 参数的默认值
@ApiResponses 用于表示一组响应
@ApiResponse 用在@ApiResponses中,一般用于表达一个错误的响应信息
code 数字,例如400
message 信息,例如”请求参数没填好”
response 抛出异常的类
@ApiModel 描述一个Model的信息(这种一般用在post创建的时候,使用@RequestBody这样的场景,请求参数无法使用@ApiImplicitParam注解进行描述的时候)
@ApiModelProperty 描述一个model的属性

访问http://localhost:8080/swagger-ui.html即可看到

Spring Boot中Web应用的统一异常处理

创建全局异常处理类:

  • 通过使用@ControllerAdvice定义统一的异常处理类,而不是在每个Controller中逐个定义。
  • @ExceptionHandler用来定义函数针对的异常类型
  • 最后将Exception对象和请求URL映射到error.html中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ControllerAdvice
class GlobalExceptionHandler {
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}

URL 和 URI的区别

  • req.getRequestURL()
    • http://localhost:8080/err
  • req.getRequestURI()
    • /err

实现error.html页面展示:在templates目录下创建error.html,将请求的URL和Exception对象的message输出。

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title>统一异常处理</title>
</head>
<body>
<h1>Error Handler</h1>
<div th:text="${url}"></div>
<div th:text="${exception.message}"></div>
</body>
</html>

通过实现上述内容之后,我们只需要在Controller中抛出Exception,当然我们可能会有多种不同的Exception。然后在@ControllerAdvice类中,根据抛出的具体Exception类型匹配@ExceptionHandler中配置的异常类型来匹配错误映射和处理。

返回JSON格式

  • 本质上,只需在@ExceptionHandler之后加入@ResponseBody,就能让处理函数return的内容转换为JSON格式。

  • 创建统一的JSON返回对象

    • code:消息类型
    • message:消息内容
    • url:请求的url
    • data:请求返回的数据
1
2
3
4
5
6
7
8
9
10
11
public class ErrorInfo<T> {
public static final Integer OK = 0;
public static final Integer ERROR = 100;
private Integer code;
private String message;
private String url;
private T data;
// 省略getter和setter
}
  • 创建一个自定义异常,用来实验捕获该异常,并返回json
1
2
3
4
5
6
7
public class MyException extends Exception {
public MyException(String message) {
super(message);
}
}
  • Controller中增加json映射,抛出MyException异常
1
2
3
4
5
6
7
8
@Controller
public class HelloController {
@RequestMapping("/json")
public String json() throws MyException {
throw new MyException("发生错误2");
}
}
  • 为MyException异常创建对应的处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = MyException.class)
@ResponseBody
public ErrorInfo<String> jsonErrorHandler(HttpServletRequest req, MyException e) throws Exception {
ErrorInfo<String> r = new ErrorInfo<>();
r.setMessage(e.getMessage());
r.setCode(ErrorInfo.ERROR);
r.setData("Some Data");
r.setUrl(req.getRequestURL().toString());
return r;
}
}

注意:

  • 想要统一处理异常必须在Controller层出错后,手动抛出异常
  • 由于 @ExceptionHandler(value = Exception.class)
  • 必须抛出异常后才可以进入配置的异常处理类。

Spring Boot中使用JdbcTemplate访问数据库

数据源配置

  • 连接数据库需要引入jdbc支持
  • 添加依赖spring-boot-starter-jdbc
1
compile group: 'org.springframework.boot', name: 'spring-boot-starter-jdbc', version: '1.4.0.RELEASE'

嵌入式数据库支持

  • 嵌入式数据库通常用于开发和测试环境,不推荐用于生产环境。
  • Spring Boot提供自动配置,的嵌入式数据库有H2、HSQL、Derby,你不需要提供任何连接配置就能使用。
  • 添加依赖置使用HSQL
1
compile group: 'org.hsqldb', name: 'hsqldb', version: '2.3.4'

连接生产数据源

  • 以MySQL数据库为例,先引入MySQL连接的依赖包
1
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.38'
  • src/main/resources/application.properties中配置数据源信息
1
2
3
4
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=dbuser
spring.datasource.password=dbpass
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

使用JdbcTemplate操作数据库

  • SpringJdbcTemplate是自动配置的,你可以直接使用@Autowired来注入到你自己的bean中来使用。

示例:我们在创建User表,包含属性name、age,下面来编写数据访问对象和单元测试用例。

  • 定义包含有插入、删除、查询的抽象接口UserService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface UserService {
/**
* 新增一个用户
* @param name
* @param age
*/
void create(String name, Integer age);
/**
* 根据name删除一个用户高
* @param name
*/
void deleteByName(String name);
/**
* 获取用户总量
*/
Integer getAllUsers();
/**
* 删除所有用户
*/
void deleteAllUsers();
}
  • 通过JdbcTemplate实现UserService中定义的数据访问操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void create(String name, Integer age) {
jdbcTemplate.update("insert into USER(NAME, AGE) values(?, ?)", name, age);
}
@Override
public void deleteByName(String name) {
jdbcTemplate.update("delete from USER where NAME = ?", name);
}
@Override
public Integer getAllUsers() {
return jdbcTemplate.queryForObject("select count(1) from USER", Integer.class);
}
@Override
public void deleteAllUsers() {
jdbcTemplate.update("delete from USER");
}
}
  • 创建对UserService的单元测试用例,通过创建、删除和查询来验证数据库操作的正确性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
public class ApplicationTests {
@Autowired
private UserService userSerivce;
@Before
public void setUp() {
// 准备,清空user表
userSerivce.deleteAllUsers();
}
@Test
public void test() throws Exception {
// 插入5个用户
userSerivce.create("a", 1);
userSerivce.create("b", 2);
userSerivce.create("c", 3);
userSerivce.create("d", 4);
userSerivce.create("e", 5);
// 查数据库,应该有5个用户
Assert.assertEquals(5, userSerivce.getAllUsers().intValue());
// 删除两个用户
userSerivce.deleteByName("a");
userSerivce.deleteByName("e");
// 查数据库,应该有5个用户
Assert.assertEquals(3, userSerivce.getAllUsers().intValue());
}
}

Spring Boot中使用Spring-data-jpa让数据访问更简单、更优雅

未完成待补充

Spring Boot中使用Redis数据库

引入依赖

  • Spring Boot提供的数据访问框架 Spring Data Redis 基于Jedis可以通过引入spring-boot-starter-redis来配置依赖关系。
1
compile group: 'org.springframework.boot', name: 'spring-boot-starter-redis', version: '1.4.0.RELEASE'

参数配置

  • application.properties 中加入Redis服务端的相关配置
参数 说明
spring.redis.database=0 # Redis数据库索引(默认为0)
spring.redis.host=localhost # Redis服务器地址
spring.redis.port=6379 # Redis服务器连接端口
spring.redis.password= # Redis服务器连接密码(默认为空)
spring.redis.pool.max-active=8 # 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-wait=-1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-idle=8 # 连接池中的最大空闲连接
spring.redis.pool.min-idle=0 # 连接池中的最小空闲连接
spring.redis.timeout=0 # 连接超时时间(毫秒)

其中spring.redis.database的配置通常使用0即可,Redis在配置的时候可以设置数据库数量,默认为16,可以理解为数据库的schema

Spring Boot整合MyBatis

整合依赖

1
compile group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: '1.1.1'
  • 同之前介绍的使用jdbc和spring-data连接数据库一样,在 application.properties 中配置mysql的连接配置
1
2
3
4
spring.datasource.url=jdbc:mysql://localhost:3306/blog
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

使用MyBatis

1
2
3
4
5
6
7
8
public class User {
private int id;
private String user_name;
private String password;
private int role_id;
private String image_path;
private String encryption_salt;
//省略getter和setter
1
2
3
4
5
6
7
8
9
10
@Mapper
public interface UserMapper {
@Select("SELECT * FROM USER WHERE NAME = #{name}")
User findByName(@Param("name") String name);
@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
int insert(@Param("name") String name, @Param("age") Integer age);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class UserServiceImplTest {
@Autowired
private UserService userService;
@Test
public void getAllUser() throws Exception {
List<User> allUser = userService.getAllUser();
System.out.println(allUser.size());
}
}

Spring Boot中使用AOP统一处理Web请求日志

引入依赖

1
compile group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: '1.4.0.RELEASE'
  • 在完成了引入AOP依赖包后,一般来说并不需要去做其他配置。也许在Spring中使用过注解配置方式的人会问是否需要在程序主类中增加@EnableAspectJAutoProxy来启用,实际并不需要。

  • 可以看下面关于AOP的默认配置属性,其中 spring.aop.auto 属性默认是开启的,也就是说只要引入了AOP依赖后,默认已经增加了@EnableAspectJAutoProxy

1
2
3
4
# AOP
spring.aop.auto=true # Add @EnableAspectJAutoProxy.
spring.aop.proxy-target-class=false # Whether subclass-based (CGLIB) proxies are to be created (true) as
opposed to standard Java interface-based proxies (false).

而当我们需要使用CGLIB来实现AOP的时候,需要配置spring.aop.proxy-target-class=true,不然默认使用的是标准Java的实现。

实现Web层的日志切面

实现AOP的切面主要有以下几个要素:

  • 使用@Aspect注解将一个java类定义为切面类
  • 使用@Pointcut定义一个切入点,可以是一个规则表达式,比如下例中某个package下的所有函数,也可以是一个注解等。
  • 根据需要在切入点不同位置的切入内容
    • 使用@Before在切入点开始处切入内容
    • 使用@After在切入点结尾处切入内容
    • 使用@AfterReturning在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)
    • 使用@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容
    • 使用@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Aspect
@Component
public class WebLogAspect {
private Logger logger = Logger.getLogger(getClass());
@Pointcut("execution(public * com.OKjava.web..*.*(..))")
public void webLog(){}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
logger.info("URL : " + request.getRequestURL().toString());
logger.info("HTTP_METHOD : " + request.getMethod());
logger.info("IP : " + request.getRemoteAddr());
logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
logger.info("RESPONSE : " + ret);
}
}
  • 可以看上面的例子,通过@Pointcut定义的切入点为com.OKjava.web包下的所有函数(对web层所有请求处理做切入点),然后通过@Before实现,对请求内容的日志记录(本文只是说明过程,可以根据需要调整内容),最后通过@AfterReturning记录请求返回的对象。

  • 通过运行程序并访问:http://localhost:8080/hello?name=benny,可以获得下面的日志输出

1
2
3
4
5
6
2016-05-19 13:42:13,156 INFO WebLogAspect:41 - URL : http://localhost:8080/hello
2016-05-19 13:42:13,156 INFO WebLogAspect:42 - HTTP_METHOD : http://localhost:8080/hello
2016-05-19 13:42:13,157 INFO WebLogAspect:43 - IP : 0:0:0:0:0:0:0:1
2016-05-19 13:42:13,160 INFO WebLogAspect:44 - CLASS_METHOD : com.didispace.web.HelloController.hello
2016-05-19 13:42:13,160 INFO WebLogAspect:45 - ARGS : [didi]
2016-05-19 13:42:13,170 INFO WebLogAspect:52 - RESPONSE:Hello didi

优化:AOP切面中的同步问题

  • WebLogAspect切面中,分别通过doBefore和doAfterReturning两个独立函数实现了切点头部和切点返回后执行的内容,若我们想统计请求的处理时间,就需要在doBefore处记录时间,并在doAfterReturning处通过当前时间与开始处记录的时间计算得到请求处理的消耗时间。
  • 那么我们是否可以在WebLogAspect切面中定义一个成员变量来给doBefore和doAfterReturning一起访问呢?是否会有同步问题呢?
  • 的确,直接在这里定义基本类型会有同步问题,所以我们可以引入ThreadLocal对象,像下面这样进行记录:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Aspect
@Component
public class WebLogAspect {
private Logger logger = Logger.getLogger(getClass());
ThreadLocal<Long> startTime = new ThreadLocal<>();
@Pointcut("execution(public * com.didispace.web..*.*(..))")
public void webLog(){}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
startTime.set(System.currentTimeMillis());
// 省略日志记录内容
}
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
logger.info("RESPONSE : " + ret);
logger.info("SPEND TIME : " + (System.currentTimeMillis() - startTime.get()));
}
}

优化:AOP切面的优先级

  • 由于通过AOP实现,程序得到了很好的解耦,但是也会带来一些问题,比如:我们可能会对Web层做多个切面,校验用户,校验头信息等等,这个时候经常会碰到切面的处理顺序问题。
  • 所以,我们需要定义每个切面的优先级,我们需要@Order(i)注解来标识切面的优先级。i的值越小,优先级越高。假设我们还有一个切面是CheckNameAspect用来校验name必须为didi,我们为其设置@Order(10),而上文中WebLogAspect设置为@Order(5),所以WebLogAspect有更高的优先级,这个时候执行顺序是这样的:
  • @Before中优先执行@Order(5)的内容,再执行@Order(10)的内容
  • @After和@AfterReturning中优先执行@Order(10)的内容,再执行@Order(5)的内容
  • 所以我们可以这样子总结:

在切入点前的操作,按order的值由小到大执行
在切入点后的操作,按order的值由大到小执行

SpringApplication

自定义Banner

  • 通过在classpath下添加一个banner.txt或设置banner.location来指定相应的文件可以改变启动过程中打印的banner。如果这个文件有特殊的编码,你可以使用banner.encoding设置它(默认为UTF-8)。
  • 在banner.txt中可以使用如下的变量:
变量 描述
${application.version} MANIFEST.MF中声明的应用版本号,例如1.0
${application.formatted-version MANIFEST.MF中声明的被格式化后的应用版本号(被括号包裹且以v作为前缀),用于显示,例如(v1.0)
${spring-boot.version} 正在使用的SpringBoot版本号,例如1.2.2.BUILD-SNAPSHOT
${spring-boot.formatted-version} 正在使用的SpringBoot被格式化后的版本号(被括号包裹且以v作为前缀), 用于显示,例如(v1.2.2.BUILD-SNAPSHOT)

注:如果想以编程的方式产生一个banner,可以使用SpringBootApplication.setBanner(…)方法。
使用org.springframework.boot.Banner接口,实现你自己的printBanner()方法。

自定义SpringApplicatio

  • 如果默认的SpringApplication不符合你的口味,你可以创建一个本地的实例并自定义它。例
    如,关闭banner你可以这样写:
1
2
3
4
5
6
public static void main(String[] args){
SpringApplication app = new SpringApplication(MySpringConfiguration.class);
app.setShowBanner(false);
app.run(args);
}

注:传递给SpringApplication的构造器参数是springbeans的配置源。在大多数情况下,这些将是@Configuration类的引用,但它们也可能是XML配置或要扫描包的引用

你也可以使用application.properties文件来配置SpringApplication

流畅的构建API

  • 如果你需要创建一个分层的ApplicationContext(多个具有父子关系的上下文),或你只是喜欢使用流畅的构建API,你可以使用SpringApplicationBuilder。
  • SpringApplicationBuilder允许你以链式方式调用多个方法,包括可以创建层次结构的parent和child方法。
1
2
3
4
5
new SpringApplicationBuilder()
.showBanner(false)
.sources(Parent.class)
.child(Application.class)
.run(args);

注:创建ApplicationContext层次时有些限制,比如,Web组件(components)必须包含在子上下文(child context)中,且相同的Environment即用于父上下文也用于子上下文中。

Application事件和监听器

  • 除了常见的Spring框架事件,比如ContextRefreshedEvent,一个SpringApplication也发送一
    些额外的应用事件。一些事件实际上是在ApplicationContext被创建前触发的。
  • 你可以使用多种方式注册事件监听器,最普通的是使用SpringApplication.addListeners(…)
    法。在你的应用运行时,应用事件会以下面的次序发送:
  1. 在运行开始,但除了监听器注册和初始化以外的任何处理之前,会发送一个ApplicationStartedEvent
  2. 在Environment将被用于已知的上下文,但在上下文被创建前,会发送一个ApplicationEnvironmentPreparedEvent
  3. 在refresh开始前,但在bean定义已被加载后,会发送一个ApplicationPreparedEvent
  4. 启动过程中如果出现异常,会发送一个ApplicationFailedEvent

注:你通常不需要使用应用程序事件,但知道它们的存在会很方便(在某些场合可能会使用到)。
在Spring内部,Spring Boot使用事件处理各种各样的任务。

Web环境

  • 一个SpringApplication将尝试为你创建正确类型的ApplicationContext。
  • 在默认情况下,使用AnnotationConfigApplicationContext或AnnotationConfigEmbeddedWebApplicationContext取决于你正在开发的是否是web应用。
  • 用于确定一个web环境的算法相当简单(基于是否存在某些类)。如果需要覆盖默认行为,你可以使用setWebEnvironment(boolean webEnvironment)。
  • 通过调用setApplicationContextClass(…),你可以完全控制ApplicationContext的类型。

注:当JUnit测试里使用SpringApplication时,调用setWebEnvironment(false)是可取的。

命令行启动器

  • 如果你想获取原始的命令行参数,或一旦SpringApplication启动,你需要运行一些特定的代
    码,你可以实现CommandLineRunner接口。在所有实现该接口的Spring beans上将调用
    run(String… args)方法。
1
2
3
4
5
6
7
8
9
import org.springframework.boot.*
import org.springframework.stereotype.*
@Component
public class MyBean implements CommandLineRunner {
public void run(String... args) {
// Do something...
}
}
  • 如果一些CommandLineRunner beans被定义必须以特定的次序调用,你可以额外实现
    org.springframework.core.Ordered接口或使用org.springframework.core.annotation.Order

Application退出

  • 每个SpringApplication在退出时为了确保ApplicationContext被优雅的关闭,将会注册一个JVMshutdown钩子。
  • 所有标准的Spring生命周期回调(比如,DisposableBean接口或@PreDestroy注解)都能使用。
  • 如果beans想在应用结束时返回一个特定的退出码(exitcode),可以实现org.springframework.boot.ExitCodeGenerator接口。

外化配置

  • Spring Boot允许外化(externalize)你的配置,这样你能够在不同的环境下使用相同的代码。
  • 你可以使用properties文件,YAML文件,环境变量和命令行参数来外化配置。
  • 使用@Value注解,可以直接将属性值注入到你的beans中,并通过Spring的Environment抽象或绑定到结构化对象来访问。

  • Spring Boot使用一个非常特别的PropertySource次序来允许对值进行合理的覆盖,需要以下
    面的次序考虑属性:

  1. 命令行参数
  2. 来自于java:comp/env的JNDI属性
  3. Java系统属性(System.getProperties())
  4. 操作系统环境变量
  5. 只有在random.*里包含的属性会产生一个RandomValuePropertySource
  6. 在打包的jar外的应用程序配置文件(application.properties,包含YAML和profile变量)
  7. 在打包的jar内的应用程序配置文件(application.properties,包含YAML和profile变量)
  8. 在@Configuration类上的@PropertySource注解
  9. 默认属性(使用SpringApplication.setDefaultProperties指定)
1
2
3
4
5
6
7
8
import org.springframework.stereotype.*
import org.springframework.beans.factory.annotation.*
@Component
public class MyBean {
@Value("${name}")
private String name;
// ...
}
  • 你可以将一个application.properties文件捆绑到jar内,用来提供一个合理的默认name属性值。

  • 当运行在生产环境时,可以在jar外提供一个application.properties文件来覆盖name属性。

  • 对于一次性的测试,你可以使用特定的命令行开关启动(比如,java -jar app.jar–name=”Spring”)。

配置随机值

  • RandomValuePropertySource在注入随机值(比如,密钥或测试用例)时很有用。它能产生
    整数,longs或字符串,
1
2
3
4
5
my.secret=${random.value}
my.number=${random.int}
my.bignumber=${random.long}
my.number.less.than.ten=${random.int(10)}
my.number.in.range=${random.int[1024,65536]}
  • random.int*语法是OPEN value (,max) CLOSE,此处OPEN,CLOSE可以是任何字符,并且value,max是整数。如果提供max,那么value是最小的值,max是最大的值(不包含在内)

访问命令行属性

  • 默认情况下,SpringApplication将任何可选的命令行参数(以’–’开头,比如,--server.port=9000)转化为property,并将其添加到Spring Environment中。
  • 如上所述,命令行属性总是优先于其他属性源。
  • 如果你不想将命令行属性添加到Environment里,你可以使用SpringApplication.setAddCommandLineProperties(false)来禁止它们。

Application属性文件

  • SpringApplication将从以下位置加载application.properties文件,并把它们添加到Spring
    Environment中:
  1. 当前目录下的一个/config子目录
  2. 当前目录
  3. 一个classpath下的/config
  4. classpath根路径(root)
  • 这个列表是按优先级排序的(列表中位置高的将覆盖位置低的)。

注:你可以使用YAML(’.yml’)文件替代’.properties’。

  • 如果不喜欢将application.properties作为配置文件名,
    • 你可以通过指定spring.config.name环境属性来切换其他的名称。
    • 你也可以使用spring.config.location环境属性来引用一个明确的路径(目录位置或文件路径列表以逗号分割)。
1
2
3
4
$ java -jar myproject.jar --spring.config.name=myproject
//or
$ java -jar myproject.jar --spring.config.location=classpath:/default.properties,class
path:/override.properties
  • 如果spring.config.location包含目录(相对于文件),那它们应该以/结尾(在加载前,spring.config.name产生的名称将被追加到后面)。
  • 不管spring.config.location是什么值,默认的搜索路径classpath:,classpath:/config,file:,file:config/总会被使用。
  • 以这种方式,你可以在application.properties中为应用设置默认值,然后在运行的时候使用不同的文件覆盖它,同时保留默认配置。

注:如果你使用环境变量而不是系统配置,大多数操作系统不允许以句号分割(period-separated)的key名称,但你可以使用下划线(underscores)代替(比如,使用SPRING_CONFIG_NAME代替spring.config.name)。如果你的应用运行在一个容器中,那么JNDI属性(java:comp/env)或servlet上下文初始化参数可以用来取代环境变量或系统属性,当然也可以使用环境变量或系统属性。

特定的Profile属性

  • 除了application.properties文件,特定配置属性也能通过命令惯例application-{profile}.properties来定义。
  • 特定Profile属性从跟标准application.properties相同的路径加载,并且特定profile文件会覆盖默认的配置。

属性占位符

  • 当application.properties里的值被使用时,它们会被存在的Environment过滤,所以你能够引
    用先前定义的值(比如,系统属性)。
1
2
app.name=MyApp
app.description=${app.name} is a Spring Boot application

使用YAML代替Properties

  • YAML是JSON的一个超集,也是一种方便的定义层次配置数据的格式。
  • 无论你何时将SnakeYAML库放到classpath下,SpringApplication类都会自动支持YAML作为properties的替换。

注:如果你使用’starter POMs’,spring-boot-starter会自动提供SnakeYAML。

注:你也能使用相应的技巧为存在的Spring Boot属性创建’短’变量

检查应用的运行状态。

  • (参考文章:](http://www.itnose.net/detail/6359648.html)
  • compile group: ‘org.springframework.boot’, name: ‘spring-boot-starter-actuator’, version: ‘1.4.0.RELEASE’
    • health可以用来检查应用的运行状态。
    • 它经常被监控软件用来提醒人们生产系统是否停止。
    • health端点暴露的默认信息取决于端点是如何被访问的。
    • 对于一个非安全,未认证的连接只返回一个简单的’status’信息。
    • 对于一个安全或认证过的连接其他详细信息也会展示。
ID 描述 敏感(Sensitive)
autoconfig 显示一个auto-configuration的报告,该报告展示所有auto-configuration候选者及它们被应用或未被应用的原因 true
beans 显示一个应用中所有Spring Beans的完整列表 true
configprops 显示一个所有@ConfigurationProperties的整理列表 true
dump 执行一个线程转储true
env 暴露来自Spring ConfigurableEnvironment的属性 true
health 展示应用的健康信息(当使用一个未认证连接访问时显示一个简单的’status’,使用认证连接访问则显示全部信息详情) false
info 显示任意的应用信息 false
metrics 展示当前应用的’指标’信息 true
mappings 显示一个所有@RequestMapping路径的整理列表 true
shutdown 允许应用以优雅的方式关闭(默认情况下不启用) true
trace 显示trace信息(默认为最新的一些HTTP请求) true

如果希望了解的更多一些,必须要试试这个参数 --debug

Spring boot 整合Redis

引入依赖

1
2
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-redis
compile group: 'org.springframework.boot', name: 'spring-boot-starter-redis', version: '1.4.0.RELEASE'

参数配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0

其中spring.redis.database的配置通常使用0即可,Redis在配置的时候可以设置数据库数量,默认为16,可以理解为数据库的schema

测试访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Created by benny on 2016/8/24.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = BlogWebApplication.class)
public class RedisUtilTest {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void test(){
stringRedisTemplate.opsForValue().set("name","benny");
String name = stringRedisTemplate.opsForValue().get("name");
System.out.println(name);
}
}
  • 上面的测试代码,演示了如何通过自动配置的StringRedisTemplate对象进行Redis的读写操作,该对象从命名中可知支持的是String类型。
  • 没有使用过Spring-data-redis的人一定熟悉RedisTemplate<K,V>接口,StringRedisTemplate就相当于RedisTemplate<String,String>的实现。

Redis存储对象

  • 实际情况下,我们还要用Redis来存储对象,Spring Boot 并不支持直接使用RedisTemplate<Stirng,Object>需要我们自己实现 RedisSerializable 接口来对传入的对象进行序列化和反序列化。
  • [x] 注意: 要存储的对象一定要实现Serializable接口
  • 创建要存储的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User implements Serializable {
private static final long serialVersionUID = -1L;
private String username;
private Integer age;
public User(String username, Integer age) {
this.username = username;
this.age = age;
}
// 省略getter和setter
}
  • 实现对象的序列化接口 RedisSerializer

    • 未对类进行重构,方便查看
    • 实现了 RedisSerializer 重写了两个方法
      • public byte[] serialize(Object o);
      • public Object deserialize(byte[] bytes);
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        import org.springframework.core.convert.converter.Converter;
        import org.springframework.core.serializer.support.DeserializingConverter;
        import org.springframework.core.serializer.support.SerializingConverter;
        import org.springframework.data.redis.serializer.RedisSerializer;
        import org.springframework.data.redis.serializer.SerializationException;
        /**
        * Description:
        * Created @version 1.0 2016/8/24 11:52 by Benny
        */
        public class RedisObjectSerializer implements RedisSerializer<Object> {
        private Converter<Object, byte[]> serializer = new SerializingConverter();
        private Converter<byte[], Object> deserializer = new DeserializingConverter();
        @Override
        public byte[] serialize(Object o) throws SerializationException {
        if (o == null) {
        return new byte[0];
        }
        return serializer.convert(o);
        }
        @Override
        public Object deserialize(byte[] bytes) throws SerializationException {
        if ((bytes == null || bytes.length == 0)) {
        return null;
        }
        return deserializer.convert(bytes);
        }
        }
  • 配置针对User的RedisTemplate实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration ★★★★★ 一定要配置@configuration
public class RedisConfig {
@Bean ★★★★★ 一定要加上注解@Bean
JedisConnectionFactory jedisConnectionFactory() {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
//jedisConnectionFactory.setHostName("192.168.104.139");
//jedisConnectionFactory.setPort(6379);
return jedisConnectionFactory;
}
@Bean ★★★★★ 一定要加上注解@Bean
public RedisTemplate<String, User> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, User> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
//传入自定义序列化类
template.setValueSerializer(new RedisObjectSerializer());
return template;
}
}
  • 测试用例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = BlogWebApplication.class)
public class RedisUtilTest {
@Autowired
RedisTemplate<String, User> redisTemplate;
@Test
public void test(){
User user = new User();
user.setUserId("14");
user.setUserName("benny");
user.setPassword("123456");
user.setDisplayNum(12);
redisTemplate.opsForValue().set("user",user);
User u = redisTemplate.opsForValue().get("user");
Assert.assertEquals("benny",u.getUserName());
}
}

Spring Boot 使用Redis作为缓存

引入依赖

1
2
3
4
5
6
// spring-boot-starter-cache
compile group: 'org.springframework.boot', name: 'spring-boot-starter-cache', version: '1.4.0.RELEASE'
// spring-boot-starter-redis
compile group: 'org.springframework.boot', name: 'spring-boot-starter-redis', version: '1.4.0.RELEASE'

参数配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0

编写RedisCacheConfig配置类:

缓存主要有几个要实现的类:

  • 一、是CacheManager缓存管理器
  • 二、是具体操作实现类
  • 三、是CacheManager工厂类(这个可以使用配置文件配置的进行注入,也可以通过编码的方式进行实现);
  • 四、是缓存key生产策略(当然Spring自带生成策略,但是在Redis客户端进行查看的话是系列化的key,对于我们肉眼来说就是感觉是乱码了,这里我们先使用自带的缓存策略)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Configuration
@EnableCaching // ★★★★★ 启用注解,这个注解很重要
public class RedisCacheConfig extends {
@Bean
JedisConnectionFactory jedisConnectionFactory() {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
//jedisConnectionFactory.setHostName("你的IP地址");
//jedisConnectionFactory.setPort(6379);
return jedisConnectionFactory;
}
/**
* redis模板操作类,类似于jdbcTemplate的一个类;
* 虽然CacheManager也能获取到Cache对象,但是操作起来没有那么灵活;
* 这里在扩展下:RedisTemplate这个类不见得很好操作,我们可以在进行扩展一个我们
* 自己的缓存类,比如:RedisStorage类;
*
* @param redisConnectionFactory : 通过Spring进行注入,参数在application.properties进行配置;
* @return
*/
@Bean
public RedisTemplate<String, User> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, User> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
//传入自定义序列化类
template.setValueSerializer(new RedisObjectSerializer());
return template;
}
/*----------------------------- 缓存使用 -------------------------------- */
/**
* 缓存管理器
* @param redisTemplate
* @return
*/
@Bean
public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate) {
return new RedisCacheManager(redisTemplate);
}
/**
* 自定义key.
* 此方法将会根据类名+方法名+所有参数的值生成唯一的一个key,即使@Cacheable中的value属性一样,key也会不一样。
* @ Return
*/
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
}

Redis 缓存配置:

注意:

  • RedisCacheConfig类这里也可以不用继承CachingConfigurerSupport类,也就是直接一个普通的Class就好了;
    • 这里主要我们之后要重新实现key的生成策略,只要这里修改KeyGenerator,其它位置不用修改就生效了。
    • 普通使用普通类的方式的话,那么在使用@Cacheable的时候还需要指定KeyGenerator的名称;这样编码的时候比较麻烦。

添加注解@Cacheable(…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service("userServiceImpl")
public class UserServiceImpl implements UserService {
@Autowired
UserService userServiceImpl;
/**
* 注意: ★★★★★★★
*
* 如果 RedisCacheConfig.java类不继承 CachingConfigurerSupport.java类的话
* @Cacheable(value = "user" , keyGenerator = "keyGenerator" ) 中的keyGenerator就必须要手动指定
* 如过继承了CachingConfigurerSupport.java类的话,则可以省略写keyGenerator属性,像下面的代码
*
*/
@Cacheable(value = "users")
@Override
public List<User> findAll() {
System.out.println("从数据库中获取的User");
return userDaoImpl.findAll();
}
}

缓存的生命周期

1
2
3
4
5
6
7
Hibernate: insert into user (age, name) values (?, ?)
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.name as name3_0_fromuseruser0_user0_.name=?
第一次查询:10
Hibernate: select user0_.id as id1_0_0_,user0_.ageasage2_0_0_,user0_.nameasname3_0_0_fromuseruser0_user0_.id=?
第二次查询:10
Hibernate: update user age = 20, name=? id=?
第三次查询:10

可以观察到:

  • 第一次查询的时候,执行了select语句
  • 第二次查询没有执行select语句,说明是从缓存中获得了结果;
  • 第三次查询,我们获得了一个错误的结果,根据我们的测试逻辑,在查询之前我们已经将age更新为20,但是我们从缓存中获取到的age还是为10。
  • [x] 在EhCache中没有这个问题,在Redis中出现了这个问题;
  • 因为Redis的缓存独立于我们的Spring应用之外,我们对数据库中数据做了更新以后,没有通知Redis去更新相应的内容,因此我们取到了缓存中为修改的数据,导致了数据库与缓存中的数据不一致。
  • 所以在使用缓存的时候,要注意缓存的声明周期

缓存注解 @Cacheable、@CachePut、@CacheEvict

@Cacheable@CachePut@CacheEvict 注释介绍

  • [x] @Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
可设置属性 说明 示例
value 缓存的名称,在spring配置文件中定义,必须指定至少一个 例如:@Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:@Cacheable(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回true或者false,只有为true才进行缓存 例如:@Cacheable(value=”testcache”,condition=”#userName.length()>2”)

  • [x] @CachePut 的作用主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
可设置属性 说明 示例
value 缓存的名称,在spring配置文件中定义,必须指定至少一个 例如:@Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:@Cacheable(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者false,只有为true才进行缓存 例如:@Cacheable(value=”testcache”,condition=”#userName.length()>2”)

  • [x] @CacheEvict 的作用主要针对方法配置,能够根据一定的条件对缓存进行清空 @CacheEvict 主要的参数
可设置属性 说明 示例
value 缓存的名称,在spring配置文件中定义,必须指定至少一个 例如:@CachEvict(value=”mycache”) 或者 @CachEvict(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:@CachEvict(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者false,只有为true才清空缓存 例如:@CachEvict(value=”testcache”,condition=”#userName.length()>2”)
allEntries 是否清空所有缓存内容,缺省为false,如果指定为true,则方法调用后将立即清空所有缓存 例如:@CachEvict(value=”testcache”,allEntries=true)
beforeInvocation 是否在方法执行前就清空,缺省为false,如果指定为true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 例如:@CachEvict(value=”testcache”,beforeInvocation=true)

  • [x] @Caching有时候我们可能组合多个Cache注解使用;
  • 比如用户新增成功后,我们要添加id–>user;username—>user;email—>user的缓存;就是查询的时候可以根据用户名或邮箱,或密码来查询,此时就需要@Caching组合多个注解标签了。
  • 如用户新增成功后,添加id–>user;username—>user;email—>user到缓存;
1
2
3
4
5
6
7
8
9
@Caching(
put = {
@CachePut(value = "user", key = "#user.id"),
@CachePut(value = "user", key = "#user.username"),
@CachePut(value = "user", key = "#user.email")
}
)
public User save(User user) {

额外补充:

  • @cache(“something”) 这个相当于 save() 操作
  • @cachePut 相当于 Update() 操作,只要他标示的方法被调用,那么都会缓存起来
  • @cache 是先看下有没已经缓存了,然后再选择是否执行方法。
  • @CacheEvict 相当于 Delete() 操作,用来清除缓存用的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
@Cacheable
public class MemcachedService{
@Cacheable(name="remote",key="'USER_NAME_'+#args[0]")
public String storeUserName(String accountId,String name)
{
return name;
}
@Cacheable(name="remote")
public String storeUserAddress(String accountId,String address){
return address;
}
}

使用注解@CacheConfig

  • 所有的@Cacheable() 里面都有一个name=”xxx”的属性,这显然如果方法多了,看起来肯定是不够优雅而且又烦的,如果可以一次性声明完 那就省事了,

  • 所以,有了@CacheConfig这个配置,

@CacheConfig is a class-level annotation that allows to share the cache names
  • [x] 不过不用担心,如果你在你的方法写别的名字,那么依然以方法的名字为准。
1
2
3
4
5
6
7
8
9
10
@CacheConfig("books")
public class BookRepositoryImpl implements BookRepository {
@Cacheable
public Book findBook(ISBN isbn) {...}
@Cacheable()
public String storeUserAddress(String accountId,String address){ ... }
}

自定义注解

1
2
@Cacheable(name = "book", key="#isbn",conditional=“xxx”,allEntries=true,beforeInvocation=true)
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
  • 这样的配置很长,而且有可能声明在很多个方法的,所以我们很想精简点使用自定义注解,容易配置些。
1
2
@findBookByIsbnervice
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
  • 新建一个文件findBookByIsbn,内容如下
1
2
3
4
5
6
7
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface findBookByIsbn {
...
}
  • key对应的是一样的,value对应的实际是返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
@Cacheable(key="#userId",value="user")
public User selectByPrimaryKey(String userId) {
System.out.println("从数据库中查询到的用户信息");
return userDaoImpl.selectByPrimaryKey(userId);
}
@Override
@CachePut(key = "#record.userId", value = "user")
public int updateByPrimaryKey(User record) {
return userDaoImpl.updateByPrimaryKey(record);
}

总算明白了@Cacheable注解如何缓存

缓存自定义对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.okjava.service.impl;
import com.github.pagehelper.PageHelper;
import com.okjava.beans.User;
import com.okjava.core.pageutil.PageBean;
import com.okjava.mappermy.UserDao;
import com.okjava.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author by benny on 2016/8/21.
* @version 1.0
* @description
*/
@Service("userServiceImpl")
@CacheConfig(cacheNames = "userCache") ① ★★★★★此处必须要配置否则报错
public class UserServiceImpl implements UserService {
@Autowired
UserService userServiceImpl;
@Override
@Cacheable(key = "'user:list'", sync = true) ② ★★★★★ 'user:list' 外面要有单引号
public PageBean<User> findAllPager(PageBean pageBean) {
System.out.println("从数据库中获取的User");
int pageSize = pageBean.getPageSize() == 0 ? 10 : pageBean.getPageSize();
int pageNum = pageBean.getPageNum() == 0 ? 1 : pageBean.getPageNum();
PageHelper.startPage(pageNum, pageSize);
List<User> users = userDaoImpl.findAll();
return new PageBean(users);
}
}

注意:

如果要缓存的实现类类中没有加上@CacheConfig注解,的位置,会抛异常。

1
2
At least one cache should be provided per cache operation.

Spirng Cache @Cacheable注解的key问题

  • 加上注解 @CacheConfig(cacheNames = "userCache")
  • Spring Cache 中注解的key是可以随意指定的(重点在于 key=” “)这中间是否有一对单引号。
1
2
3
@Cacheable(key="'hello'")
@Cacheable(key="hello")

这两个是不一样的,目前来看的话总结如下

  • 自己随意定义的注解要加上' '单引号。如:@Cacheable(“ benny “)
  • 如果使用spel或者ognl的话,就不需要加单引号。如:@Cacheable(“#userId”)

获取所有定义的Bean

1
2
3
4
5
String[] beanDefinitionNames = run.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
System.out.print(beanDefinitionName + ";");
}

基于Spring提供支持不同设备的页面

添加依赖

1
2
compile group: 'org.springframework.boot', name: 'spring-boot-starter-mobile', version: '1.4.0.RELEASE'
  • Spring Boot能够针对不同设备渲染不同的视图(View),只需要在应用的Properties文件中中稍加配置即可。在application.properties增加一行:
1
2
spring.mobile.devicedelegatingviewresolver.enabled: true

针对一个请求,LiteDeviceDelegatingViewResolver通过DeviceResolverHandlerInterceptor识别出的Device类型来判断返回哪种视图进行响应(桌面、手机(mobile)还是平板(tablet)),这一部分大家参考Spring如何识别设备的经验。

LiteDeviceDelegatingViewResolver会将请求代理给ThymeleafViewResolver,作为Spring自身提供的正牌ViewResolver,相比传统的视图技术如JSP,Velocity等,有不少过人之处,大家可以回顾一下Thymeleaf的介绍以及如何在Spring MVC中使用Thymeleaf。默认情况下,Spring Boot去到mobile/和tablet/文件下去寻找移动端和平板端对应的视图进行渲染。

  • 当然你也可以在属性文件中进行设置,约定大于配置,没有特别需求用约定就好了。
1
2
3
4
5
6
7
8
9
└── src
└── main
└── resources
└── templates
└── sayHello.html
└── mobile
└── sayHello.html
└── tablet
└── sayHello.html

创建定时任务

在Spring Boot的主类中加入@EnableScheduling注解,启用定时任务的配置

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

创建定时任务实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author by benny on 2016/8/28.
* @version 1.0
* @description
*/
@Component
public class ScheduleTaskTest {
@Scheduled(fixedRate=2000)
public void sayHello(){
System.out.println("大家好:我是serviceModule中");
System.out.println("要运行我,要先添加service到web依赖中");
}
}
  • 运行程序,控制台中可以看到类似如下输出,定时任务开始正常运作了。
1
2
3
4
5
6
大家好:我是serviceModule中
要运行我,要先添加service到web依赖中
Disconnected from the target VM, address: '127.0.0.1:60530', transport: 'socket'
Process finished with exit code -1

@Scheduled详解

注解参数 说明
@Scheduled(fixedRate = 5000) 上一次开始执行时间点之后5秒再执行
@Scheduled(fixedDelay = 5000) 上一次执行完毕时间点之后5秒再执行
@Scheduled(initialDelay=1000,fixedRate=5000) 第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次
@Scheduled(cron="*/5 * * * * * *") 通过cron表达式定义规则

cron 详解

  • 每个cron任务的格式如下
1
2
3
cron = " * * * * * * *(可选值) "
<秒> <分钟> <小时> <日> <月> <星期> <年>【可选值】
参数 说明
* 匹配该域的任意值;如*用在分所在的域,表示每分钟都会触发事件。
? 匹配该域的任意值。
- 匹配一个特定的范围值;如时所在的域的值是10-12,表示10、11、12点的时候会触发事件。
, 匹配多个指定的值;如周所在的域的值是2,4,6,表示在周一、周三、周五就会触发事件(1表示周日,2表示周一,3表示周二,以此类推,7表示周六)。
/ 左边是开始触发时间,右边是每隔固定时间触发一次事件,如秒所在的域的值是5/15,表示5秒、20秒、35秒、50秒的时候都触发一次事件。
L last,最后的意思,如果是用在天这个域,表示月的最后一天,如果是用在周所在的域,如6L,表示某个月最后一个周五。(外国周日是星耀日,周一是月耀日,一周的开始是周日,所以1L=周日,6L=周五。)
W weekday,工作日的意思。如天所在的域的值是15W,表示本月15日最近的工作日,如果15日是周六,触发器将触发上14日周五。如果15日是周日,触发器将触发16日周一。如果15日不是周六或周日,而是周一至周五的某一个,那么它就在15日当天触发事件。
# 用来指定每个月的第几个星期几,如6#3表示某个月的第三个星期五。

下面是一些cron任务示例。

注解参数 说明
每分钟运行。
0 * 每小时运行。
0 0 每天零点运行。
0 9,18 在每天的9AM和6PM运行。
0 9-18 在9AM到6PM的每个小时运行。
0 9-18 1-5 * 周一到周五的9AM到6PM每小时运行。
/10 每10分钟运行。
1
2
3
4
5
6
7
8
9
## Spring boot 整合 Spring Security
### 添加依赖
```java
//spring security
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '1.4.0.RELEASE'

创建Spring Security的配置类WebSecurityConfig

  • 通过@EnableWebSecurity注解开启Spring Security的功能
  • 继承WebSecurityConfigurerAdapter,并重写它的方法来设置一些web安全的细节configure(HttpSecurity http)方法
  • 通过authorizeRequests()定义哪些URL需要被保护、哪些不需要被保护。
    • 例如以上代码指定了/和/home+ 不需要任何认证就可以访问,其他的路径都必须通过身份验证。
  • 通过formLogin()定义当需要用户登录时候,转到的登录页面。
  • configure(AuthenticationManagerBuilder auth)方法,在内存中创建了一个用户,该用户的名称为user,密码为password,用户角色为USER。

  • 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Description:
* Created @version 1.0 2016/8/31 9:57 by Benny
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
  • WebSecurityConfigurerAdapterconfig(HttpSecurity http)方法提供了一个默认的配置,
1
2
3
4
5
6
7
8
9
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
  • [x] 上面的配置等同于
    • 确保我们应用中的所有请求都需要用户被认证
    • 允许用户进行基于表单的认证
    • 允许用户使用HTTP基本验证进行认证
1
2
3
4
5
6
<http>
<intercept-url pattern="/**" access="authenticated"/>
<form-login />
<http-basic />
</http>

java配置使用and()方法相当于XML标签的关闭,这样允许我们继续配置父类节点。

指定登录页地址

1
2
3
4
5
6
7
8
9
10
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login") //注 ①①⑴
.permitAll(); //注②
}
  • 1、指定登录页地址
  • 2、我们必须允许所有用户访问我们的登录页 (例如未验证的用户)
    formLogin().permitAll() 方法允许基于表单登陆的所有URL的所有用户的访问。
    登陆也配置示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<c:url value="/login" var="loginUrl"/>
<form action="${loginUrl}" method="post"> <!-- 1 -->
<c:if test="${param.error != null}"> <!-- 2 -->
<p>
Invalid username and password.
</p>
</c:if>
<c:if test="${param.logout != null}"> <!-- 3 -->
<p>
You have been logged out.
</p>
</c:if>
<p>
<label for="username">Username</label>
<input type="text" id="username" name="username"/><!-- 4 -->
</p>
<p>
<label for="password">Password</label>
<input type="password" id="password" name="password"/><!-- 5 --> </p>
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}"/><!-- 6 -->
<button type="submit" class="btn">Log in</button>
</form>
  1. 一个POST请求到/login用来验证用户
  2. 如果参数有错误,验证尝试失败
  3. 如果请求参数logout存在则登出
  4. 登录名参数必须被命名为username
  5. 密码参数必须被命名为password
  6. CSRF参数,了解更多查阅 后续“包含CSRF Token” 和 “跨站请求伪造(CSRF)”相关章节

Spring boot 过滤器 拦截器

自定义过滤器类

  1. 定义一个类 MyFilter 继承 Filterimport javax.servlet.Filter
  2. 在类上添加 注解 @WebFilter(filterName = "myFilter",urlPatterns = "/*")
  3. 在启动类SpringBootApplication上添加 注解 @ServletComponentScan
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.okjava.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
/**
* Description:
* Created @version 1.0 2016/8/31 15:58 by Benny
*/
@WebFilter(filterName = "myFilter",urlPatterns = "/*")
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("过滤器初始化");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("执行过滤器");
}
@Override
public void destroy() {
System.out.println("过滤器销毁");
}
}
  • SpringBoot启动类SpringBootApplication
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan
public class BlogWebApplication {
public static void main(String[] args) {
SpringApplication.run(BlogWebApplication.class, args);
}
}

ServletContext监听器(Listener)ServletContextListener

  1. 定义一个类 MyServletContextListener 实现 ServletContextListenerimport javax.servlet.ServletContextListener
  2. 在类上添加 注解 @WebListener
  3. 在启动类SpringBootApplication上添加 注解 @ServletComponentScan
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/**
* Description:
* Created @version 1.0 2016/8/31 16:09 by Benny
*/
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("ServletContext初始化");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("ServletContext销毁");
}
}

ServletContext监听器(Listener) HttpSessionListener

  1. 定义一个类 MyServletContextListener 实现 HttpSessionListenerimport javax.servlet.HttpSessionListener
  2. 在类上添加 注解 @WebListener
  3. 在启动类SpringBootApplication上添加 注解 @ServletComponentScan
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
/**
* Description:
* Created @version 1.0 2016/8/31 16:18 by Benny
*/
@WebListener
public class MyHttpSessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
System.out.println("Session 被创建");
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
System.out.println("ServletContex初始化");
}
}

Spring Boot 整合 Spring Security

添加依赖

1
2
3
//spring security
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '1.4.0.RELEASE'
  • 主要有一下几个页面
    • 首页 所有人可访问
    • 登录页 所有人可访问
    • 普通页 登录后的用户都可访问
    • 管理页 管理员可访问
    • 无权限提醒页【当一个用户访问了其没有权限的页面,需要有一个页面来对其进行提醒】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Controller
@RequestMapping("/")
public class LoginController extends BaseController {
public static final String INDEX_PATH = "/login/login";
public static final String FORBIDDEN_PATH = "security/forbidden";
public static final String LOGOUT_PATH = "login/logout";
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String toLogin() {
return INDEX_PATH;
}
@RequestMapping("/forbidden")
public String toForbiddenPage() {
return FORBIDDEN_PATH;
}
@RequestMapping("/logout")
public String logout(){
return LOGOUT_PATH;
}

可以看到/forbidden(也就是当http的状态码为403的时候)实际上就是当用户访问了没有权限的页面,因此我们这里需要配置,当发现请求状态码为403的时候,需要交给/forbidden处理

根据 http 状态码处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.web.servlet.ErrorPage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
/**
* Description: 没有访问权限 http状态码为304的跳转处理
* Created @version 1.0 2016/9/1 14:20 by Benny
*/
@Configuration
public class ForbiddenAccessConfig {
/**
* //对于Java 8来说可以用lambda表达式,而不需要创建该接口的一个实例.
*/
@Bean
public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
return container -> container.addErrorPages(new ErrorPage(HttpStatus.FORBIDDEN, "/forbidden"));
}
/**
* 此为内部类实现方式
*/
/*@Bean
public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
return new MyCustomizer();
}
private static class MyCustomizer implements EmbeddedServletContainerCustomizer {
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.addErrorPages(new ErrorPage(HttpStatus.FORBIDDEN, "/forbidden"));
}
}
*/
}

说明:

  • 这里我们利用了Spring自带的EmbeddedServletContainerCustomizer进行设置。当Spring发现有类型为EmbeddedServletContainerCustomizer注册进来,便会调用EmbeddedServletContainerCustomizercustomize 方法,此时,我们可以对整个Container进行设置,这里,我们添加了对于返回值为HttpStatus.FORBIDDEN的请求,将其交给/forbidden进行处理。

分析

  • 在程序启动阶段,Spring Boot检测到custoimer实例的存在,然后就会调用invoke(…)方法,并向内传递一个servlet对象的实例。在我们这个例子中,实际上传入的是TomcatEmbeddedServletContainerFactory容器对象,但是如果使用Jutty或者Undertow容器,就会用对应的容器对象。

添加spring Security配置

添加一个SecurityConfig类

  • 我们添加一个SecurityConfig类来对Security进行配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Description: Spring security 配置类
* Created @version 1.0 2016/8/31 9:57 by Benny
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
UserDetailsService customerUserService() {
return new CustomerUserService();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("helllo");
http
.authorizeRequests()
.antMatchers("/static/**").permitAll()
.antMatchers("/","/index","/login","/register").permitAll()
.antMatchers("/user").hasAnyRole("admin","user")
.antMatchers("/user/**").hasRole("admin")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error")
.permitAll()
.and()
.logout()
.logoutUrl("/logout") //指定注销的URL路径
.logoutSuccessUrl("/") //指定注销成功后跳转的页面
.permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customerUserService());
}
}

自定义实现UserDetailsService接口

  • 从数据库获取用户信息是必不可少的,我们有多重数据访问方式,可以是非关系型数据库,关系型数据库,常用的JPA等等。
  • 使AuthenticationManager使用我们的CustomUserDetailsService来获取用户信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Description: 自定义实现数据库用户信息查询
* Created @version 1.0 2016/9/1 11:10 by Benny
*/
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserService userServiceImpl;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userServiceImpl.selectUserByName(username);
if (user == null) {
throw new UsernameNotFoundException("根据用户名没有查询到用户!");
}
String roleId = user.getRoleId();
if (StringUtils.isEmpty(roleId)) {
throw new UsernameNotFoundException("没有RoleId!");
}
List<GrantedAuthority> authorities = new ArrayList<>();
if ("1".equalsIgnoreCase(user.getRoleId())) {
authorities.add(new SimpleGrantedAuthority("ROLE_admin"));
}else{
authorities.add(new SimpleGrantedAuthority("ROLE_user"));
}
return new org.springframework.security.core.userdetails.User(user.getUserName(), user.getPassword(), authorities);
}
}

角色权限:

  • 在自定义权限配置类中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
UserDetailsService customerUserService() {
return new CustomUserDetailsService();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("helllo");
http
.authorizeRequests()
.antMatchers("/user/**").hasRole("ADMIN")
.anyRequest().authenticated(); // ★★★★★hasRole()
}
  • 查看hasRole(String role)方法
1
2
3
4
5
6
7
8
9
10
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException(
"role should not start with 'ROLE_' since it is automatically inserted. Got '"
+ role + "'");
}
return "hasRole('ROLE_" + role + "')";
}

总结

  • [x] 由于Spring Security 默认给我们添加了ROLE_,所以我们在自己的数据库中存储的最好是ROLE_USER,不然在Service中还要处理一下。

请求授权

  • Spring Security 是通过重写protected void configure(HttpSecurity http)方法来实现请求拦截的。

  • Spring Security 使用一下匹配器来匹配请求路径。

    • antMatchers : 使用ant风格的路径匹配
    • regexMatchers : 使用正则表达式匹配路径
  • anyRequest :匹配所有请求路径

匹配了请求路径后,需要针对当前用户的信息对请求路径进行安全处理,如下所示:

方法 说明
access(String) Spring EL表达式结果为true时可以访问
anonymous() 匿名可以访问
denyAll() 用户不能访问
fullyAuthenticated() 用户完全认证可访问 【 非Remeber me 下自动登陆 】
hasAnyAuthority(String … str) 如果用户有参数,则其中任意权限可访问
hasAnyRole(String… Str) 如果用户有参数,则其中任意角色可访问
hasAuthority(String… str) 如果用户有参数,则其权限可访问
hasIpAddress(String) 如果用户来自参数中的IP可以访问
hasRole(String) 若用户有参数中的角色可以访问
permitAll() 用户可任意访问
rememberMe() 允许通过remember-me登陆的用户访问
authenticated() 用户登陆后可访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("helllo");
http
.authorizeRequests() // ①
.antMatchers("/static/**").permitAll() // ②设置静态资源可任意访问
.antMatchers("/","/index","/login","/register").permitAll() // ③设置首页登录注册页可任意访问
.antMatchers("/user").hasAnyRole("admin","user") // 角色是admin或user才可以访问
.antMatchers("/user/**").hasRole("admin") // 只有admin可以访问
.anyRequest().authenticated() // ④ 其余所有请求都需要认证后(登陆后)才可访问
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error")
.permitAll()
.and()
.rememberMe()
.tokenValiditySeconds(604800) // 指定cookie的有效期为604800,即一个星期
.key("myKey") // 指定cookie中的私钥
.and()
.logout() // 使用logout方法定制注销行为
.logoutUrl("/logout") // logoutURL指定注销的URL路径
.logoutSuccessUrl("/") // logoutSuccessUrl指定注销成功后跳转的页面
.permitAll();
}
  • ① 通过authorizeRequests方法来开始请求权限配置
  • ② 请求匹配/static/**,可以任意访问
  • ③ 请求匹配设置首页登录注册页可任意访问
  • ④ 其余所有的请求都需要认证后(登陆后)才可访问

定制登陆行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("helllo");
http
┝—————————————————————————————————————————————
┝.authorizeRequests()
┝ .antMatchers("/static/**").permitAll()
┝ .antMatchers("/","/index","/login","/register").permitAll()
┝ .antMatchers("/user").hasAnyRole("admin","user")
┝ .antMatchers("/user/**").hasRole("admin")
┝ .anyRequest().authenticated()
┝ .and()
┝____________________________________________
.formLogin() // ① 通过formLogin()方法定制登陆操作
.loginPage("/login") // 定制登陆页面访问地址
.defaultSuccessUrl("/") // 指定登陆成功后转向的页面
.failureUrl("/login?error") // 指定登陆失败后转向的页面
.permitAll()
.and()
.rememberMe()
.tokenValiditySeconds(604800) // 指定cookie的有效期为604800,即一个星期
.key("myKey") // 指定cookie中的私钥
.and()
.logout() // 使用logout方法定制注销行为
.logoutUrl("/logout") // logoutURL指定注销的URL路径
.logoutSuccessUrl("/") // logoutSuccessUrl指定注销成功后跳转的页面
.permitAll();
}

Spring boot 的支持

  • Spring Boot 针对Spring Security 的自动配置在org.spring.framework.boot.autoconfigure.security包中
  • 主要通过SecurityAutoConfiguration和SecurityProperties来完成配置。
  • 可以在application.yml中配置Spring Security相关的配置
  • SecurityAutoConfiguration导入了SpringBootWebSecurityConfiguration中的配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Spring Security 配置:
security:
user:
name: user # 内存中的用户默认账号为user
password: # 默认用户的密码
role: user # 默认用户角色
require-ssl: false # 是否需要ssl支持
enable-csrf: false # 是否开启跨站请求伪造支持,默认关闭
basic:
enabled: true
realm: Spring
path:
authorize-mode:
filter-order:
headers:
xss: false
cache: false
frame: false
content-type: false
hsts: all
sessions: stateless
ignored: # 用逗号分割开的无需拦截的路径
  • Spring boot 为我们做了如此多的配置,当我们需要自己扩展配置的时候,只需配置类 继承 WebSecurityConfigurerAdapter类即可,无需使用@EnableWebSecurity注解

退出功能

1
2
3
4
5
6
7
8
9
@RequestMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response){
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
}
return "redirect:/login?logout"
}

消息队列 RabbitMQ

添加依赖

1
2
3
// RabbitMQ
compile group: 'org.springframework.boot', name: 'spring-boot-starter-amqp', version: '1.4.0.RELEASE'
Contents
  1. 1. Spring Boot
  2. 2. 第一个项目
  3. 3. 项目构建
  4. 4. 项目结构
  5. 5. Spring Boot属性配置文件详解
    1. 5.1. 自定义属性与加载
    2. 5.2. 参数间的引用
    3. 5.3. 使用随机数
    4. 5.4. 通过命令行设置属性值
    5. 5.5. 多环境配置
  6. 6. Spring boot 应用的入口
  7. 7. 构建web项目应用模板
  8. 8. spring boot 配置Tomcat
    1. 8.1. 在application.java中配置, 配置文件配置tomcat
    2. 8.2. 代码配置tomcat
  9. 9. spring boot 整合 thymeleaf 热部署
  10. 10. 使用Swagger2构建强大的RESTful API文档
    1. 10.1. 添加文档内容
  11. 11. Spring Boot中Web应用的统一异常处理
    1. 11.1. 创建全局异常处理类:
    2. 11.2. 返回JSON格式
  12. 12. Spring Boot中使用JdbcTemplate访问数据库
    1. 12.1. 数据源配置
    2. 12.2. 嵌入式数据库支持
    3. 12.3. 连接生产数据源
    4. 12.4. 使用JdbcTemplate操作数据库
  13. 13. Spring Boot中使用Spring-data-jpa让数据访问更简单、更优雅
  14. 14. Spring Boot中使用Redis数据库
    1. 14.1. 引入依赖
    2. 14.2. 参数配置
  15. 15. Spring Boot整合MyBatis
    1. 15.1. 整合依赖
    2. 15.2. 使用MyBatis
  16. 16. Spring Boot中使用AOP统一处理Web请求日志
    1. 16.1. 引入依赖
    2. 16.2. 实现Web层的日志切面
    3. 16.3. 优化:AOP切面中的同步问题
    4. 16.4. 优化:AOP切面的优先级
  17. 17. SpringApplication
    1. 17.1. 自定义Banner
    2. 17.2. 自定义SpringApplicatio
    3. 17.3. 流畅的构建API
    4. 17.4. Application事件和监听器
    5. 17.5. Web环境
    6. 17.6. 命令行启动器
    7. 17.7. Application退出
  18. 18. 外化配置
    1. 18.1. 配置随机值
    2. 18.2. 访问命令行属性
  19. 19. Application属性文件
  20. 20. 特定的Profile属性
  21. 21. 属性占位符
    1. 21.1. 使用YAML代替Properties
    2. 21.2. 检查应用的运行状态。
      1. 21.2.1. 如果希望了解的更多一些,必须要试试这个参数 --debug
  22. 22. Spring boot 整合Redis
    1. 22.1. 引入依赖
    2. 22.2. 参数配置
    3. 22.3. 测试访问
    4. 22.4. Redis存储对象
  23. 23. Spring Boot 使用Redis作为缓存
    1. 23.1. 引入依赖
    2. 23.2. 参数配置
    3. 23.3. 编写RedisCacheConfig配置类:
      1. 23.3.1. Redis 缓存配置:
    4. 23.4. 添加注解@Cacheable(…)
  24. 24. 缓存的生命周期
    1. 24.1. 缓存注解 @Cacheable、@CachePut、@CacheEvict
    2. 24.2. 使用注解@CacheConfig
    3. 24.3. 自定义注解
  25. 25. 总算明白了@Cacheable注解如何缓存
    1. 25.1. 缓存自定义对象
    2. 25.2. Spirng Cache @Cacheable注解的key问题
  26. 26. 获取所有定义的Bean
  27. 27. 基于Spring提供支持不同设备的页面
    1. 27.1. 添加依赖
  28. 28. 创建定时任务
    1. 28.1. 在Spring Boot的主类中加入@EnableScheduling注解,启用定时任务的配置
    2. 28.2. 创建定时任务实现类
    3. 28.3. @Scheduled详解
    4. 28.4. cron 详解
    5. 28.5. 创建Spring Security的配置类WebSecurityConfig
    6. 28.6. 指定登录页地址
  29. 29. Spring boot 过滤器 拦截器
    1. 29.1. 自定义过滤器类
    2. 29.2. ServletContext监听器(Listener)ServletContextListener
    3. 29.3. ServletContext监听器(Listener) HttpSessionListener
  30. 30. Spring Boot 整合 Spring Security
    1. 30.1. 添加依赖
    2. 30.2. 根据 http 状态码处理请求
      1. 30.2.1. 分析
    3. 30.3. 添加spring Security配置
      1. 30.3.1. 添加一个SecurityConfig类
      2. 30.3.2. 自定义实现UserDetailsService接口
    4. 30.4. 角色权限:
    5. 30.5. 请求授权
    6. 30.6. 定制登陆行为
  31. 31. Spring boot 的支持
    1. 31.1. 退出功能
  32. 32. 消息队列 RabbitMQ
    1. 32.1. 添加依赖