Springboot + Spring Security + redis + rabbitmq 配置

本文讲述的是基于springboot项目来整合spring Security,mysql,redis,rabbitmq的配置过程。

引入项目依赖

首先利用 IntelliJ 编辑器新建一个 gradle 项目,然后将build.gradle文件改为以下内容,重新导入依赖

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
buildscript {
ext {
springBootVersion = '2.0.3.RELEASE'
}
repositories {
mavenCentral()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0'
}
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'org.junit.platform.gradle.plugin'

group 'com.ray.parctice'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
mavenCentral()
}

dependencies {
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.15'
compile group: 'org.flywaydb', name: 'flyway-core', version: '5.2.4'
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-data-redis")
compile group: 'redis.clients', name: 'jedis', version: '3.0.1'
compile("org.springframework.boot:spring-boot-starter-amqp")

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.0.0'

testCompile('org.junit.jupiter:junit-jupiter-params:5.0.0')
testCompile("org.springframework.security:spring-security-test")

testRuntime('org.junit.jupiter:junit-jupiter-engine:5.0.0')

testRuntime "com.h2database:h2"
}

Springboot + mysql 配置

这里就搭建一个最基本的Springboot项目,大概分为以下几步:

在application.yml中添加mysql的配置项

1
2
3
4
5
6
7
8
9
10
spring:
datasource:
url: jdbc:mysql://localhost:3306/myblob?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT
username: root
password: 123456

driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate

新建User Entity 以及对应的 Respository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//MyUser.java
@Entity
@Table(name = "my_user")
public class MyUser implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;

@Column(nullable = false, unique = true)
private String name;

@Column(nullable = false)
private String password;

private String feeling;

// getters and setters
}
1
2
3
4
//MyUserRepository
@Repository
public interface MyUserRepository extends JpaRepository<MyUser, Integer> {
}

新建 MyUser 的 Service 和 Controller

1
2
3
4
5
6
7
//MyUserService.java
public interface MyUserService {
MyUser getUserById(int id) throws Exception;
void registerUser(MyUser user) throws Exception;
MyUser updateUser(MyUser user) throws Exception;
void deleteUserById(int id) throws Exception;
}
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
//MyUserSeiviceImpl.java
package com.jimmy.myBlob.service.impl;

