Skip to main content

微服务阶段

我们的学习历程

javase:oop

mysql:持久化

html+css+js+jquery+框架:视图,框架不熟练,css 不好

javaweb:独立开发 MVC 架构的网站:原始

ssm:框架:简化了我们的开发流程

打包出来是:war:tomcat 运行

Spring 再简化:SpringBoot-jar:内嵌 tomcat; 微服务架构!

服务越来越多:SpringCloud;

问题总结

  1. org.springframework.core.annotation.AnnotationConfigurationException: Attribute proxyBeanMethods in annotation 应该是 jar 包的版本与其他依赖包版本不一致,解决方法是更换版本号
  2. java 类必须要有无参构造函数才能实现自动装配,否则会报错运行不了。
  3. 问题:src 外的 config 文件夹没有加载到 target 目录下所以下面的配置文件无法被识别 解决:项目要作为单独的 project,而不能作为一个 module
  4. springboot 版本问题,旧的 favicon 头像设置方式不生效 将 favicon.ico 放入资源文件夹中,然后在 index 欢迎页面引入下列语句 link rel=icon href=favicon.ico type=image/x-icon/
  5. 用自定义的视图解析器,自定义的国际化视图解析器的方法名必须是 localeResolver。
  6. 关于 restful 风格传参 th:href=@{/emp/{id}(id=${emp.getId()})} th:href=@{/emp/{id}/(id=${emp.getId()})} th:href=@{/emp/+${emp.getId()}}

SpringBoot

Spring 视为了解决企业级应用开发的复杂性而创建的,简化开发,

Spring 如何简化 java 开发

为了降低 java 开发的复杂性,Spring 采用了四种关键策略;

  1. 基于实体类的轻量级,最小入侵性变成;
  2. 通过 ioc,依赖注入和面向接口实现松耦合
  3. 基于切面(AOP)和管理进行声明式变成;
  4. 通过切面和模版减少样式代码

什么是 SpringBoot?

javaweb:Servlet+tomcat,后来是 Struts,再后来是 SpringMVC 到了现在的 SpringBoot,过一两年还会出现新的前端框架,技术快速迭代,程序员需要不断学习,

新服务架构:服务网格,可能会干掉 SpringBoot 来完成新的微服务架构

老师,领导,项目经理:

  • 培训讲师:
    • 面向面试培训,教你如何使用,快速上手!
  • 做教育的
    • 如何学习新东西,如何持续学习,如何关注这个行业!
    • 教你历史,来龙去脉,理论;积累谈资

30,经理

30:程序猿:淘汰!

java 框架的迭代

SpringBoot 其实就是 javaweb 开发框架,于 SpringMVC 类似,对比其他开发框架的 好处就是简化开发,约定大于配置,你只管训醒:you can just run 能够快速开发 web 应用,几行代码实现一个 http 接口

所有的技术框架的发展,都遵循这一个条主线规律,从复杂的应用场景,衍生出规范框架。人们在实际生产应用情况不断的精简,吸收设计精华,重构新的,更轻量级的框架,逐渐提升开发效率,之后开始提倡约定大于配置进而衍生一些一站式的解决方案,

j2EE --------> spring-------->springboot

随着发展,Spring 涉及的领域越来越多,项目的整合开发需要配置大量的配置文件妈妈那么难的变得不那么易用简单,违背了最初的理念,甚至被人称为配置地狱,SpringBoot 正是这样一个背景下被抽象出来的开发框架,目的就是为了让大家容易的使用 Spring,更容易集合中间件和开源软件

Spring boot 基于 Spring 开发,boot 本身并不提供 Spring 的核心特性以及拓展功能,只是用于快速的开发新一个基于 SPRING 框架的应用程序,也就是说,他并不是代替 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升,Spring 开发者的体验工具。Springboot**约定大于配置的核心思想,默认帮我们进行了很多的设置,多数的 SpringBoot 应用很少的 Spring 配置,同时集成了大量常用的第三方库配置,几乎所有的第三方库都是开箱即用。

**SpringBoot 主要优点:

  • 为了所有 Spring 开发这更快入门
  • 开箱即用,提供各种默认配置简化项目配置
  • 内嵌式容器简化 web 项目
  • 没有冗余代码和 xml 的配置要求

约定大于配置:maven------>spring------->springMVC---->SpringBoot

正常的程序=数据结构+算法==》程序员---》i 创造性角色

机构的程序=面向对象+框架 ==》码农--》会用别人的东西,干事

微服务

什么是微服务?

微服务是一个中架构风格,它要求我们在开发一个应用的时候,这个应用必须构建成一个系列小小服务的组合;可以通过 http 的方法进行互通。要说为微服务架构,先得说说我们以前的单体引用架构。

架构:MVC 三层架构, MVVM ,微服务架构

业务:service:userService ===>模块!

SpringMVC,controller ===>提供接口!

单体应用架构

所谓单体引用(all in one)是指,我们将一个应用的中的所有应用的中的所有应用服务封装在一个应用中,无论是 erp。crm 或是其他系统,都吧数据库访问,web 访问,等等功能方到一个 war 包内

  • 好处是:易于开发测试,,部署起来十分方便,当需要拓展的时候,只需要将 war 复制多份,然后放在多个服务器上,在做负载均衡就可以
  • 缺点是:单体应用架构的缺点是,哪怕我要修改非常小的地方,我都需要停掉整个服务,重新打包,部署这个应用 war 包,特别是对于一个大型应用,我们不可能吧所有的内容都放在一个应用里,我们如何维护,如何分工都是问题

微服务架构

all in one 的架构方式没我们把所有的功能单元放在一个应用里面。整个应用部署在服务器上,如果负载能力不行,我们将整个应用水平赋值,进行扩展,之后负载均衡

所谓微服务架构,就是打破之前的 all in one 的架构方式,把每个功能元素独立出来,把独立出来的功能元素动态组合,需要的功能元素才组合在一起,需要时间多一些,可以整合多个功能的元素。所以微服务架构是对功能元素进行复制,

好处:

  1. 节省,调用资源
  2. 每个功能元素的服务都是一个可替换的可独立升级的软件代码,

详细阐述了什么是微服务:https://www.martinfowler.com/articles/microservices.html

中文版:https://www.cnblogs.com/liuning8023/p/4493156.html

http: rpc

用户下单: controller! 1000ms

消息队列:

仓库冻结:资金冻结,验证,购买成功,仓库数量减少,仓库解冻,资金解冻 10 s

如何构建微服务

一个大型系统的微服务架构,就像一个复杂交织,神经网络,每一个神经元就像是一个功能元素,它们各自完成自己的功能,然后通过 http 相互请求调用。比如一个电商系统,查缓存,连数据库,浏览页面,结账,支付等服务都是一个一个独立的功能服务,都被微化了,他们作为一个个微服务共同构建了一个庞大的系统,如果修改其中的一个功能,只需要更新升级其中一个功能服务单元即可。

​ 但是这种庞大的系统架构给部署和运维带来很大的难度。于是,spring 为我们带来了构建大型分布式微服务的全套、全程产品:

  • ​ 构建一个个功能独立的微服务应用单元,可以使用 SpringBoot,可以帮助我们快速的构建一个应用;
  • 大型分布式网络服务的调用,这部分由 Springcloud 来完成,实现分布式;
  • 在分布式中间,进行流式数据计算,批处理,我们有 spring cloud data flow。
  • spring 为我们想清楚了整个从开始构建应用到大型分布式应用全流程方案

高内聚,低耦合,

面试:

8k-13k:软实力:聊天+举止+谈吐+见解

你主导面试官:13k:聊天 30 分钟

面试官主导你:8k

第一个 SpringBoot 程序

环境:

  • jdk1.8
  • maven 3.6.2
  • SpringBoot 最新版
  • idea

官方提供了一个快速生成网站!idea 集成了这个网站!

  • 可以在官网直接下载,导入 idea 开发(官网在哪里)
  • 直接使用 idea 创建一个 SpringBoot 项目(常用)

简单编写个 controller 类来看看如何运行的

package com.hyc.spring01helloworld.Controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/hello")
public class helloController {

@GetMapping("/hello")
@ResponseBody
public String hello(){
return "hello";
}

}

maven 打包注意事项

utf-8 报错:

	<project.build.sourceEncoding>
UTF-8
</project.build.sourceEncoding>

原理初探

自动装配:

pom.xml:

  • Spring-boot-dependencies:核心依赖在父项目
  • 我们在写或者引入一些 Springboot 依赖的时候,不需要指定版本,就应为这些版本仓库

启动器

  •     <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    </dependency>
  • 启动器:就是 Springboot 的启动场景

  • 比如:spring-boot-starter-web,他就会帮我们自动当如相关依赖

  • springboot 会将所有的功能场景,都变成一个个的启动器

  • 我们要使用什么功能,就只需要找到对应的启动器就可以了

主程序:

@SpringBootApplication
public class Spring01HelloworldApplication {

public static void main(String[] args) {
SpringApplication.run(Spring01HelloworldApplication.class, args);
}

}
  • 注解:

    • @SpringBootConfiguration springboot的 配置
      @Configuration spring配置类
      @Component spring组件
      @EnableAutoConfiguration 自动装配
      @AutoConfigurationPackage:spring自动配置包
      @Import({AutoConfigurationImportSelector.class}) 自动装配包 注册
      @Import({Registrar.class}) 配置导入选择

      //获取所有的配置
      List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);

获取候选的配置

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
}

加载项目自动装配文件

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();

try {
Enumeration urls = classLoader.getResources("META-INF/spring.factories");

while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();

while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;

for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}

result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}

自动配置的核心文件

META-INF/spring.factories

image-20210617101138398

加载到 properties 中:

 Properties properties = PropertiesLoaderUtils.loadProperties(resource);

判断条件成立才自动装配

@ConditionalOnClass(Advice.class)

结论:springboot 所有的自动配置都是在启动的时候扫描并加载:META-INF/spring.factories所有的自动配置类都在这里面,但是不一定生效,判断条件是否成立,只要导入了对应的 start,就有对应的启动器,有了启动器我们自动装配就会生效,然后就配置成功了!

