1、SpringBoot常用
1.1、官方文档
https://docs.spring.io/spring-boot/docs/2.2.4.RELEASE/reference/html/index.html
1.2、搭建SpringBoot程序
使用spring网站:Spring Initializr
1.3 application.yaml
#数据库连接 配置数据源spring:datasource:username: ***password: ***url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8driver-class-name: com.mysql.cj.jdbc.Driver#整合mybatismybatis:#使用别名type-aliases-package: com.qing.pojo#扫描对应的mapper.xml文件,路径要与resources下的xml文件对应mapper-locations: classpath:mybatis/mapper/*.xml
1.4 pom.xml
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--thymeleaf--><dependency><groupId>org.thymeleaf</groupId><artifactId>thymeleaf-spring5</artifactId></dependency><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-java8time</artifactId></dependency></dependencies>
1.5 命名空间
<!--thymeleaf--><html lang="en" xmlns:th="http://www.thymeleaf.org"><!--shiro--><!--有提示,但不能使用":" 如 shiro:hasPermission="user:add"--><html lang="en" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"><!--没有提示,但格式不受限制: 可以使用 shiro:hasPermission="user:add"--><html lang="en" xmlns:th="http://www.thymeleaf.org/thymeleaf-extras-shiro"><!--springsecurity--><html lang="en" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
1.6 mapper.xml
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.qing.mapper.admin.AdminUserMapper"><select id="getById" parameterType="int" resultType="com.xf.pojo.User">SELECT * FROM t_user WHERE id=#{id}</select></mapper>
2、创建SpringBoot程序方法
2.1 使用spring网站快速搭建
解压压缩包文件,在idea中导入,即可运行一个初始化的SpringBoot程序
2.2 在idea中创建
建议删除package中的后缀,直接使用com.qing
3、SpringBoot原理
3.1 自动配置(重要)
- SpringBoot启动会加载大量的自动配置类
- 看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;
- 再看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在其中,我们就不需要再手动配置了)
- 给容器中自动配置类添加组件的时候,会从properties类中获取某些属性,只需要在配置文件中指定这些属性的值即可;xxxxAutoConfigurartion:自动配置类;给容器中添加组件
开启debug可以在启动的时候在控制台输出已经生效的配置
debug: true
3.2 pom.xml
- spring-boot-dependencies:在spring-boot-starter-parent中,且核心依赖在父工程中!
- 在写或者引入一些SpringBoot依赖时,不需要指定版本,spring-boot-dependencies中封装了所有常用jar包的版本
3.3 启动器
<!--主启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><!--web启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--test测试启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
- 启动器:SpringBoot的启动场景
- 如spring-boot-starter-web就会自动帮我们导入web环境的所有依赖
- springboot将所有的功能场景都封装成了一个个启动器
3.4 主程序
//标注这个类是一个springboot应用@SpringBootApplicationpublic class Springboot01HelloApplication {public static void main(String[] args) {//启动springbootSpringApplication.run(Springboot01HelloApplication.class, args);}}
3.4.1 SpringBootApplication注解
//1.springboot的配置@SpringBootConfiguration//1.1 spring配置类@Configuration//1.2 说明这也是一个spring的组件@Component//2.自动配置(核心)@EnableAutoConfiguration//2.1 自动配置包@AutoConfigurationPackage//2.1.1 注册扫描路径到全局变量,提供查询@Import(AutoConfigurationPackages.Registrar.class)//2.2 加载配置,完成自动装配@Import(AutoConfigurationImportSelector.class)//3.扫描路径 默认扫描启动类所在的当前目录@ComponentScan//2.2中的AutoConfigurationImportSelector.class获取所有的配置List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
//getCandidateConfigurations方法 获取候选的配置protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "+ "are using a custom packaging, make sure that file is correct.");return configurations;}//SpringFactoriesLoader.loadFactoryNames方法实现了将META-INF/spring.factories中所有的配置资源加载到配置类中
META-INF/spring.factories 自动配置的核心文件:在spring-boot-test-autoconfigure.jar中
springboot所有的自动配置都在这里
# AutoConfigureCache auto-configuration importsorg.springframework.boot.test.autoconfigure.core.AutoConfigureCache=\org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration# AutoConfigureDataCassandra auto-configuration importsorg.springframework.boot.test.autoconfigure.data.cassandra.AutoConfigureDataCassandra=\org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration......
3.4.2 流程总结
- springboot在启动的时候,从类路径下 /META-INF/spring.factories.获取指定的值
- 将这些自动配置的类导入容器,自动配置就会生效,帮我们进行自动配置
- 以前我们需要手动配置的东西,现在Springboot帮我们做了
- 整合javaEE,解决方案和自动配置的东西都在spring-boot-autoconfigure包下
- 它会把所有需要导入的组件,以类名的方式返回,这些组件就会添加到容器
- 容器中也会存在XXXXautoConfiguration的文件(@Bean),就是这些类给容器中导入了这个场景所需要的所有组件,并自动配置,@Configuration,JavaConfig
- 有了自动配置类,免去了我们自己编写配置文件的步骤
3.4.3 SpringApplication类
主要做了四件事:
- 推断应用的类型是普通的项目还是Web项目
- 查找并加载所有可用初始化器 , 设置到initializers属性中
- 找出所有的应用程序监听器,设置到listeners属性中
- 推断并设置main方法的定义类,找到运行的主类
3.4.4 run方法流程