import com.jimmy.myBlob.model.MyUser;
import com.jimmy.myBlob.repository.MyUserRepository;
import com.jimmy.myBlob.service.MyUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class MyUserServiceImpl implements MyUserService {
@Autowired
private MyUserRepository myUserRepository;

public void registerUser(MyUser user) throws Exception{
myUserRepository.save(user);
}

public MyUser getUserById(int id) throws Exception {
return myUserRepository.findById(id).get();
}

@Override
public MyUser updateUser(MyUser user) throws Exception {
return myUserRepository.save(user);
}

@Override
public void deleteUserById(int id) throws Exception {
myUserRepository.deleteById(id);
}
}

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
//MyUserController.java
@RestController
public class MyUserController {
@Autowired
private MyUserService myUserService;

@PostMapping("/users")
public ResponseEntity<?> registerUser(@RequestBody MyUser user) throws Exception {
myUserService.registerUser(user);
return new ResponseEntity<>(HttpStatus.CREATED);
}

@GetMapping("/users/{userId}")
public ResponseEntity<?> getUserById(@PathVariable int userId) throws Exception {
return new ResponseEntity<>(myUserService.getUserById(userId), HttpStatus.OK);
}

@PutMapping("/users")
public ResponseEntity<?> updateUser(@RequestBody MyUser user) throws Exception {
return new ResponseEntity<>(myUserService.updateUser(user), HttpStatus.OK);
}

@DeleteMapping("/users/{userId}")
public ResponseEntity<?> deleteUserById(@PathVariable int userId) throws Exception {
myUserService.deleteUserById(userId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}

到这里为止就完成了一个最基础的Springboot + mysql 项目的基本配置,这个项目采用的是Restful风格的请求,可以处理User的CRUD。

配置 redis 作为缓存

在我们的某些项目中会存在大量的数据,当数据量变大的时候,我们的数据库的效率就会变成我们项目的一个瓶颈,这一点是无法避免的,所以我们只能改变一种方式来避开大量数据在数据库中查询慢的情况。

虽然项目中可能会存在大量数据,但是有的数据使用频率比较高或者说是对于速度要求比较高,比如某个热点视频的数据,它会被大量请求同时又要求速度够快,而有的数据则正好相反,前者叫做热数据,后者叫做冷数据,对于热数据我们可以将它们放到缓存中来提高效率。

所以我们需要一种方式来帮助我们管理缓存的数据,redis 作为一种存储在内存中的 nosql 型数据库可以很好地帮助我们解决这些问题。redis 是单线程的,但是因为它是内存数据库,所以读写速度非常快,同时由于它是单线程的所以避免了多线程带来的进程切换和加锁等机制带来的消耗;redis 也存在快照机制,帮助我们在突然断电的情况下保护我们的数据。

虽然redis 很强大,但是整合到我们的spring项目中非常简单。

安装redis

和其他数据库一样,首先你需要在自己的系统上安装redis并将其运行

修改application.yml配置文件

将application.yml文件改为以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
datasource:
url: jdbc:mysql://localhost:3306/myblob?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT
username: root
password: 123456

driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate
cache:
type: redis
redis:
jedis:
pool:
max-idle: 10
max-wait: 1000000
max-active: 100
host: 127.0.0.1
port: 6379
timeout: 10000

修改Service文件,在每个请求上面添加cache注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@CachePut(cacheNames="myUser", key="#user.id")
public void registerUser(MyUser user) throws Exception{
myUserRepository.save(user);
}

@Cacheable(cacheNames="myUser", key="#id")
public MyUser getUserById(int id) throws Exception {
simulateSlowService();
return myUserRepository.findById(id).get();
}

@CachePut(cacheNames="myUser", key="#user.id")
@Override
public MyUser updateUser(MyUser user) throws Exception {
return myUserRepository.save(user);
}

@CacheEvict(cacheNames="myUser", key="#id")
@Override
public void deleteUserById(int id) throws Exception {
myUserRepository.deleteById(id);
}

这里有两点需要注意

  1. key的值需要与传入的参数对应,比如第一个请求的key就是user中的id属性,那就需要写成#user.id,这里不能写成#myUser.idcacheNames.id)或者#my_user.id(tableName.id),一旦填错就会发现你的请求报了500但是数据库写入成功。
  2. CRUD用到的注解不同,在支持Spring Cache的环境下,对于使用@Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。@CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。 @CacheEvict是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。

在项目入口添加@EnableCaching

1
2
3
4
5
6
7
@SpringBootApplication
@EnableCaching
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}

Spring Security 配置

添加 WebSecurityConfig.java 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
@EnableWebSecurity
public class WebSecurityConfig {
@Configuration
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().and()
.authorizeRequests()
.antMatchers("/users", "/users/**").permitAll().anyRequest().authenticated()
.and().csrf().disable();
}
}
}

RabbitMQ 配置

安装并启动 RabbitMQ

这一步可以去网上搜索一下,有很多教程

修改application.yml配置

在spring 配置下添加rabbitmq的配置

1
2
3
4
5
6
7
8
rabbitmq:
host: localhost
port: 15672
username: guest
password: guest
publisher-confirms: true
publisher-returns: true
template.mandatory: true

添加RabbitConstants类去获取配置文件中的配置信息

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
//RabbitConstants.java
@Component
@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitConstants {
private String username;

private String password;

private int port;

private String host;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public void setPort(int port) {
this.port = port;
}

public void setHost(String uhost) {
this.host = uhost;
}


public int getPort() {
return port;
}

public String getHost() {
return host;
}


}