执行原理图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TAuyWXVl-1648917634750)(https://gitee.com/cold-abyss_admin/my-image-host/raw/master/img/%E6%9C%AA%E5%91%BD%E5%90%8D%E6%96%87%E4%BB%B6%20(1)].png)

  1. Springboot 在启动的时候,从类路径下的 META-INF/spring.factories获取指定的值;
  2. 将这些自动配置的类导入容器,自动配置类就会生效,帮我们进行自动配置
  3. 以前我们需要自动配置的东西,现在只要符合条件 Springboot 帮我们做了
  4. 整合 javaEE 的解决方案和自动配置的东西都在 spring-boot-autoconfigure-2.5.1.jar 这个包下
  5. 它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器;
  6. 容器中也会存在非常多的 XXXXautoconfiguration 的文件,就是这些类容器中导入了这个场景需要的所有组件并且自动配置,@Configuration
  7. 有了自动配置类,就免去了我们手动编写配置文件的工作,远离配置地狱!!!

run 执行原理

run

开启服务

package com.hyc.spring01helloworld;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//@SpringBootApplication 标注这个类是一个springboot的应用:启动类下的资源全部被导入
@SpringBootApplication
public class Spring01HelloworldApplication {

public static void main(String[] args) {
SpringApplication.run(Spring01HelloworldApplication.class, args);
}

}

SpringBootApplication.run 分析

主要是两个部分,一个是 SpringBootApplication 实例化,一个是 run 的执行

SpringBootApplication

执行的步骤:

  1. 推断应用类型是普通的 java 项目还是 web 项目
  2. 查找并加载所有可用的初始化器,设置到 initializers 实行中
  3. 找出所有的应用程序监听器,设置到 listeners 属性中
  4. 推断并且设置 main 方法定义类,找到运行的主类

查看构造器

    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.sources = new LinkedHashSet();
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.addConversionService = true;
this.headless = true;
this.registerShutdownHook = true;
this.additionalProfiles = Collections.emptySet();
this.isCustomEnvironment = false;
this.lazyInitialization = false;
this.applicationContextFactory = ApplicationContextFactory.DEFAULT;
this.applicationStartup = ApplicationStartup.DEFAULT;
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = this.getBootstrapRegistryInitializersFromSpringFactories();
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}

SpringBoot 配置

  1. 官方配置太多,
  2. 如何配置 tml
  3. 学习原理,一通百通

官方配置链接:https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties

yml 是什么?

YAML是"YAML Ain't a Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言),但为了强调这种语言以数据做为中心,而不是以标记语言为重点,而用反向缩略语重命名。

标记语言:

以前的配置文件,大多数都是用 xml 来配置,比如一个简单的端口配置,我们来对比下 yaml 和 xml 的区别

yml:

server:
port: 8080

xml:

<server>
<port>8080</port>
</server>

yml:特点

  1. K:V
  2. 对空格的要求十分高
  3. 普通的 key-value
  4. 可以注入到我们的配置类中

两种赋值方式

1.注解赋值

@Component
public class Dog {
@Value("郑文杰")
private String name;
@Value("3")
private Integer age;

2.YML 注入

实体类:

@Component
@ConfigurationProperties(prefix = "person")
public class person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;

YML:

person:
name: hyc
age : 3
happy: false
birth: 2019/11/02
maps: {k1: 123, k2: 456}
lists:
-code
-music
-girl
dog:
name: 旺财
age: 3

@ConfigurationProperties()作用:

将配置文件中每个属性的值,映射这个组件中;

告诉 SpringBoot 将奔雷所有的属性和配置文件中相关的配置进行绑定

参数 perfix = “person” 将 person 组件(类)和 yml 中 person 的值对应绑定

@PropertySource

指定配置文件:properties 文件

@PropertySource(value = "classpath:application.properties")

@value赋值

SpringBoot 推荐我们用 yml

对比:

应为 yml 十分灵活:

person:
name: hyc
age : ${random.int}
happy: false
birth: 2019/11/02
maps: {k1: 123, k2: 456}
lists:
-code
-music
-girl
dog:
name: 旺财
age: 3


java 类

package com.hyc.pojo;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.Email;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Component
@ConfigurationProperties(prefix = "person")
@Validated //数据校验
public class person {
@Email(message = "邮箱格式错误")
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;

@Override
public String toString() {
return "person{" +
"name='" + name + '\'' +
", age=" + age +
", happy=" + happy +
", birth=" + birth +
", maps=" + maps +
", lists=" + lists +
", dog=" + dog +
'}';
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public Boolean getHappy() {
return happy;
}

public void setHappy(Boolean happy) {
this.happy = happy;
}

public Date getBirth() {
return birth;
}

public void setBirth(Date birth) {
this.birth = birth;
}

public Map<String, Object> getMaps() {
return maps;
}

public void setMaps(Map<String, Object> maps) {
this.maps = maps;
}

public List<Object> getLists() {
return lists;
}

public void setLists(List<Object> lists) {
this.lists = lists;
}

public Dog getDog() {
return dog;
}

public void setDog(Dog dog) {
this.dog = dog;
}
}

jsr303 校验注解:

@Validated //数据校验

校验示例:

java 类:

@Component
@ConfigurationProperties(prefix = "person")
@Validated //数据校验
public class person {
@Email(message = "邮箱格式错误")
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;

YML:此时我们的 name 并不是邮箱格式

person:
name: hyc
age : ${random.int}
happy: false
birth: 2019/11/02
maps: {k1: 123, k2: 456}
lists:
-code
-music
-girl
dog:
name: 旺财
age: 3


执行效果:

image-20210618102330148

使用了@Validated //数据校验注解的类可以对自己的属性设置格式数据校验

全部注解:

常用注解:

image-20210618101136873

image-20210618101250935

正则表达式:相对核心

源码位置:

image-20210618101835578

小结:

学会找到源码位置,尝试自己阅读源码,探索过程才是成功学习的关键

SpringBoot 配置文件优先级

官方给的执行优先级

image-20210618105008155

多环境配置:

在真实工作中,我们会有很多配置文件,比如 test,dev,等等,

格式:application-{什么类型的配置}.yml/properties

使用的:spring.profiles.active

properties:

主文件

#Springboot多环境配置,可以选择激活那个配置文件
spring.profiles.active=dev

可以有多个配置文件,从主文件选择使用什么开发环境

application-dev.properties 开发项目环境:

server.port=8081

application-test.properties 测试开发环境

server.port=8082

yml:

server:
port: 8080
spring:
profiles:
active: dev
---
server:
port: 8081
spring:
profiles:dev

---
server:
port: 8082
spring:
profiles:test



#配置文件到底可以写什么

再理解自动装配

SpringBoot 使用一个全局配置文件,配置文件名字是固定的

  • application.properties
    • 语法结构:key=value
  • application.yml
    • 语法结构:key:空格 value

分析自动装配的原理:

  1. 启动的时候加载住配置类,开启了自动装配功能,@EnableAutoConfiguration

  2. @EnableAutoConfiguration的作用:

    • 利用 AutoConfigurationImportSelector 给容器导入组件,导入什么组件呢?

    • 查看 selectImports()方法的内容,他返回了一个 autoConfigurationEntry,getAutoConfigurationEntry获取组件实体,

    • 通过getCandidateConfigurations()方法来加载候选配置,

    • loadFactoryNames()获取配置名字,从META-INF/spring.factories这个配置文件中,加载全部配置存放到 Properties 中;返回封装好的结果,其中的加载参数就是 EnableAutoConfiguration;

    • 	protected Class<?> getSpringFactoriesLoaderFactoryClass() {
      return EnableAutoConfiguration.class;
      }
    • 过程描述就是,把META-INF/spring.factories把里面所有的 EnableAutoConfiguration 值加入到容器中

@ConditionalOn

SpringBoot 有大量的配置,有的生效有的不生效,他通过这个注解来判断是否符合调减,符合条件就生效这个配置,我们可以通过自动装配注解来实现对应的自动装配。

如何配置的

在我们的配置文件中,能配置的东西都存在一个规律

他们一定会有个文件叫 xxxProperties ,

  • xxxProperties:绑定配置文件,@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
  • xxxAutoConfiguration:自动装配默认值
  • 我们就可以自定义配置了

如何看看我们的自动配置类是否生效

debug:true,在我们启动类之后会看到那些生效那些没生效,会以类似日志的方式输出

Positive matches:

已经自动装配并且生效的

negative matches:

没有生效的

总结:

  1. SpringBoot 启动会加载大量的配置类,用来自动装配
  2. 如果我们要使用功能,要去查看功能是否在 SpringBoot 默认写好的自动装配类中
  3. 自动装配类中配置了很多组件,只要我们用的组件存在就不需要手动配置了
  4. 容器中自动配置类添加属性的时候,会从 properties 类中获取某些属性,我们只需要在配置文件中指定这些属性的值即可
  5. xxxProperties:绑定配置文件,封装配置文件中的相关属性
  6. xxxAutoConfiguration:自动装配类,给容器添加组件

SpringBoot web 开发

jar:webapp 在哪里

最大特点:自动装配

SpringBoot 帮我们配置了什么,能不能进行修改,能修改那些东西,能不能拓展

  • xxxxAutoConfiguration..向容器中自动配置组件
  • XXXXProperties:实现自动配置类装配配置文件中自定义的内容!

要解决的问题:

  • 导入静态资源,如何导入!
  • 首页问题
  • 模版引擎,thymeleaf
  • 装配扩展 SpringMvc
  • 剩下的就只有增删改了
  • 拦截器
  • 扩展国际化

静态资源

@Override
public 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

一个网站是和 maven 仓库类似的导入依赖的网站

导入的依赖结构是

image-20210621101902358

我们的静态资源路径方法中

addResourceHandler(registry, "/webjars/", "classpath:/META-INF/resources/webjars/");

就是去类路径下找到/META-INF/resources/webjars/下的文件,

例子:

访问 http://localhost:8080/webjars/jquery/3.6.0/jquery.js

实测成功

总结:

  1. SpringBoot 处理静态资源的方式
  2. 优先级:resources>static(默认)》public

首页如何定制

在 web 配置类中共有对首页的一系列处理

image-20210622104019841

如何找得到资源下的 index?

image-20210622104120358

调用查找资源方法,找到 index 并且返回,没找到的话相对处理后返回空

image-20210622104317099

SpringBoot 页面跳转:

@Controller
public class HelloController {
@RequestMapping("/a")
public String hello(){
return "index";
}

}

注意:

  • 在 template 目录下的所有页面,只能通过 controller 来跳转
  • 需要模版引擎的支持

模版引擎:Thymeleaf

我们以前用 jsp 来展示数据,模版引擎的作用就是我们来写一个页面模版,比如一些值,表达式,tomcat 支持 jsp 但是由于我们用的是嵌入式的 tomcat,所以他现在默认是不支持 jsp 的

thymeleaf:

Thymeleaf 是适用于 Web 和独立环境的现代服务器端 Java 模板引擎,能够处理 HTML、XML、JavaScript、CSS 甚至纯文本。

Thymeleaf 的主要目标是提供一种优雅且高度可维护的模板创建方式。为了实现这一点,它建立在自然模板的概念之上,以不影响模板用作设计原型的方式将其逻辑注入模板文件。这改善了设计的沟通并弥合了设计和开发团队之间的差距。

Thymeleaf 也已经从一开始就设计了 Web 标准记-尤其是HTML5 -允许您创建充分验证模板,如果这是一个需要你。

thymeleaf 与 mvc 时讲到的视图解析器十分相似,

Springboot 推荐使用模版引擎来简化开发,

image-20210622141436953

引入依赖:

        <dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

使用只需要导入依赖,我们将 html 放到 templeats 下就可以跳转了

注意:如果导入 jar 失败尝试回退版本,即可

thymeleaf 基础语法:

<!--所有的html元素都可以被thymeleaf接管,如何接管? th:元素-->
<div th:text="${msg}"></div>

表达式:

