SpringBoot
1、SpringBoot简介
SpringBoot是一个javaweb的开发框架,简化开发,约定大于配置!
SpringBoot的主要优点:
- 让Spring开发者更快的入门
- 开箱即用,提供各种默认配置来简化项目配置
- 内嵌式容器简化Web项目
- 没有冗余代码生成和XML配置的要求
2、微服务简介
- 单体架构:打包成一个独立的单元(导入一个jar包或者是一个war包)部署完成应用之后,应用通过一个进程的方式来运行,例如,MVC三层架构
- 微服务架构:一个大型的复杂软件应用,由一个或者多个微服务组成,系统中的各个微服务可以被独立部署,各个微服务之间是松耦合的,每个微服务仅仅关注于完成一件任务并很好的完成该任务
3、第一个SpringBoot程序
使用idea创建工程
- 1、创建一个新项目
- 2、选择spring initalizr
- 3、填写项目信息
- 4、选择初始化的组件(勾选 Web:Spring Web )
- 5、填写项目路径
- 6、等待项目构建成功
项目结构分析src
- main/java底下程序主启动类:TestApplication.java
- main/resources底下配置文件:application.properties,语法结构 :key=value,只能键值对,中文会有乱码 , 我们需要去IDEA中设置编码格式为UTF-8;更推荐使用application.yaml
- test/java底下测试类:TestApplicationTests.java
- pom.xml依赖:
- spring-boot-starter-parent核心依赖,导入依赖默认是不需要写版本,但是如果导入的包没有在依赖中管理着就需要手动配置版本
- spring-boot-starter-xxx是spring-boot的场景启动器,要用什么功能就导入什么样的场景启动器即可,例如spring-boot-starter-web帮我们导入了web模块正常运行所依赖的组件
编写一个http接口
在主程序的同级目录新建controller包
在controller包下新建一个HelloController类
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @RequestMapping("/hello") public String hello(){ return "hello world"; } }
浏览器访问http://localhost:8080/hello,页面出现hello world
点击idea上Maven的Lifecycle/package打成jar包,在target目录下生成一个jar包,可以在任何地方运行
4、yaml语法
- 空格不能省略
- 以缩进来控制层级关系,左边对齐的一列数据都是同一个层级的
- 属性和值的大小写敏感
#数字,布尔值,字符串
k1: v1
#对象、map
k2:
v1:1
v2:2
#对象、map-行内写法
k22: {v1:1,v2:2}
#数组
k3:
- v1
- v2
#数组-行内写法
k33: [v1,v2]
#使用占位符生成随机数
name: ${random.uuid}
age: ${random.int}
#松散绑定:last-name和lastName一样,-后面的字母默认是大写
5、属性赋值
5.1、@Value方式
- 实体类 Dog
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
//注册bean
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Dog {
@Value("三七")
private String name;
@Value("2")
private Integer age;
}
- 测试类中测试
@SpringBootTest
class TestApplicationTests {
@Autowired
private Dog dog;
@Test
void contextLoads() {
System.out.println(dog);
}
}
//控制台打印:Dog(name=三七, age=2)
5.2、yaml注入方式
- 在springboot项目中的resources目录下新建一个文件 application.yaml
cat:
name: 二十一
age: 1
- 实体类Cat
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
//注册bean
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
//将配置文件中配置的每一个属性的值,映射到这个组件中,prefix指定key
@ConfigurationProperties(prefix = "cat")
public class Cat {
private String name;
private Integer age;
}
- pom.xml导入依赖
<!-- 导入配置文件处理器,需要重启idea -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
- 测试类中测试
@SpringBootTest
class TestApplicationTests {
@Autowired
private Cat cat;
@Test
void contextLoads() {
System.out.println(cat);
}
}
//控制台打印:Cat(name=二十一, age=1)
5.3、指定配置文件方式
- 在springboot项目中的resources目录下新建一个文件 pig.properties
name=小猪
age=1
- 实体类Pig
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
//注册bean
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
//加载指定的配置文件
@PropertySource("classpath:pig.properties")
public class Pig {
//指定属性值
@Value("${name}")
private String name;
@Value("${age}")
private Integer age;
}
- 测试类中测试
@SpringBootTest
class TestApplicationTests {
@Autowired
private Pig pig;
@Test
void contextLoads() {
System.out.println(pig);
}
}
//控制台打印:Pig(name=小猪, age=1)
6、JSR303数据校验
- pom.xml导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- people.yaml
people:
name: 小雷
age: 18
email: 123456789@qq.com
- 实体类People
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Email;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@ConfigurationProperties(prefix = "people")
//@Validated数据校验
@Validated
public class People {
@NotNull(message = "用户名不能为空")
private String userName;
@Max(value = 100,message = "年龄不能超过100岁")
private int age;
@Email(message = "邮箱格式错误")
private String email;
}
/**
* 空检查
* @Null 验证对象是否为null
* @NotNull 验证对象是否不为null, 无法查检长度为0的字符串
* @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0, 只对字符串, 且会去掉前后空格.
* @NotEmpty 检查约束元素是否为NULL或者是EMPTY.
* Booelan检查
* @AssertTrue 验证 Boolean 对象是否为 true
* @AssertFalse 验证 Boolean 对象是否为 false
* 长度检查
* @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
* @Length(min=, max=) string is between min and max included.
* 日期检查
* @Past 验证 Date 和 Calendar 对象是否在当前时间之前
* @Future 验证 Date 和 Calendar 对象是否在当前时间之后
* @Pattern 验证 String 对象是否符合正则表达式的规则
* */
7、多环境配置
7.1、使用properties
新建application-test.properties 代表测试环境配置
新建application-dev.properties 代表开发环境配置
springboot默认使用application.properties,需要在改该文件里指定使用的环境
spring.profiles.active=dev
7.2、使用yaml多文档块
不需要创建多个yaml,只需要在application.yaml中配置多个环境即可
#选择要激活那个环境块
spring:
profiles:
active: dev
---
server:
port: 8082
spring:
profiles: dev #配置环境的名称
---
server:
port: 8083
spring:
profiles: prod #配置环境的名称
8、SpringBoot Web开发
8.1、静态资源映射规则
8.1.1、webjars
访问:localhost:8080/webjars/xxx
idea中按shift+shift,搜索WebMvcAutoConfigurationAdapter类(SpringBoot中,SpringMVC的web配置类),有一个方法addResourceHandlers是用来添加资源处理
所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源
Webjars本质就是以jar包的方式引入静态资源 ,直接导入即可
例如,pom.xml中引入jquery,访问http://localhost:8080/webjars/jquery/3.4.1/jquery.js即可
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
registration.addResourceLocations(new Resource[]{resource});
}
});
}
}
8.1.2、resources、static、public
访问:localhost:8080/xxx
- ResourceProperties类可以设置和我们静态资源有关的参数,指向了它会去寻找资源的文件夹
以下四个目录存放的静态资源都可以被识别,优先级resources>static>public
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
resources根目录下新建对应的文件夹,都可以存放我们的静态文件
8.2、首页、图标、404定制
页面:
- 在resources下的文件夹下,新建index.html,被 /** 映射
- 访问http://localhost:8080/展示index页面
图标:
在resources下的文件夹下,放入图标文件favicon.ico
关闭SpringBoot默认图标
spring.mvc.favicon.enabled=false
清除浏览器缓存,刷新网页显示图标
404页面:
- 在template文件夹下创建error文件夹,创建404.html页面,访问到未知url时,springboot会自动显示404页面
8.3、Thymeleaf模板引擎
8.3.1、Thymeleaf使用方法
pom.xml导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
只需要把页面放在templates底下就可以被自动识别
在templates下新建test.html
<!DOCTYPE html> <!--导入thymeleaf命名空间的约束--> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!--th:text将div中的内容设置为它指定的值--> <div th:text="${msg}"></div> </body> </html>
controller
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class TestController { @RequestMapping("/test") public String test(Model model){ //存入数据 model.addAttribute("msg","thymeleaf模板引擎测试"); //classpath:/templates/test.html return "test2"; } }
浏览器访问http://localhost:8080/test,展示test.html的内容,msg值为controller中传入的
8.3.2、Thymeleaf语法
参考网上即可…
- th属性,常用th属性如下:
- th:text:文本替换
- th:utext:支持html的文本替换
- th:value:属性赋值
- th:each:遍历循环元素
- th:if:判断条件,类似的还有th:unless,th:switch,th:case
- th:insert:代码块引入,类似的还有th:replace,th:include,常用于公共代码块提取的场景
- th:fragment:定义代码块,方便被th:insert引用
- th:object:声明变量,一般和*{}一起配合使用,达到偷懒的效果
- th:attr:设置标签属性,多个属性可以用逗号分隔
8.4、员工管理系统实战-无数据库
无数据库,使用模拟数据(首页、登录拦截、增删改查、404页面)
链接:https://pan.baidu.com/s/1pL55cudzdVC-gPUpJM9_gw
提取码:8qzo
9、SpringBoot整合数据库
9.1、整合JDBC
- pom.xml导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
- application.yaml配置
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/ums?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
- dataSource连接测试
@Autowired
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
Connection connection = dataSource.getConnection();
connection.close();
}
- JdbcTemplate增删改查
@Autowired
JdbcTemplate jdbcTemplate;
/**
* Spring Boot默认提供了数据源,默认提供了JdbcTemplate
* JdbcTemplate自动注入数据源,自动连接和关闭,用于简化JDBC操作
*
* 查询query、queryForXXX
* 新增、修改、删除:update、batchUpdate
* 执行DDL语句:execute,例如create、alter、drop、truncate
* 执行存储过程、函数相关语句:call
*/
@Test
void contextLoads2() throws SQLException {
//查询
List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from employee");
System.out.println(maps);
//新增
jdbcTemplate.update("insert into employee(ename, email, gender, birthday, did) VALUES ('test', '1234567@qq.com', 1, '2022-03-11 10:39:42', 101)");
//修改
Object[] objects = new Object[2];
objects[0]="姓名修改";
objects[1]=1;
jdbcTemplate.update("update employee set ename=? where id=?",objects);
//删除
Object[] objects1 = new Object[1];
objects[0]=1;
jdbcTemplate.update("delete employee where id=?",objects1);
}
9.2、整合Druid
Druid是监控 DB 池连接和 SQL 的执行情况连接池
- pom.xml导入依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<!--监控用到-->
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
- application.yaml配置
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/ums?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源
#Spring Boot默认不注入以下属性,需要自己绑定
#druid数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
- Druid配置类:数据源属性注入、数据源监控、监控过滤器
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.HashMap;
@Configuration
public class DruidConfig {
/**
* 将自定义的 Druid数据源添加到容器中,不再让SpringBoot自动创建
* 将全局配置文件中前缀为spring.datasource的属性值注入到com.alibaba.druid.pool.DruidDataSource的同名参数中
*/
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() {
return new DruidDataSource();
}
/**
* Druid 数据源具有监控的功能,并提供了一个 web界面方便用户查看
* 配置Druid数据源监控
* 启动项目后,访问http://localhost:8080/druid/login.html
*/
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
HashMap<String, String> initParams = new HashMap<>();
//后台登录账号密码
initParams.put("loginUsername", "admin");
initParams.put("loginPassword", "123456");
//后台允许谁可以访问:localhost只允许本机访问,为空或者为null允许所有访问
initParams.put("allow", "");
//后台拒绝谁访问
//initParams.put("username", "192.168.1.20");
//设置初始化参数
bean.setInitParameters(initParams);
return bean;
}
/**
* 配置 Druid web监控 filter过滤器
*/
@Bean
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
//exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
HashMap<String, String> initParams = new HashMap<>();
initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
bean.setInitParameters(initParams);
//"/*" 表示过滤所有请求
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
}
- dataSource连接测试,看到配置参数已经生效
@Autowired
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
Connection connection = dataSource.getConnection();
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
//打印druidDataSource 数据源最大连接数:20
System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
connection.close();
}
9.3、整合mybatis(重点)
- 数据库准备
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`id` int(11) NOT NULL,
`departmentName` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
INSERT INTO `department` VALUES (101, '教学部');
INSERT INTO `department` VALUES (102, '市场部');
INSERT INTO `department` VALUES (103, '教研部');
INSERT INTO `department` VALUES (104, '运营部');
INSERT INTO `department` VALUES (105, '后勤部');
- pom.xml导入依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
- application.yaml中datasource配置不变(同章节10),新增mybatis配置
mybatis:
type-aliases-package: com.leijiao.system.pojo # 实体类路径
mapper-locations: classpath:mybatis/mapper/*.xml # maper.xml路径(resource底下)
- pojo
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
//部门表
public class Department {
private Integer id;
private String dname;
}
- mapper
import com.leijiao.system.pojo.Department;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Mapper
@Repository
public interface DepartmentMapper {
// 获取所有部门信息
List<Department> getAllDepartments();
// 通过id获得部门
Department getDepartmentById(@Param("id") int id);
}
- mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.leijiao.system.mapper.DepartmentMapper">
<select id="getAllDepartments" resultType="Department">
select * from department
</select>
<select id="getDepartmentById" resultType="Department" parameterType="int">
select * from department where id=#{id}
</select>
</mapper>
- 测试
@Autowired
DepartmentMapper departmentMapper;
@Test
void contextLoads3(){
System.out.println(departmentMapper.getAllDepartments());
}
12、安全框架
- 用户认证(Authentication):凭据,如用户名/密码
- 用户授权(Authorization):用户认证后,授予访问资源的完全权限
- Spring Security功能比shiro强大,shiro更加简化。大项目用Spring Security,小项目用shiro
12.1、Spring Security
- pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- Spring Security配置类,继承WebSecurityConfigurerAdapter
- 重写configure(HttpSecurity http)方法,定制授权规则,常用方法如下
- formLogin():开启自动配置的登录功能,表单身份验证
- logout():开启自动配置的注销的功能
- rememberMe():记住我
- authorizeHttpRequests():访问放行/限制
- csrf():CSRF支持,跨站请求伪造
- 重写configure(AuthenticationManagerBuilder auth)方法, 定义认证规则
- inMemoryAuthentication():内存验证
- jdbcAuthentication():基于JDBC的验证
- userDetailsService:添加UserDetailsService
- authenticationProvider(AuthenticationProvider authenticationProvider):添加AuthenticationProvider
- ldapAuthentication():LADP验证
- 重写configure(WebSecurity web)方法:用于配置静态资源的处理方式,可使用 Ant 匹配规则
- 重写configure(HttpSecurity http)方法,定制授权规则,常用方法如下
package com.leijiao.system.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
//开启WebSecurity模式
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定制授权规则
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* antMatchers("/").permitAll():放行,首页所有人可以访问
*.anyRequest().authenticated():所有请求都要认证
*/
http.authorizeHttpRequests().antMatchers("/","/index.html").permitAll()
.antMatchers("/user/login").permitAll()
.antMatchers(
"/**/*.css",
"/**/*.js",
"/**/*.svg"
).permitAll()
.anyRequest().authenticated()
/**
* 关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
*/
http.csrf().disable();
/**
* 开启自动配置的登录功能:如果没有权限,就会跳转到登录页面
* loginPage("/") 指定登录页
* loginProcessingUrl("/user/login") 指定登录提交页
* /login?error 重定向到这里表示登录失败
*/
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/")
.loginProcessingUrl("/user/login");
/**
* 开启自动配置的注销的功能
* /logout 注销请求
* .logoutSuccessUrl("/"); 注销成功来到首页
*/
http.logout().logoutSuccessUrl("/");
/**
* 记住我
*/
http.rememberMe().rememberMeParameter("remember");
}
/**
* 定义认证规则
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/**
* 可以在内存中定义,也可以在jdbc中取
* bcrypt密码加密
*/
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("admin");
}
}
12.2、shiro
12.2.1、简介
GitHub:https://github.com/apache/shiro.git
Shiro三大功能模块
- Subject:主体,一般指用户(把操作交给SecurityManager)
- SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件(关联Realm)
- Realms:用于进行权限信息的验证,shiro链接数据的桥梁
细分功能
- Authentication:身份认证/登录,验证用户是否拥有相应的身份(账号密码验证)
- Authorization:授权,验证某个已认证的用户是否拥有某权限
- Session Manager:会话管理,用户登录后,用户信息保存在session会话中
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储
- Web Support:Web支持,集成Web环境
- Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中
- Concurrency:多线程并发验证,在一个线程中开启另一个线程,可以把权限自动传播过去
- Testing:测试支持
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问
- Remember Me:记住我
12.2.2、quickstart分析
- 分析GitHub下载\shiro-main\samples\quickstart.java
//获取当前用户
Subject currentUser = SecurityUtils.getSubject();
//获取当前用户session
Session session = currentUser.getSession();
//判断当前用户是否被认证
currentUser.isAuthenticated();
//token令牌
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
//记住我
token.setRememberMe(true);
//登录
currentUser.login(token);
//获取当前用户的认证
currentUser.getPrincipal();
//判断当前用户是否拥有xx角色
currentUser.hasRole("xx");
//判断当前用户是否拥有xx权限
currentUser.isPermitted("xx");
//注销
currentUser.logout()
12.2.3、shiro内置过滤器
- 认证相关
- anon:
/admins/**=anon # 表示该 uri 可以匿名访问
- authc:
/admins/**=authc # 表示该 uri 需要认证才能访问
- authcBasic:
/admins/**=authcBasic # 表示该 uri 需要 httpBasic 认证
- logout:
/logout=logout # 表示注销
- user:
/admins/**=user # 表示该 uri 需要认证或记住我认证才能访问
- anon:
- 授权相关
- roles:
/admins/**=roles[admin] # 表示该 uri 需要认证用户拥有 admin 角色才能访问
- perms:
/admins/**=perms[user:add:*] # 表示该 uri 需要认证用户拥有 user:add:* 权限才能访问
- port:
/admins/**=port[8081] # 表示该 uri 需要使用 8081 端口
- rest:
/admins/**=rest[user] # 相当于 /admins/**=perms[user:method],其中,method 表示 get、post、delete等
- ssl:
/admins/**=ssl # 表示该 uri 需要使用 https 协议
- noSessionCreation:
需要指定权限才能访问
- roles:
12.2.4、springboot整合shiro
主要就是CustonRealm类、shiroConfig类
- pom.xml依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.8.0</version>
</dependency>
- 编写CustonRealm类
package com.leijiao.shiro02.config;
import com.leijiao.shiro02.pojo.User;
import com.leijiao.shiro02.service.impl.UserServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
public class CustomRealm extends AuthorizingRealm {
@Autowired
UserServiceImpl userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获取当前用户的权限(数据库中存的)
Subject subject = SecurityUtils.getSubject();
User currentUser = (User)subject.getPrincipal();
//设置当前用户的权限
info.addStringPermission(currentUser.getPermission());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
//用户名和密码从数据库取
User user = userService.getUserByUsername(token.getUsername());
//校验用户名
if (user==null){
return null;//抛出异常UnknownAccountException
}
//校验密码
return new SimpleAuthenticationInfo(user,user.getPassword(),"");
}
}
- 编写shiroConfig类
package com.leijiao.shiro02.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class ShiroConfig {
/** shiro三大对象
* Subject 用户
* SecurityManager 安全管理器:管理用户(关联Realm)
* Realms 用户数据和Shiro数据交互的桥梁
* @return
*/
//3、获得过滤器,设置安全管理器
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//设置登录请求
shiroFilterFactoryBean.setLoginUrl("/login");
//设置未授权的请求
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
//添加shiro内置过滤器
HashMap<String, String> filterChainDefinitionMap = new HashMap<>();
//认证
filterChainDefinitionMap.put("/user/**","authc");
//授权
filterChainDefinitionMap.put("/user/add","perms[test:add]");
filterChainDefinitionMap.put("/user/edit","perms[test:edit]");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
//2、获得安全管理器,关联Realm
@Bean(name = "securityManager")
public DefaultSecurityManager defaultWebSecurityManager(@Qualifier("customRealm") CustomRealm customRealm){
DefaultSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//关联Realm
defaultWebSecurityManager.setRealm(customRealm);
return defaultWebSecurityManager;
}
//1、创建Realm对象
@Bean
public CustomRealm customRealm(){
return new CustomRealm();
}
}
- controller
package com.leijiao.shiro02.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyController {
@RequestMapping(value = {"/","/login"}, method = RequestMethod.GET)
public String login(){
return "login";
}
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(String username, String password,Model model){
//获取当前用户
Subject subject = SecurityUtils.getSubject();
//封装用户登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
} catch (UnknownAccountException uae) {//用户名不存在
model.addAttribute("msg","用户名不存在");
return "login";
}catch (IncorrectCredentialsException ice){//密码错误
model.addAttribute("msg","密码错误");
return "login";
}
return "home";
}
@RequestMapping("/unauthorized")
@ResponseBody
public String unauthorized(){
return "未授权,无法访问该页面";
}
@RequestMapping("/user/add")
public String addUser(){
return "user/add";
}
@RequestMapping("/user/edit")
public String editUser(){
return "user/edit";
}
}
- pojo、mapper、service、template省略
12.2.5、shiro整合thymeleaf
- pom.xml依赖
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>
- shiroConfig类中加入方法:shiro标签与thymeleaf标签结合
//整合thymeleaf
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
- 页面需要引入命名空间 xmlns:shiro=“http://www.pollix.at/thymeleaf/shiro”,使用标签shiro:xxx
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
首页
<br>
<!-- 有权限时显示a标签,无权限不显示 -->
<div shiro:hasPermission="test:add">
<a th:href="@{/user/add}">新增用户</a>
</div>
<br>
<div shiro:hasPermission="test:edit">
<a th:href="@{/user/edit}">修改用户</a>
</div>
</body>
</html>
13、SpringBoot集成Swagger
13.1、简介
官网:https://swagger.io/
Swagger是一款让你更好的书写API文档规范且完整的框架
Restful Api 文档在线自动生成器:API 文档 与API 定义同步更新
直接运行,在线测试API
13.2、使用方法
- pom.xml导入依赖
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
- SwaggerConfig配置类
import org.springframework.context.annotation.Configuration;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2 //开启Swagger2的自动配置
public class SwaggerConfig {
}
- 随便写一个controller,启动项目测试,访问http://localhost:8080/swagger-ui.html
- 若是出现报错:Failed to start bean ‘documentationPluginsBootstrapper’,可能是版本问题。解决方法:降低springboot版本到2.6.0以下(pom.xml中修改)
13.3、配置Swagger信息
在SwaggerConfig类中配置
/**
* 配置swagger信息.apiInfo()
* 定义apiInfo,配置swagger的Docket实例,关联apiInfo
*/
//配置swagger信息,主要是标题、描述
private ApiInfo apiInfo() {
Contact contact = new Contact("leijiao", "http://xxx.xxx.com", "12345678@qq.com");
return new ApiInfo(
"Swagger标题", // 标题
"Swagger描述", // 描述
"v1.0", // 版本
"http://terms.service.url", // 组织链接
contact, // 联系人信息
"Apach 2.0", // 许可
"https://www.apache.org/licenses/LICENSE-2.0", // 许可连接
new ArrayList<>()// 扩展
);
}
//配置swagger的Docket实例
@Bean
public Docket docket(){
//关联apiInfo()
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}
13.4、配置扫描接口
在SwaggerConfig类中配置
/**
* 扫描接口 .select().apis().build()
* RequestHandlerSelectors:扫描接口的方式
* - basePackage():指定要扫描的包
* - any():扫描所有
* - none():不扫描
* - withClassAnnotation(Controller.class):扫描类上的注解
* - withMethodAnnotation(GetMapping.class):扫描方法上的注解
*
* 扫描接口 .select().paths().build()
* PathSelectors:扫描接口的方式
* - any():扫描所有
* - none():不扫描
* - regex():通过正则表达式控制
* - ant():路径匹配
*/
//配置swagger的Docket实例
@Bean
public Docket docket(){
//关联apiInfo
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
//扫描接口:只扫描com.leijiao.swaggertest.controller包下的接口
.select().apis(RequestHandlerSelectors.basePackage("com.leijiao.swaggertest.controller")).build();
}
13.4、配置Swagger开关
在SwaggerConfig类中配置:动态配置当项目处于test、dev环境时显示swagger,处于rd时不显示
//配置Swagger开关 .enable(b)
//配置swagger的Docket实例
@Bean
public Docket docket(Environment environment){
// 设置要显示swagger的环境
Profiles of = Profiles.of("dev", "test");
// 判断当前是否处于该环境
boolean b = environment.acceptsProfiles(of);
//关联apiInfo
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
//配置是否启用Swagger,如果是false,在浏览器将无法访问
.enable(b)
//扫描接口:只扫描com.leijiao.swaggertest.controller包下的接口
.select().apis(RequestHandlerSelectors.basePackage("com.leijiao.swaggertest.controller")).build();
}
13.5、配置API分组
//配置API分组 .groupName("hello")
//若需配置多个分组只需要配置多个docket即可
@Bean
public Docket docket1(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
}
@Bean
public Docket docket2(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
}
13.6、常用注解
- @Api(tags = “xxx模块说明”):作用在controller模块类上
- @ApiOperation(“xxx接口说明”):作用在controlle接口方法上
- @ApiParam(“xxx参数说明”):作用在参数、方法和字段上
- @ApiModel(“xxxPOJO说明”):作用在pojo实体类上
- @ApiModelProperty(value = “xxx属性说明”,hidden = true):作用在pojo实体类的属性上
14、任务相关
14.1、异步任务
先响应,后台继续执行
//在service中的方法上添加该注解,表示异步方法
@Async
//在主程序类上添加该注解,开启异步功能
@EnableAsync
14.2、邮件任务
- pom.xml导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
- application.properties配置
spring.mail.username=XXXXXXXXX@qq.com
spring.mail.password=你的qq授权码
spring.mail.host=smtp.qq.com
# qq需要配置ssl
spring.mail.properties.mail.smtp.ssl.enable=true
- 测试
@Autowired
JavaMailSenderImpl mailSender;
@Test
public void contextLoads() {
//一个简单的邮件
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject("主题");
message.setText("内容");
message.setTo("xxx@qq.com");//接收方
message.setFrom("xxx3@qq.com");//发送方
mailSender.send(message);
}
@Test
public void contextLoads2() throws MessagingException {
//一个复杂的邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("主题");
helper.setText("<b style='color:red'>内容</b>",true);
helper.addAttachment("1.jpg",new File(""));//发送附件
helper.setTo("xxx@qq.com");
helper.setFrom("24736743@qq.com");
mailSender.send(mimeMessage);
}
14.3、定时任务
//在service中的方法上添加该注解,表示定时方法
@Scheduled(cron = "* * * * * *")
//在主程序类上添加该注解,开启定时功能
@EnableScheduling
/**
* crom表达式:秒、分、时、日、月、周几
* 常用:
* 0/2 * * * * ? 表示每2秒 执行任务
* 0 0/2 * * * ? 表示每2分钟 执行任务
* 0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务
* 0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
* 0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
* 0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
* 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
* 0 0 12 ? * WED 表示每个星期三中午12点
* 0 0 12 * * ? 每天中午12点触发
* 0 15 10 ? * * 每天上午10:15触发
* 0 15 10 * * ? 每天上午10:15触发
* 0 15 10 * * ? 每天上午10:15触发
* 0 15 10 * * ? 2005 2005年的每天上午10:15触发
* 0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
* 0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
* 0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
* 0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
* 0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
* 0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
* 0 15 10 15 * ? 每月15日上午10:15触发
* 0 15 10 L * ? 每月最后一日的上午10:15触发
* 0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
* 0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
* 0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
*/
15、SpringBoot集成redis
- pom.xml导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- application.properties配置
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=123456
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000
- 配置类RedisConfig
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> template = new RedisTemplate<>();
//json序列化配置
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//String序列化配置
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key的序列化采用StringRedisSerializer
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
//value值的序列化采用jackson2JsonRedisSerializer
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}
- 工具类RedisUtils
package com.leijiao.redistest.utils;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Component
@SuppressWarnings({"unchecked", "all"})
public class RedisUtils {
private static final Logger log = LoggerFactory.getLogger(RedisUtils.class);
private RedisTemplate<Object, Object> redisTemplate;
public RedisUtils(RedisTemplate<Object, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
return true;
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @param timeUnit 单位
*/
public boolean expire(String key, long time, TimeUnit timeUnit) {
try {
if (time > 0) {
redisTemplate.expire(key, time, timeUnit);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
return true;
}
/**
* 根据 key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(Object key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 查找匹配key
*
* @param pattern key
* @return /
*/
public List<String> scan(String pattern) {
ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection rc = Objects.requireNonNull(factory).getConnection();
Cursor<byte[]> cursor = rc.scan(options);
List<String> result = new ArrayList<>();
while (cursor.hasNext()) {
result.add(new String(cursor.next()));
}
try {
RedisConnectionUtils.releaseConnection(rc, factory);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return result;
}
/**
* 分页查询 key
*
* @param patternKey key
* @param page 页码
* @param size 每页数目
* @return /
*/
public List<String> findKeysForPage(String patternKey, int page, int size) {
ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection rc = Objects.requireNonNull(factory).getConnection();
Cursor<byte[]> cursor = rc.scan(options);
List<String> result = new ArrayList<>(size);
int tmpIndex = 0;
int fromIndex = page * size;
int toIndex = page * size + size;
while (cursor.hasNext()) {
if (tmpIndex >= fromIndex && tmpIndex < toIndex) {
result.add(new String(cursor.next()));
tmpIndex++;
continue;
}
// 获取到满足条件的数据后,就可以退出了
if (tmpIndex >= toIndex) {
break;
}
tmpIndex++;
cursor.next();
}
try {
RedisConnectionUtils.releaseConnection(rc, factory);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return result;
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
public void del(String... keys) {
if (keys != null && keys.length > 0) {
if (keys.length == 1) {
boolean result = redisTemplate.delete(keys[0]);
log.debug("--------------------------------------------");
log.debug(new StringBuilder("删除缓存:").append(keys[0]).append(",结果:").append(result).toString());
log.debug("--------------------------------------------");
} else {
Set<Object> keySet = new HashSet<>();
for (String key : keys) {
keySet.addAll(redisTemplate.keys(key));
}
long count = redisTemplate.delete(keySet);
log.debug("--------------------------------------------");
log.debug("成功删除缓存:" + keySet.toString());
log.debug("缓存删除数量:" + count + "个");
log.debug("--------------------------------------------");
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 批量获取
*
* @param keys
* @return
*/
public List<Object> multiGet(List<String> keys) {
List list = redisTemplate.opsForValue().multiGet(Sets.newHashSet(keys));
List resultList = Lists.newArrayList();
Optional.ofNullable(list).ifPresent(e-> list.forEach(ele-> Optional.ofNullable(ele).ifPresent(resultList::add)));
return resultList;
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间
* @param timeUnit 类型
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time, TimeUnit timeUnit) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, timeUnit);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
// ================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return /
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
return redisTemplate.opsForList().remove(key, count, value);
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* @param prefix 前缀
* @param ids id
*/
public void delByKeys(String prefix, Set<Long> ids) {
Set<Object> keys = new HashSet<>();
for (Long id : ids) {
keys.addAll(redisTemplate.keys(new StringBuffer(prefix).append(id).toString()));
}
long count = redisTemplate.delete(keys);
// 此处提示可自行删除
log.debug("--------------------------------------------");
log.debug("成功删除缓存:" + keys.toString());
log.debug("缓存删除数量:" + count + "个");
log.debug("--------------------------------------------");
}
}
16、分布式
16.1、分布式简介
分布式系统
- 建立在网络之上的软件系统
- 是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统
- 利用更多的机器,处理更多的数据
分布式服务架构(RPC:Remote Procedure Cal)
指远程过程调用,是一种进程间通信方式
允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节
RPC就是要像调用本地的函数一样去调远程函数
RPC两个核心模块:通讯,序列化
基本原理图如下
16.2、Dubbo
16.2.1、Dubbo简介
官网:https://dubbo.apache.org/zh/
Apache Dubbo是一款分布式服务框架,高性能和透明化的RPC远程服务调用方案,SOA服务治理方案
架构流程图如下:
服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务
服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用
注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。官方推荐使用Zookeeper
监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
服务运行容器(Container):服务运行容器
16.2.2、Dubbo环境搭建
16.2.2.1、zookeeper
官网:https://zookeeper.apache.org/
到官网下载压缩包后解压,更名为zookeeper
/bin/zkServer.cmd的末尾添加pause,避免闪退无法排查出错原因
setlocal call "%~dp0zkEnv.cmd" set ZOOMAIN=org.apache.zookeeper.server.quorum.QuorumPeerMain echo on call %JAVA% "-Dzookeeper.log.dir=%ZOO_LOG_DIR%" "-Dzookeeper.root.logger=%ZOO_LOG4J_PROP%" -cp "%CLASSPATH%" %ZOOMAIN% "%ZOOCFG%" %* pause endlocal
conf/zoo_sample.cfg复制一份改名为zoo.cfg,可修改以下配置
- dataDir:临时数据存储的目录(可写相对路径)
- dataLogDir:存储log的目录
- clientPort:zookeeper的端口号
以管理员身份运行/bin/zkServer.cmd,即可启动zookeeper
16.2.2.2、dubbo-admin
一个可视化的监控程序,让用户更好的管理监控众多的dubbo服务。可以不使用
下载地址:https://github.com/apache/dubbo-admin/tree/master(README_ZH.md里有教程)
下载解压后可修改注册中心地址(和zookeeper配置的一致):dubbo-admin-master\dubbo-admin-server\src\main\resources\application.properties
构建:进入dubbo-admin-master目录执行cmd打包:mvn clean package -Dmaven.test.skip=true
启动:进入dubbo-admin-distribution\target目录下执行cmd:java -jar dubbo-admin-0.3.0.jar
访问地址http://localhost:8080,默认用户名root、密码root(zookeeper需启动)
16.2.3、SpringBoot + Dubbo + zookeeper
创建module:provider-server,表示服务提供者
- pom.xml导入依赖
<!-- Dubbo Spring Boot Starter --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.3</version> </dependency> <!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient --> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> </dependency> <!-- 引入zookeeper --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.14</version> <!--排除slf4j-log4j12,解决日志冲突--> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency>
- application.properties配置
#服务端口 server.port=8081 #当前应用名字 dubbo.application.name=provider-server #注册中心地址 dubbo.registry.address=zookeeper://127.0.0.1:2181 #扫描指定包下服务 dubbo.scan.base-packages=com.leijiao.service #如果出现端口被占用,可以修改一下 dubbo.protocol.port=12345
- server接口
public interface TicketService { public String getTicket(); }
- server接口实现类
import com.leijiao.service.TicketService; import org.springframework.stereotype.Component; //使用了dubbo后应该导入apache的 //import org.springframework.stereotype.Service; import org.apache.dubbo.config.annotation.Service; @Service //发布服务 @Component //放在容器:应用启动,dubbo就会扫描指定的包下带有@Component注解的服务,将它发布在指定的注册中心中 public class TicketServiceImpl implements TicketService { @Override public String getTicket() { return "xxx"; } }
- 启动zookeeper和dubbo-admin,启动provider-server,在http://localhost:8080看到服务列表中有provider-server服务
创建module:consumer-server,表示服务消费者
- pom.xml导入依赖,和provider-server的一样
- application.properties配置
#服务端口 server.port=8082 #当前应用名字 dubbo.application.name=consumer-server #注册中心地址 dubbo.registry.address=zookeeper://127.0.0.1:2181
- 复制服务提供者项目中service的全路径,,路径名要和消费者项目的路径名一样,。只需要接口类,不需要实现类
public interface TicketService { public String getTicket(); }
- 消费者服务类
import com.leijiao.service.TicketService; import org.apache.dubbo.config.annotation.Reference; //使用spring下的Service import org.springframework.stereotype.Service; @Service //注入到容器 public class UserService { //需要去拿注册中心的服务 @Reference //远程引用指定的服务,按照全类名匹配 TicketService ticketService; public void bugTicket(){ String ticket = ticketService.getTicket(); System.out.println("在注册中心买到"+ticket); } }
测试一下(启动zookeeper、启动dubbo-admin(可以不用)、启动服务提供者、启动服务消费者测试)
import com.leijiao.service.TicketService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;
@Service //注入到容器
public class UserService {
//需要去拿注册中心的服务
@Reference //远程引用指定的服务,按照全类名匹配
TicketService ticketService;
public void bugTicket(){
String ticket = ticketService.getTicket();
System.out.println("在注册中心买到"+ticket);
}
}
//控制台打印:在注册中心买到xxx
- application.properties配置
```properties
#服务端口
server.port=8081
#当前应用名字
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#扫描指定包下服务
dubbo.scan.base-packages=com.leijiao.service
#如果出现端口被占用,可以修改一下
dubbo.protocol.port=12345
- server接口
public interface TicketService {
public String getTicket();
}
- server接口实现类
import com.leijiao.service.TicketService;
import org.springframework.stereotype.Component;
//使用了dubbo后应该导入apache的
//import org.springframework.stereotype.Service;
import org.apache.dubbo.config.annotation.Service;
@Service //发布服务
@Component //放在容器:应用启动,dubbo就会扫描指定的包下带有@Component注解的服务,将它发布在指定的注册中心中
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket() {
return "xxx";
}
}
启动zookeeper和dubbo-admin,启动provider-server,在http://localhost:8080看到服务列表中有provider-server服务
创建module:consumer-server,表示服务消费者
- pom.xml导入依赖,和provider-server的一样
- application.properties配置
#服务端口 server.port=8082 #当前应用名字 dubbo.application.name=consumer-server #注册中心地址 dubbo.registry.address=zookeeper://127.0.0.1:2181
- 复制服务提供者项目中service的全路径,,路径名要和消费者项目的路径名一样,。只需要接口类,不需要实现类
public interface TicketService { public String getTicket(); }
- 消费者服务类
import com.leijiao.service.TicketService; import org.apache.dubbo.config.annotation.Reference; //使用spring下的Service import org.springframework.stereotype.Service; @Service //注入到容器 public class UserService { //需要去拿注册中心的服务 @Reference //远程引用指定的服务,按照全类名匹配 TicketService ticketService; public void bugTicket(){ String ticket = ticketService.getTicket(); System.out.println("在注册中心买到"+ticket); } }
测试一下(启动zookeeper、启动dubbo-admin(可以不用)、启动服务提供者、启动服务消费者测试)
import com.leijiao.service.TicketService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;
@Service //注入到容器
public class UserService {
//需要去拿注册中心的服务
@Reference //远程引用指定的服务,按照全类名匹配
TicketService ticketService;
public void bugTicket(){
String ticket = ticketService.getTicket();
System.out.println("在注册中心买到"+ticket);
}
}
//控制台打印:在注册中心买到xxx