目录
1、我的第一个SpringBoot项目
创建SpringBoot项目
选择导入的依赖,完成即可构建成功!
测试一下
启动springboot
成功访问
如何修改端口号
修改后重启
8080无响应
8081可以访问
2、SpringBoot自动装配原理
3、yaml语法
用key:空格 value表示
用yaml赋值
实体类。使用@ConfigurationProperties注解要导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
yaml文件中进行对实体类的赋值,birth一定要注意格式
测试类和测试结果
property赋值
测试结果
配置文件占位符
配置文件还可以编写占位符生成随机数
对比:
@Value这个使用起来并不友好,我们需要为每个属性单独注解赋值,比较麻烦;我们来看个功能对比图
1、@ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加
2、松散绑定:这个什么意思呢 比如我的yml中写的last-name,这个和lastName是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定。可以测试一下
3、JSR303数据校验 , 这个就是我们可以在字段是增加一层过滤器验证 , 可以保证数据的合法性
4、复杂类型封装,yml中可以封装对象 , 使用value就不支持
结论:
配置yml和配置properties都可以获取到值 , 强烈推荐 yml;
如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;
如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!
4、JSR303数据校验及多环境切换
Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。我们这里来写个注解让我们的name只能支持Email格式;
运行会直接报错
使用数据校验,可以保证数据的正确性;
@NotNull(message="名字不能为空")
private String userName;
@Max(value=120,message="年龄最大不能查过120")
private int age;
@Email(message="邮箱格式错误")
private String email;
空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
.......等等
除此以外,我们还可以自定义一些数据校验规则
5、整合JDBC,Druid,Mybatis
1、整合JDBC
application.yml文件中配置JDBC
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
2、整合Druid
导入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
Druid数据源的yml配置
#SpringBoot默认是不注入这些的,需要自己绑定
#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.Properity
#则导入log4j 依赖就行
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
设置一个后台监控
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource(){
return new DruidDataSource();
}
//后台监控 : web,xml, ServletRegistrationBean
//因为SpringBoot内置了servlet容器,所以没有web.xml,替代方法 ServletRegistrationBean
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(),"/druid/*");
//后台需要有人登录,账号密码配置
HashMap<String, String> initParameters = new HashMap<>();
//增加配置
initParameters.put("loginUsername","admin");//登录key 是固定的 loginUsername loginPassword
initParameters.put("loginPassword","123456");
//允许谁可以访问
initParameters.put("allow","");//参数为空,所有人都可以访问
//禁止谁能访问 initParameters.put("lqh","192.168.1.1");
bean.setInitParameters(initParameters);//设置初始化参数
return bean;
}
}
登录访问localhost:8080/druid
设置一个简单的过滤器
//filter过滤器
@Bean
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
//可以过滤哪些请求
Map<String,String> initParameters = new HashMap<>();
//这些东西不进行统计
initParameters.put("exclusions","*.js,*.css,/druid/*");
bean.setInitParameters(initParameters);
return bean;
}
3、整合Mybatis
实体类
mapper接口
在resources下新建mybatis/mapper/UserMapper.xml 来存放xml文件
application.properties配置mybatis的配置信息
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#整合mybatis
mybatis.type-aliases-package=com.lqh.pojo
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
Controller层
6、SpringSecurity
实验环境搭建
1、新建一个初始的springboot项目web模块,thymeleaf模块
2、导入静态资源
3、controller跳转
认识SpringSecurity
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
-
WebSecurityConfigurerAdapter:自定义Security策略
-
AuthenticationManagerBuilder:自定义认证策略
-
@EnableWebSecurity:开启WebSecurity模式
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在Spring Security 中存在。
认证和授权
目前,我们的测试环境,是谁都可以访问的,我们使用Spring Security增加上认证和授权的功能
1、引入 Spring Security 模块
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、编写 Spring Security 配置类
@EnableWebSecurity // 开启WebSecurity模式
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");
//没有权限会默认到登录页面
http.formLogin();
}
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("liuqianhao").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("liangzhu").password(new BCryptPasswordEncoder().encode("123456")).roles("vip3");
}
}
权限控制和注销
1、开启自动配置的注销的功能
2、在前端中添加一个注销按钮
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
3、其他功能实现
导入maven依赖
<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
修改我们的 前端页面
导入命名空间
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
修改导航栏,增加认证判断
如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭csrf功能;我们试试:在 配置中增加 http.csrf().disable();
//开启自动配置的注销的功能
// /logout 注销请求
http.logout().logoutSuccessUrl("/");
//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.csrf().disable();
继续将下面的角色功能块认证完成
测试一下就搞定了
记住我
现在的情况,我们只要登录之后,关闭浏览器,再登录,就会让我们重新登录,但是很多网站的情况,就是有一个记住密码的功能,这个该如何实现呢?很简单
开启记住我功能
//记住我
http.rememberMe();
定制登录页
现在这个登录页面都是spring security 默认的,怎么样可以使用我们自己写的Login界面呢?
1.在刚才的登录页配置后面指定 loginpage
http.formLogin().loginPage("/toLogin");
2.然后前端也需要指向我们自己定义的 login请求
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登录
</a>
3.我们登录,需要将这些信息发送到哪里,我们也需要配置,login.html 配置提交请求及方式,方式必须为post
4、这个请求提交上来,我们还需要验证处理,怎么做呢?我们可以查看formLogin()方法的源码!我们配置接收登录的用户名和密码的参数
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login"); // 登陆表单提交请求
5、在登录页增加记住我的多选框
<input type="checkbox" name="remember"> 记住我
6、后端验证处理
//定制记住我的参数!
http.rememberMe().rememberMeParameter("remember");
7、Shiro
1、springboot整合shiro环境搭建
1.新建一个springboot模块
2.添加maven依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.8.0</version>
</dependency>
3.编写首页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
</body>
</html>
4.编写controller
@Controller
public class MyController {
@RequestMapping({"/", "/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello shiro");
return "index";
}
}
5.测试
6.创建ShiroConfig和UserRealm
@Configuration
public class ShiroConfig {
//ShiroFilterFactoryBean:第三步
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
//DefaultWebSecurityManager:第二步
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联UserRealm
securityManager.setRealm(userRealm);
return securityManager;
}
//创建Realm对象,需要自定义类:第一步
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
}
ShiroConfig
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;
}
}
UserRealm
这里ShiroConfig需要从下往上写,每一层都要用@Bean注入到spring容器中进行托管
7.编写add和update跳转页面
8.controller层实现跳转
@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";
}
}
最后是测试,能成功跳转就代表我们的环境已经搭建好了!
2、shiro实现登录拦截
首先要记住这些:
anon:无需认证就可以访问
authc:必须认证了才能访问
user:必须拥有 记住我 功能才可使用
perms: 拥有对某个资源的权限才可访问
role: 拥有某个角色权限才可访问
1.添加shiro的内置过滤器
@Configuration
public class ShiroConfig {
//ShiroFilterFactoryBean:第三步
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
/*
anon:无需认证就可以访问
authc:必须认证了才能访问
user:必须拥有 记住我 功能才可使用
perms: 拥有对某个资源的权限才可访问
role: 拥有某个角色权限才可访问
*/
//由于是链式的,一般用LinkedHashMap
Map<String, String> filterMap = new LinkedHashMap<>();
// filterMap.put("/user/add","authc");
// filterMap.put("/user/update","authc");
filterMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
}
2.编写登录表单及按钮
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登录</h1>
<hr>
<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>
3.controller层添加跳转请求
4.设置登录的请求
3、Shiro实现用户认证
1.获取用户的数据并且封装,捕获用户名或密码的异常并实现拦截
@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";
}
}
在Controller文件中编写
2.登录页面添加错误提示
<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>
3.在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");
//用户名,密码 数据库中取
String name = "root";
String password = "123456";
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//用户认证
if (!name.equals(token.getUsername())){
return null;//抛出异常 UnknownAccountException
}
//密码认证 shiro已经帮我们做了
return new SimpleAuthenticationInfo("",password,"");
}
}
4.最后进行测试
4、Shiro整合Mybatis
1.导入依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
2.配置jdbc和druid数据源
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#SpringBoot默认是不注入这些的,需要自己绑定
#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.Properity
#则导入log4j 依赖就行
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
3.配置mybatis的properties文件
mybatis.type-aliases-package=com.lqh.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
4.编写实体类
5.编写mapper接口
6.配置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.lqh.mapper.UserMapper">
<select id="queryUserByName" parameterType="String" resultType="User">
select *
from mybatis.user
where name = #{name};
</select>
</mapper>
7.编写service层
UserService接口
public interface UserService {
public User queryUserByName(String name);
}
UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Override
public User queryUserByName(String name) {
return userMapper.queryUserByName(name);
}
}
8.测试
@SpringBootTest
class ShiroSpringbootApplicationTests {
@Autowired
UserServiceImpl userService;
@Test
void contextLoads() {
System.out.println(userService.queryUserByName("张三"));
}
}
基本环境就搭好了,接下来去修改认证信息
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 authenticationToken) throws AuthenticationException {
System.out.println("执行了=>认证 doGetAuthenticationInfo");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
User user = userService.queryUserByName(token.getUsername());
if (user==null){//如果没有这个人
return null;
}
//密码认证 shiro已经帮我们做了
return new SimpleAuthenticationInfo("",user.getPassword(),"");
}
}
就可以从数据库中读取用户名和密码了
5、Shiro请求授权实现
1.设置授权
添加了以后就会被拦截进去add页面
2.自己再写一个页面,被拦截以后跳转过去
@RequestMapping("/noauth")
@ResponseBody
public String unauthorized(){
return "未经授权无法访问此页面";
}
未授权时就会跳转
当然也要在UserRealm中添加授权实现拦截
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权 doGetAuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//添加一个权限 所有角色拥有user:add才可以通过,不然会被拦截
info.addStringPermission("user:add");
//注意返回值不要写错
return info;
}
然后进行测试。但是发现在测试的过程中,登录其他的用户也可以进入,所以要做进一步的授权功能。
3.数据库中添加字段perms,手动给两个用户添加权限,修改实体类
用户在认证里查的信息,怎么在授权里做呢?
首先我们需要拿到User的对象
这里的user,上面授权就可以拿到
然后拿到登录对象,设置权限
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权 doGetAuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//添加一个权限 所有角色拥有user:add才可以通过,不然会被拦截
info.addStringPermission("user:add");
//拿到当前登录的对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();//拿到登录对象
//设置当前用户的权限
info.addStringPermission(currentUser.getPerms());
return info;
}
测试
root因为在数据库中有这是update权限,可以正常登录进入update页面,但是aaaa并没有设置,所以无法进入,提醒未授权
这里设置的,需要注释掉
6、Shiro整合thymeleaf
1.导入thymeleaf和shiro的整合包
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>
2.在ShiroConfig中用ShiroDialect整合shiro和thymeleaf
//整合ShiroDialect:用来整合shiro thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
3.去首页编写Permission权限
先导入命名空间
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
添加权限,最后再添加一个登录按钮
<h1>首页</h1>
<p>
<a th:href="@{/toLogin}">登录</a>
</p>
<p th:text="${msg}"></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>
测试
root只有一个权限,所以只会显示update
aaaa一个权限都没有,所以什么都不显示
如果想进入首页去掉登录按钮,添加shiro:notAuthenticated这个标签就可以了
<h1>首页</h1>
<div shiro:notAuthenticated>
<a th:href="@{/toLogin}">登录</a>
</div>
登录按钮就被干掉了!
8、Swagger
1.SpringBoot集成Swagger
1、新建一个springboot-web项目
2、导入相关依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>3.0.0</version>
</dependency>
3、编写HelloController,测试确保运行成功
4、要使用Swagger,我们需要编写一个配置类-SwaggerConfig来配置 Swagger
@Configuration //配置类
@EnableSwagger2// 开启Swagger2的自动配置
public class SwaggerConfig {
}
5、访问测试 :http://localhost:8080/swagger-ui.html ,可以看到swagger的界面;
2.配置Swagger
1、Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger。
@Bean //配置docket以配置Swagger具体参数
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2);
}
2、可以通过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<>()// 扩展
);
}
3、Docket 实例关联上 apiInfo()
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}
4、重启项目,访问测试 http://localhost:8080/swagger-ui.html 看下效果;
3.配置扫描接口
1、构建Docket时通过select()方法配置怎么扫描接口。
2、重启项目测试,由于我们配置根据包的路径扫描接口,所以我们只能看到一个类
3、除了通过包路径配置扫描接口外,还可以通过配置其他方式扫描接口,这里注释一下所有的配置方式:
//配置了Swagger的Docket的bean实例
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
//basePackage:指定要扫描的包
//RequestHandlerSelectors.any() :扫描全部
//RequestHandlerSelectors.none():都不扫描
//RequestHandlerSelectors.withClassAnnotation():扫描类上的注解,参数是一个注解的反射对象
//RequestHandlerSelectors.withMethodAnnotation():扫描方法上的注解
.apis(RequestHandlerSelectors.basePackage("com.example.swagger.controller"))
.build();
}
4、除此之外,我们还可以配置接口扫描过滤:
//配置了Swagger的Docket的bean实例
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.swagger.controller"))
//paths() 过滤什么路径
.paths(PathSelectors.ant("example/**"))
.build();
}
5、这里的可选值还有
any() // 任何请求都扫描
none() // 任何请求都不扫描
regex(final String pathRegex) // 通过正则表达式控制
ant(final String antPattern) // 通过ant()控制
4、配置Swagger开关
1、通过enable()方法配置是否启用swagger,如果是false,swagger将不能在浏览器中访问了
//配置了Swagger的Docket的bean实例
@Bean
public Docket docket(Environment environment){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(flag)//enable是否启动Swagger,如果为false,则Swagger不能在浏览器中访问
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.swagger.controller"))
//paths() 过滤什么路径
//.paths(PathSelectors.ant("example/**"))
.build();
}
2、如何动态配置当项目处于test、dev环境时显示swagger,处于prod时不显示?
//配置了Swagger的Docket的bean实例
@Bean
public Docket docket(Environment environment){
//设置要显示的Swagger环境
Profiles profiles = Profiles.of("dev");
//通过environment.acceptsProfiles()判断是否处在自己设定的环境当中
//通过 enable() 接收此参数判断是否要显示
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(flag)//enable是否启动Swagger,如果为false,则Swagger不能在浏览器中访问
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.swagger.controller"))
//paths() 过滤什么路径
//.paths(PathSelectors.ant("example/**"))
.build();
}
3、可以在项目中增加一个dev的配置文件查看效果
选择环境
5、配置API分组
1、如果没有配置分组,默认是default。通过groupName()方法即可配置分组:
//配置了Swagger的Docket的bean实例
@Bean
public Docket docket(Environment environment){
//设置要显示的Swagger环境
Profiles profiles = Profiles.of("dev");
//通过environment.acceptsProfiles()判断是否处在自己设定的环境当中
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("lqh")
.enable(flag)//enable是否启动Swagger,如果为false,则Swagger不能在浏览器中访问
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.swagger.controller"))
//paths() 过滤什么路径
//.paths(PathSelectors.ant("example/**"))
.build();
}
2、重启项目查看分组
3、如何配置多个分组?配置多个分组只需要配置多个docket即可:
@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");
}
4、重启项目查看即可
6、实体配置
1、新建一个实体类
@ApiModel("用户实体类")
public class User {
@ApiModelProperty("用户名")
public String username;
@ApiModelProperty("密码")
public String password;
}
2、只要这个实体在请求接口的返回值上(即使是泛型),都能映射到实体项中:
//只要我们的接口中,返回值存在实体类,它就会被扫描到Swagger中
@PostMapping(value = "/user")
public User user(){
return new User();
}
3、重启查看测试
注:并不是因为@ApiModel这个注解让实体显示在这里了,而是只要出现在接口方法的返回值上的实体都会显示在这里,而@ApiModel和@ApiModelProperty这两个注解只是为实体添加注释的。
@ApiModel为类添加注释
@ApiModelProperty为类属性添加注释
7、常用注解
Swagger的所有注解定义在io.swagger.annotations包下
下面列一些经常用到的,未列举出来的可以另行查阅说明:
@Api(tags = “xxx模块说明”):作用在模块类上
@ApiOperation(“xxx接口说明”):作用在接口方法上
@ApiModel(“xxxPOJO说明”):作用在模型类上:如VO、BO@ApiModelProperty(value = “xxx属性说明”,hidden = true)作用在类方法和属性上,hidden设置为true可以隐藏该属性@ApiParam(“xxx参数说明”):作用在参数、方法和字段上,类似@ApiModelProperty
我们也可以给请求的接口配置一些注释
//Operation接口,不是放在类上的,是方法
@ApiOperation("Hello控制类")
@GetMapping(value = "/hello2")
@ResponseBody
public String hello2(@ApiParam("用户名") String username){
return "hello"+username;
}
这样的话,可以给一些比较难理解的属性或者接口,增加一些配置信息,让人更容易阅读!
相较于传统的Postman或Curl方式测试接口,使用swagger简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。
Swagger是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出Word接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭Swagger,一来出于安全考虑二来也可以节省运行时内存。
9、异步、定时、邮件任务
1.异步任务
1、创建一个service包
2、创建一个类AsyncService
异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。
编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况;
@Service
public class AsyncService {
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据正在处理....");
}
}
3、编写controller包
4、编写AsyncController类
我们去写一个Controller测试一下
@RestController
public class AsynController {
@Autowired
AsynService asynService;
@RequestMapping("/hello")
public String hello(){
asynService.hello();
return "OK";
}
}
5、访问http://localhost:8080/hello进行测试,3秒后出现OK,这是同步等待的情况。
问题:我们如果想让用户直接得到消息,就在后台使用多线程的方式进行处理即可,但是每次都需要自己手动去编写多线程的实现的话,太麻烦了,我们只需要用一个简单的办法,在我们的方法上加一个简单的注解即可,如下:
6、给hello方法添加@Async注解;
//告诉Spring这是一个异步方法
@Async
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据正在处理......");
}
SpringBoot就会自己开一个线程池,进行调用!但是要让这个注解生效,我们还需要在主程序上添加一个注解@EnableAsync ,开启异步注解功能;
@EnableAsync //开启异步注解功能
@SpringBootApplication
public class Springboot09TestApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot09TestApplication.class, args);
}
}
7、重启测试,网页瞬间响应,后台代码依旧执行!
2.定时任务
项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring为我们提供了异步执行任务调度的方式,提供了两个接口。
-
TaskExecutor接口
-
TaskScheduler接口
两个注解:
-
@EnableScheduling
-
@Scheduled
测试步骤:
1、创建一个ScheduledService
我们里面存在一个hello方法,他需要定时执行,怎么处理呢?
@Service
public class ScheduleService {
//在一个特定的时间执行这个方法
//cron表达式
//秒 分 时 日 月 周几
@Scheduled(cron = "0 * * * * 0-7")
public void hello(){
System.out.println("hello,你被执行了");
}
}
2、这里写完定时任务之后,我们需要在主程序上增加@EnableScheduling 开启定时任务功能
@EnableAsync //开启异步注解功能
@EnableScheduling //开启基于注解的定时任务
@SpringBootApplication
public class SpringbootTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTaskApplication.class, args);
}
}
3、我们来详细了解下cron表达式;
http://www.bejson.com/othertools/cron/
4、常用的表达式
(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触发
3.邮件任务
邮件发送,在我们的日常开发中,也非常的多,Springboot也帮我们做了支持
-
邮件发送需要引入spring-boot-start-mail
-
SpringBoot 自动配置MailSenderAutoConfiguration
-
定义MailProperties内容,配置在application.yml中
-
自动装配JavaMailSender
-
测试邮件发送
1、引入pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
2、修改配置文件
spring:
mail:
username: 835788261@qq.com
password: 你的qq授权码
host: smtp.qq.com
properties: {mail.smtl.ssl.enable: true}
3、Spring单元测试
@Autowired
JavaMailSenderImpl mailSender;
@Test
void contextLoads() {
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject("你好呀");
message.setText("你是猪");
message.setTo("835788261@qq.com");
message.setFrom("835788261@qq.com");
mailSender.send(message);
}
@Test
void contextLoads1() throws MessagingException {
MimeMessage mimeMessage = mailSender.createMimeMessage();
//组装
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
helper.setSubject("你好呀plus");
helper.setText("<p style='color:red'>你是猪</p>",true);
helper.addAttachment("1.jpg",new File("C:\Users\1\Desktop\1.jpg"));
helper.addAttachment("2.jpg",new File("C:\Users\1\Desktop\1.jpg"));
helper.setTo("835788261@qq.com");
helper.setFrom("835788261@qq.com");
mailSender.send(mimeMessage);
}
最后成功接收到消息和附件。
10、Dubbo&Zookeeper
1.Dubbo
Apache Dubbo |db| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
dubbo官网 http://dubbo.apache.org/zh-cn/index.html
1.了解Dubbo的特性
2.查看官方文档
服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
调用关系说明
l 服务容器负责启动,加载,运行服务提供者。
l 服务提供者在启动时,向注册中心注册自己提供的服务。
l 服务消费者在启动时,向注册中心订阅自己所需的服务。
l 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
l 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
l 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
2.Dubbo+Zookeeper环境搭建
Zookeeper
1、下载zookeeper
2、运行/bin/zkServer.cmd ,初次运行会报错,没有zoo.cfg配置文件;
可能遇到问题:闪退 !
解决方案:编辑zkServer.cmd文件末尾添加pause 。这样运行出错就不会退出,会提示错误信息,方便找到原因。
3、修改zoo.cfg配置文件
将conf文件夹下面的zoo_sample.cfg复制一份改名为zoo.cfg即可。
注意几个重要位置:
dataDir=./ 临时数据存储的目录(可写相对路径)
clientPort=2181 zookeeper的端口号
修改完成后再次启动zookeeper
Dubbo
dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。
但是为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不过这个监控即使不装也不影响使用。
我们这里来安装一下:
1、下载dubbo-admin
地址 :GitHub - apache/dubbo-admin at master-0.2.0
2、解压进入目录
修改 dubbo-adminsrcmain esources 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
3、在项目目录下打包dubbo-admin
mvn clean package -Dmaven.test.skip=true
4、执行 dubbo-admin arget 下的dubbo-admin-0.0.1-SNAPSHOT.jar
java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
【注意:zookeeper的服务一定要打开!】
执行完毕,我们去访问一下 http://localhost:7001/ , 这时候我们需要输入登录账户和密码,我们都是默认的root-root;
登录成功后,查看界面
3.SpringBoot+Dubbo+Zookeeper
1. 启动zookeeper !
2. IDEA创建一个空项目;
3.创建一个模块,实现服务提供者:provider-server , 选择web依赖即可
4.项目创建完毕,我们写一个服务,比如卖票的服务;
package com.lqh.service;
public interface TicketService {
public String getTicket();
}
编写实现类
public class TicketServiceImpl implements TicketService{
@Override
public String getTicket() {
return "123";
}
}
5.创建一个模块,实现服务消费者:consumer-server , 选择web依赖即可
6.项目创建完毕,我们写一个服务,比如用户的服务;
编写service
public class UserService {
//想拿到provider-server提供的票,要去注册中心拿到服务
}
服务提供者
1、将服务提供者注册到注册中心,我们需要整合Dubbo和zookeeper,所以需要导包。我们从dubbo官网进入github,看下方的帮助文档,找到dubbo-springboot,找到依赖包。这里将所需的全部导入
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.8</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<!-- 引入zookeeper -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
<!--排除这个slf4j-log4j12-->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
2、在springboot配置文件中配置dubbo相关属性
#端口号
server.port=8001
#服务应用名字
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#哪些服务要被注册
dubbo.scan.base-packages=com.lqh.service
3、在service的实现类中配置服务注解,发布服务。注意导包问题
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Component;
@DubboService //将服务发布出去
@Component //这里用@Component注入到容器中,也可以用@Service,但是需要注意导包
public class TicketServiceImpl implements TicketService{
@Override
public String getTicket() {
return "123";
}
}
为了区分Dubbo的Service和Spring的Service,这里已经将注解改为了@DubboService
服务消费者
1、导入依赖,和之前的依赖一样
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.8</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<!-- 引入zookeeper -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
<!--排除这个slf4j-log4j12-->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
2、配置参数
#端口号
server.port=8002
#消费者去哪里拿服务,需要暴露自己的名字
dubbo.application.name=consumer-server
#注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
3. 本来正常步骤是需要将服务提供者的接口打包,然后用pom文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同;
4. 完善消费者的服务类
package com.lqh.service;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;
@Service
public class UserService {
//想拿到provider-server提供的票,要去注册中心拿到服务
@DubboReference //远程引用指定的服务,他会按照全类名进行匹配,看谁给注册中心注册了这个全类名
TicketService ticketService;
public void buyTicket(){
String ticket = ticketService.getTicket();
System.out.println("在注册中心拿到了=>"+ticket);
}
}
5. 编写测试类
@SpringBootTest
class ConsumerServerApplicationTests {
@Autowired
UserService userService;
@Test
void contextLoads() {
userService.buyTicket();
}
}
启动测试
1. 开启zookeeper
2. 打开dubbo-admin实现监控【可以不用做】
3. 开启服务者
4. 消费者消费测试,结果:
监控中心 :
文章部分转载自:终于,狂神说SSM及SpringBoot系列文章完更!!!_狂神说-CSDN博客_狂神说springboot
个人学习及复习使用,仅供参考