  • ${x}将返回x存储在 Thymeleaf 上下文中或作为请求属性的变量。
  • ${param.x}将返回一个名为(可能是多值的)的请求参数x
  • ${session.x}将返回一个会话属性x
  • ${application.x}将返回一个名为的servlet 上下文属性x

常用语法:

  • 简单的表达:
    • 变量表达式: ${...}
    • 选择变量表达式: *{...}
    • 消息表达: #{...}
    • 链接 URL 表达式: @{...}
    • 片段表达式: ~{...}
  • 文字
    • 文本字面量:'one text', 'Another one!',...
    • 数字字面量:0, 34, 3.0, 12.3,...
    • 布尔文字:true,false
    • 空字面量: null
    • 文字标记:one, sometext, main,...
  • 文字操作:
    • 字符串连接: +
    • 字面替换: |The name is ${name}|
  • 算术运算:
    • 二元运算符:+, -, *, /,%
    • 减号(一元运算符): -
  • 布尔运算:
    • 二元运算符:and,or
    • 布尔否定(一元运算符):!,not
  • 比较与相等:
    • 比较器:>, <, >=, <=( gt, lt, ge, le)
    • 等式运算符:==, !=( eq, ne)
  • 条件运算符:
    • 如果-那么: (if) ? (then)
    • 如果-然后-其他: (if) ? (then) : (else)
    • 默认: (value) ?: (defaultvalue)

常用代码示例:

controller:index

@Controller
public class HelloController {
@RequestMapping("/index")
public String hello(Model model){
model.addAttribute("msg","<h1>hello SpringBoot</h1>");
model.addAttribute("users", Arrays.asList("hyc","lhy"));

return "index";
}

}

index.html

<!--所有的html元素都可以被thymeleaf接管,如何接管? th:元素-->
<!--不转义-->
<div th:text="${msg}"></div>
<!--转义-->
<div th:utext="${msg}"></div>

<hr>
<!--th:text显式数据-->
<h3 th:each="user:${users}" th:text="${user}"></h3>
<!--行内显式数据-->
<h3 th:each="user:${users}" >[[${user}]]</h3>
</body>

SpringBoot 装配并且扩展 SpringMvc

以视图解析器为例子:

原理:孙建平,真是我的好兄弟应该说是不是你的好办法真是我的好儿子我真的。

ContentNegotiatingViewResolver类中有方法,getCandidateViews()获取候选的视图,选择最好的视图返回,

官方文档是这样说的:

Spring Boot 为 Spring MVC 提供了自动配置,适用于大多数应用程序。

自动配置在 Spring 的默认值之上添加了以下功能:

  • 包括ContentNegotiatingViewResolverBeanNameViewResolverbean。
  • 支持提供静态资源,包括对 WebJars 的支持。
  • 自动注册ConverterGenericConverterFormatterbean 类。
  • 支持HttpMessageConverters
  • 的自动注册MessageCodesResolver
  • 静态index.html支持。
  • ConfigurableWebBindingInitializerbean 的自动使用。

如果您想保留那些 Spring Boot MVC 自定义并进行更多(拦截器、格式化程序、视图控制器和其他功能),您可以添加自己@Configuration的类型类,WebMvcConfigurer但不添加 @EnableWebMvc.

如果你想提供的定制情况RequestMappingHandlerMappingRequestMappingHandlerAdapter或者ExceptionHandlerExceptionResolver,仍然保持弹簧引导 MVC 自定义,你可以声明类型的豆WebMvcRegistrations,并用它来提供这些组件的定制实例。

如果你想利用 Spring MVC 中的完全控制,你可以添加自己的@Configuration注解为@EnableWebMvc,或者添加自己的@Configuration-annotatedDelegatingWebMvcConfiguration中的 Javadoc 中所述@EnableWebMvc

如果我们想自定义视图解析器参考如下代码:

package com.hyc.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.Locale;
//如果你想diy或者自定义动能,只需要写这个组件然后将它交给SpringBoot,SpringBoot就会帮我们自动装配
//拓展mvc
@Configuration
public class MyMvc implements WebMvcConfigurer {
//public interface ViewResolver 实现了视图解析器的类我们就可以把他看成试图解析器
@Bean
public MyViewResolver myViewResolver(){
return new MyViewResolver();
}
//自定义了一个自己的视图解析器,只要我们自定义了试图解析器,SpringBoot就会帮我们自动装配
public static class MyViewResolver implements ViewResolver {
@Override
public View resolveViewName(String s, Locale locale) throws Exception {
return null;
}
}

}

装配原理小结:

  1. 芸芸之多的配置,原理其实是一样的,通过获取 webmvc 的自动配置原理分析,我们要学会一种方式,去在源码中得出结论,都是属于自己,一通百通
  2. Spring 整个框架的底层有太多精妙的设计细节,阅读源码可以让我们对编写代码和理解原理的能力大大提升!
  3. 在自动配置很多组件的时候,SpringBoot 会先查看容器中有没有用户自己配置的@bean,如果有就用用户配置的,如果没有就用默认配置,组件存在多个的时候,如视图解析器,就将用户配置的和自己默认的组合起来

员工管理系统

1、首页配置

1.首页配置,自己设置页面跳转

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/").setViewName("index.html");
}
}

2.首页设置,thymeleaf

xmlns:th="http://www.thymeleaf.org"

常用的 thymeleaf 文件头

注意点:所有的页面静态资源使用 thymeleaf 接管:url:@{}

页面国际化

  1. 我们需要配置 i18n 文件

  2. 我们如果需要在项目中进行按钮自动切换,我们需要自定义国际化组件

    package com.hyc.managesystem.config;

    import org.springframework.util.StringUtils;
    import org.springframework.web.servlet.LocaleResolver;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.Locale;

    public class myLocalresolver implements LocaleResolver {
    //解析请求
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
    // 获取请求的语言参数
    String language = request.getParameter("l");
    System.out.println(language);
    Locale locale = Locale.getDefault();
    if (!StringUtils.isEmpty(language)){
    String[] split = language.split("_");
    // 国家地区
    locale= new Locale(split[0], split[1]);

    }
    return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
    }

  3. 记得将自己写的组件配置到配置文件中

        @Bean
    public LocaleResolver localeResolver(){
    return new myLocalresolver();
    }
  4. 使用#{}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0oH1S1RT-1648917634756)(C:/Users/Administrator/AppData/Roaming/Typora/typora-user-images/image-20210503093036786.png)]

位置如下

image-20210503093130687

1.2、编写 pojo 层

员工表

//员工表
@Data
@NoArgsConstructor
public class Employee {

private Integer id;
private String lastName;
private String email;
private Integer gender; //性别 0 女, 1,男
private Department department;
private Date birth;

public Employee(Integer id, String lastName, String email, Integer gender, Department department) {
this.id = id;
this.lastName = lastName;
this.email = email;
this.gender = gender;
this.department = department;
this.birth = new Date();
}
}

部门表

//部门表
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
private int id; //部门id
private String departmentName; //部门名字
}

添加 lombok 依赖

 <!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

1.3、编写 dao 层

这里我们模拟数据库,springboot 和数据库的连接在后序课程中。

部门 dao

package com.kuang.dao;

import com.kuang.pojo.Department;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

//部门dao
@Repository
public class DepartmentDao {

//模拟数据库中的数据

private static Map<Integer, Department>departments = null;

static {
departments = new HashMap<Integer, Department>(); //创建一个部门表

departments.put(101,new Department(101,"教学部"));
departments.put(102,new Department(102,"市场部"));
departments.put(103,new Department(103,"教研部"));
departments.put(104,new Department(104,"运营部"));
departments.put(105,new Department(105,"后勤部"));
}

//获取所有的部门信息
public Collection<Department> getDepartments(){
return departments.values();
}
//通过id得到部门
public Department getDepartmentById(Integer id){
return departments.get(id);
}
}

员工 dao

package com.kuang.dao;

import com.kuang.pojo.Department;
import com.kuang.pojo.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

//员工dao
@Repository //被string托管
public class EmployeeDao {

//模拟数据库中的数据
private static Map<Integer, Employee> employees= null;
//员工所属的部门
@Autowired
private DepartmentDao departmentDao;
static {
employees = new HashMap<Integer,Employee>(); //创建一个部门表

employees.put(1001,new Employee( 1001,"AA","1622840727@qq.com",1,new Department(101,"教学部")));
employees.put(1002,new Employee( 1002,"BB","2622840727@qq.com",0,new Department(102,"市场部")));
employees.put(1003,new Employee( 1003,"CC","4622840727@qq.com",1,new Department(103,"教研部")));
employees.put(1004,new Employee( 1004,"DD","5628440727@qq.com",0,new Department(104,"运营部")));
employees.put(1005,new Employee( 1005,"FF","6022840727@qq.com",1,new Department(105,"后勤部")));
}
//主键自增
private static Integer ininId = 1006;
//增加一个员工
public void save(Employee employee){
if(employee.getId() == null){
employee.setId(ininId++);
}
employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId()));
employees.put(employee.getId(),employee);
}
//查询全部的员工
public Collection<Employee>getALL(){
return employees.values();
}

//通过id查询员工
public Employee getEmployeeById(Integer id){
return employees.get(id);
}

//删除一个员通过id
public void delete(Integer id){
employees.remove(id);
}
}

2、首页实现

2.1、引入 Thymeleaf

pom.xml导入依赖

<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

2.2、编写 MyMvcConfig

package com.kuang.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

//扩展使用SpringMVC
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
}
}

更改静态资源路径

image-20210503104737514

所有的静态资源都需要使用 thymeleaf 接管:@{}

application.properties 修改

# 关闭模板引擎的缓存
spring.thymeleaf.cache=false

server.servlet.context-path=/kuang

2.3、测试首页

输入路径

http://localhost:8080/kuang/index.html
image-20210503105044008

测试成功!

3、页面国际化

3.1、 File Encodings 设置

先在 IDEA 中统一设置 properties 的编码问题!

image-20210503110109556

编写国际化配置文件,抽取页面需要显示的国际化页面消息。我们可以去登录页面查看一下,哪些内容

我们需要编写国际化的配置!

3.2、配置文件编写

1、我们在 resources 资源文件下新建一个 i18n 目录,存放国际化配置文件

2、建立一个 login.properties 文件,还有一个 login_zh_CN.properties;发现 IDEA 自动识别了我们要做国际化操作;文件夹变了!

image-20210503110930479

3、我们可以在这上面去新建一个文件;

image-20210503111124893

弹出如下页面:我们再添加一个英文的;

image-20210503110848529

这样就快捷多了!

image-20210503111145791

4、接下来,我们就来编写配置,我们可以看到 idea 下面有另外一个视图;

image-20210503111517483

这个视图我们点击 + 号就可以直接添加属性了;我们新建一个 login.tip,可以看到边上有三个文件框可以输入

image-20210503111823725

我们添加一下首页的内容!

image-20210503111915846

然后依次添加其他页面内容即可!

image-20210503112654522

然后去查看我们的配置文件;

login.properties :默认

login.btn=登录
login.password=密码
login.remember=记住我
login.tip=请登录
login.username=用户名

英文:

