下载地址:http://shiro.apache.org/
从外部来看Shiro,即从应用程序角度的来观察如何使用Shiro完成工作
<dependencies>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>1.5.3version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>jcl-over-slf4jartifactId>
<version>1.7.26version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.26version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
dependencies>
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] - %m %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 verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
如果idea安装了ini插件,文档将高亮显示
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5
直接从官网案例中复制,先运行,再慢慢分析
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simple Quickstart application showing how to use Shiro's API.
* 简单入门Shiro使用API
*
* @since 0.9 RC2
*/
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
// The easiest way to create a Shiro SecurityManager with configured
// realms, users, roles and permissions is to use the simple INI config.
// We'll do that by using a factory that can ingest a .ini file and
// return a SecurityManager instance:
// Use the shiro.ini file at the root of the classpath
// (file: and url: prefixes load from files and urls respectively):
// 读取配置文件:
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
// for this simple example quickstart, make the SecurityManager
// accessible as a JVM singleton. Most applications wouldn't do this
// and instead rely on their container configuration or web.xml for
// webapps. That is outside the scope of this simple quickstart, so
// we'll just do the bare minimum so you can continue to get a feel
// for things.
SecurityUtils.setSecurityManager(securityManager);
// Now that a simple Shiro environment is set up, let's see what you can do:
// get the currently executing user:
// 获取当前的用户对象 Subject
Subject currentUser = SecurityUtils.getSubject();
// Do some stuff with a Session (no need for a web or EJB container!!!)
//通过当前用户拿到Shiro的Session 可以脱离web存值取值
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 + "]");
}
// let's login the current user so we can check against roles and permissions:
//判断当前的用户是否被认证
if (!currentUser.isAuthenticated()) {
//Token 令牌
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 more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
//test a role:
// 检查角色
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
//test a typed permission (not instance-level)
//粗粒度
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.");
}
//a (very powerful) Instance Level permission:
//细粒度
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!");
}
//all done - log out!
//注销
currentUser.logout();
//结束
System.exit(0);
}
}
启动测试
从注释中,我们可以看出,前几行代码其实可以通过配置来完成,官方也推荐这么做,因此,去掉注释,精简代码
核心代码为
// 获取当前的用户对象 Subject
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
currentUser.isAuthenticated()
currentUser.getPrincipal()
currentUser.hasRole("schwartz")
currentUser.isPermitted("lightsaber:wield")
currentUser.logout();
与 spring security 非常详相近
@Controller
public class MyController {
@RequestMapping({
"/","/index"})
public String toIndex(Model model) {
model.addAttribute("msg","hello,Shiro");
return "index";
}
@RequestMapping("/user/add")
public String add() {
return "user/add";
}
@RequestMapping("/user/update")
public String update() {
return "user/update";
}
}
首页 index.html页面
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页title>
head>
<body>
<div>
<h1>首页h1>
<p th:text="${msg}">p>
<hr>
<a th:href="@{/user/add}">adda> | <a th:href="@{/user/update}">updatea>
div>
body>
html>
新建一个add.html页面
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>addh1>
body>
html>
新建一个update.html页面
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>updateh1>
body>
html>
项目结构
运行测试
添加 shiro 整合 spring 的依赖
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.5.3version>
dependency>
编写 ShiroConfig 配置类
@Configuration
public class ShiroConfig {
//3. shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
//2. DefaultWebSecurityManager
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关联userRealm
securityManager.setRealm(userRealm);
return securityManager;
}
//1. 创建realm对象,需要自定义类
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
}
这里有个细节注意:UserRealm 被注册bean,默认名字是 userRealm,因此在上面的方法参数中我们用注解@Qualifier(“userRealm”)来进行诸如,其实和使用 @Autowired 是一样的
编写一个自定义类 UserRealm
//自定义的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("执行了=>认证doGetAuthorizationInfo");
return null;
}
}
在ShiroConfig中的getShiroFilterFactoryBean方法中添加如下配置
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/add","authc");
filterMap.put("/user/update","authc");
bean.setFilterChainDefinitionMap(filterMap);
点击首页的add或者update之后
添加拦截成功页面,登录页面login.html
shiro没有security那样的默认登录页,需要我们自定义
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面title>
head>
<body>
<h1>登录h1>
<hr>
<form action="">
<p>用户名:<input type="text" name="username">p>
<p>密码:<input type="text" name="password">p>
<p>密码:<input type="submit">p>
form>
body>
html>
在MyConfig中添加
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
在ShiroConfig
中的getShiroFilterFactoryBean
方法中添加如下配置
//设置登录的请求
bean.setLoginUrl("/toLogin");
ShiroConfig
主要用于登录拦截配置。
UserRealm
主要用于用户的认证,授权操作,这两个类相互联动,实现功能
@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);//执行登录的方法,如果没有异常就说明ok了
return "index";
} catch (UnknownAccountException e) {
//用户名不存在
model.addAttribute("msg","用户名错误");
return "login";
} catch (IncorrectCredentialsException e) {
//密码不存在
model.addAttribute("msg","密码错误");
return "login";
}
}
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页面title>
head>
<body>
<h1>登录h1>
<hr>
<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>
body>
html>
shiro为了增加安全性,我们从数据库得到密码直接封装到 AuthenticationInfo
对象中返回即可,shiro自行判断
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthorizationInfo");
// 用户名、密码, 从数据库中取
String name = "root";
String password = "123456";
// 这个token中包含了用户请求传过来的账户信息
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
if (!userToken.getUsername().equals(name)) {
return null;//这里返回null.controller中的验证就会抛出异常 UnknownAccountException,
}
// 密码认证,shiro做
// 1.3参数可以为空串,不影响
return new SimpleAuthenticationInfo("",password,"");
}
看似controller获取账户,封装token,验证token,realm中配置账户信息,之间没有什么联系,其实底层经过了很多层方法,实现关联
测试成功,shiro成功实现了拦截、认证
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.23version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.3version>
dependency>
spring:
datasource:
username: root
password: admin
#?serverTimezone=UTC解决时区的报错
url: jdbc:mysql://localhost:3306/my_study?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
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注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
mybatis:
type-aliases-package: com.swy.pojo
mapper-locations: classpath:mapper/*.xml
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
<mapper namespace="com.swy.mapper.UserMapper">
<select id="queryUserList" resultType="User">
select * from mybatis.user;
select>
<select id="queryUserByName" resultType="User">
select * from mybatis.user where name = #{name};
select>
<insert id="addUser" parameterType="User">
insert into mybatis.user (id, name, pwd) values (#{id},#{name},#{pwd});
insert>
<update id="updateUser" parameterType="User">
update mybatis.user set name=#{name},pwd = #{pwd} where id = #{id};
update>
<delete id="deleteUser" parameterType="int">
delete from mybatis.user where id = #{id}
delete>
mapper>
@Repository
@Mapper
public interface UserMapper {
public User queryUserByName(String name);
}
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);
}
}
@SpringBootTest
class ShiroSpringbootApplicationTests {
@Autowired
UserService userService;
@Test
void contextLoads() {
System.out.println(userService.queryUserByName("rose"));
}
}
@Autowired
UserService userService;
//...
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthorizationInfo");
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
// 真实数据库 用户名、密码, 数据中取
User user = userService.queryUserByName(userToken.getUsername());
if (user == null) {
//没有这个人
return null;
}
// 密码认证,shiro做
return new SimpleAuthenticationInfo("",user.getPwd(),"");
}
由此可见,在shiro我们根本不用出现明文密码,就可以直接提取数据校验,防止安全问题
默认是SimpleCredentialsMatcher加密,进入这个CredentialsMatcher
接口中可以发现,
查看 CredentialsMatcher
实现类可以看到有哪些支持的加密算法,默认使用了 SimpleCredentialsMatcher
简单加密,也就是明文密码
我们也可以选择 MD5 加密方式,
MD5加密测试链接 http://tool.chinaz.com/tools/md5.aspx
在实现了拦截、认证之后,我们还要确保,当用户访问有权限要求的页面的时候,该如何处置,
实现逻辑:如果用户有授权,那么可以访问;如果用户没有授权,则返回无授权页面
修改 ShiroConfig
,使用 getShiroFilterFactoryBean
方法,对指定的请求url做权限设置
将之前的无权限可以访问代码去掉
filterMap.put("/user/add","authc");
filterMap.put("/user/update","authc");
//授权,正常情况下,没有授权会跳转到为授权页面
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");
这个代码的含义是,必须是 user 用户的 add 权限才可以访问 /user/add
测试页面,发现即使我们已经登录,也无法访问,401对应的就是未授权,因为此时我们的登录用户还没有添加权限
@RequestMapping("/noauto")
@ResponseBody
public String unauthorized() {
return "未经授权,无法访问此页面";
}
ShiroConfig
中的getShiroFilterFactoryBean
方法中添加
// 未授权页面
bean.setUnauthorizedUrl("/noauto");
那么如何给用户添加权限?在 realm中进行配置
我们可以直接使用
info.addStringPermission("user:add");
这种方式授权,这样所有用户都有这个权限,但是我们需要根据不同的用户来判断是否授权,
所以这里,我们针对单个用户user授权,需要修改user表,增加表字段用于存放权限符号
手动在表中给用户添加权限
修改 User 类,添加对应属性
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
private String perms;
}
这样我们可以根据当前登录的用户,得到该用户有哪些权限标识符,在交给 info 即可
如何在 realm 中获取当前用户对象?
可以使用 Subject 获取,subject就代表当前用户对象;
传值也可以使用session传递
//自定义的UserRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//拿到当前登录的这个对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User)subject.getPrincipal();//拿到user对象
//设置当前用户的权限
info.addStringPermission(currentUser.getPerms());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
......
// 密码认证,shiro做
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}
}
为什么subject.getPrincipal()
可以强转user对象,因为下面return new SimpleAuthenticationInfo(user,user.getPwd(),"")
中我们将当前用户的对象user添加在第一个参数上了,作为用户资源
再次重启测试,发现不同的登录用户有不同的访问权限
通过整合,可以在thymeleaf模板页面中实现shiro功能实现
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.0.0version>
dependency>
// 整合ShiroDialect: 用来整合 Shiro thymeleaf
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
添加命名空间,给标签添加权限属性
<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>
head>
<body>
<div>
<h1>首页h1>
<p th:text="${msg}">p>
<div shiro:notAuthenticated>
<a th:href="@{/toLogin}">登录a>
div>
<hr>
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}">adda>
div>
<div shiro:hasPermission="user:update">
<a th:href="@{/user/update}">updatea>
div>
div>
body>
html>
这样在前端页面,我们可以实现定制化动态页面,不同的登录用户展示不同的菜单
这里的 shiro:hasPermission
就是从 realm 中获取的当前用户前线,放在这里判断是否可以显示该标签
ShiroConfig
@Configuration
public class ShiroConfig {
//3. shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
Map<String, String> filterMap = new LinkedHashMap<>();
/*filterMap.put("/user/add","authc");
filterMap.put("/user/update","authc");*/
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");
bean.setFilterChainDefinitionMap(filterMap);
//设置登录的请求
bean.setLoginUrl("/toLogin");
// 未授权页面
bean.setUnauthorizedUrl("/noauto");
return bean;
}
//2. DefaultWebSecurityManager
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关联userRealm
securityManager.setRealm(userRealm);
return securityManager;
}
//1. 创建realm对象,需要自定义类
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
// 整合ShiroDialect: 用来整合 Shiro thymeleaf
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
}
realm
//自定义的UserRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//拿到当前登录的这个对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User)subject.getPrincipal();//拿到user对象
//设置当前用户的权限
info.addStringPermission(currentUser.getPerms());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthorizationInfo");
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
// 真实数据库 用户名、密码, 数据中取
User user = userService.queryUserByName(userToken.getUsername());
if (user == null) {
//没有这个人
return null;
}
// 密码认证,shiro做
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}
}
MyController
@Controller
public class MyController {
@RequestMapping({
"/","/index"})
public String toIndex(Model model) {
model.addAttribute("msg","hello,Shiro");
return "index";
}
@RequestMapping("/user/add")
public String add() {
return "user/add";
}
@RequestMapping("/user/update")
public String update() {
return "user/update";
}
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
@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);//执行登录的方法,如果没有异常就说明ok了
return "index";
} catch (UnknownAccountException e) {
//用户名不存在
model.addAttribute("msg","用户名错误");
return "login";
} catch (IncorrectCredentialsException e) {
//密码不存在
model.addAttribute("msg","密码错误");
return "login";
}
}
@RequestMapping("/noauto")
@ResponseBody
public String unauthorized() {
return "未经授权,无法访问此页面";
}
}
pom
<dependencies>
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.0.0version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.23version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.3version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.5.3version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
扩展阅读:让 Apache Shiro 保护你的应用
https://blog.csdn.net/weixin_44635198/article/details/107701061?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-2&spm=1001.2101.3001.4242
开源博客:https://github.com/WinterChenS/my-site
项目里已经包含数据库,以及部署方法,点赞!
非常规范容易理解的项目,强烈建议学习使用
扩展:七牛云服务
学习目标:
前后端分离:Vue+SpringBoot
产生的问题
解决方案
Swagger
SpringBoot集成Swagger => springfox,两个jar包
要求:jdk 1.8 + 否则swagger2无法运行
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.9.2version>
dependency>
编写HelloController,测试确保运行成功
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "hello swagger";
}
}
要使用Swagger,我们需要编写一个配置类-SwaggerConfig来配置 Swagger
@Configuration //配置类
@EnableSwagger2// 开启Swagger2的自动配置
public class SwaggerConfig {
}
访问测试 :http://localhost:8080/swagger-ui.html ,可以看到swagger的界面;
在 config 配置类中添加
@Bean //配置docket以配置Swagger具体参数
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2);
}
DocumentationType有三个值,对应三种版本
查看 Docker 源码可以看到默认已经有了很多属性配置,我们只需要返回 docker实例即可,用 docker调用各种属性或者方法来添加配置参数
apiInfo()方法需要 ApiInfo 对象作为参数,我们将配置信息放在 ApiInfo 对象中
添加一个方法用来配置参数,并返回 ApiInfo 对象
//配置文档信息
private ApiInfo apiInfo() {
// 作者信息
Contact contact = new Contact("联系人名字", "http://xxx.xxx.com/联系人访问链接", "联系人邮箱");
return new ApiInfo(
"Swagger学习", // 标题
"学习演示如何配置Swagger", // 描述
"v1.0", // 版本
"http://terms.service.url/组织链接", // 组织链接
contact, // 联系人信息
"Apach 2.0 许可", // 许可
"许可链接", // 许可连接
new ArrayList<>()// 扩展
);
}
Docket 实例关联上 apiInfo()
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}
重启项目,访问测试 http://localhost:8080/swagger-ui.html 看下效果;
需要注意的是,ApiInfo 源码没有给属性提供set方法,必须使用构造器,因此,不重要的信息我们只能使用空串占位了,醉了
由于我们在controller中,给方法使用@RequestMapping,没有指明具体的请求方式,所以这里出现了这么多接口,因此推荐注解中指明具体的请求方式
为什么我们要访问 /swagger-ui.html 路径,看源码就知道了
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
.apis(RequestHandlerSelectors.basePackage("com.swy.controller"))
.build();
}
通过 select 调出 apis 调出 build
除了通过包路径配置扫描接口外,还可以通过配置其他方式扫描接口,这里注释一下所有的配置方式:
any() // 扫描所有,项目中的所有接口都会被扫描到
none() // 不扫描接口
// 通过方法上的注解扫描,如withMethodAnnotation(GetMapping.class)只扫描get请求
withMethodAnnotation(final Class<? extends Annotation> annotation)
// 通过类上的注解扫描,如.withClassAnnotation(Controller.class)只扫描有controller注解的类中的接口
withClassAnnotation(final Class<? extends Annotation> annotation)//这里要使用注解的class
basePackage(final String basePackage) // 根据包路径扫描接口
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
.apis(RequestHandlerSelectors.basePackage("com.swy.controller"))
// 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口
.paths(PathSelectors.ant("/kuang/**"))
.build();
}
这里的可选值还有
any() // 任何请求都扫描
none() // 任何请求都不扫描
regex(final String pathRegex) // 通过正则表达式控制
ant(final String antPattern) // 通过ant()控制
注意:select后面接了一套方法,因此我们要向做其他配置应该在select前面加
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(false) //配置是否启用Swagger,如果是false,在浏览器将无法访问
.select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
.apis(RequestHandlerSelectors.basePackage("com.swy.controller"))
// 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口
.paths(PathSelectors.ant("/kuang/**"))
.build();
}
通过判断当前的配置环境,来返回 true 或 false,决定是否启用swagger,添加 Environment 可以获取当前环境
还需要我们在配置文件中添加当前的环境名称比如,dev test prod
@Bean
public Docket docket(Environment environment) {
// 设置要显示swagger的环境
Profiles of = Profiles.of("dev", "test");
// 判断当前是否处于该环境
// 通过 enable() 接收此参数判断是否要显示
boolean b = environment.acceptsProfiles(of);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(b) //配置是否启用Swagger,如果是false,在浏览器将无法访问
.select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
.apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
// 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口
.paths(PathSelectors.ant("/kuang/**"))
.build();
}
其实不一定非要使用 Environment ,properties文件也可以将外部环境的参数传进来
@Bean
public Docket docket(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.groupName("hello") // 配置分组
// 省略配置....
}
各组扫描各自的接口
@Bean
public Docket docket1(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
}
@Bean
public Docket docket2(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
}
@Bean
public Docket docket3(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group3");
}
配置我们的实体类,可以在 swagger 上看到我们的实体类信息
@ApiModel("用户实体")
public class User {
@ApiModelProperty("用户名")
public String username;
@ApiModelProperty("密码")
public String password;
}
@RequestMapping("/getUser")
public User getUser(){
return new User();
}
注意:并不是因为@ApiModel这个注解让实体显示在这里了,而是只要出现在接口方法的返回值上的实体都会显示在这里,
Swagger的所有注解定义在io.swagger.annotations包下
下面列一些经常用到的,未列举出来的可以另行查阅说明:
Swagger注解 | 简单说明 |
---|---|
@Api(tags = “xxx模块说明”) | 作用在模块类上 |
@ApiOperation(“xxx接口说明”) | 作用在接口方法上 |
@ApiModel(“xxxPOJO说明”) | 作用在模型类上:如VO、BO |
@ApiModelProperty(value = “xxx属性说明”,hidden = true) | 作用在类方法和属性上,hidden设置为true可以隐藏该属性 |
@ApiParam(“xxx参数说明”) | 作用在参数、方法和字段上,类似@ApiModelProperty |
我们也可以给请求的接口配置一些注释
@ApiOperation("狂神的接口")
@PostMapping("/kuang")
@ResponseBody
public String kuang(@ApiParam("这个名字会被返回")String username){
return username;
}
这样的话,可以给一些比较难理解的属性或者接口,增加一些配置信息,让人更容易阅读!
相较于传统的Postman或Curl方式测试接口,使用swagger简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。
Swagger是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出Word接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭Swagger,一来出于安全考虑二来也可以节省运行时内存。
我们可以导入不同的包实现不同的皮肤定义:
默认的 访问 http://localhost:8080/swagger-ui.html
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>swagger-bootstrap-uiartifactId>
<version>1.9.1version>
dependency>
3. Layui-ui 访问 http://localhost:8080/docs.html
<dependency>
<groupId>com.github.caspar-chengroupId>
<artifactId>swagger-ui-layerartifactId>
<version>1.1.3version>
dependency>
4. mg-ui 访问 http://localhost:8080/document.html
<dependency>
<groupId>com.zyplayergroupId>
<artifactId>swagger-mg-uiartifactId>
<version>1.0.6version>
dependency>
在我们的工作中,常常会用到异步处理任务,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。还有一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息。还有就是邮件的发送,微信的前身也是邮件服务呢?这些东西都是怎么实现的呢?其实SpringBoot都给我们提供了对应的支持,我们上手使用十分的简单,只需要开启一些注解支持,配置一些配置文件即可!那我们来看看吧~
新建项目 springboot-09-test
异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。
编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况;
@Service
public class AsyncService {
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("业务进行中....");
}
}
编写controller包
编写AsyncController类
我们去写一个Controller测试一下
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@GetMapping("/hello")
public String hello(){
asyncService.hello();
return "success";
}
}
问题:我们如果想让用户直接得到消息,就在后台使用多线程的方式进行处理即可,但是每次都需要自己手动去编写多线程的实现的话,太麻烦了,我们只需要用一个简单的办法,在我们的方法上加一个简单的注解即可,如下:
//告诉Spring这是一个异步方法
@Async
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("业务进行中....");
}
SpringBoot就会自己开一个线程池,进行调用!但是要让这个注解生效,我们还需要在主程序上添加一个注解@EnableAsync ,开启异步注解功能;
主启动类添加注解
@EnableAsync //开启异步注解功能
@SpringBootApplication
public class SpringbootTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTaskApplication.class, args);
}
}
启动测试,我们发现请求立刻既可以得到相应,打印 “业务进行中”,但是中间的异步线程业务则继续执行业务
邮件发送,在我们的日常开发中,也非常的多,Springboot也帮我们做了支持
这样我们可以在我们的Java项目登录我们的邮箱,向外界发送邮件
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>
看它引入的依赖,可以看到 jakarta.mail
<dependency>
<groupId>com.sun.mailgroupId>
<artifactId>jakarta.mailartifactId>
<version>1.6.4version>
<scope>compilescope>
dependency>
这个类中存在bean,JavaMailSenderImpl
然后我们去看下配置文件
@ConfigurationProperties(
prefix = "spring.mail"
)
public class MailProperties {
private static final Charset DEFAULT_CHARSET;
private String host;
private Integer port;
private String username;
private String password;
private String protocol = "smtp";
private Charset defaultEncoding;
private Map<String, String> properties;
private String jndiName;
}
比如我们使用qq邮箱发送
# 邮箱账户
[email protected]
# qq邮箱提供的授权码
spring.mail.password=你的qq授权码
# 发送邮件所需的服务器
spring.mail.host=smtp.qq.com
# qq需要配置ssl开启加密验证(qq特有)
spring.mail.properties.mail.smtp.ssl.enable=true
获取授权码:在QQ邮箱中的设置->账户->开启pop3和smtp服务
@Autowired
JavaMailSenderImpl mailSender;
@Test
public void contextLoads() {
//邮件设置1:一个简单的邮件
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject("通知-明天来狂神这听课");
message.setText("今晚7:30开会");
message.setTo("[email protected]");
message.setFrom("[email protected]");
mailSender.send(message);
}
@Test
public void contextLoads2() throws MessagingException {
//邮件设置2:一个复杂的邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("通知-明天来狂神这听课");
helper.setText("今天 7:30来开会",true);// 开启解析html
//发送附件
helper.addAttachment("1.jpg",new File("D:\picture\1.jpg"));
helper.addAttachment("2.jpg",new File("D:\picture\2.jpg"));
helper.setTo("[email protected]");
helper.setFrom("[email protected]");
mailSender.send(mimeMessage);
}
附件路径可以写绝对路径
测试发送,然后查看邮箱
我们只需要使用Thymeleaf进行前后端结合即可开发自己网站邮件收发功能了
项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring为我们提供了异步执行任务调度的方式,提供了两个接口。
自带功能,不用添加依赖,拿来即用,
两个注解:
cron表达式:
字段 | 允许值 | 允许特殊字符 |
---|---|---|
秒 | 0-59 | , - * / |
分 | 0-59 | , - * / |
小时 | 0-23 | , - * / |
日期 | 1-31 | , - * / ? L W C |
月份 | 1-12 | , - * / |
星期 | 0-1或SUN-SAT 0,7是SUN | , - * / ? L W C |
特殊字符 | 代表含义 |
---|---|
, | 枚举 |
- | 区间 |
* | 任意 |
/ | 步长 |
? | 日/星期冲突匹配 |
L | 最后 |
W | 工作日 |
C | 和calendar练习后计算过的值 |
# | 星期,4#2 第二个星期三 |
具体使用技巧可以在百度搜多到很多资料
测试步骤:
我们里面存在一个hello方法,他需要定时执行,怎么处理呢?
@Service
public class ScheduledService {
// 在一个特定的时间执行这个方法——Timer
//cron表达式
// 秒 分 时 日 月 周几
/*
0 49 11 * * ? 每天的11点49分00秒执行
0 0/5 11,12 * * ? 每天的11点和12点每个五分钟执行一次
0 15 10 ? * 1-6 每个月的周一到周六的10点15分执行一次
0/2 * * * * ? 每2秒执行一次
*/
@Scheduled(cron = "0/2 * * * * ?")
public void hello() {
System.out.println("hello,你被执行了");
}
}
@EnableAsync //开启异步注解功能
@EnableScheduling //开启基于注解的定时任务
@SpringBootApplication
public class SpringbootTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTaskApplication.class, args);
}
}
启动测试,只要程序启动,定时任务就会自动执行,不需要其他程序再触发
http://www.bejson.com/othertools/cron/
(1)0/2 * * * * ? 表示每2秒 执行任务
(1)0 0/2 * * * ? 表示每2分钟 执行任务
(1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务
(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
(6)0 0 12 ? * WED 表示每个星期三中午12点
(7)0 0 12 * * ? 每天中午12点触发
(8)0 15 10 ? * * 每天上午10:15触发
(9)0 15 10 * * ? 每天上午10:15触发
(10)0 15 10 * * ? 每天上午10:15触发
(11)0 15 10 * * ? 2005 2005年的每天上午10:15触发
(12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
(13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
(14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
(15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
(18)0 15 10 15 * ? 每月15日上午10:15触发
(19)0 15 10 L * ? 每月最后一日的上午10:15触发
(20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
(22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
SpringBoot 操作数据:spring-data jpa jdbc mongodb redis
springdata 也是和 springboot 齐名的项目
官网:https://spring.io/projects/spring-data-mongodb
创建springboot 项目,spring-10-redis
说明:
redis配置:
RedisAutoConfiguration
和 RedisProperties
@Bean
@ConditionalOnMissingBean( // 当我们定义template时,这个默认的就会失效,通常我们都会自己定义
name = {
"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 默认的redistemplate 没有过多设置,redis对象都是需要序列化的
// 两个泛型都是object,因此我们需要强转
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean // 由于string使我们的最长使用类型,所以单独提取一个方法,如果操作string直接用这个方法
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
如果使用连接池,推荐lettuce,不在推荐使用jedis,而且jedis连接池已经没有源码,使用可能出问题
spring.redis.host=127.0.0.1
spring.redis.port=6379
修改springboot测试类
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
// redistemplate方法
// opsForValue opsForList opsForSet opsForHash opsForZSet opsForGeo opsForValue opsForHyperLogLog
// 每一个方法对应操作不同数据类型,通过连式编程继续对本类型的数据进行处理
// api和我们的指令是一样的
// 除了基本操作,一些常用的操作也被单独设置了
// 获取redis连接对象
// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushDb();
// connection.flushAll();
redisTemplate.opsForValue().set("mykey", "swy");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
查看 redistemplate 源码,可以看到,redistemplate中有序列化参数,
为什么默认情况下当我们向redis中存储中文时会出现乱码
继续往下看源码,可以发现redistemplate默认使用了jdk序列化,jdk序列化会让字符串转义,
Redis配置类
我们可以自己创建一个配置类,RedisConfig,实现redis配置,包括重建 redistemplate
@Configuration
public class RedisConfig {
// 重建 RedisTemplate
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 详细配置template...
return template;
}
}
准备一个实体类,这里先不序列化,在template中处理
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private int age;
}
添加测试类方法,向redis中保存对象
首先我们通过对象转json字符串的方式传递
@Test
void contextLoads1() throws JsonProcessingException {
// 真实开发,一般使用json来传递对象
User user = new User("泥萌好", 18);
// 将对象序列化,springboot提供了jackon功能实现
String jsonUser = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user", jsonUser);
System.out.println(redisTemplate.opsForValue().get("user"));
}
关于对象保存:
Serializable
的方法为了解决序列化问题,redis默认template已经不能满足需要,需要我们自定义redistemplate
以下是企业常用的固定模板redistemplate配置,可以拿来即用
@Configuration
public class RedisConfig {
// 自定义RedisTemplate,这是企业中常用的固定模板,拿来即用
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// redistemplate默认泛型为
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// jackson的序列化配置
Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);//object指序列化针对任意对象
// 对象转json时涉及到转义,所以还要再配置 ObjectMapper
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// String的序列化配置
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用string的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化采用jackson
template.setValueSerializer(objectJackson2JsonRedisSerializer);
// hash的value序列化也采用jackson
template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
当我们配置了自己的 redistemplate 之后,当我们再次注入 RedisTemplate 属性,此时就是我们自己配置的redistemplate
我们可以点击注入属性左侧的spring图标进入查看
由于底层也有默认的redistemplate,如果担心重名,我们也可以在属性上添加注解@Qualifier来指定名字
现在,我们拿到实体对象之后不再转义,直接将对象作为参数传递
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads1() throws JsonProcessingException {
// 真实开发,一般使用json来传递对象
User user = new User("泥萌好", 18);
// 将对象序列化,springboot提供了jackon功能实现
//String jsonUser = new ObjectMapper().writeValueAsString(user);
// 直接传递对象
redisTemplate.opsForValue().set("user", user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
启动测试,
效果非常良好,如果使用的是jdk序列化,保存redis还会出现转义符,非常麻烦
RedisTemplate 提供的原生方法比较麻烦,还要链式编程写很多代码,通常我们不直接使用,而是自己封装一个工具类,方便使用
附上Redis工具类
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @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) {
e.printStackTrace();
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) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
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 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
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) {
e.printStackTrace();
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) {
e.printStackTrace();
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) {
e.printStackTrace();
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)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
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) {
e.printStackTrace();
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) {
e.printStackTrace();
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) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
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) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
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) {
e.printStackTrace();
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) {
e.printStackTrace();
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) {
e.printStackTrace();
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) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
分布式系统(distributed system)是建立在网络之上的软件系统。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。
https://dubbo.apache.org/zh/docs/v2.7/user/
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
缺点:
1、性能扩展比较难
2、协同开发问题
3、不利于升级维护
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。
缺点:公用模块无法重复利用,开发性的浪费
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;
对象想要传输必须先序列化,
Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
dubbo官网 http://dubbo.apache.org/zh-cn/index.html
1.了解Dubbo的特性
调用关系说明
点进dubbo官方文档,推荐我们使用Zookeeper 注册中心
什么是zookeeper呢?https://zookeeper.apache.org/
下载zookeeper :https://mirror.bit.edu.cn/apache/zookeeper/, 我们下载3.6.1, 最新版!解压zookeeper
下载带bin的
运行/bin/zkServer.cmd ,初次运行会报错,没有zoo.cfg配置文件;
可能遇到问题:闪退 !
解决方案:编辑zkServer.cmd文件末尾添加pause 。这样运行出错就不会退出,会提示错误信息,方便找到原因。
修改zoo.cfg配置文件
将conf文件夹下面的zoo_sample.cfg复制一份改名为zoo.cfg即可。
注意几个重要位置:
dataDir=./ 临时数据存储的目录(可写相对路径)
clientPort=2181 zookeeper的端口号
ls /:列出zookeeper根下保存的所有节点
报错别着急,多试几次就好了
create –e /kuangshen 123:创建一个kuangshen节点,值为123
get /kuangshen:获取/kuangshen节点的值
dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。
但是为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不过这个监控即使不装也不影响使用。
我们这里来安装一下:
地址 :https://github.com/apache/dubbo-admin/tree/master
修改 dubbo-admin\src\main\resources \application.properties 指定zookeeper地址
server.port=7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default.vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root
spring.guest.password=guest
# 注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
mvn clean package -Dmaven.test.skip=true
第一次打包的过程有点慢,需要耐心等待!直到成功!
java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
【注意:zookeeper的服务一定要打开!】
执行完毕,我们去访问一下 http://localhost:7001/ , 这时候我们需要输入登录账户和密码,我们都是默认的root-root;
总结:
启动zookeeper !
IDEA创建一个空项目;
创建一个模块,实现服务提供者:provider-server , 选择web依赖即可
项目创建完毕,我们写一个服务,比如卖票的服务;
编写接口
package nuc.ss.service;
public interface TicketService {
public String getTicket();
}
编写实现类
package nuc.ss.service;
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket() {
return "《狂神说Java》";
}
}
创建一个模块,实现服务消费者:consumer-server , 选择web依赖即可
项目创建完毕,我们写一个服务,比如用户的服务;
编写service
package nuc.ss.service;
public interface UserService {
//我们需要去拿去注册中心的服务
}
需求:现在我们的用户想使用买票的服务,这要怎么弄呢 ?
我们从dubbo官网进入github,看下方的帮助文档,找到dubbo-springboot,找到依赖包
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubbo-spring-boot-starterartifactId>
<version>2.7.3version>
dependency>
zookeeper的包我们去maven仓库下载,zkclient;
<dependency>
<groupId>com.github.sgroschupfgroupId>
<artifactId>zkclientartifactId>
<version>0.1version>
dependency>
【新版的坑】zookeeper及其依赖包,解决日志冲突,还需要剔除日志依赖;
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>2.12.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>2.12.0version>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.14version>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
exclusion>
exclusions>
dependency>
server.port=8001
#当前应用名字
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#扫描指定包下服务
dubbo.scan.base-packages=nuc.ss.service
package nuc.ss.service;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.stereotype.Component;
@Service //可以被扫描到,在项目一启动就自动注册到注册中心
@Component //使用Dubbo后尽量不要用Service注解
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket() {
return "《狂神说Java》";
}
}
逻辑理解 :应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它发布在指定的注册中心中!
<dependency>
<groupId>org.apache.dubbogroupId>
<artifactId>dubbo-spring-boot-starterartifactId>
<version>2.7.3version>
dependency>
<dependency>
<groupId>com.github.sgroschupfgroupId>
<artifactId>zkclientartifactId>
<version>0.1version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>2.12.0version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>2.12.0version>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.14version>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
exclusion>
exclusions>
dependency>
消费者到注册中心去拿服务
server.port=8002
#当前应用名字
dubbo.application.name=consumer-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
package nuc.ss.service;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;
@Service //注入到容器中
public class UserService {
// 想拿到provider-server提供的票,要去注册中心拿到服务
@Reference //引用,Pom坐标,可以定义路径相同的接口名
TicketService ticketService;
public void bugTicket(){
String ticket = ticketService.getTicket();
System.out.println("在注册中心买到"+ticket);
}
}
直接使用@Reference注入拿不到提供者的对象,有两种办法解决
第一种,在本服务中定义一个和目标借口一样的接口,包括路径,名字,方法
@SpringBootTest
public class ConsumerServerApplicationTests {
@Autowired
UserService userService;
@Test
public void contextLoads() {
userService.bugTicket();
}
}
开启zookeeper
打开dubbo-admin实现监控【可以不用做】
开启服务者
消费者消费测试,结果:
dubbo 使用步骤
1.提供者提供服务
2.消费者消费服务
注意:
狂神说-富文本编辑器