4、SpringBoot配置文件
4.1 配置文件的两种方式
SpringBoot使用一个全局的配置文件 , 配置文件名称是固定的
方式一:application.properties
语法结构 :键值对 key=value 如:
server.port=8081
方式二:application.yml(推荐使用)
语法结构 :key:空格 value 如:
server:port: 8081
注:port: 这里必须带一个空格
4.2 yaml语法
#yaml对空格的要求及其高server:port: 8081# 普通的key-valuename: 小明#对象student:name: qingfanage: 3#行内写法student1: {name: qingfan,age: 3}#数组pets1:- cat- dog- pigpets2: [cat,dog,pig]
4.3 使用yaml给实体类注入值
yaml中配置属性:
person:name: qingfanage: 3birthday: 2020/1/1map: {k1: v1,k2: v2}list:- code- musicdog:name: wangcaiage: 3
person实体类设置:
//@ConfigurationProperties绑定yaml配置中的参数 这里的person对应yaml文件中的person对象名@ConfigurationProperties(prefix = "person")public class Person {private String name;private String age;private Date birthday;private Map<String,Object> map;private List<Object> list;private Dog dog;}
若添加@ConfigurationProperties注解爆红,则在pom.xml配置文件中添加:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId></dependency>
测试:
@SpringBootTestclass Springboot02ConfigApplicationTests {@AutowiredPerson person;@Testvoid contextLoads() {System.out.println(person.toString());//Person(name=qingfan, age=3, birthday=Wed Jan 01 00:00:00 CST 2020, map={k1=v1, k2=v2}, list=[code, music],dog=Dog(name=wangcai,age=3))}}
4.4 yaml松散绑定
比如我的yaml中写的last-name,这个和lastName是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定。
4.5 JSR303数据校验
pom.xml中导入依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
在实体类上添加@Validated注解进行数据校验
若在person实体类中name字段上添加@Email()注解,那么就会对name字段进行一个邮箱验证,如果配置的属性不是一个邮箱,则就会报错:
验证格式:
空检查@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=) Validates that the annotated string is between min and max included.日期检查@Past 验证 Date 和 Calendar 对象是否在当前时间之前,验证成立的话被注释的元素一定是一个过去的日期@Future 验证 Date 和 Calendar 对象是否在当前时间之后 ,验证成立的话被注释的元素一定是一个将来的日期@Pattern 验证 String 对象是否符合正则表达式的规则,被注释的元素符合制定的正则表达式,regexp:正则表达式 flags: 指定 Pattern.Flag 的数组,表示正则表达式的相关选项。数值检查建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为”“,Integer为null@Min 验证 Number 和 String 对象是否大等于指定的值@Max 验证 Number 和 String 对象是否小等于指定的值@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度@Digits 验证 Number 和 String 的构成是否合法@Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。@Range(min=, max=) 被指定的元素必须在合适的范围内@Range(min=10000,max=50000,message=”range.bean.wage”)@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)@CreditCardNumber信用卡验证@Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。@ScriptAssert(lang= ,script=, alias=)@URL(protocol=,host=, port=,regexp=, flags=)
4.6 配置文件的位置
配置文件的优先级由高到低分别为项目路径下的config/application.yaml>application.yaml>resources/config/application.yaml>resources/application.yaml
优先级1-2-3-4
4.7 yaml多环境切换
用---来分隔多个文件,使用profiles的active来选择调用哪个环境
server:port: 8081spring:profiles:active: dev#active中不填写则默认启动8081---server:port: 8082spring:profiles: dev---server:port: 8083spring:profiles: test
5、SpringBoot Web开发
5.1 要解决的问题
- 导入静态资源…
- 首页
- jsp,模版引擎Thymeleaf
- 装配扩展SpringMVC
- 增删改查
- 拦截器
- 国际化
5.2 静态资源
@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {if (!this.resourceProperties.isAddMappings()) {logger.debug("Default resource handling disabled");return;}addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {registration.addResourceLocations(this.resourceProperties.getStaticLocations());if (this.servletContext != null) {ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);registration.addResourceLocations(resource);}});}
方式一:通过webjars获得静态资源
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
访问路径: localhost:8080/wbjars/
方式二:
访问路径:localhost:8080/
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/" };
优先级:1-2-3 resources>static>(默认)>public
注意:一但自定义资源路径后,上面的静态资源访问方式都会失效
#配置文件中自定义静态资源路径spring.mvc.static-path-pattern=/qing
//源码解释if (!this.resourceProperties.isAddMappings()) {logger.debug("Default resource handling disabled");return;}
5.3 thymeleaf模板引擎
Thymeleaf 官网:https://www.thymeleaf.org/
Thymeleaf 在Github 的主页:https://github.com/thymeleaf/thymeleaf
Spring官方文档找到我们对应的版本:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter
thymeleaf中文文档:https://www.docs4dev.com/docs/zh/thymeleaf/3.0/reference/using_thymeleaf.htm
5.3.1 页面跳转
导入thymeleaf依赖:
<dependency><groupId>org.thymeleaf</groupId><artifactId>thymeleaf-spring5</artifactId></dependency><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-java8time</artifactId></dependency>
在templates文件夹下创建html页面
1、Controller下跳转访问页面
@Controller@RequestMapping("test")public class TestController {@RequestMapping("/hello")public String hello(){return "test";}}http://localhost:8080/test/hello
2、扩展springmvc实现跳转
@Configurationpublic class MyConfig implements WebMvcConfigurer {@Overridepublic void addViewControllers(ViewControllerRegistry registry) {registry.addViewController("/hello").setViewName("test");}}
5.3.2 前端取值
html页面引入thymeleaf命名空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
前端页面获取后台mode中的值
<body><h1>卿帆很帅</h1><!--所有的html元素都可以被thymeleaf替换接管: th:元素名--><h2 th:text="${msg}"></h2><div th:text="${msg}"></div></body>
5.3.3 其他语法
<!--前端遍历model传来的数组-->model.addAttribute("array", Arrays.asList("qingfan","chenxinying"));方式一:<h3 th:each="user:${array}" th:text="${user}"></h3>方式二:<h3 th:each="user:${array}">[[${user}]]</h3><!--引入静态资源链接--><link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"><!--图片引用--><img class="mb-4" th:src="@{/img/bootstrap-solid.svg}"><!--文本取值--><h1 class="..." th:text="#{login.tip}">Please sign in</h1>
5.3.4 解决错误
前端获取model的值时如果出现报错,如下图:
但又能正确得到值时,解决方法是在html的开头添加<!--suppress ALL-->
<!DOCTYPE html><!--suppress ALL-->
6、第一个SpringBoot程序
员工系统
6.1 导入静态资源
6.2 实现首页语言国际化
6.2.1 创建i18n文件夹
application.properties配置文件:
#关掉缓存引擎spring.thymeleaf.cache=false#国际化配置spring.messages.basename=i18n.login
6.2.2 编辑中英文对应的properties资源
login.properties:默认语言,一般选择中文,与login_zh_CN.properties文件内容一样
login_zh_CN.properties:
login.btn=登录login.password=密码login.remember=记住我login.tip=请登录login.username=用户名
login_en_US.properties:
login.btn=Sign inlogin.password=Passwordlogin.remember=Remember melogin.tip=Please login inlogin.username=Username
6.2.3 自定义解析器
语言解析类MyLocaleResolver:
public class MyLocaleResolver implements LocaleResolver {//解析请求@Overridepublic Locale resolveLocale(HttpServletRequest request) {String language = request.getParameter("l");System.out.println(language);Locale locale = Locale.getDefault();//没有携带国际化参数就走默认//如果携带了国际化的参数if (language!=null) {//分割 国家:地区String[] split = language.split("_");locale = new Locale(split[0], split[1]);}return locale;}@Overridepublic void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {}}
6.3 index首页
<!DOCTYPE html><html lang="en_US" xmlns:th="http://www.thymeleaf.org"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"><meta name="description" content=""><meta name="author" content=""><title>Signin Template for Bootstrap</title><!-- Bootstrap core CSS --><link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"><!-- Custom styles for this template --><link th:href="@{/css/signin.css}" rel="stylesheet"></head><body class="text-center"><form class="form-signin" action="dashboard.html"><img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72"><h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1><input type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus=""><input type="password" class="form-control" th:placeholder="#{login.password}" required=""><div class="checkbox mb-3"><label><input type="checkbox" value="remember-me"> [[#{login.remember}]]</label></div><button class="btn btn-lg btn-primary btn-block" type="submit">[[#{login.btn}]]</button><p class="mt-5 mb-3 text-muted">© 2017-2018</p><!--点击链接实现语言切换--><a class="btn btn-sm" th:href="@{/index(l='zh_CN')}">中文</a><a class="btn btn-sm" th:href="@{/index(l='en_US')}">English</a></form></body></html>
6.4 拦截器
public class MyInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//登录拦截器Object loginUser = request.getSession().getAttribute("loginUser");//判断session是否为空,即是否已经登录if (loginUser == null) {request.setAttribute("msg", "请先登录");request.getRequestDispatcher("/index").forward(request, response);return false;} else {return true;}}}
6.5 MVC配置
MyMvcConfig类:
@Configurationpublic class MyMvcConfig implements WebMvcConfigurer {//自定义视图解析器@Overridepublic void addViewControllers(ViewControllerRegistry registry) {//action路径走‘/’时 会跳转到index.html页面 如 http://localhost:8080/registry.addViewController("/").setViewName("index");//action路径走‘/index’时 会跳转到index.html页面registry.addViewController("/index").setViewName("index");registry.addViewController("/mainIndex").setViewName("dashboard");}//登录拦截//放行的页面private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "/index","/login","/static/**","/"};//excludePathPatterns:添加被放行的页面@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").excludePathPatterns(CLASSPATH_RESOURCE_LOCATIONS);}//自定义国际化组件@Beanpublic LocaleResolver localeResolver(){return new MyLocaleResolver();}}
6.6 静态页面抽取公共部分
项目中的需要导航栏都会在多个页面重复存在,这样冗余性很大,可以使用thymeleaf进行页面提取
<!--如头部导航栏主体 被抽取的公共部分添加 th:fragment="topbar"--><nav class="..." th:fragment="topbar">...</nav><!--在需要使用公共代码部分的地方添加 th:insert="~{common::topbar}"--><div th:insert="~{common::topbar}"></div>
7、整合JDBC
7.1 选择数据库模块
选择SQL中的JDBC API和数据库驱动MySQL Drive
pom.xml中会自动导入包
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency>
7.2 配置数据库文件
application.yaml
spring:datasource:username: namepassword: pwd#如果出现时区问题,则添加参数 &serverTimezone=UTC dbName:要连接的数据库名url: jdbc:mysql://localhost:3306/dbName?useUnicode=true&characterEncoding=UTF-8driver-class-name: com.mysql.cj.jdbc.Driver
7.3 连接测试
检查连接是够成功
@SpringBootTestclass Test {@AutowiredDataSource dataSource;@Testvoid contextLoads() throws SQLException {//获取默认数据源 springboot默认是hikariSystem.out.println(dataSource.getClass());// 获取数据库连接 HikariProxyConnectionConnection connection = dataSource.getConnection();System.out.println(connection);}}
7.4 JdbcTemplate
//jdbcTemplate类中封装了所有Mybatis对数据库的操作//方式一,在sql语句中填写参数String sql="select * from user";jdbcTemplate.queryForList(sql); //查询数据,返回list集合jdbcTemplate.update() //执行sql语句 更新数据//方式二:使用数组给sql语句中的参数自动填值String sql2="update user set name=?,pwd=? where id="+id;Object[] objects=new Object[2];objects[1]="name";objects[2]="pwd";jdbcTemplate.update(sql2,objects);
使用JdbcTemplate不用添加事务,其继承了spring的声明式事务,事务默认开启,且自动回滚
8、Druid数据源
- Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。
- Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。
- Hikari 与 Driud 都是当前 Java Web上最优秀的数据源
8.1 导入xml依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/druid --><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.6</version></dependency>
8.2 application.yaml文件配置
#springboot默认数据源是hikari 可以使用type选择其他数据源spring:datasource:username: ...password: ...url: ...driver-class-name: com.mysql.cj.jdbc.Driver#更改为druid数据源type: com.alibaba.druid.pool.DruidDataSource#druid其他配置 需自定义配置类#Spring Boot 默认是不注入这些属性值的,需要自己绑定#druid 数据源专有配置initialSize: 5minIdle: 5maxActive: 20maxWait: 60000timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQuery: SELECT 1 FROM DUALtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: true#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4jfilters: stat,wall,log4jmaxPoolPreparedStatementPerConnectionSize: 20useGlobalDataSourceStat: trueconnectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
8.3 绑定DataSource
给DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,要使8.3中druid其他配置生效,就还需编写一个DruidConfig配置类:
@Configurationpublic class DruidConfig {/*将自定义的 Druid数据源添加到容器中,不再让 Spring Boot 自动创建绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource从而让它们生效@ConfigurationProperties(prefix = "spring.datasource"):作用就是将全局配置文件中前缀为spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中*/@ConfigurationProperties(prefix = "spring.datasource")@Beanpublic DataSource druidDataSource() {return new DruidDataSource();}}
绑定application.yaml配置文件,使其中的spring配置生效
8.4 配置Druid数据源监控
Druid 数据源具有监控功能,并提供了一个 web 界面方便用户查看,所以设置 Druid 的后台管理页面,比如 登录账号、密码 等;配置后台管理
直接在8.4的DruidConfig类中编写
//内置Servlet容器时,所以没有web.xml文件,只能使用SpringBoot的注册:Servlet方式@Beanpublic ServletRegistrationBean statViewServlet() {//配置登录页面访问路径 http://localhost:8080/druid/login.htmlServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");// 这些参数可以在StatViewServlet的父类ResourceServlet中找到Map<String, String> initParams = new HashMap<>();initParams.put("loginUsername", "admin"); //后台管理界面的登录账号initParams.put("loginPassword", "123456"); //后台管理界面的登录密码//后台允许谁可以访问//initParams.put("allow", "localhost"):表示只有本机可以访问//initParams.put("allow", ""):为空或者为null时,表示允许所有访问initParams.put("allow", "");//deny:Druid后台拒绝谁访问//initParams.put("qingfan", "192.168.1.20");表示禁止此ip访问//设置初始化参数bean.setInitParameters(initParams);return bean;}
8.5 配置Druid监控的过滤器
也直接在8.4的DruidConfig类中编写
//WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计@Beanpublic FilterRegistrationBean webStatFilter() {FilterRegistrationBean bean = new FilterRegistrationBean();bean.setFilter(new WebStatFilter());//exclusions:设置哪些请求进行过滤排除掉,从而不进行统计Map<String, String> initParams = new HashMap<>();initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");bean.setInitParameters(initParams);// "/*" 表示过滤所有请求bean.setUrlPatterns(Arrays.asList("/*"));return bean;}
9、整合Mybatis
9.1 导入依赖
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.0</version></dependency>
9.2 Mapper
UserMapper接口:
//Mapper层注解方式:@Mapper@Mapperpublic interface UserMapper {List<User> queryList();User queryUserById(Integer id);int addUser(User user);int updateUser(User user);int deleteUserById(Integer id);}
在resources资源下编写对应的UserMapper.xml配置文件:
9.3 Service
//Service层添加注解的两种方法/*方法一:@Mapper+@RepositoryMapper注解表示这是mybatis的mapper层,接口在编译时会自动生成相应的实现类Repository注解是将接口的实现类交给spring管理,添加到接口的实现类中,若没有接口的实现类,则可以省略方法二:在对应的实现类中添加@Service注解,并在注解中表名本类实现的service接口 如:@Service("EmployeeService")public class EmployeeServiceImpl implements EmployeeService{...}*/
9.3 配置application.yaml
#数据库连接spring:datasource:username: ***password: ***url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8driver-class-name: com.mysql.cj.jdbc.Driver#整合mybatismybatis:#使用别名type-aliases-package: com.qing.pojo#扫描对应的mapper.xml文件,路径要与resources下的xml文件对应mapper-locations: classpath:mybatis/mapper/*.xml
10、SpringSecurity
SpringSecurity有内置的登录、注销界面,我们可以直接使用,且只需要通过简单的配置就能实现用户授权和控制登录等功能
10.1 导入pom依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
10.2 编写配置类
@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter {//授权@Overrideprotected void configure(HttpSecurity http) throws Exception {//首页所有人可以访问,但功能页限制有对应权限的人访问http.authorizeRequests()// “/”请求跳转首页 且所有人可以访问.antMatchers("/").permitAll()// level1/下的页面只能角色为vip1的可以访问.antMatchers("/level1/**").hasAnyRole("vip1").antMatchers("/level2/**").hasAnyRole("vip2").antMatchers("/level3/**").hasAnyRole("vip3");//没有权限时跳转到登录页面//loginPage指定登录请求,若不指定则默认走springboot内置的登录页面,loginProcessingUrl与form表单中的路径对应http.formLogin().loginPage("/toLogin").loginProcessingUrl("/login");//开启注销功能//logoutSuccessUrl指定注销后返回的请求,若不指定则默认返回内置登录页面http.logout().logoutSuccessUrl("/");/*登录时开启记住用户功能 cookie保存 默认2周http.rememberMe()自定义:rememberMeParameter中的参数与前端页面对应 <input type="checkbox" name="remember">*/http.rememberMe().rememberMeParameter("remember");}//认证 springboot2.1可以直接使用//其他版本使用时需要进行密码加密 new BCryptPasswordEncoder()@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("qingfan").password(new BCryptPasswordEncoder().encode("123")).roles("vip1").and().withUser("root").password(new BCryptPasswordEncoder().encode("123")).roles("vip1", "vip2", "vip3").and().withUser("chen").password(new BCryptPasswordEncoder().encode("123")).roles("vip3");}}
11、Shiro
shiro官网:http://shiro.apache.org/
11.1 入门案例
创建一个普通的maven项目即可
11.1.1 导入依赖
<dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.9</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-simple</artifactId><version>1.7.21</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>jcl-over-slf4j</artifactId><version>1.7.21</version></dependency><dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.1.3</version></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>1.2.2</version></dependency></dependencies>
11.1.2 shiro.ini
[users]root = secret, adminguest = guest, guestpresidentskroob = 12345, presidentdarkhelmet = ludicrousspeed, darklord, schwartzlonestarr = vespa, goodguy, schwartz[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
11.1.3 log4j.properties
log4j.rootLogger=INFO, stdoutlog4j.appender.stdout=org.apache.log4j.ConsoleAppenderlog4j.appender.stdout.layout=org.apache.log4j.PatternLayoutlog4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n# General Apache librarieslog4j.logger.org.apache=WARN# Springlog4j.logger.org.springframework=WARN# Default Shiro logginglog4j.logger.org.apache.shiro=INFO# Disable verbose logginglog4j.logger.org.apache.shiro.util.ThreadContext=WARNlog4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
11.1.4 测试
public class Tutorial {private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class);public static void main(String[] args) {Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");SecurityManager securityManager = factory.getInstance();SecurityUtils.setSecurityManager(securityManager);//1.获取当前用户对象Subject currentUser = SecurityUtils.getSubject();//2.通过当前用户拿到sessionSession session = currentUser.getSession();session.setAttribute("someKey", "aValue");String value = (String) session.getAttribute("someKey");if (value.equals("aValue")) {log.info("Retrieved the correct value! [" + value + "]");}// 3.判断当前用户是否被认证if (!currentUser.isAuthenticated()) {UsernamePasswordToken token = new UsernamePasswordToken("admin", "12345");//设置记住我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?}}//4.获取用户认证信息log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");//5.判断用户角色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.");}//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!");}//6.注销currentUser.logout();System.exit(0);}}
11.2 boot整合Shiro(Mybatis)
11.2.1 pom.xml
<dependencies><!--shiro整合spring--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.5.3</version></dependency><!--shiro整合thymeleaf--><dependency><groupId>com.github.theborakompanioni</groupId><artifactId>thymeleaf-extras-shiro</artifactId><version>2.0.0</version></dependency><!--Mysql驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!--druid数据源--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.6</version></dependency><!--springboot整合Mybatis--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.0</version></dependency></dependencies>
11.2.2 编写Shiro配置类
@Configurationpublic class ShiroConfig {//ShiroFilterFactoryBean@Beanpublic ShiroFilterFactoryBean filterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager defaultWebSecurityManager){ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();//设置安全管理器bean.setSecurityManager(defaultWebSecurityManager);//添加shiro内置过滤器/*anon:无需认证就可以访问authc:必须认证才能访问user:必须有 记住我 功能才能访问perms:拥有对某个资源权限才能访问role:拥有某个角色权限才能访问*///拦截Map<String,String> filterMap=new LinkedHashMap<>();/*授权参数一:指定控制的页面参数二:限制用户进入add页面必须被授权 info.addStringPermission("user-add");*/filterMap.put("/user/add","perms[user-add]");filterMap.put("/user/update","perms[user-update]");// 参数一:控制访问的页面 参数二:权限过滤filterMap.put("/user/*","authc");bean.setFilterChainDefinitionMap(filterMap);//设置登录请求 没有权限时自动跳转bean.setLoginUrl("/toLogin");//跳转自定义未授权页面 不指定则走默认404页面bean.setUnauthorizedUrl("/noAuth");return bean;}//DefaultWebSecurityManager@Bean(name = "securityManager")public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();securityManager.setRealm(userRealm);return securityManager;}//创建realm对象 需自定义类UserRealm@Bean(name = "userRealm")public UserRealm userRealm(){return new UserRealm();}//ShiroDialect:整合shiro与thymeleaf@Beanpublic ShiroDialect getShiroDialect(){return new ShiroDialect();}}
11.2.3 自定义UserRealm类
public class UserRealm extends AuthorizingRealm {@AutowiredUserService userService;//授权@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {System.out.println("-----执行授权----");SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();//获取当前用户Subject subject = SecurityUtils.getSubject();User user = (User) subject.getPrincipal();//给用户增加权限info.addStringPermission(user.getPerms());return info;}//用户认证 判断前端输入的用户名密码是否正确@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {System.out.println("-----执行认证----");//取出token中的用户名/密码 前端传回来的UsernamePasswordToken userToken = (UsernamePasswordToken) token;//正确的用户名密码,从数据库获取User user = userService.queryUserByName(userToken.getUsername());//判断用户名是否正确if (user == null) {//直接返回null即可,自动抛出异常:UnknownAccountExceptionreturn null;}//密码验证 shiro内置封装return new SimpleAuthenticationInfo(user, user.getPwd(), "");}}
11.2.4 Controller控制器
@Controllerpublic class MyController {//登录@RequestMapping("/login")public String login(String username, String password, Model model) {//获取当前用户Subject subject = SecurityUtils.getSubject();//封装用户的登录数据到token中UsernamePasswordToken token = new UsernamePasswordToken(username, password);try {//执行登录操作subject.login(token);//登录成功 返回首页return "index";} catch (UnknownAccountException|IncorrectCredentialsException exception) {//登录失败,前端返回提示model.addAttribute("msg", "用户名或密码错误");return "login";}}//注销@RequestMapping("/logout")public String logout(){Subject currentUser = SecurityUtils.getSubject();currentUser.logout();return "redirect:/toLogin";}}
11.3 Shiro常用标签
<!--验证当前用户是否拥有指定权限。 --><a shiro:hasPermission="user:add" href="#" >add用户</a><!-- 拥有权限 --><!--与hasPermission标签逻辑相反,当前用户没有制定权限时,验证通过。--><p shiro:lacksPermission="user:del"> 没有权限 </p><!--验证当前用户是否拥有以下所有权限。--><p shiro:hasAllPermissions="user:view, user:add"> 权限与判断 </p><!--验证当前用户是否拥有以下任意一个权限。--><p shiro:hasAnyPermissions="user:view, user:del"> 权限或判断 </p><!--验证当前用户是否属于该角色。--><a shiro:hasRole="admin" href="#">拥有该角色</a><!--与hasRole标签逻辑相反,当用户不属于该角色时验证通过。--><p shiro:lacksRole="developer"> 没有该角色 </p><!--验证当前用户是否属于以下所有角色。--><p shiro:hasAllRoles="developer, admin"> 角色与判断 </p><!--验证当前用户是否属于以下任意一个角色。--><p shiro:hasAnyRoles="admin, vip, developer"> 角色或判断 </p><!--验证当前用户是否为“访客”,即未认证(包含未记住)的用户。--><p shiro:guest="">访客 未认证</a></p><!--认证通过或已记住的用户--><p shiro:user=""> 认证通过或已记住的用户 </p><!--已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在。--><p shiro:authenticated=""> <span shiro:principal=""></span> </p><!--输出当前用户信息,通常为登录帐号信息--><p> <shiro:principal/> </p><!--未认证通过用户,与authenticated标签相对应。--><!--与guest标签的区别是,该标签包含已记住用户。--><p shiro:notAuthenticated=""> 未认证通过用户 </p>
12、Swagger
- 号称世界上最流行的API框架
- Restful Api 文档在线自动生成器 => API 文档 与API 定义同步更新
- 直接运行,在线测试API
- 支持多种语言 (如:Java,PHP等)
12.1 常用
依赖:
<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency>
12.2 基本配置
导入pom依赖,编写SwaggerConfig配置类
@Configuration//开启swagger2@EnableSwagger2public class SwaggerConfig {@Bean//Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swagggerpublic Docket docket(){return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());}private ApiInfo apiInfo(){//作者信息Contact contact = new Contact("卿帆", "https://cn.bing.com", "2259479766@qq.com");//配置文档信息return new ApiInfo("卿帆的Swagger文档", //标题"11111111", //描述"1.0", //版本号"https://cn.bing.com:", // 组织链接contact, // 作者信息"Apache 2.0", // 许可"http://www.apache.org/licenses/LICENSE-2.0", // 许可连接new ArrayList<VendorExtension>()); // 扩展}}
访问:http://localhost:8080/swagger-ui.html
11.3 配置扫描接口及开关
@Beanpublic Docket docket() {//设置要显示Swagger的生产环境 项目中可能会有多套环境//配置当项目处于test、dev环境时显示swagger,处于prod时不显示Profiles profiles=Profiles.of("dev","test");//判断是否在自己设定的环境中boolean flag=environment.acceptsProfiles(profiles);/*enable:配置是否启用Swagger,默认为true,如果是false,在浏览器将无法访问select:通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口basePackage:配置要扫描的包paths: 配置扫描请求路径,ant():指定请求扫描 any():任何请求都扫描 none():任何请求都不扫描groupName:配置分组 默认:Default*/return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).enable(flag).select().apis(RequestHandlerSelectors.basePackage("com.qing.controller")).paths(PathSelectors..any()).build.groupName("qing");}
多套配置环境:
11.4 接口注释
User实体类:
//@ApiModel给实体类添加注释,@ApiModelProperty给属性添加注释@ApiModel("用户实体类")public class User {@ApiModelProperty("用户名")private String name;@ApiModelProperty("密码")private String pwd;//set、get方法}
Controller:
@RestControllerpublic class HelloController {//@ApiOperation不能放在类上,只能作用在方法接口上,描述方法@ApiOperation("这个方法返回Hello")@GetMapping(value = "/hello")public String hello(@ApiParam("用户名") String username){return "hello";}//只要在方法中,返回值中存在实体类,就会被swagger扫描@PostMapping(value = "/user")public User User(){return new User();}}
Swagger页面显示:
13、任务
13.1 异步任务
//开启异步注解 在springboot主程序上添加@EnableAsyncpublic class Springboot09TaskApplication {}
13.2 发送邮件
13.1.1 导入依赖
<!--邮件--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency>
13.1.2 application.properties
spring.mail.username=2259479766@qq.comspring.mail.password=xyqyfuunjkcsdiaespring.mail.host=smtp.qq.com#设置加密spring.mail.properties.mail.smtp.ssl.enable=true
13.1.3 简单测试
@SpringBootTestclass Springboot09TaskApplicationTests {@AutowiredJavaMailSenderImpl javaMailSender;@Testvoid Test() {SimpleMailMessage mailMessage = new SimpleMailMessage();//标题mailMessage.setSubject("第一封信");//内容mailMessage.setText("hello,qing");//收件人mailMessage.setTo("2259479766@qq.com");//发件人mailMessage.setFrom("2259479766@qq.com");javaMailSender.send(mailMessage);}}
13.1.4 复杂邮件
void Test2() throws MessagingException {//一个复杂的邮件MimeMessage mailMessage=javaMailSender.createMimeMessage();//组装MimeMessageHelper helper = new MimeMessageHelper(mailMessage,true);helper.setSubject("第二封邮件");//true:设置为html格式helper.setText("<p>1111111111</p>",true);//附件helper.addAttachment("1.jpg",new File(""));//收件人helper.setTo("2259479766@qq.com");//发件人helper.setFrom("2259479766@qq.com");}
13.3 定时任务
//开启定时功能 在springboot主程序上添加@EnableSchedulingpublic class Springboot09TaskApplication {}
//cron表达式//秒 分 时 日 月 周几@Scheduled(cron = "* 5 12 2 8 *")public void Test() {System.out.println("hello");}
14、登录验证码实现
SpringBoot结合DefaultKaptcha实现验证码的功能
14.1 自定义KaptchaConfig配置类
import com.google.code.kaptcha.impl.DefaultKaptcha;import org.springframework.context.annotation.Bean;import com.google.code.kaptcha.util.Config;import org.springframework.stereotype.Component;import java.util.Properties;@Componentpublic class KaptchaConfig {@Beanpublic DefaultKaptcha getDefaultKaptcha(){// 创建DefaultKaptcha对象DefaultKaptcha defaultKaptcha = new DefaultKaptcha();Properties properties = new Properties();properties.put("kaptcha.border", "no");properties.put("kaptcha.textproducer.font.color", "72,118,255");properties.put("kaptcha.image.width", "160");properties.put("kaptcha.image.height", "40");properties.put("kaptcha.textproducer.font.size", "30");properties.put("kaptcha.session.key", "verifyCode");properties.put("kaptcha.textproducer.char.space", "5");Config config = new Config(properties);defaultKaptcha.setConfig(config);return defaultKaptcha;}}
14.2 编写VerifyCodeController控制器
@Controller@RequestMapping("/verify")public class VerifyCodeController {@Autowiredprivate DefaultKaptcha captchaProducer;//定义DefaultKaptcha对象@GetMapping("/kaptcha")public void defaultKaptcha(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {// 创建字节数组用于存放图片信息byte[] captchaOutputStream = null;// 创建字节数组用于存放图片信息ByteArrayOutputStream imgOutputStream = new ByteArrayOutputStream();try {//生产验证码字符串并保存到session中// 通过DefaultKaptcha获得随机验证码String verifyCode = captchaProducer.createText();// 将生成的验证码存放在session中httpServletRequest.getSession().setAttribute("verifyCode", verifyCode);// 使用生成的验证码字符串,完成图片的生成BufferedImage challenge = captchaProducer.createImage(verifyCode);// 将图片写入到流中ImageIO.write(challenge, "jpg", imgOutputStream);} catch (IllegalArgumentException e) {//将图片写入到输入流出现错误httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);return;}// 使用HttpServletResponse将图片写入到浏览器中captchaOutputStream = imgOutputStream.toByteArray();// 通过response设定响应请求类型// no-store用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。httpServletResponse.setHeader("Cache-Control", "no-store");// no-cache指示请求或响应消息不能缓存httpServletResponse.setHeader("Pragma", "no-cache");/* expires是response的一个属性,它可以设置页面在浏览器的缓存里保存的时间 ,超过设定的时间后就过期 。过期后再次浏览该页面就需要重新请求服务器发送页面数据,如果在规定的时间内再次访问次页面 就不需从服务器传送 直接从缓存中读取。*/httpServletResponse.setDateHeader("Expires", 0);// servlet接受request请求后接受图片形式的响应httpServletResponse.setContentType("image/jpeg");//通过response获得输出流ServletOutputStream responseOutputStream = httpServletResponse.getOutputStream();responseOutputStream.write(captchaOutputStream);responseOutputStream.flush();responseOutputStream.close();}}
14.3 前端随即获取验证码
<div class="col-6"><img alt="单击图片刷新!" class="pointer" th:src="@{/verify/kaptcha}"onclick="this.src='/verify/kaptcha?d='+new Date()*1"></div>
这里onclick路径中加一个?是为了防止点击更换验证码前后因为路径一样验证码没有变化的问题。