login.btn=Sign in
login.password=Password
login.remember=Remember me
login.tip=Please sign in
login.username=Username

中文:

login.btn=登录
login.password=密码
login.remember=记住我
login.tip=请登录
login.username=用户名

OK,配置文件步骤搞定!

配置文件生效探究

我们去看一下 SpringBoot 对国际化的自动配置!这里又涉及到一个类:MessageSourceAutoConfiguration

里面有一个方法,这里发现 SpringBoot 已经自动配置好了管理我们国际化资源文件的组件 ResourceBundleMessageSource;

// 获取 properties 传递过来的值进行判断
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
if (StringUtils.hasText(properties.getBasename())) {
// 设置国际化文件的基础名(去掉语言国家代码的)
messageSource.setBasenames(
StringUtils.commaDelimitedListToStringArray(
StringUtils.trimAllWhitespace(properties.getBasename())));
}
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}

我们真实的情况是放在了 i18n 目录下,所以我们要去配置这个 messages 的路径;

spring.messages.basename=i18n.login

配置页面国际化值

去页面获取国际化的值,查看 Thymeleaf 的文档,找到 message 取值操作为:#{...}。我们去页面测试下:

IDEA 还有提示,非常智能的!

image-20210503114532955

我们可以去启动项目,访问一下,发现已经自动识别为中文的了!

image-20210503114542116

但是我们想要更好!可以根据按钮自动切换中文英文!

配置国际化解析

在 Spring 中有一个国际化的 Locale (区域信息对象);里面有一个叫做 LocaleResolver (获取区域信息对象)的解析器!

我们去我们 webmvc 自动配置文件,寻找一下!看到 SpringBoot 默认配置:

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
// 容器中没有就自己配,有的话就用用户配置的
if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
// 接收头国际化分解
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}

AcceptHeaderLocaleResolver 这个类中有一个方法

public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = this.getDefaultLocale();
// 默认的就是根据请求头带来的区域信息获取Locale进行国际化
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
} else {
Locale requestLocale = request.getLocale();
List<Locale> supportedLocales = this.getSupportedLocales();
if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
} else {
return defaultLocale != null ? defaultLocale : requestLocale;
}
} else {
return requestLocale;
}
}
}

那假如我们现在想点击链接让我们的国际化资源生效,就需要让我们自己的 Locale 生效!

我们去自己写一个自己的 LocaleResolver,可以在链接上携带区域信息!

修改一下前端页面的跳转连接:

<!-- 这里传入参数不需要使用 ?使用 (key=value)-->
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>

我们去写一个处理的组件类!

package com.kuang.component;

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

//可以在链接上携带区域信息
public class MyLocaleResolver implements LocaleResolver {

//解析请求
@Override
public Locale resolveLocale(HttpServletRequest request) {

String language = request.getParameter("l");
Locale locale = Locale.getDefault(); // 如果没有获取到就使用系统默认的
//如果请求链接不为空
if (!StringUtils.isEmpty(language)){
//分割请求参数
String[] split = language.split("_");
//国家,地区
locale = new Locale(split[0],split[1]);
}
return locale;
}

@Override
public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {

}
}

为了让我们的区域化信息能够生效,我们需要再配置一下这个组件!在我们自己的 MvcConofig 下添加 bean;

@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}

我们重启项目,来访问一下,发现点击按钮可以实现成功切换!搞定收工!

==注意点==

image-20210503175316167

4、登录+拦截器

4.1、登录

禁用模板缓存

说明:页面存在缓存,所以我们需要禁用模板引擎的缓存

#禁用模板缓存
spring.thymeleaf.cache=false

模板引擎修改后,想要实时生效!页面修改完毕后,IDEA 小技巧 : Ctrl + F9 重新编译!即可生效!

登录

我们这里就先不连接数据库了,输入任意用户名都可以登录成功!

1、我们把登录页面的表单提交地址写一个 controller!

<form class="form-signin" th:action="@{/user/login}" method="post">
//这里面的所有表单标签都需要加上一个name属性
</form>

2、去编写对应的 controller

@Controller
public class LoginController {
@RequestMapping("/user/login")
public String login(
@RequestParam("username") String username ,
@RequestParam("password") String password,
Model model){
//具体的业务
if(!StringUtils.isEmpty(username)&&"123456".equals(password)){
return "redirect:/main.html";
}
else{
//告诉用户,你登录失败
model.addAttribute("msg","用户名或者密码错误!");
return "index";
}
}
}

OK ,测试登录成功!

image-20210503190856881

3、登录失败的话,我们需要将后台信息输出到前台,可以在首页标题下面加上判断

<!--判断是否显示,使用if, ${}可以使用工具类,可以看thymeleaf的中文文档-->
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

重启登录失败测试:

image-20210503191007772

优化,登录成功后,由于是转发,链接不变,我们可以重定向到首页!

4、我们再添加一个视图控制映射,在我们的自己的 MyMvcConfifig 中:

registry.addViewController("/main.html").setViewName("dashboard");

5、将 Controller 的代码改为重定向;

//登录成功!防止表单重复提交,我们重定向
return "redirect:/main.html";

重启测试,重定向成功!后台主页正常显示!

4.2、登录拦截器

但是又发现新的问题,我们可以直接登录到后台主页,不用登录也可以实现!怎么处理这个问题呢?我

们可以使用拦截器机制,实现登录检查!

1、在 LoginController 添加 serssion

 session.setAttribute("loginUser",username);

2、自定义一个拦截器:

//自定义拦截器
public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取 loginUser 信息进行判断
Object user = request.getSession().getAttribute("loginUser");
if(user == null){//未登录,返回登录页面
request.setAttribute("msg","没有权限,请先登录");
request.getRequestDispatcher("/index.html").forward(request,response);
return false;
}else{
//登录,放行
return true;
}
}
}

3、然后将拦截器注册到我们的 SpringMVC 配置类当中!

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器,及拦截请求和要剔除哪些请求!
// 我们还需要过滤静态资源文件,否则样式显示不出来
registry.addInterceptor(new LoginHandlerInterceptor())
.addPathPatterns("/")
.excludePathPatterns("/index.html","/user/login","/","/css/*","/img/","/js/");
}

4、我们然后在后台主页,获取用户登录的信息

<!--后台主页显示登录用户的信息-->
[[${session.loginUser}]]
<!--$取EL表达式-->

然后我们登录测试拦截!完美!

5、展示员工列表

5.1、员工列表页面跳转

我们在主页点击 Customers,就显示列表页面;我们去修改下

1、将首页的侧边栏 Customers 改为员工管理

2、a 链接添加请求

<a class="nav-link" th:href="@{/emps}">员工管理</a>

3、将 list 放在 emp 文件夹下

image-20210504075819096

4、编写处理请求的 controller

//员工列表
@Controller
public class EmployeeController {

@Autowired
EmployeeDao employeeDao;

@RequestMapping("/emps")
public String list(Model model){
Collection<Employee> employees = employeeDao.getALL();
model.addAttribute("emps",employees);
return "emp/list";
}
}

我们启动项目,测试一下看是否能够跳转,测试 OK!我们只需要将数据渲染进去即可!

但是发现了一个问题,侧边栏和顶部都相同,我们是不是应该将它抽取出来呢?

5.2、Thymeleaf 公共页面元素抽取

步骤:

1、抽取公共片段 th:fragment 定义模板名

2、引入公共片段 th:insert 插入模板名

实现:

1、我们来抽取一下,使用 list 列表做演示!我们要抽取头部 nav 标签,我们在 dashboard 中将 nav 部分定

义一个模板名;

<!--顶部导航栏-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a> <!--$取EL表达式-->
<input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
<ul class="navbar-nav px-3">
<li class="nav-item text-nowrap">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">注销</a>
</li>
</ul>
</nav>

2、然后我们在 list 页面中去引入,可以删掉原来的 nav

<!--引入抽取的topbar-->
<!--模板名 : 会使用thymeleaf的前后缀配置规则进行解析 使用~{模板::标签名}-->
<!--顶部导航栏-->
<div th:insert="~{dashboard::topbar}"></div>

3、启动再次测试,可以看到已经成功加载过来了!

说明:

除了使用 insert 插入,还可以使用 replace 替换,或者 include 包含,三种方式会有一些小区别,可以见名

知义;

我们使用 replace 替换,可以解决 div 多余的问题,可以查看 thymeleaf 的文档学习

侧边栏也是同理,当做练手,可以也同步一下!

定义模板:

<!--侧边栏-->
<nav
th:fragment="sitebar"
class="col-md-2 d-none d-md-block bg-light sidebar"
></nav>

然后我们在 list 页面中去引入:

<!--侧边栏-->
<div th:insert="~{dashboard::sitebar}"></div>

启动再试试,看效果!

image-20210504080516865

我们发现一个小问题,侧边栏激活的问题,它总是激活第一个;按理来说,这应该是动态的才对!

为了重用更清晰,我们建立一个 commons 文件夹,专门存放公共页面;

image-20210504080721577

我们去页面中引入一下

<!--顶部导航栏-->
<div th:replace="~{commons/commons::topbar}"></div>
<!--侧边栏-->
<div th:replace="~{commons/commons::sidebar}"></div>

我们先测试一下,保证所有的页面没有出问题!ok!

侧边栏激活问题:

1、将首页的超链接地址改到项目中

2、我们在 a 标签中加一个判断,使用 class 改变标签的值;

<a
th:class="${active=='list.html'?'nav-link active':'nav-link'}"
th:href="@{/index.html}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-home"
>
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
首页 <span class="sr-only">(current)</span>
</a>

<a
th:class="${active=='list.html'?'nav-link active':'nav-link'}"
th:href="@{/emps}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-shopping-cart"
>
<circle cx="9" cy="21" r="1"></circle>
<circle cx="20" cy="21" r="1"></circle>
<path
d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"
></path>
</svg>
员工管理
</a>

3、修改请求链接

<div th:replace="~{commons/commons::topbar(active='main.html')}"></div>
<div th:replace="~{commons/commons::sidebar(active='list.html')}"></div>

4、我们刷新页面,去测试一下,OK,动态激活搞定!

5.3、员工信息页面展示

现在我们来遍历我们的员工信息!顺便美化一些页面,增加添加,修改,删除的按钮!

<thead>
<tr>
<th>id</th>
<th>lastName</th>
<th>email</th>
<th>gender</th>
<th>department</th>
<th>birth</th>
</tr>
</thead>
<tbody>
<tr th:each="emp:${emps}">
<td th:text="${emp.getId()}"></td>
<td th:text="${emp.getLastName()}"></td>
<td th:text="${emp.getEmail()}"></td>
<td th:text="${emp.getGender()==0?'女':'男'}"></td>
<td th:text="${emp.department.getDepartmentName()}"></td>
<td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td>
<td>
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger">删除</button>
</td>
</tr>
</tbody>

image-20210504081556725

**OK,显示全部员工 OK!

6、添加员工实现

6.1、表单及细节处理

1、将添加员工信息改为超链接

<h2><a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a></h2>

2、编写对应的 controller

//to员工添加页面
@GetMapping("/emp")
public String toAddPage(){
return "emp/add";
}

