Spring Boot
第一个项目
- 选择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; }
|
- 通过单元测试来验证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项目应用模板
提别注意:
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 热部署
- spring.thymeleaf.cache=
false
- 修改完代码后,
Ctrl + F9
,重新make一下
使用Swagger2构建强大的RESTful API文档
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
函数创建Docket
的Bean
之后,
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") 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.getRequest
URL
()
http://localhost:8080/err
- req.getRequest
URI
()
实现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格式
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; }
|
- 创建一个自定义异常,用来实验捕获该异常,并返回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"); } }
|
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: spring.datasource.username=dbuser spring.datasource.password=dbpass spring.datasource.driver-class-name=com.mysql.jdbc.Driver
|
使用JdbcTemplate操作数据库
- Spring的JdbcTemplate是自动配置的,你可以直接使用
@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() { userSerivce.deleteAllUsers(); } @Test public void test() throws Exception { userSerivce.create("a", 1); userSerivce.create("b", 2); userSerivce.create("c", 3); userSerivce.create("d", 4); userSerivce.create("e", 5); Assert.assertEquals(5, userSerivce.getAllUsers().intValue()); userSerivce.deleteByName("a"); userSerivce.deleteByName("e"); 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: 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;
|
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'
|
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); } }
|
1 2 3 4 5 6
| 2016-05-19 13:42:13,156 INFO WebLogAspect:41 - URL : http: 2016-05-19 13:42:13,156 INFO WebLogAspect:42 - HTTP_METHOD : http: 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(…)
方
法。在你的应用运行时,应用事件会以下面的次序发送:
- 在运行开始,但除了监听器注册和初始化以外的任何处理之前,会发送一个ApplicationStartedEvent
- 在Environment将被用于已知的上下文,但在上下文被创建前,会发送一个ApplicationEnvironmentPreparedEvent
- 在refresh开始前,但在bean定义已被加载后,会发送一个ApplicationPreparedEvent。
- 启动过程中如果出现异常,会发送一个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) { } }
|
- 如果一些CommandLineRunner beans被定义必须以特定的次序调用,你可以额外实现
org.springframework.core.Ordered
接口或使用org.springframework.core.annotation.Order
注
解
Application退出
- 每个SpringApplication在退出时为了确保ApplicationContext被优雅的关闭,将会注册一个
JVM
的shutdown钩子。
- 所有标准的Spring生命周期回调(比如,DisposableBean接口或@PreDestroy注解)都能使用。
- 如果beans想在应用结束时返回一个特定的退出码(exitcode),可以实现
org.springframework.boot.ExitCodeGenerator
接口。
外化配置
- 命令行参数
- 来自于java:comp/env的JNDI属性
- Java系统属性(System.getProperties())
- 操作系统环境变量
- 只有在random.*里包含的属性会产生一个RandomValuePropertySource
- 在打包的jar外的应用程序配置文件(application.properties,包含YAML和profile变量)
- 在打包的jar内的应用程序配置文件(application.properties,包含YAML和profile变量)
- 在@Configuration类上的@PropertySource注解
- 默认属性(使用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中:
- 当前目录下的一个
/config
子目录
- 当前目录
- 一个classpath下的
/config
包
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
| 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; } }
|
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(); 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
| compile group: 'org.springframework.boot', name: 'spring-boot-starter-cache', version: '1.4.0.RELEASE' 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(); 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
@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 详解
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
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"); } }
|
- WebSecurityConfigurerAdapter在
config(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>
|
- 一个POST请求到/login用来验证用户
- 如果参数有错误,验证尝试失败
- 如果请求参数logout存在则登出
- 登录名参数必须被命名为username
- 密码参数必须被命名为password
- CSRF参数,了解更多查阅 后续“包含CSRF Token” 和 “跨站请求伪造(CSRF)”相关章节
Spring boot 过滤器 拦截器
自定义过滤器类
- 定义一个类 MyFilter 继承 Filter(
import javax.servlet.Filter
)
- 在类上添加 注解
@WebFilter(filterName = "myFilter",urlPatterns = "/*")
- 在启动类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
- 定义一个类 MyServletContextListener 实现 ServletContextListener(
import javax.servlet.ServletContextListener
)
- 在类上添加 注解
@WebListener
- 在启动类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
- 定义一个类 MyServletContextListener 实现 HttpSessionListener(
import javax.servlet.HttpSessionListener
)
- 在类上添加 注解
@WebListener
- 在启动类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
| 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")); } * 此为内部类实现方式 */ 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
注册进来,便会调用EmbeddedServletContainerCustomizer 的 customize 方法,此时,我们可以对整个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") .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(); }
|
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中还要处理一下。
请求授权
匹配了请求路径后,需要针对当前用户的信息对请求路径进行安全处理,如下所示:
方法 |
说明 |
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") .antMatchers("/user/**").hasRole("admin") .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/") .failureUrl("/login?error") .permitAll() .and() .rememberMe() .tokenValiditySeconds(604800) .key("myKey") .and() .logout() .logoutUrl("/logout") .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() .loginPage("/login") .defaultSuccessUrl("/") .failureUrl("/login?error") .permitAll() .and() .rememberMe() .tokenValiditySeconds(604800) .key("myKey") .and() .logout() .logoutUrl("/logout") .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
| compile group: 'org.springframework.boot', name: 'spring-boot-starter-amqp', version: '1.4.0.RELEASE'
|