SpringBoot基础(二)
spring官网:https://spring.io/
https://mp.weixin.qq.com/mp/homepage?__biz=Mzg2NTAzMTExNg==&hid=1&sn=3247dca1433a891523d9e4176c90c499
该笔记记录的是SpringBoot2,更新的SpringBoot3版本需要参考最新官方文档
7、连接数据库
7.0 SpringData
对于数据访问层,无论是 SQL 还是 NoSQL,Spring Boot 底层都是采用 Spring Data 的方式进行统一处理。
SpringData官网:https://spring.io/projects/spring-data
数据库相关启动器:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter
7.1 整合JDBC
创建一个springboot项目,引入JDBC相关依赖
自动生成的pom.xml依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
编写application.yml 配置文件连接数据库
spring:
datasource:
username: root
password: 密码
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
springboot底层会处理这些配置内容。接下来可以直接使用jdbc
@SpringBootTest
public class SpringbootDataJdbcTest {
@Resource
DataSource dataSource;
@Test
public void contextLoads() throws SQLException{
System.out.println(dataSource.getClass());
Connection connection = dataSource.getConnection();
System.out.println(connection);
connection.close();
}
}
注:项目创建后会生成一个总测试类,自己写的测试类要和这个总测试类处于同级目录
输出
由输出可以看到,默认配置的数据源是class com.zaxxer.hikari.HikariDataSource 。
数据源的自动配置写在DataSourceAutoConfiguration 类中,打开它的源码,源码中有一处默认数据源配置相关的内部类
@Configuration(
proxyBeanMethods = false
)
@Conditional({DataSourceAutoConfiguration.PooledDataSourceCondition.class})
@ConditionalOnMissingBean({DataSource.class, XADataSource.class})
@Import({Hikari.class, Tomcat.class, Dbcp2.class, OracleUcp.class, Generic.class, DataSourceJmxConfiguration.class})
protected static class PooledDataSourceConfiguration {
protected PooledDataSourceConfiguration() {
}
}
源码中通过@Import 注解导入了Hikari.class ,即前面打印的默认配置数据源。
可以在配置文件中设置spring.datasources.type 的值来配置数据源类型(mysql、oracle…)。
JDBCTemplate
目的和MyBatis差不多,都是为了简化数据库相关的代码书写。JDBCTemplate也是springboot底层的一个默认配置。
相关配置类是JdbcTemplateConfiguration 。
常用方法:
excute :执行sql语句update、batchUpdate :前者执行增删改语句,后者执行批处理相关语句query、queryForXXX :执行查询语句call :执行存储过程、函数相关的语句
使用例
编写一个Controller,注入 jdbcTemplate,编写测试方法进行访问测试.
@RestController
@RequestMapping("/jdbc")
public class JdbcController {
@Autowired
JdbcTemplate jdbcTemplate;
@GetMapping("/list")
public List<Map<String,Object>> userList(){
String sql="select * from user";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
return maps;
}
@GetMapping("/add")
public String addUser(){
String sql="insert into user(id,name,pwd) values(5,'田七',888888)";
jdbcTemplate.update(sql);
return "addOk";
}
@GetMapping("/del/{id}")
public String delUser(@PathVariable("id")int id){
String sql="delete from user where id=?";
jdbcTemplate.update(sql,id);
return "deleteOk";
}
@GetMapping("/update/{id}")
public String updateUser(@PathVariable("id")int id){
String sql="update user set name=?,pwd=? where id="+id;
Object[] objects = new Object[2];
objects[0]="XXXX";
objects[1]="999999";
jdbcTemplate.update(sql,objects);
return "updateOk";
}
}
测试
7.2 整合Druid
开源仓库:https://github.com/alibaba/druid
Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。Druid能够提供强大的监控和扩展功能。
com.alibaba.druid.pool.DruidDataSource 基本配置参数:https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8
添加依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
在配置文件中修改数据源类型
spring:
datasource:
username: root
password: 密码
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
利用之前写好的SpringbootDataJdbcTest 测试类打印查看当前的默认数据源
根据官方提供的参数,在配置文件中进行设置。
spring:
datasource:
username: root
password: 密码
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
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,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
导入Log4j依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
配置文件中写好的Druid参数需要手动绑定属性,再添加到容器中。
创建一个config.DruidConfig 类进行绑定
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix="spring.datasource")
@Bean
public DataSource druidDataSource(){
return (DataSource) new DruidDataSource();
}
}
测试类
@SpringBootTest
public class SpringbootDataDruidTest {
@Autowired
DataSource dataSource;
@Test
public void contextLoads() throws SQLException{
System.out.println(dataSource.getClass());
Connection connection = dataSource.getConnection();
System.out.println(connection);
DruidDataSource druidDataSource=(DruidDataSource) dataSource;
System.out.println("druidDateSource 数据源的最大连接数:"+druidDataSource.getMaxActive());
System.out.println("druidDateSource 数据源的初始化连接数:"+druidDataSource.getInitialSize());
connection.close();
}
}
输出
配置Druid数据源监控
Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看。
在DruidConfig 类中添加以下方法来设置 Druid 的后台管理页面,如账号、密码。
@Bean
public ServletRegistrationBean statViewServlet() {
ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
Map<String, String> initParams = new HashMap<>();
initParams.put("loginUsername", "admin");
initParams.put("loginPassword", "123456");
initParams.put("allow", "");
bean.setInitParameters(initParams);
return bean;
}
测试
配置Druidweb监控的过滤器(filter)
在DruidConfig 类中添加以下方法来设置监控网页的过滤器
@Bean
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
Map<String, String> initParams = new HashMap<>();
initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
bean.setInitParameters(initParams);
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
7.3 整合Mybatis
官方文档:http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
Maven仓库地址:https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter/2.1.1
添加依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
配置文件中的数据库连接信息保持之前(7.2)的配置。(测试数据库能否连上)
使用例
创建实体类
package com.example.springboot04data.pojo;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
创建mapper目录以及实体类对应的Mapper接口
@Mapper
@Repository
public interface UserMapper {
List<User> getUsers();
User getUser(int id);
int addUser(User user);
int deleteUser(int id);
int updateUser(User user);
}
在resources 目录下创建mybatis/mapper 目录,编写对应的Mapper映射文件。
<?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.example.springboot04data.mapper.UserMapper">
<select id="getUsers" resultType="User">
select * from user
</select>
<select id="getUser" resultType="User" parameterType="int">
select * from user where id=#{id}
</select>
<insert id="addUser" parameterType="User">
insert into user(id,name,pwd) values(#{id},#{name},#{pwd})
</insert>
<delete id="deleteUser" parameterType="int">
delete from user where id=#{id}
</delete>
<update id="updateUser" parameterType="User">
update user set name=#{name}, pwd=#{pwd} where id=#{id}
</update>
</mapper>
在配置文件中添加以下配置以简化对象返回值的书写(否则resultType要写出完整对象名)
mybatis:
type-aliases-package: com.example.springboot04data.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
编写UserController 进行测试
@RestController
public class UserController {
@Autowired
UserMapper userMapper;
@GetMapping("/getUsers")
public List<User> getUsers(){
return userMapper.getUsers();
}
@GetMapping("/getUser/{id}")
public User getUser(@PathVariable("id") int id){
return userMapper.getUser(id);
}
public String addUser(User user){
userMapper.addUser(user);
return "addOk";
}
public String deleteUser(int id){
userMapper.deleteUser(id);
return "delOk";
}
public String updateUser(User user){
userMapper.updateUser(user);
return "updateOk";
}
}
测试
8、信息安全
目的:保护用户信息安全、后期系统维护
市面上可用的框架:Shiro、Spring Security
Spring Security是一个功能强大且高度可定制身份验证和访问控制的框架(基于Spring)。侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它易于扩展以满足定制需求。
一般来说,Web 应用的安全性包括用户认证(Authentication)和用户权限(Authorization)两个部分。
在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
8.1 spring Security
8.1.1 环境搭建
新建一个springboot项目,引入web、thymeleaf模块
导入静态资源
创建controller.RouterController 实现页面跳转
@Controller
public class RouterController {
@RequestMapping({"/","/index"})
public String index(){
return "index";
}
@RequestMapping("toLogin")
public String toLogin(){
return "views/login";
}
@RequestMapping("/level1/{id}")
public String level1(@PathVariable("id") int id){
return "views/level1/"+id;
}
@RequestMapping("/level2/{id}")
public String level2(@PathVariable("id") int id){
return "views/level2/"+id;
}
@RequestMapping("/level3/{id}")
public String level3(@PathVariable("id") int id){
return "views/level3/"+id;
}
}
测试
8.1.2 认证和授权
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
常见的相关类:
WebSecurityConfigurerAdapter : 自定义Security策略AuthenticationManagerBuilder : 自定义认证策略@EnableWebSecurity : 开启WebSecurity模式
认证(Authentication):
- 通过用户名、密码等凭据验证用户的身份。
- 身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
授权(Authorization):
- 系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限(可以联想数据库的权限)。
编写SpringSecurity配置类
参考官网:https://spring.io/projects/spring-security
https://docs.spring.io/spring-security/reference/servlet/index.html
编写基础配置类config.SecurityConfig
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception{
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
}
}
测试发现只有首页能显示,其他页面由于权限无法进入
修改configure() 方法,开启自动配置的登录功能
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception{
http.formLogin();
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
}
}
重写configure(AuthenticationManagerBuilder auth) 方法,实现自定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("alice").password("123456").roles("vip2","vip3")
.and()
.withUser("root").password("123456").roles("vip1","vip2","vip3")
.and()
.withUser("guest").password("123456").roles("vip1","vip2");
}
不过测试发现登录失败
前端传到后端的密码需要进行加密,修改之前的代码
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("alice").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
}
8.1.3 权限控制和注销
在configure(HttpSecurity http) 方法中开启自动配置的注销功能
@Override
protected void configure(HttpSecurity http) throws Exception{
http.logout();
}
在index.html 中增加一个注销按钮以发送/logout 请求
<a class="item" th:href="@{/logout}">
<i class="address card icon"></i> 注销
</a>
测试发现注销后会退到登录界面,为了实现注销后回到主页可以修改configure(HttpSecurity http) 方法
@Override
protected void configure(HttpSecurity http) throws Exception{
http.logout().logoutSuccessUrl("/");
}
需求:希望没有登录时,主页右上角有登录按钮。登录后,主页右上角的按钮由用户名代替。另外,主页中只显示当前登录用户有权限点进去的链接。比如alice没有进入level1的权限,那level1这一块链接就不显示
这里需要使用thymeleaf中的另一些功能
导入依赖
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
修改前端页面index.html
给页面导入命名空间xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
修改导航栏,增加认证判断(其他页面会复用主页的导航栏代码)
<div class="right menu">
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/login}">
<i class="address card icon"></i> 登录
</a>
</div>
<div sec:authorize="isAuthenticated()">
<a class="item">
<i class="address card icon"></i>
用户名:<span sec:authentication="principal.username"></span>
角色:<span sec:authentication="principal.authorities"></span>
</a>
</div>
<div sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/logout}">
<i class="address card icon"></i> 注销
</a>
</div>
</div>
测试
注销后可能会出现404,是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,可以将请求改为post表单提交,或者在spring security中关闭csrf功能;在 configure(HttpSecurity http) 方法中增加 http.csrf().disable(); 进行配置
修改主页中的代码实现根据用户权限显示对应的链接
<div class="column" sec:authorize="hasRole('vip1')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 1</h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
</div>
</div>
</div>
</div>
<div class="column" sec:authorize="hasRole('vip2')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 2</h5>
<hr>
<div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
<div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
<div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
</div>
</div>
</div>
</div>
<div class="column" sec:authorize="hasRole('vip3')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 3</h5>
<hr>
<div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
<div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
<div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
</div>
</div>
</div>
</div>
8.1.4 记住我功能
需求:记住用户名密码,重开浏览器后可以快速登录
在configure(HttpSecurity http) 方法中添加http.rememberMe(); 即可,具体实现可以读源码
@Override
protected void configure(HttpSecurity http) throws Exception {
http.rememberMe();
}
测试登录时会出现记住我的选项
登录后,重开浏览器,再进入主页,发现会自动登录
注销后,springsecurity会自动删除cookie
8.1.5 定制登录页
前面的登录页面样式是spring security 默认的,下面要使用自定义样式
在configure(HttpSecurity http) 方法中修改http.formLogin(); ,指定自己的登录页面
http.formLogin().loginPage("/toLogin");
在index.html 中指向自定义的login请求
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登录
</a>
登录时需要将这些信息发送到哪里,也需要自己配置。
可以在login.html 配置提交请求及方式,方式必须为post
<div class="ui form">
<form th:action="@{/login}" method="post">
<div class="field">
<label>Username</label>
<div class="ui left icon input">
<input type="text" placeholder="Username" name="username">
<i class="user icon"></i>
</div>
</div>
<div class="field">
<label>Password</label>
<div class="ui left icon input">
<input type="password" name="password">
<i class="lock icon"></i>
</div>
</div>
<input type="submit" class="ui blue submit button"/>
</form>
</div>
请求提交后,还需要验证。可以查看formLogin() 方法的源码。
配置接收登录的用户名和密码的参数
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login");
在登录页中增加记住我的多选框
<input type="checkbox" name="remember"> 记住我
后端验证处理
http.rememberMe().rememberMeParameter("remember");
测试
SecurityConfig完整代码
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception{
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login");
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
http.logout().logoutSuccessUrl("/");
http.rememberMe().rememberMeParameter("remember");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("alice").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
}
}
8.2 Shiro
官网:https://shiro.apache.org/
github地址:https://github.com/apache/shiro
Shiro是一个Apache的java安全框架(是个maven项目),可以实现认证、授权、加密、会话管理、web集成、缓存等功能。
8.2.0 Shiro外部架构
subject : 代码的直接交互对象,即Shiro的对外API的核心是Subject类。Subject代表当前用户,与当前应用交互的任何要素都可以是Subject类,如网络爬虫,机器人等,与Subject类的所有交互都会委托给SecurityManager执行。SecurityManager : 安全管理器,所有安全相关的操作都会与SecurityManager交互,并且它管理着所有的Subject,是shiro的核心。它负责与Shiro的其他组件进行交互,相当于SpringMVC的DispatcherServletRealm : shiro通过Realm获取安全数据(用户、角色、权限),SecurityManager负责验证用户身份,而验证对象需要通过Realm获取,用来判定当前输入的用户身份是否合法。另外,也需要通过Realm得到用户相应的角色、权限,来验证用户的操作是否能够执行。Realm类似于DataSource。
8.2.1 使用例
新建maven项目。
下载官方源码,将源码的\samples\quickstart 文件内容复制到项目中。
依赖
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
main/java 和main/resources 下的内容复制源码即可
QuickStart 源码
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
catch (AuthenticationException ae) {
}
}
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
currentUser.logout();
System.exit(0);
}
}
源码中会有一个shiro.ini 文件,需要预先下载相关的插件
配置/resources/log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %n %n
# General Apache libraries
log4j.logger.org.apache=WARN
# spring
log4j.logger.org.springframework=WARN
# default shiro logging
log4j.logger.org.apache.shiro=INFO
# disable verbase logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
启动测试
8.2.2 springboot整合shiro
新建springboot项目,添加web、thymeleaf模块
新建\template\index.html 首页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
</body>
</html>
创建controller.MyController
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello,Shiro");
return "index";
}
}
至此,初步搭建结束。
导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
</dependency>
编写config.UserRealm
public class UserRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了=>授权doGetAuthenticationInfo");
return null;
}
}
编写配置类config.ShiroConfig
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(
@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
return bean;
}
@Bean(name="defaultWebSecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
@Bean(name="userRealm")
public UserRealm userRealm(){
return new UserRealm();
}
}
新建前端页面templates/user/add.html 和templates/user/update.html 用于测试。
在MyController 中添加相关方法
@RequestMapping("user/add")
public String add(){
return "user/add";
}
@RequestMapping("user/update")
public String update(){
return "user/update";
}
在index.html 中添加相关跳转链接
<a th:href="@{/user/add}">add</a>
<a th:href="@{/user/update}">update</a>
实现登录拦截
修改ShiroConfig 的内容
@Configuration
public class ShiroConfig {
@Bean(name="shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(
@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
Map<String,String> filterMap=new LinkedHashMap<>();
filterMap.put("/user/add","authc");
filterMap.put("/user/update","authc");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
}
编写一个登录页面templates/login.html
<form th:action="@{/login}">
<p>用户名: <input type="text" name="username"></p>
<p>密码: <input type="text" name="password"></p>
<p><input type="submit"></p>
</form>
在MyController 中添加相关方法
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
在ShiroConfig 的getShiroFilterFactoryBean() 方法中设置登录的请求
@Bean(name="shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(
@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
Map<String,String> filterMap=new LinkedHashMap<>();
filterMap.put("/user/add","authc");
filterMap.put("/user/update","authc");
bean.setFilterChainDefinitionMap(filterMap);
bean.setLoginUrl("/toLogin");
return bean;
}
实现用户认证
在MyController 中添加登录请求相关方法
@RequestMapping("/login")
public String login(String username,String password,Model model){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try{
subject.login(token);
return "index";
}
catch (UnknownAccountException e){
model.addAttribute("msg","用户名错误");
return "login";
}
catch (IncorrectCredentialsException e){
model.addAttribute("msg","密码错误");
return "login";
}
}
修改login.html 中的内容
<p th:text="${msg}" style="color:red"></p>
<form th:action="@{/login}">
<p>用户名: <input type="text" name="username"></p>
<p>密码: <input type="text" name="password"></p>
<p><input type="submit"></p>
</form>
在UserRealm 中修改认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>授权doGetAuthenticationInfo");
String name="root";
String password="123456";
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
if(!userToken.getUsername().equals(name)){
return null;
}
return new SimpleAuthenticationInfo("",password,"");
}
整合Mybatis
导入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
编写application.yml ,进行如下配置
spring:
datasource:
username: root
password: 密码
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
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,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
在application.properties 中配置mybatis相关选项
mybatis.type-aliases-package=com.infinite.shirosptingboot.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
编写实体类pojo.User
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
(下面的内容和7.3差不多)
编写mapper.UserMapper
@Repository
@Mapper
public interface UserMapper {
public User queryUserByName(String name);
}
编写resorces/mapper/UserMapper.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.infinite.shirospringboot.mapper.UserMapper">
<select id="queryUserByName" parameterType="String" resultType="User">
select * from mybatis.user where name=#{name}
</select>
</mapper>
编写service.UserService 接口
public interface UserService {
public User queryUserByName(String name);
}
实现这个接口
@Service
public class UserServiceImpl implements UserService{
@Autowired
UserMapper userMapper;
@Override
public User queryUserByName(String name) {
return userMapper.queryUserByName(name);
}
}
修改UserRealm 的内容,使其连接数据库
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>授权doGetAuthenticationInfo");
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
User user = userService.queryUserByName(userToken.getUsername());
if(user==null){
return null;
}
return new SimpleAuthenticationInfo("",user.getPwd(),"");
}
}
请求授权实现
(user表中增加了权限字段)
需求:如果没有进入一个网页的相关权限时,自动跳转到未授权提示页
在MyController 中添加未授权时跳转的方法
@RequestMapping("/noauth")
@ResponseBody
public String unauthorized(){
return "未经授权无法访问此页面";
}
修改ShiroConfig 中的内容
filterMap.put("/user/add","authc");
filterMap.put("/user/update","authc");
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");
filterMap.put("/user/*","authc");
bean.setUnauthorizedUrl("/noauth");
修改UserRealm 中的授权方法
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission("user:add");
Subject subject= SecurityUtils.getSubject();
User currentUser = (User)subject.getPrincipal();
info.addStringPermission(currentUser.getPerms());
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}
}
整合thymeleaf
需求:登录后只显示可以进入的链接
导入依赖
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>
在ShiroConfig 中添加整合thymeleaf相关的方法
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
修改index.html 内容
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
<hr>
<p>
<a th:href="@{/toLogin}">登录</a>
</p>
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}">add</a>
</div>
<div shiro:hasPermission="user:update">
<a th:href="@{/user/update}">update</a>
</div>
</body>
</html>
测试
下面配置一下session
修改UserRealm 中的认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>授权doGetAuthenticationInfo");
Subject currentSubject = SecurityUtils.getSubject();
Session session = currentSubject.getSession();
session.setAttribute("loginUser",user);
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}
修改index.html 中的登录链接
<div th:if="session.loginUser==null">
<a th:href="@{/toLogin}">登录</a>
</div>
<div th:if="session.loginUser!=null">
<p th:text="${user.getUsername()}"></p>
</div>
MyController 的login() 方法中,在try-catch的登录成功部分添加model.addAttribute("user",token); 以向前端传输数据。
9、Swagger
前后端分离
后端时代:前端只用管理静态页面:html。 后端使用模板引擎,如jsp(主要部分)
前后端分离时代:
- 后端:后端控制层,服务层,数据访问层
- 前端:前端控制层,视图层
- 前后端未合并前,前端可以伪造后端数据,即json,测试时不用后端也能运行
前后端通过API、json交互。
前后端可以部署到不同的服务器上。
缺点:两个部分的人比较难做到及时协商。
解决方案:
- 先制定schema(提纲),实时更新最新的API,降低集成的风险
- 前端测试后端接口:postman
- 后端提供接口,需要实时更新相关消息
Swagger:
- 目前比较流行的API框架
- RestFul API文档在线自动生成工具。API文档与API定义同步更新
- 可以在线测试API接口
- 支持多种程序语言(java、php…)
https://swagger.io/
在项目中使用swagger需要用到springfox
9.1 springboor集成Swagger
新建springboot项目
导入依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>3.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
编写一个hello测试(HelloController )springboot程序(代码不再写出)
配置swagger,创建config.SwaggerConfig
@Configuration
@EnableSwagger2
public class SwaggerConfig {
}
测试时可能因为空指针问题不能启动,可以在pom文件中将springboot改成2.5.1版本
不要使用3+版本的swagger,此版本下的swagger-ui源码下没有swagger-ui.html ,导致后面测试时的查询路径返回404
测试,访问localhost:8080/swagger-ui.html
9.2 配置swagger
完善SwaggerConfig 的内容
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo());
}
private ApiInfo apiInfo(){
Contact contact = new Contact("infinite", "http://xxx.com", "123456@qq.com");
return new ApiInfo(
"自建SwaggerAPI文档",
"This is a description",
"va.b",
"http://xxx.com",
contact,
"Apache2.0",
"http://www.apache.org/lisence/LISENCE-2.0",
new ArrayList()
);
}
}
Docket 源码中有一个DocumentationType ,进入这个类的源码可以了解具体定义,常量SWAGGER_2 就写在其中
9.3 Swagger配置扫描接口
完善docket() 方法
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.infinite/springbootswagger.controller"))
.paths(PathSelectors.ant("/xxx/**"))
.build();
}
另外,可以在apiInfo() 方法后面跟一个enable() 方法来设置swagger是否启动。在多配置环境下(如测试版本、发行版本)可以使用。
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(false)
.select()
.apis(RequestHandlerSelectors.basePackage("com.infinite/springbootswagger.controller"))
.build();
}
下面要设置多种配置环境
新建application-dev.properties 代表测试环境
新建application-pro.properties 代表发布环境
在application.properties 中设置spring.profiles.active=dev 来指向测试环境的配置文件
再次修改docket() 方法
@Bean
public Docket docket(Environment environment){
Profiles profiles = Profiles.of("dev","test");
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(flag)
.select()
.apis(RequestHandlerSelectors.basePackage("com.infinite/springbootswagger.controller"))
.build();
}
9.4 API文档的分组和接口注释
在docket() 中的apiInfo() 方法后跟一个groupName(分组名) 方法以设置一个分组
配置多个Docket对象即可实现设置多个分组
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket docket1(){
return new Docket(DocumentationType.SWAGGER_2).groupName("A");
}
@Bean
public Docket docket2(){
return new Docket(DocumentationType.SWAGGER_2).groupName("B");
}
@Bean
public Docket docket3(){
return new Docket(DocumentationType.SWAGGER_2).groupName("C");
}
}
编写实体类pojo.User
@ApiModel("用户实体类")
public class User {
@ApiModelProperty("用户名")
public String username;
@ApiModelProperty("密码")
public String password;
}
在HelloController 中调用这个实体类
@RestController
public class HelloController {
@RequestMapping(value="/hello")
public String hello(){
return "hello";
}
@PostMapping(value="/user")
public User user(){
return new User();
}
}
另外,在controller类的头上添加@ApiOperation("注释") 可以呈现同样的注释效果
@RestController
public class HelloController {
@ApiOperation("Hello控制类")
@GetMapping(value="/hello2")
public String hello2(String username){
return "hello, "+username;
}
}
小结
- 可以通过Swagger给一些比较难理解的属性或接口,增加注释信息易于阅读
- 接口文档实时更新
- 在线测试接口
- 项目发行前要关闭swagger
10、任务
10.1 异步任务
新建springboot项目,导入web模块
新建service.AsyncService
@Service
public class AsyncService {
public void hello(){
try{
Thread.sleep(3000);
}
catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("数据正在处理......");
}
}
新建controller.AsyncController
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@RequestMapping("/hello")
public String hello(){
asyncService.hello();
return "OK";
}
}
到这一步,测试时若输入相关请求,浏览器会加载3秒后才跳转到相关页面,用户体验较差。
下面要实现加载和跳转两件事一同执行(即所谓的异步任务),可以利用@Async 注解
@Service
public class AsyncService {
@Async
public void hello(){
try{
Thread.sleep(3000);
}
catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("数据正在处理......");
}
}
还要在主程序类上添加@EnableAsync 以开启异步任务功能
10.2 邮件任务
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
相关的自动配置类MailSenderAutoConfiguration ,配置类MailProperties
完善配置文件application.properties
spring.mail.username=邮箱
spring.mail.password=密码 #qq邮箱可以通过手机短信获取另一种更安全的口令
spring.mail.host=smtp.qq.com
# 开启加密验证
spring.mail.properties.mail.smtp.ssl.enable=true
发送一个简单邮件
@SpringBootTest
class SpringbootMailTest(){
@Autowired
JavaMailSenderImpl mailSender;
@Test
void contextLoads1(){
SimpleMailMessage mailMessage=new SimpleMailMessage();
mailMessage.setSubject("This is a title");
mailMessage.setText("xxxxxxxxxxxxxxxxxxxx");
mailMessage.setFrom("发送方邮箱");
mailMessage.setTo("接受方邮箱");
mailSender.send(mailMessage);
}
@Test
void contextLoads2(){
MimeMailMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("This is a mime title");
helper.setText("<p style='color:red'>xxxxxxxxxxxxxxxxxxxx</p>",true);
helper.addAttachment("1.jpg",new File("file1"));
helper.addAttachment("2.jpg",new File("file2"));
mailMessage.setFrom("发送方邮箱");
mailMessage.setTo("接受方邮箱");
mailSender.send(mimeMessage);
}
}
10.3 定时任务
相关的接口TaskExecutor (执行任务)、TaskScheduler (调度任务)
给主启动类添加@EnableScheduling 以开启定时任务功能
创建一个类service.ScheduledService
@Service
public class ScheduledService {
@Scheduled(cron="0/2 * * * * ?")
public void hello(){
System.out.println("hello ,schedule test");
}
}
cron表达式
cron在线生成器
11、Redis
https://spring.io/projects/spring-data-redis
新建一个springboot项目,导入lombok、springbootdevtools、springConfigurationProcessor、springWeb这几个基础依赖,再导入一个SpringDataRedis(Access+Driver)依赖
注:springboot2.x后,原本使用的jedis被替换成了lettuce
jedis:采用直连方式,多线程操作的情况下不安全,若要避免,需要用到jedis pool连接池。更像BIO模式
lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数据。更像NIO模式
源码
相关自动配置类RedisAutoConfiguration 、配置类RedisProperties
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
11.1 使用例
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
完善配置文件内容
#配置redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
编写一个测试类
@SpringBootTest
class Springboot08RedisApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("myKey","infinite");
System.out.println(redisTemplate.opsForValue().get("myKey"));
}
}
注:本地要先开启redis服务
11.2 自定义RedisTemplate
进入RedisTemplate 源码,会发现有几个RedisSerializer 属性,这些属性是RedisTemplate 的序列化配置
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
if (this.defaultSerializer == null) {
this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
}
}
假如要实现用json序列化,可以自己实现一个RedisTemplate ,内部具体实现可以参考RedisAutoConfiguration 的源码
创建config.RedisConfig
@Configuration
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
编写实体类pojo.User
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class User {
private String name;
private int age;
}
编写测试方法
@Test
public void test() throws JsonProcessingException {
User user=new User("infinite",20);
String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user",jsonUser);
System.out.println(redisTemplate.opsForValue().get("user"));
}
假如想要在redis中直接存入对象,则要将对象序列化,即User 类要实现Serializable 接口。
需求:自定义一个序列化方式
修改RedisConfig 代码
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String,Object>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}
为了防止命名冲突,测试类中的redisTemplate 需要通过@Qualifier() 注解进行标识
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate<String,Object> redisTemplate;
实际开发中,为了提高代码复用性,可以将项目中redis的常见操作进行抽象集成,整合到自定义的RedisUtil 工具类中(和之前写jdbc、mybatis同理)。哪个类需要redis操作时,通过@Autowired 装配这个工具类即可
可参考的自定义Redis工具类
12、分布式(待完善)
分布式系统的定义:分布式系统时若干独立计算机的集合,这些计算机对于用户来说就像单个系统。
分布式系统中的计算机之间通过网络进行通信,目的是利用更多的机器,处理更多的数据
著有当单个结点的处理能力无法满足日益增长的计算、存储任务,且硬件提升的成本高、应用程序难以进一步优化的时候,才需要考虑分布式系统。
12.1 分布式(待完善)
前置内容
背景
https://dubbo.apache.org/zh/index.html
背景相关的内容可以查看https://dubbo.apache.org/zh/docsv2.7/user/preface/background/
RPC
RPC(Remote Procedure Call):远程过程调用,是一种进程间的通信方式,本质是一种思想而非规范。RPC允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不是程序员显式编码这个远程调用的细节,即调用本地和调用远程两种方式的代码基本是相同的。
例:现有两台服务器A、B,一个应用部署在A上,A现在想要调用B服务器上应用提供的函数/方法,由于不再一个内存空间中,A不能直接进行调用,需要通过网络来表达调用的予以和传输调用的数据。
为什么要用到RPC?假设有一个业务,它无法在一个进程内(或一台计算机内)通过本地调用的方式完成,比如不同系统(组织)之间的通讯。由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用,RPC实现的就是要项调用本地的函数一样去调用远程的函数。
https://www.jianshu.com/p/2accc2840a1b
RPC的两个核心模块:通讯、序列化(方便数据传输)
Dubbo概念
Dubbo是一款高性能、轻量级的开源java RPC框架。它提供了三大核心功能:面向接口的远程方法调用、只能容错和负载均衡、服务自动注册和实现
https://dubbo.apache.org/zh/docs/concepts/service-discovery/
Provider(服务提供者) : 向服务提供方暴露服务,服务提供者在启动时,向注册中心注册自己提供的服务。Consumer(服务消费者) : 调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务。消费者从提供者的地址列表中,基于软负载均衡算法,选择一台提供者进行调用,如果调用失败就换一台。Registry(注册中心) : 返回服务提供者的地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。Monitor(监控中心) : 服务消费者和提供者,让内存中累计调用次数和调用时间,每分钟发送一次统计数据到监控中心
zookeeper
充当注册中心的一个开源项目
使用:
-
下载项目压缩包并解压 -
执行bin/zkServer.cmd ,发现闪退。可以在zkServer.cmd 添加pause 以排错 -
修改conf/zoo.cfg 配置文件(没有的话就复制同目录下的zoo_sample.cfg ),重新以管理员模式运行bin/zkServer.cmd ,以及bin.zkCli.cmd 修改zoo.cfg 中的dataDir 、dataLogDir dataDir=C:\TOOLS\apache-zookeeper-3.8.0\data
dataLogDir=C:\TOOLS\apache-zookeeper-3.8.0\log
可能出现的问题 1.ZooKeeper audit is disabled
? 修改zkServer.cmd ,call %JAVA%中添加"-Dzookeeper.audit.enable=true"
客户端下测试
ls / :列出zookeeper根下保存的所有节点
create -e /indinite : 创建一个节点,后面如果跟一个值可以设置存放的内容
get /infinite : 获取/infinite节点的信息
安装dubbo-admin
这是一个springboot项目,作为监控管理后台,查看注册了哪些服务
去github上下载源码并解压
进入项目,解压lib中的jar包,修改dubbo-admin\src\main\resources\application.properties 文件中指定的zookeeper地址(默认为本地)
dubbo.registry.address=zookeeper://127.0.0.1:2181
打包项目
C:\TOOLS\dubbo-admin-0.2.0>mvn clean package -D maven.test.skip=true
可能会打包失败,可以参考https://blog.csdn.net/qq_43612538/article/details/103548650
最终打包成dubbo-admin-0.0.1-SNAPSHOT.jar
先启动zookeeper的service端,再启动这个的jar包
12.2 分布式Dubbo+Zookeeper+SpringBoot
新建一个空项目
新建一个provider-server 模块(spring),进入这个模块,端口配置为server.port=8001
新建接口service.TicketService
public interface TicketService {
public String getTicket();
}
实现接口
public class TicketServiceImpl implements TicketService{
@Override
public String getTicket(){
return "infinite";
}
}
新建一个consumer-server 模块(spring),进入这个模块,端口配置为server.port=8002
创建service.UserService 类
导入依赖(两个模块分别导入)
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>3.0.7</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.13</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
完善provider-server 的配置文件
server.port=8001
#服务应用名
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#哪些服务需要注册
dubbo.scan.base-packages=com.infinite.service
给TicketServiceImpl 添加dubbo的@DubboService 注解,启动时可以被扫描到并注册
@DubboService
public class TicketServiceImpl implements TicketService{
@Override
public String getTicket(){
return "infinite";
}
}
给主启动类添加@EnableDubbo 注解
启动zookeeper的服务端,再启动provider-server ,测试是否注册成功
完善consumer-server 的配置
server.port=8002
# 消费者去哪里拿服务。需要暴露自己的名字
dubbo.application.name=consumer-server
# 注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
在service目录下复制一份provider-server 的TicketService 接口。
完善UserService
@DubboService
public class UserService {
@DubboReference
TicketService ticketService;
public void butTicket(){
String ticket=ticketService.getTicket();
System.out.println("在注册中心拿到==>"+ticket);
}
}
编写测试
步骤
前提:开启zookeeper服务
- 提供者提供服务
- 导入依赖
- 配置注册中心的地址,以及服务发现名、要扫描的包
- 在想要被注册的服务上,增加一个注解
@DubboService - 消费者如何消费
- 导入依赖
- 配置注册中心的地址,配置自己的服务名
- 从远程注入服务
@DubboReference 新建一个provider-server 模块(spring),进入这个模块,端口配置为server.port=8001
新建接口service.TicketService
public interface TicketService {
public String getTicket();
}
实现接口
public class TicketServiceImpl implements TicketService{
@Override
public String getTicket(){
return "infinite";
}
}
新建一个consumer-server 模块(spring),进入这个模块,端口配置为server.port=8002
创建service.UserService 类
导入依赖(两个模块分别导入)
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>3.0.7</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.13</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
完善provider-server 的配置文件
server.port=8001
#服务应用名
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#哪些服务需要注册
dubbo.scan.base-packages=com.infinite.service
给TicketServiceImpl 添加dubbo的@DubboService 注解,启动时可以被扫描到并注册
@DubboService
public class TicketServiceImpl implements TicketService{
@Override
public String getTicket(){
return "infinite";
}
}
给主启动类添加@EnableDubbo 注解
启动zookeeper的服务端,再启动provider-server ,测试是否注册成功
完善consumer-server 的配置
server.port=8002
# 消费者去哪里拿服务。需要暴露自己的名字
dubbo.application.name=consumer-server
# 注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
在service目录下复制一份provider-server 的TicketService 接口。
完善UserService
@DubboService
public class UserService {
@DubboReference
TicketService ticketService;
public void butTicket(){
String ticket=ticketService.getTicket();
System.out.println("在注册中心拿到==>"+ticket);
}
}
编写测试
步骤
前提:开启zookeeper服务
- 提供者提供服务
- 导入依赖
- 配置注册中心的地址,以及服务发现名、要扫描的包
- 在想要被注册的服务上,增加一个注解
@DubboService - 消费者如何消费
- 导入依赖
- 配置注册中心的地址,配置自己的服务名
- 从远程注入服务
@DubboReference
|