3、添加前端页面;复制 list 页面,修改即可

bootstrap 官网文档 : https://v4.bootcss.com/docs/4.0/components/forms/

我们去可以里面找自己喜欢的样式!我这里给大家提供了编辑好的:

<form th:action="@{/emp}" method="post">
<div class="form-group">
<label>LastName</label>
<input
class="form-control"
placeholder="kuangshen"
type="text"
name="lastName"
/>
</div>
<div class="form-group">
<label>Email</label>
<input
class="form-control"
placeholder="24736743@qq.com"
type="email"
name="email"
/>
</div>
<div class="form-group">
<label>Gender</label><br />
<div class="form-check form-check-inline">
<input class="form-check-input" name="gender" type="radio" value="1" />
<label class="form-check-label"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" name="gender" type="radio" value="0" />
<label class="form-check-label"></label>
</div>
</div>
<div class="form-group">
<label>department</label>
<select class="form-control" name="department.id">
<option
th:each="dept:${departments}"
th:text="${dept.getDepartmentName()}"
th:value="${dept.getId()}"
></option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input
class="form-control"
placeholder="kuangstudy"
type="text"
name="birth"
/>
</div>
<button class="btn btn-primary" type="submit">添加</button>
</form>

4、部门信息下拉框应该选择的是我们提供的数据,所以我们要修改一下前端和后端

Controller

@GetMapping("/emp")
public String toAddPage(Model model){
//查询所有的部门信息
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments",departments);
return "emp/add";
}

前端

<select class="form-control" name="department.id">
<option
th:each="dept:${departments}"
th:text="${dept.getDepartmentName()}"
th:value="${dept.getId()}"
></option>
</select>

**OK,修改了 controller,重启项目测试!

6.2、具体添加功能

1、修改 add 页面 form 表单提交地址和方式

<form th:action="@{/emp}" method="post">1</form>

2、编写 controller;

//员工添加功能
//接收前端传递的参数,自动封装成为对象[要求前端传递的参数名,和属性名一致]
@PostMapping ("/emp")
public String addEmp(Employee employee){
//保存员工的信息
System.out.println(employee);
employeeDao.save(employee);
// 回到员工列表页面,可以使用redirect或者forward,就不会被视图解析器解析
return "redirect:/emps";
}

image-20210504161017922

回忆:重定向和转发以及 /的问题?

时间格式问题

image-20210504161215722

生日我们提交的是一个日期 , 我们第一次使用的 / 正常提交成功了,后面使用 - 就错误了,所以这里面

应该存在一个日期格式化的问题;

SpringMVC 会将页面提交的值转换为指定的类型,默认日期是按照 / 的方式提交 ; 比如将 2019/01/01

转换为一个 date 对象。

那思考一个问题?我们能不能修改这个默认的格式呢?

这个在配置类中,所以我们可以自定义的去修改这个时间格式化问题,我们在我们的配置文件中修改一

下;

spring.mvc.date-format=yyyy-MM-dd

这样的话,我们现在就支持 - 的格式了,但是又不支持 / 了 , 2333 吧

测试 OK!

7、修改员工信息

逻辑分析:

我们要实现员工修改功能,需要实现两步;

1、点击修改按钮,去到编辑页面,我们可以直接使用添加员工的页面实现

2、显示原数据,修改完毕后跳回列表页面!

实现

1、我们去实现一下,首先修改跳转链接的位置;

<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.getId()}">编辑</a>

2、编写对应的 controller

//员工修改页面
@GetMapping("/emp/{id}")
public String toUpdateEmp(@PathVariable("id") Integer id,Model model){
Employee employee = employeeDao.getEmployeeById(id);
model.addAttribute("emp",employee);

//查询所有的部门信息
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments",departments);
return "emp/update";
}

3、我们需要在这里将 add 页面复制一份,改为 update 页面;需要修改页面,将我们后台查询数据回显

<form th:action="@{/emp}" method="post">
<input type="hidden" name="id" th:value="${emp.getId()}" />
<div class="form-group">
<label>LastName</label>
<input
th:value="${emp.getLastName()}"
class="form-control"
placeholder="kuangshen"
type="text"
name="lastName"
/>
</div>
<div class="form-group">
<label>Email</label>
<input
th:value="${emp.getEmail()}"
class="form-control"
placeholder="24736743@qq.com"
type="email"
name="email"
/>
</div>
<div class="form-group">
<label>Gender</label><br />
<div class="form-check form-check-inline">
<input
th:checked="${emp.getGender()==1}"
class="form-check-input"
name="gender"
type="radio"
value="1"
/>
<label class="form-check-label"></label>
</div>
<div class="form-check form-check-inline">
<input
th:checked="${emp.getGender()==0}"
class="form-check-input"
name="gender"
type="radio"
value="0"
/>
<label class="form-check-label"></label>
</div>
</div>
<div class="form-group">
<label>department</label>
<select class="form-control" name="department.id">
<option
th:selected="${dept.id==emp.getDepartment().getId()}"
th:each="dept:${departments}"
th:text="${dept.getDepartmentName()}"
th:value="${dept.getId()}"
></option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input
th:value="${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}"
class="form-control"
placeholder="2021-02-02"
type="text"
name="birth"
/>
</div>
<button class="btn btn-primary" type="submit">修改</button>
</form>

数据回显 OK!

8、删除员工实现与 404 页面处理

1、list 页面,编写提交地址

<a class="btn btn-sm btn-danger" th:href="@{/delEmp/}+${emp.getId()}">删除</a>

2、编写 Controller

//删除员工
@GetMapping("/delEmp/{id}")
public String delEmp(@PathVariable("id") Integer id){
employeeDao.delete(id);
return "redirect:/emps";
}

测试OK

、、

> 注销

1、注销请求