这里提供了两种方式去获取配置,第一种是直接在类上方加@ConfigurationProperties(prefix = “spring.rabbitmq”)注解,那么类中与配置文件中名字相同的属性会被自动匹配过来;第二种方式是直接在属性上加@Value注解,但是这种方式就需要完全指定。

添加RabbitmqConfig文件

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
@Configuration
public class RabbitmqConfig {

public static final String EXCHANGE = "spring-boot-exchange2";
public static final String ROUTINGKEY = "spring-boot-routingKey2";

@Autowired
private RabbitConstants rabbitConstants;
@Bean
public CachingConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setAddresses(rabbitConstants.getHost());
connectionFactory.setUsername(rabbitConstants.getUsername());
connectionFactory.setPassword(rabbitConstants.getPassword());
connectionFactory.setVirtualHost("/");
// 这里需要显示调用才能进行消息的回调 必须要设置
connectionFactory.setPublisherConfirms(true);
return connectionFactory;
}

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
return template;
}


@Bean
public DirectExchange defaultExchange() {
return new DirectExchange(EXCHANGE);
}

@Bean
public org.springframework.amqp.core.Queue queue() {
return new Queue("spring-boot-queue", true);
}

@Bean
public Binding binding() {
return BindingBuilder.bind(queue()).to(defaultExchange()).with(RabbitmqConfig.ROUTINGKEY);
}


@Bean
public SimpleMessageListenerContainer messageContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory());
container.setQueues(queue());
container.setExposeListenerChannel(true);
container.setMaxConcurrentConsumers(1);
container.setConcurrentConsumers(1);
// 设置确认模式手工确认
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
container.setMessageListener(new ChannelAwareMessageListener() {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
byte[] body = message.getBody();
System.out.println("receive msg : " + new String(body));
//确认消息成功消费
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
});
return container;
}

}

添加RabbitMQ消息发送器

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
@Component
public class RabbitmqSendMessage implements RabbitTemplate.ConfirmCallback {
private RabbitTemplate rabbitTemplate;


/**
* 构造方法注入
*/
@Autowired
public RabbitmqSendMessage(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
//这是是设置回调能收到发送到响应,confirm()在下面解释
rabbitTemplate.setConfirmCallback(this);
}

public void sendMsg(String content) {
CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
//convertAndSend(exchange:交换机名称,routingKey:路由关键字,object:发送的消息内容,correlationData:消息ID)
rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE, RabbitmqConfig.ROUTINGKEY, content, correlationId);
}

@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println(" 回调id:" + correlationData);
if (ack) {
System.out.println("消息成功消费");
} else {
System.out.println("消息消费失败:" + cause);
}
}
}

修改Controller文件,添加rabbitmq

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
//MyUserController.java
@RestController
public class MyUserController {
@Autowired
private MyUserService myUserService;

@Resource
private RabbitTemplate rabbitTemplate;

@Resource
private RabbitmqSendMessage rabbitmqSendMessage;

@PostMapping("/users")
public ResponseEntity<?> registerUser(@RequestBody MyUser user) throws Exception {
myUserService.registerUser(user);
return new ResponseEntity<>(HttpStatus.CREATED);
}

@GetMapping("/users/{userId}")
public ResponseEntity<?> getUserById(@PathVariable int userId) throws Exception {
rabbitmqSendMessage.sendMsg("123");
return new ResponseEntity<>(myUserService.getUserById(userId), HttpStatus.OK);
}

@PutMapping("/users")
public ResponseEntity<?> updateUser(@RequestBody MyUser user) throws Exception {
return new ResponseEntity<>(myUserService.updateUser(user), HttpStatus.OK);
}

@DeleteMapping("/users/{userId}")
public ResponseEntity<?> deleteUserById(@PathVariable int userId) throws Exception {
myUserService.deleteUserById(userId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}