​```html

9、404 及注销

注销请求

<a class="nav-link" th:href="@{/user/logout}">注销
</a>

404:

我们只需要在模板目录下添加一个 error 文件夹,文件夹中存放我们相应的错误页面;

比如 404.html 或者 4xx.html 等等,SpringBoot 就会帮我们自动使用了!

测试使用!

2、对应的 controller

   @RequestMapping("/user/logout")
public String logout(HttpSession session){
session.invalidate();
return "redirect:/index.html";
}

后端人员开发如何开发一个网站

框架:vue,layui,bootstrap,

模版:由他人写好,功能齐全,简单修改后上手直接开发,

  1. 如何快速的搭建一个 web 应用:
  2. 前端:页面长什么样子
  3. 数据库:设计数据库: 难点
  4. 前端能让他自动运行:独立化工程,如果实在是不想,就 all in one
  5. 前后端联调!

layui 免费模版,x-admin

SpringData

对于数据访问层,无论是 nosql 还是 sql 数据库,SpringBoot 的底层都是用 Springdata 去统一处理

SpringBoot:Springdata 的官方文档地址:

https://spring.io/projects/spring-data

新项目:Spring-boot-data

项目搭建完成后肯定导入相关启动器,

之后是数据源的配置:

  1. 使用学习 mybtis 的数据库,配置相关 yml

    image-20210713083022233

  2. SpringBoot 的默认是用的 mysql8,所以如果出现时区报错,那么我们就添加时区,

    serverTimezone="UTC"来解决异 durid

druid

durid 的常用配置参数

配置缺省值说明
name配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。 如果没有配置,将会生成一个名字,格式是:"DataSource-" + System.identityHashCode(this)
jdbcUrl连接数据库的 url,不同数据库不一样。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username连接数据库的用户名
password连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用 ConfigFilter。详细看这里:https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter
driverClassName根据 url 自动识别这一项可配可不配,如果不配置 druid 会根据 url 自动识别 dbType,然后选择相应的 driverClassName(建议配置下)
initialSize0初始化时建立物理连接的个数。初始化发生在显示调用 init 方法,或者第一次 getConnection 时
maxActive8最大连接池数量
maxIdle8已经不再使用,配置了也没效果
minIdle最小连接池数量
maxWait获取连接时最大等待时间,单位毫秒。配置了 maxWait 之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置 useUnfairLock 属性为 true 使用非公平锁。
poolPreparedStatementsfalse是否缓存 preparedStatement,也就是 PSCache。PSCache 对支持游标的数据库性能提升巨大,比如说 oracle。在 mysql 下建议关闭。
maxOpenPreparedStatements-1要启用 PSCache,必须配置大于 0,当大于 0 时,poolPreparedStatements 自动触发修改为 true。在 Druid 中,不会存在 Oracle 下 PSCache 占用内存过多的问题,可以把这个数值配置大一些,比如说 100
validationQuery用来检测连接是否有效的 sql,要求是一个查询语句。如果 validationQuery 为 null,testOnBorrow、testOnReturn、testWhileIdle 都不会其作用。
testOnBorrowtrue申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnReturnfalse归还连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能
testWhileIdlefalse建议配置为 true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行 validationQuery 检测连接是否有效。
timeBetweenEvictionRunsMillis有两个含义: 1) Destroy 线程会检测连接的间隔时间 2) testWhileIdle 的判断依据,详细看 testWhileIdle 属性的说明
numTestsPerEvictionRun不再使用,一个 DruidDataSource 只支持一个 EvictionRun
minEvictableIdleTimeMillis
connectionInitSqls物理连接初始化的时候执行的 sql
exceptionSorter根据 dbType 自动识别当数据库抛出一些不可恢复的异常时,抛弃连接
filters属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的 filter:stat 日志用的 filter:log4j 防御 sql 注入的 filter:wall
proxyFilters类型是 List<com.alibaba.druid.filter.Filter>,如果同时配置了 filters 和 proxyFilters,是组合关系,并非替换关系
  1. 配置 druid 步骤

    1. 导入相关依赖,

              <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jdbc</artifactId>
      </dependency>
      <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
      </dependency>
      <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.2.6</version>
      </dependency>
      <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
      </dependency>
    2. 配置对应的数据源

      1. 对应配置文件

        spring:
        datasource:
        driver-class-name: com.mysql.jdbc.Driver
        password: xxx
        username: xxx
        url: jdbc:mysql://localhost:3306/mybatis #?serverTimezone=UTC&useUnicode=true&charcterEncoding=UTF-8
        type: com.alibaba.druid.pool.DruidDataSource

  2. 去将我们的增删改查方法测试一下就好了,用的是 jdbc 模版,由 boot 提供:jdbcTemplate

    package com.hyc.controller;


    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.List;
    import java.util.Map;

    @RestController
    public class Datacontroller {
    @Autowired
    JdbcTemplate jdbcTemplate;
    @RequestMapping("/userlist")
    public List<Map<String,Object>> userlist(){
    String sql = "select * from mybatis.user";
    List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
    return maps;
    }

    @RequestMapping("/addUser")
    public String addUser(){
    String sql = "insert into mybatis.user(id,name,pwd) values(1,'小胡','123456')";
    jdbcTemplate.update(sql);
    return "insert";
    }

    @RequestMapping("/upddateUser/{id}")
    public String upddateUser(@PathVariable("id") int id){
    String sql = "update mybatis.user set name=?,pwd=? where id ="+id;
    Object[] obj = new Object[2];
    obj[0] = "小明";
    obj[1] = "66666";
    jdbcTemplate.update(sql,obj);
    return "update";
    }

    @RequestMapping("/DeleteUser/{id}")
    public String DeleteUser(@PathVariable("id") int id){
    String sql = "delete from mybatis.user where id=? ";
    jdbcTemplate.update(sql,id);
    return "DeleteUser";
    }
    }

  3. YML 配置 druid 常用的一些配置

    #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
  4. 配置 sql 监控,和过滤请求:这里注意不要私自改动:loginusername 等 key

    package com.hyc.config;

    import com.alibaba.druid.pool.DruidDataSource;
    import com.alibaba.druid.support.http.StatViewServlet;
    import com.alibaba.druid.support.http.WebStatFilter;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.boot.web.servlet.FilterRegistrationBean;
    import org.springframework.boot.web.servlet.ServletRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    import javax.servlet.Filter;
    import javax.servlet.ServletRegistration;
    import javax.sql.DataSource;
    import java.util.HashMap;

    @Configuration
    public class DruidConfing {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
    return new DruidDataSource();
    }
    //后台监控

    @Bean
    public ServletRegistrationBean ServletRegistrationBean(){
    ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
    HashMap<String, String> InitParameters = new HashMap<>();
    InitParameters.put("loginUsername","admin");
    InitParameters.put("loginPassword","123456");
    InitParameters.put("allow","");
    //后台监控
    bean.setInitParameters(InitParameters);
    return bean;
    }

    @Bean
    public FilterRegistrationBean filterRegistrationBean(){
    FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
    //可以过滤那些请求?
    bean.setFilter(new WebStatFilter());
    bean.addUrlPatterns("/*");
    bean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
    return bean;
    }
    }

SpringBoot 整合 mybatis

准备工作,

寻找依赖,添加,这是个第三方的启动器

        <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>

去创建使用 mybatis 的需要文件,

实体类:user

mapper 接口:usermapper

配置文件:usermapper.xml

创建完毕后我们去 boot 的配置文件配置 mybatis:

#mybatis配置
mybatis.mapper-location=classpath:mapper/*.xml
mybatis.type-aliases-package=com.hyc.pojo

配置文件模板:

<?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">
<!-- namespace 写入 自己 dao 里的接口名称-->
<mapper namespace="com.hyc.mapper.userMapper">
</mapper>

用 mybatis 的注解来 映射,实体类:

package com.hyc.mapper;

import com.hyc.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

import java.util.List;
@Mapper
@Repository
public interface userMapper {

List<User> getuser();

User getuserByid(int id);

int addUser(User user);

int UpdUser(User user);

int DelUser(int id);
}

之后编写映射文件:

<?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">
<!-- namespace 写入 自己 dao 里的接口名称-->
<mapper namespace="com.hyc.mapper.userMapper">

<insert id="addUser">
insert into mybatis.user(id, name, pwd) VALUES (#{id},#{name},#{pwd});
</insert>

<update id="UpdUser">
update mybatis.user
set user.name = #{name},user.pwd =#{pwd}
where id = #{id};
</update>

<delete id="DelUser">
delete from mybatis.user where id=#{id}
</delete>

<select id="getuser" resultType="com.hyc.pojo.User">
select * from mybatis.user
</select>

<select id="getuserByid" resultType="com.hyc.pojo.User">
select *
from mybatis.user where id=#{id};
</select>
</mapper>

编写控制层测试即可:

@RestController
public class usercontroller {
@Autowired
private userMapper userMapper;
@RequestMapping("/queryList")
public List<User> queryList(){
List<User> getuser = userMapper.getuser();
for (User user : getuser) {
System.out.println(user);
}
return getuser;
}
}

SpringSecurity(安全)

Springsecurity 简介

Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC,DI(控制反转 Inversion of Control ,DI:Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

web 开发中 ,安全是第一位的,我们学过的有,过滤器,拦截器

spring security 的核心功能主要包括:

  • 认证 (你是谁)
  • 授权 (你能干什么)
  • 攻击防护 (防止伪造身份)

功能性需求:否

做网站需要考虑什么?

  1. 漏洞,隐私泄露问题
  2. 架构一旦确定,再加入安全十分浪费人力,

目前两大火热的安全框架:SpringSecurity 和 shrio,他们两个十分相似

  • 功能权限
  • 访问权限
  • 菜单权限
  • 以前我们都是用拦截器,过滤器,来做,这样会有一个缺点,大量的原生代码,冗余

其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在 spring security 中一种过滤器处理一种认证方式。

img

记住几个类:

  • webSecurityConfigurerAdapter:自定义 security 策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnablewebSecurity:开启 webSercurity

官网地址:https://spring.io/projects/spring-security

帮助文档:https://docs.spring.io/spring-security/site/docs/current/reference/html5/

Security 实战

导入相关依赖

        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

一般我们,配置 Security 的配置类都有一个模式

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}

接下来就可以来制定我们的需求了

需求 1:首页所有人可以访问,功能页只有对应的权限才能进去

编写:SecurityConfig 类

    @Override
protected void configure(HttpSecurity http) throws Exception {
//首页所有人可以访问,功能页只有对应的权限才能进去
http.authorizeRequests()//授权请求方法
.antMatchers("/").permitAll()
.antMatchers("views/level1/*").hasRole("vip1")//什么角色可以访问什么位置
.antMatchers("views/level2/*").hasRole("vip2")
.antMatchers("views/level3/*").hasRole("vip3");

}

给不同的用户,添加访问权限,

PS:在 5.x 之后密码要加密才可以有效: 使用以这个为后缀的类passwordEncoder

//密码编码:passwordencoder
//5.x之后更新了很多的加密方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("hyc").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}

设置注销:

   http.logout().logoutSuccessUrl("/index");

thymeleaf 常用命名空间:

xmlns:th=http://www.thymeleaf.org
xmlns:sec=http://www.thymeleaf.org/extras/spring-security
xmlns:shiro=http://www.pollix.at/thymeleaf/shiro

html lang=en xmlns:th=http://www.thymeleaf.org
xmlns:sec=http://www.thymeleaf.org/extras/spring-security
xmlns:shiro=http://www.pollix.at/thymeleaf/shiro

常用的内容:

PS:如果 403:csrf().disable();关闭跨站攻击安全

匹配的路径不需要认证:.antMatchers("/","/test/hello","/user/login").permitAll()

按照权限显示: sec:authorize="hasRole("xxx")

是否登陆显示:sec:authorize="!isAuthenticated()"

指定登陆的页面:.loginPage("/toLogin")

登陆访问路径:提交表单之后跳转的地址,可以看作一个中转站,这个步骤就是验证 user 的一个过程:.loginProcessingUrl("/login");

注销之后转发的页面:.logoutSuccessUrl("/index");

记住我:

         //开启记住我功能 cookie 默认保存两周
http.rememberMe();

自定义用户参数和密码参数

//.usernameParameter()
//.passwordParameter()

shiro(安全)

阿帕奇的安全框架

Apache Shiro 是一个 Java 的安全管理框架,可以用在 JavaEE 环境下,也可以用在 JavaSE 环境下。

此前我们学习了很多有关阿帕奇的东西:maven,tomcat,等等

官方号称十分钟就可以入门,

官网:https://shiro.apache.org/

为什么学他?:

(1)spring security 功能完善,学习成本偏高; (2)shiro 学习成本低,简单的安全框架,基本功能存在(登录认证,权限认证);  (3)spring mvc interceptor(拦截器) 只能做登录认证,不能做权限认证。

他能做什么?

image-20210716144222719

image-20210716144350511

Authentication:身份认证/登录;

Authorization:授权;

Session Manager:会话管理;

Cryptography:加密;

Web Support:**Web 支持,可以非常容易的集成到 Web 环境;

Caching:缓存;

Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing:提供测试支持;

Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me:记住我。

Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给 Shiro。

shiro 架构

image-20210716144256590

img

Subject:主体;

SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。

Authenticator:认证器;

Authrizer:授权器,;

Realm:可以有 1 个或多个 Realm,是安全实体数据源;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;

SessionManager:**Shiro 自己的 Session 来管理主体与应用之间交互的数据;

SessionDAO:**DAO 大家都用过,数据访问对象,用于会话的 CRUD;同时 SessionDao 也可以使用 Cache 进行缓存以提高性能。

CacheManager:缓存控制器,管理如用户、角色、权限等的缓存

Cryptography:密码模块

helloshiro

导入相关依赖:官方依赖

    <dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

 <resources>
<resource>
<directory>${basedir}/src/main/webapp</directory>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
<resource>
<directory>${basedir}/src/main/java</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
<include>**/*.yml</include>
</includes>
</resource>
</resources>

修改后的 Quickstart 类,

最新版本建议将:

FactorySecurityManager factory = new IniSecurityManagerFactory(classpath:shiro.ini);
SecurityManager securityManager = factory.getInstance();

转换位下面这样

DefaultSecurityManager securityManager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm(classpath:shiro.ini);
securityManager.setRealm(iniRealm);

下面代码是 Quickstart 类:

/*
* 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.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.text.IniRealm;
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.
*
* @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):
DefaultSecurityManager securityManager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
securityManager.setRealm(iniRealm);

// 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 currentUser = SecurityUtils.getSubject();

// Do some stuff with a Session (no need for a web or EJB container!!!)
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()) {
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);
}
}

三大对象:

  1. Subject
  2. SecurityManage
  3. fealm

shiro 整个流程

常用过滤器如下

  • anon:无需认证访问
  • authc:必须认证了才能访问
  • user:记住我开启了,才可以用
  • perms:拥有对某个资源的权限才能访问
  • role:该资源必须得到角色权限才可以访问

代码实战:

首先是 shiroconfig,我们从下往上配置:

  1. 首先是创建一个 realm 类
  2. 之后是创建 shiroconfig
  3. 之后从下往上配置
    1. 首先是引入 realm 类
    2. 配置安全管理器
    3. 之后设置过滤工厂
package com.hyc.config;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;

@Configuration
public class shrioconfig {
// shirofilterfactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
/*
* 常用过滤器如下
* anon:无需认证访问
* authc:必须认证了才能访问
* user:记住我开启了,才可以用
* perms:拥有对某个资源的权限才能访问
* */
* */
Map<String,String> filter = new LinkedHashMap();
filter.put("/add","anon");
filter.put("/upd","authc");
bean.setFilterChainDefinitionMap(filter);
bean.setLoginUrl("/tologin");
return bean;
}
// dafultwebSecurityManager
@Bean(name="SecurityManager")
public DefaultWebSecurityManager getdefaultWebSecurityManager(@Qualifier("Userrealm") userrealm userrealm){
DefaultWebSecurityManager SecurityManager = new DefaultWebSecurityManager();
// 关联Userrealm
SecurityManager.setRealm(userrealm);
return SecurityManager;

}
// 创建realm对象,需要自定义类
@Bean
public userrealm Userrealm() {
return new userrealm();
}
}

realm 对象需要引用外面的类 userrealm,我们需要继承 AuthorizingRealm 来获得授权,认证方法

package com.hyc.config;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class userrealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权=========>");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证=========>");
return null;
}
}

用户认证:

我们需要去编写 config 类,设置权限,什么路径需要什么权限,

  @Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
/*
* 常用过滤器如下
* anon:无需认证访问
* authc:必须认证了才能访问
* user:记住我开启了,才可以用
* perms:拥有对某个资源的权限才能访问
role:该资源必须得到角色权限才可以访问
* */
Map<String,String> filter = new LinkedHashMap();
filter.put("/user/add","perms[user:add]");
filter.put("/user/upd","perms[user:upd]");
bean.setFilterChainDefinitionMap(filter);
bean.setLoginUrl("/tologin");
bean.setUnauthorizedUrl("/unauth");
return bean;
}

之后去 realm 去认证,认证的信息是我们从数据库 user 表中查询出来的数据

  //认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

System.out.println("认证=========>");
// 获取当前的用户
Subject subject = SecurityUtils.getSubject();
// 封装用户数据
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
user user = userService.queryUserByName(token.getUsername());
if (user==null){
return null;
}
//认证的时候创建用户登陆的session
Session session = subject.getSession();
//将用户的属性传入到session中
session.setAttribute("loginUser",user);
//如何让我们的user可以全局使用,我们需要设置info中第一个参数为user
return new SimpleAuthenticationInfo(user,user.getPassword() ,"");
}

之后再认证之后获取用户对象授权,什么对象可以访问什么页面

    //授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权=========>");
//授权信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//传递用户信息
Subject subject = SecurityUtils.getSubject();
user currentUser = (user) subject.getPrincipal();
//从数据库中获取授权角色
info.addStringPermission(currentUser.getParms());
info.addRole("user:add");
info.addRole("user:upd");
return info;
}

登陆功能

controller

    @RequestMapping("/login")
public String login(String username,String password,Model model){
//获取角色对象
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
subject.login(token);//判断令牌是否正确
return "index";
} catch (UnknownAccountException uae) {//用户名不存在
model.addAttribute("msg","用户名不存在");
return "login";
} catch (IncorrectCredentialsException ice) {//密码不存在
model.addAttribute("msg","密码错误");
return "login";
}
}

这里有一个拓展可以做:就是密码加密处理

我们这里调用的 login()方法会走上面我们配置的一系列流程

整合 shiro 和 thymeleaf

需要的命名空间

xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"

要使用整合我们还需要导入整合包依赖

        <!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>

之后需要去配置类配置一个新的 bean

    //整合ShiroDialect:用来整合shiro thymeleaf
@Bean
public ShiroDialect getshiroDialect(){
return new ShiroDialect();
}


完成以上步骤就可以在模版引擎上使用 shiro 了

前端页面内容

<!DOCTYPE html>
<html lang="en"xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"
>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>你好</h1>
<span th:text="${msg}"></span>
<!--从session中判断值-->
<div th:if="${session.get('loginUser')==null}">
<a th:href="@{/tologin}">登录</a>
</div>
<a th:href="@{/logout}">注销</a>
<p th:text="${msg}"></p>
<hr>
<!--通过shiro中的hasPermission方法,判断登录的用户是否有这个权限,有权限才显示-->
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}" >add</a>

</div>
<div shiro:hasPermission="user:upd">
<a th:href="@{/user/upd}">update</a>
</div>
</body>
</html>

源码

配置相关

shiroconfig

package com.hyc.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;

@Configuration
public class shrioconfig {
// shirofilterfactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
/*
* 常用过滤器如下
* anon:无需认证访问
* authc:必须认证了才能访问
* user:记住我开启了,才可以用
* perms:拥有对某个资源的权限才能访问
* */
Map<String,String> filter = new LinkedHashMap();
filter.put("/user/add","perms[user:add]");
filter.put("/user/upd","perms[user:upd]");
bean.setFilterChainDefinitionMap(filter);
bean.setLoginUrl("/tologin");
bean.setUnauthorizedUrl("/unauth");
return bean;
}
// dafultwebSecurityManager
@Bean(name="SecurityManager")
public DefaultWebSecurityManager getdefaultWebSecurityManager(@Qualifier("Userrealm") userrealm userrealm){
DefaultWebSecurityManager SecurityManager = new DefaultWebSecurityManager();
// 关联Userrealm
SecurityManager.setRealm(userrealm);
return SecurityManager;

}
// 创建realm对象,需要自定义类
@Bean
public userrealm Userrealm() {
return new userrealm();
}


//整合ShiroDialect:用来整合shiro thymeleaf
@Bean
public ShiroDialect getshiroDialect(){
return new ShiroDialect();
}



}

userrealm

package com.hyc.config;

import com.hyc.pojo.user;
import com.hyc.service.userServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;

public class userrealm extends AuthorizingRealm {
@Autowired
userServiceImpl userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权=========>");
//授权信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//传递用户信息
Subject subject = SecurityUtils.getSubject();
user currentUser = (user) subject.getPrincipal();
//授权角色
info.addStringPermission(currentUser.getParms());
info.addRole("user:add");
info.addRole("user:upd");
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

System.out.println("认证=========>");
// 获取当前的用户
Subject subject = SecurityUtils.getSubject();
// 封装用户数据
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
user user = userService.queryUserByName(token.getUsername());
if (user==null){
return null;
}
Session session = subject.getSession();
session.setAttribute("loginUser",user);

return new SimpleAuthenticationInfo(user,user.getPassword() ,"");
}
}

控制层

package com.hyc.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.session.ProxiedSession;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@Controller
public class Mycontroller {
@RequestMapping({"/","/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello");
return "index";
}
@RequestMapping("/user/add")
public String add(){
return "user/add";
}
@RequestMapping("/user/upd")
public String upd(){
return "user/upd";
}
@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);
return "index";
} catch (UnknownAccountException uae) {//用户名不存在
model.addAttribute("msg","用户名不存在");
return "login";
} catch (IncorrectCredentialsException ice) {//密码不存在
model.addAttribute("msg","密码错误");
return "login";
}
}
@ResponseBody
@RequestMapping("/unauth")
public String unauth(){
return "您没有权限";
}
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
session.removeAttribute("loginUser");
return "index";
}
}

前端页面:

index

<!DOCTYPE html>
<html lang="en"xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"
>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>你好</h1>
<span th:text="${msg}"></span>
<!--从session中判断值-->
<div th:if="${session.get('loginUser')==null}">
<a th:href="@{/tologin}">登录</a>
</div>
<a th:href="@{/logout}">注销</a>
<p th:text="${msg}"></p>
<hr>
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}" >add</a>

</div>
<div shiro:hasPermission="user:upd">
<a th:href="@{/user/upd}">update</a>
</div>
</body>
</html>

login

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>登陆</p>
<p th:text="${msg}" style="color: red"></p>
<form th:action="@{/login}">
<p> 用户名:<input type="text" name="username"></p>
<p> 密码:<input type="password" name="password"></p>
<p><input type="submit" value="提交"></p>
</form>
</body>
</html>

安全框架总结:

学习了 Springsecurity 和 shiro 之后,总结出一些学习方法

  1. 安全框架的核心思想都十分相似,授权,认证,防伪等
  2. 他们通常都有几个对象,如 shiro 中的 subject,securityManager 一样,
  3. 源码的注释写有方法使用的模版,我们可以通过下载源码去查看注释,
  4. Springsercurity 和 shiro 的区别,两个我个人认为,除了一个基于 Spring 之外功能上两者几乎一致
  5. 使用的感受
    1. Spring Security 基于 Spring 开发,项目中如果使用 Spring 作为基础,配合 Spring Security 做权限更加方便,而 Shiro 需要和 Spring 进行整合开发
    2. 感觉 shiro 没有类似于 Spring Security 那样的安全防护
    3. shiro 不需要基于任何框架,依赖性低
    4. 个人认为:配置的麻不麻烦关键在于项目用不用 Spring,我看大神们写博客都说 shrio 配置要更简单一些,但是简单的上手了两个安全框架之后,我觉得使用了 Spring 的项目上手 security 要比 shiro 简单的多,
    5. 还有个个人感想,帮助文档的阅读能力太重要了,学习和接触新技术在没有教程的情况下,文档的阅读能力决定了你的学习上限(个人中间有一段只照着官方文档学习,十分痛苦)
  6. 以上就是安全框架简单上手的全内容啦,

swagger

学习目标:

  • 了解 Swagger 的作用和概念
  • 了解前后端分离
  • 在 SpringBoot 中集成 Swagger

Swagger 简介

故事还是要从前后端分离讲起啊

**前后端分离:**VUE+SpringBoot 基本上都用这一套

**后端时代:**前端只用管理静态页面,html===》后端,使用模版引擎 jsp=》后端主力

前后端分离时代

  • 后端:后端控制层,服务层,数据访问层【后端团队】
  • 前端:前端控制层,视图层,【前端团队】
    • 伪造后端数据,json,已经存在数据,不需要后端,前端工程依旧可以跑起来
  • 前后端如何交互 ====》API
  • 前后端相对独立,松耦合
  • 前后端甚至可以部署在不同的服务器上

产生一个问题:

  • 前后端联调,前端和后端人员无法做到及时协商,解决问题,导致问题爆发
  • 需要一个东西可以解决这个问题

解决问题:

  • 首先指定计划,实时更新 API,较低集成风险
  • 早些年:指定 word 计划文档
  • 前后端分离:
    • 前端测试后端接口:postman
    • 后端提供接口,需要使用更新最新的消息及改动!

官网:https://swagger.io/

Swagger

  • 号称世界上最流行的 api 框架
  • Restful Api 文档在线自动生成工具==》api 文档和 api 定义开发
  • 直接运行,可以在线测试 api 接口;
  • 执行多种语言(c#,java,php)

在项目中使用 Swagger 需要 Springfox

  • swagger2
  • ui

SpringBoot 集成 Swagger

  1. 新建项目:SpringBoot-Swagger

  2. 导入相关依赖

    <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>3.0.0</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>3.0.0</version>
    </dependency>

    新版(3.0)的直接加入启动器

            <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
    </dependency>
  3. 创建一个 helloword 的项目

  4. 配置 Swagger==>就可以启动看看效果了 3.0 版本后不需要在加入@enableopenapi,和@enableswagger2 这两个注解,

    package com.hyc.springbootswagger.config;

    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;

    @Configuration
    public class swaggerconfig {

    }

    路径:http://localhost:8080/swagger-ui/index.html

    image-20210720101545889

配置 Swagger

配置呢,Swagger 有自己的实例

我们使用 docket 来配置 swagger 的基本信息

@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo());
}
// 配置swagger基本信息
private ApiInfo apiInfo(){
Contact contact = new Contact("xxx", "hyc.com", "3132774018@qq.com");
return new ApiInfo(
"XXX的swagger",
"签名",
"1.0",
"hyc.com",
contact,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList());
}

应为没有 set 方法所以我们只能用构造器,貌似,还有一个什么 biuder 可以使用,有机会去试试

swagger 配置扫描接口

select()来设置扫描

扫描接口配置的方法:

apis:

  • RequestHandlerSelectors 扫描接口的方式
  • basePackage 指定扫描包
  • any()扫描全部
  • none()不扫描
  • withclassannotation 扫描类的注解(里面必须放注解的反射对象)

path:过滤哪里什么路径

  • paths(PathSelectors.ant("/hyc/**"))
 .select()
//指定我们需要基于什么包扫描
.apis(RequestHandlerSelectors.basePackage("com.hyc.springbootswagger.controller"))
.build();

使用了自定义,那么 swagger 就不会去扫描其他的位置,会扫描你指定的这个报下的请求

image-20210720105603000可以发现,现在只有 controller 下的请求才会被扫描

是否开启 Swagger

.enable(false)//eanble决定了是否启动swagger

如果为 false 那我们就无法进入 swagger-ui/index.html 了

如何让我在测试的时候用 swagger,发布的时候不用 swagger

environment.acceptsProfiles来判断是否处在环境中

  //配置swagger要使用的环境
Profiles profiles = Profiles.of("dev", "test");

用 profiles 来配置使用环境

.enable(flag)//eanble判断是否启动swagger

api 分组

分组,如何分组,

 .groupName("胡宇辰")

分组,如何多个分组?,我有多个 docket 就可以有多个.groupName

    @Bean
public Docket docket(Environment environment){
//配置swagger要使用的环境
Profiles profiles = Profiles.of("dev", "test");
//environment。acceptsProfiles判断自己是否在自己设定的环境中
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("胡宇辰")
.enable(flag)//eanble决定了是否启动swagger
.select()
//指定我们需要基于什么包扫描
/*apis
* RequestHandlerSelectors扫描接口的方式
* basePackage指定扫描包
* any()扫描全部
* none()不扫描
* withclassannotation 扫描类的注解(里面必须放注解的反射对象)
*
*/
.apis(RequestHandlerSelectors.basePackage("com.hyc.springbootswagger.controller"))
/*path:过滤哪里什么路径
*
* */
// .paths(PathSelectors.ant("/hyc/**"))
.build();
}
@Bean
public Docket docket1(Environment environment){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("小刘");
}
@Bean
public Docket docket2(Environment environment){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("小郑");
}

配置多个组

就是有很多个 docket,

效果:

image-20210720140340646

实体类

只要我们的接口中,有接口返回的是实体类,那么就是会被 swagger 扫描

我们写一个方法

    @PostMapping("/user")
public User user(){
return new User();
}

返回的是实体类 user,user 里有两个字段,name 和 age

页面效果图:

image-20210720141029645

那我们看到的如@API 这些注解是干什么的呢?

Swagger 注解

用来解释类的用@Apimodel

@ApiModel("用户信息实体类")
public class User{

}

用来解释类中的属性用@ApiModelProperty()

  @ApiModelProperty("用户名字")
public String name;
@ApiModelProperty("用户年龄")
public int age;

小疑问:我用 private 修饰的变量这么写就不显示,怎么办?

解决方案:写在 get 方法上就可以有效果了

Swagger 测试接口

测试接口十分好用,

我们可以测试自己的接口是否有效

小测试:

测试接口:

    @PostMapping("/userJY")
public User user2(String name,int age){
User user = new User(name,age);
return user;
}

测试页面步骤图

image-20210720143503693

查看提交后的接口信息

image-20210720143653156

Swagger 总结

  1. Swagger 最重大的使命就是使前后端人员之间的和谐关系有所好转
  2. 接口文档可以实时更新
  3. 可以在线测试后端接口,这个功能好评,爽的一批

Swagger 是一个十分好用的工具,很多公司在使用

PS:处于安全考虑,我们在发布的时候需要关闭 Swagger

功能

文件上传

配置:

# 上传文件总的最大值
spring.servlet.multipart.max-request-size=10MB
# 单个文件的最大值
spring.servlet.multipart.max-file-size=10MB

## jsp
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp


单个文件上传的示例

@Controller
public class UploadController {
private static final Logger LOGGER = LoggerFactory.getLogger(UploadController.class);

@GetMapping("/upload")
public String upload() {
return "upload";
}

@PostMapping("/upload")
@ResponseBody
public String upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return "上传失败,请选择文件";
}

String fileName = file.getOriginalFilename();
String filePath = "/Users/itinypocket/workspace/temp/";
File dest = new File(filePath + fileName);
try {
file.transferTo(dest);
LOGGER.info("上传成功");
return "上传成功";
} catch (IOException e) {
LOGGER.error(e.toString(), e);
}
return "上传失败!";
}


}

多个文件上传的示例

@GetMapping("/multiUpload")
public String multiUpload() {
return "multiUpload";
}

@PostMapping("/multiUpload")
@ResponseBody
public String multiUpload(HttpServletRequest request) {
List<MultipartFile> files = ((MultipartHttpServletRequest) request).getFiles("file");
String filePath = "/Users/itinypocket/workspace/temp/";
for (int i = 0; i < files.size(); i++) {
MultipartFile file = files.get(i);
if (file.isEmpty()) {
return "上传第" + (i++) + "个文件失败";
}
String fileName = file.getOriginalFilename();

File dest = new File(filePath + fileName);
try {
file.transferTo(dest);
LOGGER.info("第" + (i + 1) + "个文件上传成功");
} catch (IOException e) {
LOGGER.error(e.toString(), e);
return "上传第" + (i++) + "个文件失败";
}
}

return "上传成功";

}

任务

异步任务

小案例:我如何解决假如我访问了一个 hello 页面,页面调用的方法是睡眠 3 秒,再返回 ok,那么再前端页面我就要等待空白页面 3 秒钟。如何解决?

Springboot 中有一个注解叫@async异步任务

我们要使用他需要在主启动类开启注解支持@enableasync

代码示例

@Service
public class AsyncService {
@Async
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("您好");
}
}

这段代码会让执行方法的时候网页加载三秒。

控制层

@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;

@RequestMapping("/hello")
public String hello(){
asyncService.hello();
return "ok";
}
}

我们访问 hello 这个请求,就会等待三秒,之后页面返回 ok,

执行顺序是:

​ 发起请求-----》执行方法---》等三秒---》返回 ok

我们启动了异步任务,并且将等待方法上加入注解@Aynsc

再次启动的时候,顺序就变成了这样 发起请求-----》执行方法---》等三秒(同时)---》等待完毕

​ ---》返回 ok(同时)

定时任务

gogogo 函数接口

第一个接口:TaskExecutor // 任务执行者
第二个接口:TaskScheduler// 任务调度者

注解
@EnableScheduling//开启定时任务功能注解
@Scheduled


使用定时任务首先我们要在主启动类上添加注解开启定时任务:

@EnableScheduling//开启定时任务功能注解

之后就可以通过@Scheduled来设置 cron 表达式实现定时任务了

cron 表达式

​ 计划任务,是任务在约定的时间执行已经计划好的工作,这是表面的意思。在 Linux 中,我们经常用到 cron 服务器来完成这项工作。cron 服务器可以根据配置文件约定的时间来执行特定的任务。

一个 cron 表达式有至少 6 个(也可能 7 个)有空格分隔的时间元素。 按顺序依次为

秒(059) 分钟(059) 小时(023) 天(月)(031,但是你需要考虑你月的天数) 月(011) 天(星期)(17 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT) 年份(1970-2099)

字段 允许值 允许的特殊字符

秒 0-59 , - _ / 分 0-59 , - _ / 小时 0-23 , - _ / 日期 1-31 , - _ ? / L W C 月份 1-12 或者 JAN-DEC , - _ / 星期 1-7 或者 SUN-SAT , - _ ? / L C # 年(可选) 留空, 1970-2099 , - * /

PS:

  1. ?只能用在日期和星期,月上
  2. 在使用“L”参数时,不要指定列表或范围,因为这会导致问题

测试小例子:

@Service
public class ScheduledService {
//需求:在一定的时间执行这个方法 方法千百种
//cron表达式
@Scheduled(cron = "0 14 10 * * ?")
public void hello(){
System.out.println("你被执行了");
}
}

我想在每天的 10 点 14 分的时候,打印你被执行了,

邮件任务

首先要去 qq 邮箱开启服务

image-20210720230503628

spring 配置文件中的邮件配置

spring.mail.username=3132774018@qq.com
spring.mail.password=vymozuqhwxmadhae
spring.mail.host= smtp.qq.com
#开启加密验证
spring.mail.properties.mail.stmp.ssl.enable=true

spring 中关于邮件的实现类:JavaMailSenderImpl

image-20210720230822737

他是一个 bean 所以我们可以拿来使用

    @Autowired
JavaMailSenderImpl sender;

简单的邮件发送

@SpringBootTest
class Spring8TestApplicationTests {
@Autowired
JavaMailSenderImpl sender;
@Test
void contextLoads() {
//一个简单的邮件的发送
SimpleMailMessage message = new SimpleMailMessage();
//标题
message.setSubject("你好啊,hyc组内的成员,看到这条消息的时候 hyc还在为了大厂努力");
//内容
message.setText("冲进大厂没有什么是不可以的,好好努力,一切都在掌握之中");
//发送给谁
message.setTo("2549273958@qq.com");
//谁来发送
message.setFrom("3132774018@qq.com");
sender.send(message);
}

}

复杂邮件,带有附件,html,

我们这里使用sender.createMimeMessage的方式来创建复杂的邮件发送对象

springboot 给我们提供了一个帮助类,MimeMessageHelper我们用这个 helper 对象来操作MImeMessage,

就可以完成复杂邮件发送了

    @Test
void contextLoads1() throws MessagingException {
//复杂的邮件的发送
MimeMessage mimeMessage = sender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
helper.setSubject("hyc-plus");
helper.setText("<p style='color:red'>你好</p>",true);
//附件
helper.addAttachment("1.jpg",new File("D:\\java工程师\\SpringBoot\\spring-8-test\\src\\main\\resources\\public\\1600837482167.jpg"));
//发送给谁
helper.setTo("3132774018@qq.com");
//谁来发送
helper.setFrom("3132774018@qq.com");
sender.send(mimeMessage);
}

邮件发送工具类

话不多说都在码里面了

MailsenderUtils:

@Component
public class MailsenderUtils {
@Autowired
private JavaMailSenderImpl mailSender;

/**
*
* @param subject :邮件标题
* @param text 邮件内容
* @param html 是否开启html,
* @param to 发送给谁
* @param from 谁来发送
* @param filename 附件名字
* @param acc 附件路径
* @return
* 1 :发送成功
* -1:发送失败
*/
public int MailsenderbyHyc(String subject, String text, boolean html, String to, String from,String filename, File acc){
//复杂的邮件的发送
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = null;
try {
helper = new MimeMessageHelper(mimeMessage,true);
helper.setSubject(subject);
helper.setText(text,html);
//附件
helper.addAttachment(filename,acc);
//发送给谁
helper.setTo(to);
//谁来发送
helper.setFrom(from);
mailSender.send(mimeMessage);
return 1;
} catch (MessagingException e) {
e.printStackTrace();
return -1;
}
}

}