7. SpringBoot整理

251 阅读23分钟

一、回顾

1.1 什么是Spring

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

为了降低Java开发的复杂性,Spring采用一下4中策略:

  • 基于POJO的轻量级和最小侵入编程
  • 通过IOC,依赖注入DI和面向接口实现松耦合
  • 基于切面AOP和惯例进行声明式编程
  • 通过切面和模板减少样式代码

1.2 什么是SpringBoot

开发一个web,从最初接触Servlet结合Tomcat,要经理特别多的步骤,后来就用了Struts,再后来使用SpringMVC,到了现在的SpringBoot,过一两年又会有其他的web框架出现。

SpringBoot就是一个java Web开发框架,和SpringMVC相似,对比其他java Web框架,简化开发,约定大于配置,能够迅速开发web应用。

随着Spring不断发展,涉及领域越来越多,项目整合开发需要配合各种各样的文件,慢慢编的不再简单易用。SpringBoot正是在这样的背景下被抽象出来的开发框架,==目的是为了让大家更容易使用Spring、更容易集成各种常用的中间件、开源软件==。

SpringBoot主要优点:

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

1.3 微服务

1. 什么是微服务

微服务是一种架构风格,他要求我们在开发一个应用的时候,必须构建成一系列小服务的组合,通过http(或者rpc)的方式进行互通,要说微服务架构,先要说说单体应用架构。

单体应用架构

单体应用架构就是将所有应用服务都封装在一个应用中。无论是REP、CRM或者其他什么系统,把数据访问、web访问等各个功能放到一个war包中。

  • 好处易于开发和测试,方便部署,当需要扩展时只需要将war复制多份然后放到多个服务器上,再做个负载均衡即可
  • 缺点是要修改一个小地方,需要停整个服务,重新打包部署,对于大型应用,不可能把所有内容放在一个应用里面

微服务架构

把每个功能元素独立出来,把独立出来的功能元素动态组合,需要的功能元素才去拿来组合。所以微服务架构是对功能元素进行复制,而没有对整个应用进行复制。这样做的好处是:

  • 节省了调用资源
  • 每个功能元素的服务都是一个可替换的、可独立升级的软件代码
  • 高内聚、低耦合

二、第一个SpringBoot程序

2.1 创建SpringBoot项目

image.png

2.2 准备工作

  • 导入web依赖
<!--导入web依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.6.3</version>
</dependency>
  • 启动项目

image.png

  • 查看结果

image.png

只有当添加了web启动器依赖时,springBoot启动后才会集成Tomcat,否则启动后会直接停止

2.3 编写Controller

  • @Controller:要返回一个视图名称
  • @ResponseBody:返回字符串,不使用视图解析器
@Controller
@RequestMapping("/hello")
public class HelloController {
    @RequestMapping("/h1")
    @ResponseBody
    public String hello() {
        return "hello";
    }
}

测试

image.png

2.4 配置文件

  • 修改默认端口号
# 修改项目的默认端口号
server.port=8081

三、SpringBoot自动装配原理

3.1 pom.xml

  • dependencies

spring-boot-dependencies:核心依赖在父工程中

  • 启动器

spring-boot-starter-xxx:是springBoot的启动器,说白了就是springBoot的启动场景。

springBoot会有很多已经内置好的启动场景,如果要使用什么功能,只需要找到对应的启动器就可以了。

NameDescription
spring-boot-starterCore starter, including auto-configuration support, logging and YAML(==默认主启动器==)
spring-boot-starter-activemqStarter for JMS messaging using Apache ActiveMQ
spring-boot-starter-amqpStarter for using Spring AMQP and Rabbit MQ
spring-boot-starter-aopStarter for aspect-oriented programming with Spring AOP and AspectJ(==AOP==)
spring-boot-starter-artemisStarter for JMS messaging using Apache Artemis
spring-boot-starter-batchStarter for using Spring Batch
spring-boot-starter-cacheStarter for using Spring Framework’s caching support(==缓存==)
spring-boot-starter-data-cassandraStarter for using Cassandra distributed database and Spring Data Cassandra
spring-boot-starter-data-cassandra-reactiveStarter for using Cassandra distributed database and Spring Data Cassandra Reactive
spring-boot-starter-data-couchbaseStarter for using Couchbase document-oriented database and Spring Data Couchbase
spring-boot-starter-data-couchbase-reactiveStarter for using Couchbase document-oriented database and Spring Data Couchbase Reactive
spring-boot-starter-data-elasticsearchStarter for using Elasticsearch search and analytics engine and Spring Data Elasticsearch
spring-boot-starter-data-jdbcStarter for using Spring Data JDBC
spring-boot-starter-data-jpaStarter for using Spring Data JPA with Hibernate
spring-boot-starter-data-ldapStarter for using Spring Data LDAP
spring-boot-starter-data-mongodbStarter for using MongoDB document-oriented database and Spring Data MongoDB
spring-boot-starter-data-mongodb-reactiveStarter for using MongoDB document-oriented database and Spring Data MongoDB Reactive
spring-boot-starter-data-neo4jStarter for using Neo4j graph database and Spring Data Neo4j
spring-boot-starter-data-r2dbcStarter for using Spring Data R2DBC
spring-boot-starter-data-redisStarter for using Redis key-value data store with Spring Data Redis and the Lettuce client(==Redis==)
spring-boot-starter-data-redis-reactiveStarter for using Redis key-value data store with Spring Data Redis reactive and the Lettuce client
spring-boot-starter-data-restStarter for exposing Spring Data repositories over REST using Spring Data REST
spring-boot-starter-freemarkerStarter for building MVC web applications using FreeMarker views
spring-boot-starter-groovy-templatesStarter for building MVC web applications using Groovy Templates views
spring-boot-starter-hateoasStarter for building hypermedia-based RESTful web application with Spring MVC and Spring HATEOAS
spring-boot-starter-integrationStarter for using Spring Integration
spring-boot-starter-jdbcStarter for using JDBC with the HikariCP connection pool(==JDBC==)
spring-boot-starter-jerseyStarter for building RESTful web applications using JAX-RS and Jersey. An alternative to spring-boot-starter-web
spring-boot-starter-jooqStarter for using jOOQ to access SQL databases with JDBC. An alternative to spring-boot-starter-data-jpa or spring-boot-starter-jdbc
spring-boot-starter-jsonStarter for reading and writing json
spring-boot-starter-jta-atomikosStarter for JTA transactions using Atomikos
spring-boot-starter-mailStarter for using Java Mail and Spring Framework’s email sending support
spring-boot-starter-mustacheStarter for building web applications using Mustache views
spring-boot-starter-oauth2-clientStarter for using Spring Security’s OAuth2/OpenID Connect client features
spring-boot-starter-oauth2-resource-serverStarter for using Spring Security’s OAuth2 resource server features
spring-boot-starter-quartzStarter for using the Quartz scheduler
spring-boot-starter-rsocketStarter for building RSocket clients and servers
spring-boot-starter-securityStarter for using Spring Security
spring-boot-starter-testStarter for testing Spring Boot applications with libraries including JUnit Jupiter, Hamcrest and Mockito(==测试==)
spring-boot-starter-thymeleafStarter for building MVC web applications using Thymeleaf views
spring-boot-starter-validationStarter for using Java Bean Validation with Hibernate Validator
spring-boot-starter-webStarter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container(==web==)
spring-boot-starter-web-servicesStarter for using Spring Web Services
spring-boot-starter-webfluxStarter for building WebFlux applications using Spring Framework’s Reactive Web support
spring-boot-starter-websocketStarter for building WebSocket applications using Spring Framework’s WebSocket support

3.2 主程序

@SpringBootApplication
public class Springboot01HelloworldApplication {

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

}
  • @SpringBootApplication:标注这个类是一个SpringBoot的应用
    • @SpringBootConfiguration:SpringBoot的配置
      • @Configuration:Spring注解,代表是一个Spring配置类
        • @Component:是一个Spring组件
    • @EnableAutoConfiguration:==自动配置==
      • @AutoConfigurationPackage:自动配置包
        • @Import(AutoConfigurationPackages.Registrar.class):自动配置包注册
      • @Import(AutoConfigurationImportSelector.class):==导入选择器==
        • List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);:==获得所有的配置==
  • SpringApplication:将SpringBoot应用启动,通过反射将类启动

获取所有候选配置的方法:

private ClassLoader beanClassLoader;
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;
}

/**
 * 这个类返回标注了EnableAutoConfiguration这个类的所有包
 * 而标准这个类的类是SpringBootApplication标注的
 * SpringBootApplication类又是项目启动类标注的,因此启动类下所有资源被导入
 */
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
    return EnableAutoConfiguration.class;
}

protected ClassLoader getBeanClassLoader() {
    return this.beanClassLoader;
}
  1. META-INF/spring.factories文件

其中META-INF/spring.factories是自动配置的核心文件,其位置在:

image.png

spring.factories中,配置许多spring的自动配置项:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
......
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
......

其中WebMvcAutoConfiguration就是web相关的自动配置,其中就有视图解析器等相关的默认配置:

private ResourceResolver getVersionResourceResolver(Strategy properties) {
    VersionResourceResolver resolver = new VersionResourceResolver();
    if (properties.getFixed().isEnabled()) {
        String version = properties.getFixed().getVersion();
        String[] paths = properties.getFixed().getPaths();
        resolver.addFixedVersionStrategy(version, paths);
    }
    if (properties.getContent().isEnabled()) {
        String[] paths = properties.getContent().getPaths();
        resolver.addContentVersionStrategy(paths);
    }
    return resolver;
}
  1. loadFactoryNames方法:所有的资源加载到配置类中

image.png

其中FACTORIES_RESOURCE_LOCATION的位置就是autoConfig包下的META-INF/spring.factories文件:

image.png

3.3 自动配置原理总结

结论:SpringBoot所有的自动配置都在启动类中被扫描并加载,所有自动配置类都在spring.factories里面,但是不一定生效,要判断条件是否成立,只要导入对应的starter,就有对应的启动器,自动装配就会生效,配置就成功了。

一切从@SpringBootApplication开始,该注解中有一个自动导入配置的注解@EnableAutoConfiguration,该注解引入了一个自动配置引入选择器AutoConfigurationImportSelector,该类负责导入候选配置,首先调用getAutoConfigurationEntry方法获得所有实体,该方法又调用了getCandidateCofigrations方法获取候选配置,该方法从标注了@EnableAutoConfiguration注解的类中加载配置,具体方法是从META-INF/spring.factories中读取资源,将其封装成为配置

image.png

image.png

  1. SpringBoot在启动的时候,从类路径下/META-INF/spring.factories获取指定的值
  2. 将这些自动配置的类导入容器,自动配置类就会生效
  3. 以前需要自动配置的东西,现在SpringBoot帮我们做了
  4. 整合javaEE解决方案和自动配置的东西都在spring-boot-autoconfigure-x.x.x.RELEASE.jar
  5. 它会将所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器中
  6. 容器中会存在很多xxxAutoConfiguration的类,这些类就是给容器导入这个场景需要的所有组件(@Configutation
  7. 有了自动配置类,免去我们手动编写配置文件工作

四、SpringBoot主启动类如何运行

image.png

4.1 SpringApplication类

这个类主要做了一下四件事情:

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

4.2 run方法

img

五、SpringBoot配置

5.1 配置文件

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

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

配置文件的作用:修改SpringBoot自动配置的默认值,因为SpringBoot在底层给我们自动配置好了

5.2 YAML

YAMLYet Another Markup Language(YAML是一种标记语言),是一种可读性高,用来表达数据序列化的格式。

  • yml配置
server:
	prot: 8080
  • xml配置
<server>
    <port>8080</port>
</server>

yaml基础语法

# 普通key-value
name: nick

# 对象
student:
	name: nick
	age: 28
# 对象行内写法
student: {name: nick, age: 28}

# 数组
pets:
	- cat
	- dog
	- pig
# 数组行内写法
pets: [cat, dog, pig]

5.3 yaml给实体类赋值

  • 实体类Dog
@Component
@Data
public class Dog {
    private String name;
    private Integer age;
}
  • 实体类Person
@Component
@Data
public class Person {
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birthday;
    private Map<String, Object> maps;
    private List<Object> list;
    private Dog dog;
}
  • 通过YAML赋值
person:
  name: nick
  age: 28
  happy: true
  birthday: 1994/12/30
  maps: {k1: v1, k2: v2}
  lists: [1, 2, 3, 4]
  dog:
    name: 旺财
    age: 3

==通过在实体类中指定yaml中的key==

@Component
@Data
@ConfigurationProperties(prefix = "person")
public class Person {
    ......
}

==注意:==

@ConfigurationProperties这个注解是为了将yaml中的配置与实体类进行绑定

该注解会将配置文件中的每一个属性值,映射到这个组件中,告诉SpringBoot将本类中所有属性和配置文件中的相关配置进行绑定,

参数prefix是将文件中的person下面的属性进行一一对应

只有这个组件是容器中的组件,才能使用容器提供的ConfigurationProperties功能

要是用该注解,需要添加一个依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
  • 测试
Person(name=nick, age=28, happy=true, birthday=Fri Dec 30 00:00:00 CST 1994, maps={k1=v1, k2=v2}, lists=[1, 2, 3, 4], dog=Dog(name=旺财, age=3))

5.4 JSR303数据校验

spring-boot中可以用@Validated来校验数据,如果数据异常,则会统一抛出异常,方便异常中心统一处理。

使用前需要导入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.6.3</version>
</dependency>

具体使用为:

  • 首先在类上使用@Validated注解开启校验
  • 在具体属性上使用注解进行注入限制
  • 空检查
    • @Null:元素必须为null
    • @NotNull:元素必须不为null
    • @NotBlank:元素是不是null且trim后长度是否大于0
    • @NotEmpty:元素是否为null或者empty
  • Boolean检查
    • @AssertTrue:元素必须为True
    • @AssertFalse:元素必须为False
  • 长度检查
    • @Size(max, min):元素大小必须在指定范围内
    • @Length:字符串大小必须在指定范围内
  • 日期检查
    • @Past:元素必须是一个过去的日期
    • @Future:元素必须是一个将来的日期
  • 格式检查
    • @Email:所注入的值格式必须为邮箱格式
    • @Pattern(value):元素必须符合指定正则表达式
  • 数字检查
    • @Min(value):元素必须是一个数字且大于等于value
    • @Max(value):元素必须是一个数组且小于等于value
    • @Digits(integer, fraction):元素必须是一个数字且必须在可接受范围内
    • @Range:元素必须在核实范围内

类似的限制还有很多,具体可以看org.springframework.validation.annotation下面的类:

image.png

当我们使用@Email修饰name后:

@Component
@Data
@ConfigurationProperties(prefix = "person")
@Validated
public class Person {
    @Email(message="该名称并非邮箱地址")
    private String name;
    ......
}

注入yaml中的值时就会报错:

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'person' to com.nick.pojo.Person failed:

    Property: person.name
    Value: nick
    Origin: class path resource [application.yml] - 2:9
    Reason: 该名称并非邮箱地址


Action:

Update your application's configuration

修改yaml后执行成功:

image.png

5.5 配置文件位置及多环境配置

1. 配置文件位置

默认支持的配置文件位置有(默认的加载优先级如下):

  • file:./config/
  • file:./
  • classpath:/config/
  • classpath:/(默认的配置文件位置)

其中file:指的是项目路径

image.png

classpath:指的是类路径

image.png

2. 多环境配置

  • properties方式

我们一般可以在项目中配置多个环境:

image.png

==使用多环境时,需要在配置环境中添加激活配置文件:==

如上所示,三个环境分别是application.propertiesapplication-dev.propertiesapplication-test.properties三个,因此我们需要在application.properties中指定选择激活哪一个:

spring.profiles.active=dev
  • yaml方式

使用---来分隔不同环境

server:
	port: 8081
spring:
	profiles:
		active: dev
---
server:
	port: 8081
spring:
	profiles: dev
---
server:
	port: 8082
spring:
	profiles: test

5.6 自动配置原理再理解

第一次理解见==3.3 自动配置原理总结==

本小结将说明配置文件究竟能够写什么

yaml中能够配置的东西,实际上和spring.factories中的xxAutoConfiguration类与可配置项息息相关,具体的拿HttpEncodingAutoConfiguration进行举例:

// 表示这是一个配置类,在spring.factories中的所有配置类都是以该句开头,表示都会被Spring接管配置
@Configuration(proxyBeanMethods = false)
// 自动配置属性:指定一个ServerProperties,点入后可以看到@ConfigurationProperties,前缀为server
@EnableConfigurationProperties(ServerProperties.class)
// spring的底层注解,根据不同条件判断当前配置是否生效
// OnWebApplication: 如果不是一个web应用就无效
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
// OnClass: 如果存在类CharacterEncodingFilter
@ConditionalOnClass(CharacterEncodingFilter.class)
// OnProperty: 是否存在server.servlet.encoding配置
@ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {

	private final Encoding properties;

	public HttpEncodingAutoConfiguration(ServerProperties properties) {
		this.properties = properties.getServlet().getEncoding();
	}

	@Bean
	@ConditionalOnMissingBean
	public CharacterEncodingFilter characterEncodingFilter() {
		CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
		filter.setEncoding(this.properties.getCharset().name());
		filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
		filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
		return filter;
	}

	@Bean
	public LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
		return new LocaleCharsetMappingsCustomizer(this.properties);
	}

	static class LocaleCharsetMappingsCustomizer
			implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {

		private final Encoding properties;

		LocaleCharsetMappingsCustomizer(Encoding properties) {
			this.properties = properties;
		}

		@Override
		public void customize(ConfigurableServletWebServerFactory factory) {
			if (this.properties.getMapping() != null) {
				factory.setLocaleCharsetMappings(this.properties.getMapping());
			}
		}

		@Override
		public int getOrder() {
			return 0;
		}

	}

}
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
    ......
}
  • @ConfigurationProperties注解绑定了配置文件中的server.xxx,因此配置文件中能够写的,就是对应类中的东西

  • @Conditional注解说明:

image.png

总结:在我们配置文件中,能够配置的东西,都存在一个规律,首先在spring.factories中存在一个xxxAutoConfiguration类存放默认值,然后有一个类xxxProperties用于改变默认值,该类通过注解@ConfigurationProperties与配置文件绑定

六、Web开发探究

6.1 SpringBoot Web开发

要解决的问题:

  • 导入静态资源
  • 首页
  • jsp,模板引擎
  • 装配扩展mvc
  • 增删改查
  • 拦截器
  • 国际化

6.2 静态资源导入

创建一个新的spring-boot项目,添加web支持,目录结构如下所示:

image.png

可以看出,我们从名字上可以大致推断静态资源应该放在==static==目录下,但其实静态资源还可以放在其他位置,我们可以查看==WebMvcAutoConfiguration==类:

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    private final Resources resourceProperties;
    private String staticPathPattern = "/**";
    public String getStaticPathPattern() {
		return this.staticPathPattern;
	}
    // ......
    
    @Configuration(proxyBeanMethods = false)
	@Import(EnableWebMvcConfiguration.class)
	@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
	@Order(0)
	public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
        // ......
        
        @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);
				}
			});
		}
    }
}
  1. 从webjars中获取静态资源

在==WebMvcAutoConfiguration==类中,有一个静态内部类==WebMvcAutoConfigurationAdapter==,该类中有一个==addResourceHandlers==方法,可以看到会从==classpath:/META-INF/resources/webjars/==中去加载静态资源。那么什么是webjars呢?

webjars网站链接

image.png

此时,在网页中输入的/webjars/**就会被映射到classpath:/META-INF/resources/webjars/

image.png

如上图所示,localhost:8080/webjars会被映射到classpath:/META-INF/resources/webjars/,因此访问/jquery/3.4.1/jquery.js就可以访问到js资源

  1. 从resourceProperties中获取静态资源
public static class Resources {
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
                         "classpath:/resources/", "classpath:/static/", "classpath:/public/" };
}

WebMvcAutoConfiguration类中,有一个静态内部类WebMvcAutoConfigurationAdapter,该类中第二个addResourceHandlers方法:

  • 通过***this.mvcProperties.getStaticPathPattern()***获取静态资源路径,具体位置为/**
  • 通过this.resourceProperties获取静态资源路径,具体位置:classpath:/META-INF/resources/(就是==webjars==的路径)、classpath:/resources/classpath:/static/classpath:/public/
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations())};

因此这局话的意思是:在url中localhost:8080/后面的任何内容(/**)都会映射到CLASSPATH_RESOURCE_LOCATIONS位置,综上所述,能够存放静态资源的位置是下图中的三个位置(/**访问)和webjars/webjars/**访问)的位置。

image.png

访问结果如下:

image.png

==这三个路径的优先级为:resources > static > public==

6.3 定制首页

配置类:==WebMvcAutoConfiguration==

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    // ......
    
    @Configuration(proxyBeanMethods = false)
	@EnableConfigurationProperties(WebProperties.class)
	public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
        // ......
        
        @Bean
		public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
				FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
            // 第一种:从this.mvcProperties配置文件中获取getStaticPathPattern目录
			WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
					new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
					this.mvcProperties.getStaticPathPattern());
			welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
			welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
			return welcomePageHandlerMapping;
		}
        
        private Resource getWelcomePage() {
            // 第二种:从getStaticLocations目录中获取
			for (String location : this.resourceProperties.getStaticLocations()) {
				Resource indexHtml = getIndexHtml(location);
				if (indexHtml != null) {
					return indexHtml;
				}
			}
			ServletContext servletContext = getServletContext();
			if (servletContext != null) {
				return getIndexHtml(new ServletContextResource(servletContext, SERVLET_LOCATION));
			}
			return null;
		}
        
        private Resource getIndexHtml(Resource location) {
			try {
                // 从指定目录中找index.html
				Resource resource = location.createRelative("index.html");
				if (resource.exists() && (resource.getURL() != null)) {
					return resource;
				}
			}
			catch (Exception ex) {
			}
			return null;
		}
    }
}

方法==welcomePageHandlerMapping==可以获取找首页的路径,首先可以根据==this.mvcProperties.getStaticPathPattern()==从配置文件中获取路径,其次可以从==this.resourceProperties.getStaticLocations()==获取路径,最后在上述路径中找index.html作为首页

具体的我们可以在classpath:/publicclasspath:/resourcesclasspath:/static下放index.html

但是有一种更好的方式是放在 templates下面,需要注意的是==templates下面的网页只能通过controller来访问==(需要模板引擎的支持)

image.png

6.4 模板引擎

前端交给我们的页面是html,如果我们是以前的开发,需要把它们转成jsp,jsp好处就是当我们查出一些数据转发到jsp页面以后,可以利用jsp轻松实现数据的显示以及交互。但是spring-boot项目是一个jar包,不是war包,默认不支持jsp。

SpringBoot推荐使用Thymeleaf模板引擎来替代jsp。

image.png

模板引擎的作用就是写一个页面的模板,它会按照我们提供的数据帮你把表达式进行解析、填充到指定位置,然后把这个数据生成一个我们想要的内容。

SpringBoot要使用Thymeleaf需要引入对应的starter:

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

我们可以根据ThymeleafProperties类看到要想使用模板引擎,就必须要在classpath:/templates/下创建.html文件。

image.png

我们可以使用上一小节创建的index.html,编写对应的controller:

@Controller
public class IndexController {
    @RequestMapping("/index")
    public String index() {
        return "index";
    }
}

访问测试可以正常跳转:

image.png

通过模板引擎,可以从controller跳转到html页面

6.5 Thymeleaf使用

  1. 首先要导入thymeleaf的约束
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head></head>
<body></body>
</html>
  1. 基础语法
  • Simple expressions:

    • Variable Expressions: ${...}
    • Selection Variable Expressions: *{...}
    • Message Expressions: #{...}
    • Link URL Expressions: @{...}
    • Fragment Expressions: ~{...}
  • Literals

    • Text literals: 'one text' , 'Another one!' ,…
    • Number literals: 0 , 34 , 3.0 , 12.3 ,…
    • Boolean literals: true , false
    • Null literal: null
    • Literal tokens: one , sometext, main ,…
  • Text operations:

    • String concatenation: +
    • Literal substitutions: |The name is ${name}|
  • Boolean operations:

    • Binary operators: and , or
    • Boolean negation (unary operator): ! , not
  • Conditional operators:

    • If-then: (if) ? (then)
    • If-then-else: (if) ? (then) : (else)
    • Default: (value) ?: (defaultvalue)
  1. 测试
  • controller
@Controller
public class IndexController {
    @RequestMapping("/index")
    public String index(Model model) {
        model.addAttribute("msg", "Hello Thymeleaf!!");
        return "index";
    }
}
  • html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>首页</h1>
    <h2 th:text="${msg}"></h2>
</body>
</html>
  • 结果

image.png

  1. 特性语法
  • 转义
@Controller
public class IndexController {
    @RequestMapping("/index")
    public String index(Model model) {
        model.addAttribute("msg", "<h3 style=\"color:red;\">Hello Thymeleaf!!</h3>");
        return "index";
    }
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>首页</h1>
    <h2 th:text="${msg}"></h2>
    <h2 th:utext="${msg}"></h2>
</body>
</html>

image.png

  • 遍历
@Controller
public class IndexController {
    @RequestMapping("/index")
    public String index(Model model) {
        model.addAttribute("msg", "<h3 style=\"color:red;\">Hello Thymeleaf!!</h3>");
        model.addAttribute("msg2", Arrays.asList("nick", "is", "cool"));
        return "index";
    }
}
<body>
    <h1>首页</h1>
    <h2 th:text="${msg}"></h2>
    <h2 th:utext="${msg}"></h2>
    <div th:each="item : ${msg2}"> [[ ${item} ]] </div>
</body>

image.png

6.6 MVC装配原理

Spring MVC Auto-configuration

The auto-configuration adds the following features on top of Spring’s defaults:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
  • Support for serving static resources, including support for WebJars (covered later in this document).
  • Automatic registration of Converter, GenericConverter, and Formatter beans.
  • Support for HttpMessageConverters (covered later in this document).
  • Automatic registration of MessageCodesResolver (covered later in this document).
  • Static index.html support.
  • Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.

官方说明要扩展MVC需要添加注解@Configuration,但是不能添加注解@EnableWebMvc,查看该注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

注解@EnableWebMvc会导入一个类DelegatingWebMvcConfiguration,该类会从容器中获取所有的WebMvcConfigurer,该类继承自WebMvcConfigurationSupport

@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport{
    @Autowired(required = false)
	public void setConfigurers(List<WebMvcConfigurer> configurers) {
		if (!CollectionUtils.isEmpty(configurers)) {
			this.configurers.addWebMvcConfigurers(configurers);
		}
	}
}

而在自动配置类WebMvcAutoConfiguration中,通过注解@ConditionalOnMissingBean明确说明不能有WebMvcConfigurationSupport这个类。

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}

因此在我们定义的自动配置类中添加@EnableWebMvc后,整个WebMvcAutoConfiguration就不会生效了。

1. 自定义视图解析器

查看类ContentNegotiatingViewResolver,该类实现了接口ViewResolver,就是视图解析器,这是SpringBoot自动帮我们装配的:

public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
		implements ViewResolver, Ordered, InitializingBean {
    // ......
    @Override
	@Nullable
	public View resolveViewName(String viewName, Locale locale) throws Exception {
		RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
		Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
		List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
		if (requestedMediaTypes != null) {
			List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
			View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
			if (bestView != null) {
				return bestView;
			}
		}

		String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
				" given " + requestedMediaTypes.toString() : "";

		if (this.useNotAcceptableStatusCode) {
			if (logger.isDebugEnabled()) {
				logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
			}
			return NOT_ACCEPTABLE_VIEW;
		}
		else {
			logger.debug("View remains unresolved" + mediaTypeInfo);
			return null;
		}
	}
    
    private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
			throws Exception {

		List<View> candidateViews = new ArrayList<>();
		if (this.viewResolvers != null) {
			Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
			for (ViewResolver viewResolver : this.viewResolvers) {
				View view = viewResolver.resolveViewName(viewName, locale);
				if (view != null) {
					candidateViews.add(view);
				}
				for (MediaType requestedMediaType : requestedMediaTypes) {
					List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
					for (String extension : extensions) {
						String viewNameWithExtension = viewName + '.' + extension;
						view = viewResolver.resolveViewName(viewNameWithExtension, locale);
						if (view != null) {
							candidateViews.add(view);
						}
					}
				}
			}
		}
		if (!CollectionUtils.isEmpty(this.defaultViews)) {
			candidateViews.addAll(this.defaultViews);
		}
		return candidateViews;
	}
}

接口ViewResolver要实现方法resolveViewName,该方法中getCandidateViews获取候选的视图,getBestView获取最好的视图。其中获取候选视图需要遍历所有的视图this.viewResolvers,实际上是从==bean==中获取所有的视图。

根据官方提示,需要自己编写配置类,实现WebMvcConfigurer接口。为了在获取候选视图的时候能够获取到,我们需要将自定义的视图解析器放入==bean==中。

@Configuration
public class MyMVCConfig implements WebMvcConfigurer {

    @Bean
    public ViewResolver myViewResolver() {
        return new MyViewResolver();
    }

    /**
     * 自定义视图解析器
     */
    public static class MyViewResolver implements ViewResolver {
        @Override
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            return null;
        }
    }
}

如何确定自己编写的视图解析器成功了呢?由于所有的网络请求都会走DispatcherServletdoDispatch方法,因此在上面打断点,然后访问网络请求。

image.png

结论:要想自定义视图解析器,只需要自己实现WebMvcConfigurer,并且将其放到bean中,spring会自动帮我们装配

2. 增加视图跳转

  • MyMVCConfig类中重写方法addViewControllers
  • registry.addViewController参数是用户访问路径,setViewName是跳转视图名称
@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/nick").setViewName("index");
}

==在SpringBoot中,有非常多的xxxConfiguration帮助我们进行扩展配置,只要看到这个东西,就是对默认配置的扩展==

七、整合JDBC使用

7.1 简介

对于数据访问层,无论是SQL(关系型数据库)还是NOSQL(非关系型数据库),SpringBoot底层都是采用Spring Data的方式进行统一处理。

Spring Data也是Spring中与SpringBoot和SpringCloud齐名的指明项目。

Spring Data官网:SpringData

7.2 项目实践

创建项目时,需要勾选如下扩展:

  • JDBC API
  • MySQL Driver

image.png

如果勾选上图两个,SpringBoot会自动在POM文件中添加如下依赖:

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

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

1. 编写application.yaml

spring:
  datasource:
    username: root
    password: wang2995
    url: jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

2. 编写测试文件

  • 测试数据源
@SpringBootTest
class Springboot04DataApplicationTests {
    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() {
        // 查看默认数据源
        System.out.println(dataSource.getClass());
    }

}
class com.zaxxer.hikari.HikariDataSource

只要你加载了spring-boot-starter-jdbcmysql-connector-java,SpringBoot就会默认给你创建一些类,其中DataSource就是默认创建好的,可以直接拿来使用。从输出可以看出,默认的数据源使用的是Hikari,这个数据源比c3p0要快。

  • 测试连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
connection.close();
2022-02-07 16:58:52.835  INFO 81904 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-02-07 16:58:53.341  INFO 81904 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
HikariProxyConnection@956061012 wrapping com.mysql.cj.jdbc.ConnectionImpl@5e8cda75

2022-02-07 16:58:53.358  INFO 81904 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-02-07 16:58:53.359  INFO 81904 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

加入获取数据库连接时,数据库时区报错,则在url中添加时区设置:

jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC

  • 从结果看出来,数据库连接使用JDBC进行连接

7.3 查看数据库连接源码

查看类DataSourceProperties

可以看到我们能够配置的所有变量:

image.png

查看对应配置类:DataSourceAutoConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@AutoConfigureBefore(SqlInitializationAutoConfiguration.class)
// 可以通过`DataSourceProperties`类在yaml文件中进行配置
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
		DataSourceInitializationConfiguration.InitializationSpecificCredentialsDataSourceInitializationConfiguration.class,
		DataSourceInitializationConfiguration.SharedCredentialsDataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {
	// 配置数据库
	@Configuration(proxyBeanMethods = false)
	@Conditional(EmbeddedDatabaseCondition.class)
	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
	@Import(EmbeddedDataSourceConfiguration.class)
	protected static class EmbeddedDatabaseConfiguration {

	}

    // 配置连接池
	@Configuration(proxyBeanMethods = false)
	@Conditional(PooledDataSourceCondition.class)
	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
	@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
			DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
			DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
	protected static class PooledDataSourceConfiguration {

	}

	/**
	 * 连接池相关配置
	 */
	static class PooledDataSourceCondition extends AnyNestedCondition {

		PooledDataSourceCondition() {
			super(ConfigurationPhase.PARSE_CONFIGURATION);
		}

		@ConditionalOnProperty(prefix = "spring.datasource", name = "type")
		static class ExplicitType {

		}

		@Conditional(PooledDataSourceAvailableCondition.class)
		static class PooledDataSourceAvailable {

		}

	}

	// ......
}

7.4 查看数据库操作源码

SpringBoot自动转配了很多xxxTemplate类可以直接使用,对于数据库操作的类为JdbcTemplateConfiguration,具体路径如下:

路径:External Libraries -> spring-boot-autoconfiguration -> org.spring.framework.boot.autoconfigure -> jdbc -> JdbcTemplateConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JdbcOperations.class)
class JdbcTemplateConfiguration {

	@Bean
	@Primary
	JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		JdbcProperties.Template template = properties.getTemplate();
		jdbcTemplate.setFetchSize(template.getFetchSize());
		jdbcTemplate.setMaxRows(template.getMaxRows());
		if (template.getQueryTimeout() != null) {
			jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds());
		}
		return jdbcTemplate;
	}

}

该类自动装配了jdbcTemplate,使用时需要传入DataSourceJdbcProperties.

接下来我们通过示例来使用jdbcTemplate:

  • 首先需要导入web依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 编写Controller(如果没有导入web依赖,@RestController就无法使用)
@RestController
public class JdbcController {
    @Autowired
    JdbcTemplate jdbcTemplate;

    /**
     * 查询数据库表并显示
     */
    @GetMapping("/userList")
    public List<Map<String, Object>> userList() {
        String sql = "select * from student";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        return maps;
    }

}
  • 测试结果

image.png

八、整合Druid数据源

8.1 介绍

Druid是阿里巴巴开源平台上一个数据源连接池实现,结合C3P0DBCPPROXOOL等DB池的优点,同时加入了日志监控。

Druid可以很好监控DB连接池和SQL执行情况,天生就是针对监控而生的DB连接池。

SpringBoot 2.0以上默认使用Hikari数据源,可以说HikariDruid都是当前Java web上最优秀的数据源。

8.2 引入

  • 导入依赖
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
</dependency>
  • 查看源码结构

image.png

8.3 使用

  • 配置文件中指定数据源类型

通过type来执行数据源为DruidDataSource

spring:
  datasource:
    username: root
    password: wang2995
    url: jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
  • 测试(相同的测试文件)
@SpringBootTest
class Springboot04DataApplicationTests {
    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        // 查看默认数据源 class com.zaxxer.hikari.HikariDataSource
        System.out.println(dataSource.getClass());

        // 获取数据库连接
        Connection connection = dataSource.getConnection();
        System.out.println(connection);
        connection.close();
    }

}
class com.alibaba.druid.pool.DruidDataSource
2022-02-07 17:46:06.629  INFO 80864 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
com.mysql.cj.jdbc.ConnectionImpl@7c847072
2022-02-07 17:46:07.109  INFO 80864 --- [ionShutdownHook] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} closing ...
2022-02-07 17:46:07.111  INFO 80864 --- [ionShutdownHook] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} closed

可以看到使用了DruidDataSource的数据源,数据库连接还是JDBC

8.4 Druid相关配置

1. yaml配置

DruidDataSource类继承自DruidAbstractDataSource类,父类中可以看到各种Druid的配置项:

public abstract class DruidAbstractDataSource extends WrapperAdapter implements DruidAbstractDataSourceMBean, DataSource, DataSourceProxy, Serializable {
    private static final long                          serialVersionUID                          = 1L;
    private final static Log                           LOG                                       = LogFactory.getLog(DruidAbstractDataSource.class);

    public final static int                            DEFAULT_INITIAL_SIZE                      = 0;
    public final static int                            DEFAULT_MAX_ACTIVE_SIZE                   = 8;
    public final static int                            DEFAULT_MAX_IDLE                          = 8;
    public final static int                            DEFAULT_MIN_IDLE                          = 0;
    public final static int                            DEFAULT_MAX_WAIT                          = -1;
    public final static String                         DEFAULT_VALIDATION_QUERY                  = null;                                                //
    public final static boolean                        DEFAULT_TEST_ON_BORROW                    = false;
    public final static boolean                        DEFAULT_TEST_ON_RETURN                    = false;
    public final static boolean                        DEFAULT_WHILE_IDLE                        = true;
    public static final long                           DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS = 60 * 1000L;
    public static final long                           DEFAULT_TIME_BETWEEN_CONNECT_ERROR_MILLIS = 500;
    public static final int                            DEFAULT_NUM_TESTS_PER_EVICTION_RUN        = 3;

    public static final long                           DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS    = 1000L * 60L * 30L;
    public static final long                           DEFAULT_MAX_EVICTABLE_IDLE_TIME_MILLIS    = 1000L * 60L * 60L * 7;
    public static final long                           DEFAULT_PHY_TIMEOUT_MILLIS                = -1;

    protected volatile boolean                         defaultAutoCommit                         = true;
    protected volatile Boolean                         defaultReadOnly;
    protected volatile Integer                         defaultTransactionIsolation;
    protected volatile String                          defaultCatalog                            = null;

    protected String                                   name;

    protected volatile String                          username;
    protected volatile String                          password;
    protected volatile String                          jdbcUrl;
    protected volatile String                          driverClass;
    protected volatile ClassLoader                     driverClassLoader;
    protected volatile Properties                      connectProperties                         = new Properties();

    protected volatile PasswordCallback                passwordCallback;
    protected volatile NameCallback                    userCallback;

    protected volatile int                             initialSize                               = DEFAULT_INITIAL_SIZE;
    protected volatile int                             maxActive                                 = DEFAULT_MAX_ACTIVE_SIZE;
    protected volatile int                             minIdle                                   = DEFAULT_MIN_IDLE;
    protected volatile int                             maxIdle                                   = DEFAULT_MAX_IDLE;
    protected volatile long                            maxWait                                   = DEFAULT_MAX_WAIT;
    protected int                                      notFullTimeoutRetryCount                  = 0;

    // ......
}
  • 在项目中配置Druid参数
spring:
  datasource:
    username: root
    password: wang2995
    url: jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    # 初始化大小,最小,最大
    initial-size: 5
    min-idle: 5
    max-active: 20
    # 配置获取连接等待超时的时间
    max-wait: 60000
    # 配置一个连接在池中最小生存的时间,单位是毫秒
    min-evictable-idle-time-millis: 300000
    validation-query: SELECT 1 FROM DUAL
    test-while-idle: true
    test-on-borrow: false
    test-on-return: false
    pool-prepared-statements: true
    # 配置监控统计:stat
    filters: stat,wall,log4j
    max-pool-prepared-statement-per-connection-size: 20
    use-global-data-source-stat: true
    connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

因为里面日志记录使用了log4j,因此要导入对应的依赖:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

2. 后台监控自定义配置

@Configuration
public class DruidConfig {
    /**
     * 将创建的DataSource与yaml中的DataSource绑定
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }

    /**
     * 后台监控功能:web.xml
     */
    @Bean
    public ServletRegistrationBean statViewServlet() {
        ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");

        /**
         * 增加配置
         * 账号密码配置 loginUsername loginPassword
         * 允许谁能访问 allow ("":任何人都可以,"localhost":本机访问)
         */
        HashMap<String, String> initParameters = new HashMap<>();
        // 账号密码配置
        initParameters.put("loginUsername", "admin");
        initParameters.put("loginPassword", "123456");
        // 允许谁能访问
        initParameters.put("allow", "localhost");
        bean.setInitParameters(initParameters);
        return bean;
    }
}
  • 访问/duid/*

image.png

  • 登录查看界面

image.png

其中在SQL监控中可以看到SQL的执行情况

3. 过滤器自定义配置

/**
 * 过滤器自定义配置
 * profileEnable
 * sessionStatEnable
 * sessionStatMaxCount
 * exclusions
 * principalSessionName
 * principalCookieName
 * realIpHeader
 */
@Bean
public FilterRegistrationBean webStatFilter() {
    FilterRegistrationBean bean = new FilterRegistrationBean();
    bean.setFilter(new WebStatFilter());

    Map<String, String> initParameters = new HashMap<>();
    // 这些东西不进行统计
    initParameters.put("exclusions", "*.js,*.css,/druid/*");

    bean.setInitParameters(initParameters);
    return bean;
}

九、整合Mybatis框架

9.1 创建项目

  • 添加对应扩展

image.png

  • 导入依赖
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
  • 配置数据源
spring:
  datasource:
    username: root
    password: wang2995
    url: jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
  • 测试
@SpringBootTest
class Springboot05MybatisApplicationTests {
    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() {
        System.out.println(dataSource.getClass());
    }

}
class com.zaxxer.hikari.HikariDataSource

项目搭建成功:heavy_check_mark:

9.2 创建实体层和业务层

1. 实体类

@Data
@Validated
public class Users {
    private int id;
    private String name;
    private String password;
    @Email(message="邮件格式错误")
    private String email;
    private Date birthday;
}

2. Mapper

  • 接口
@Mapper
// 表示这是一个Mybatis的mapper类, 或者在启动类上使用@MapperScan
@Repository
public interface UsersMapper {
    List<Users> queryUserList();
}
  • xml(位置在***resoiurces->mybatis->mapper***)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nick.mapper.UsersMapper">
    <select id="queryUserList" resultType="Users">
        select * from users
    </select>
</mapper>

这里resultType直接使用了Users需要进行别名配置

  • yaml
mybatis:
  type-aliases-package: com.nick.pojo
  mapper-locations: classpath:mybatis/mapper/*.xml
  • Users实体类
@Alias("Users")

3. Controller

应该是写service调用mapper,然后controller调用service,这里省略了

@RestController
public class UsersController {
    @Autowired
    private UsersMapper usersMapper;

    @GetMapping("/queryUserList")
    public List<Users> queryUserList() {
        return usersMapper.queryUserList();
    }
}
  • 测试

image.png

十、Spring安全框架 :new:

在Web开发中,安全是第一位,是一个非功能性需求。做网站时安全应该是放在设计之初考虑

市面上知名的安全框架有:==shiro==、==spring security==

二者很像,主要负责认证、授权

10.1 Spring Security

Spring Security is a powerful and highly customizable authentication and access-control framework

  • 权限
    • 功能权限
    • 访问权限
    • 菜单权限

1. 创建项目

  • 引入包
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.6.3</version>
</dependency>
  • 导入静态资源

下载地址

image.png

  • yaml
# 关闭模板引擎缓存,方便调试
spring:
  thymeleaf:
    cache: false
  • 添加页面跳转的controller
@Controller
public class RouterController {
    @RequestMapping({"/", "/index"})
    public String index() {
        return "index";
    }

    @RequestMapping("/toLogin")
    public String toLogin() {
        return "views/login";
    }

    @RequestMapping("/level1/{id}")
    public String level1(@PathVariable("id") int id) {
        return "views/level1/" + id;
    }

    @RequestMapping("/level2/{id}")
    public String level2(@PathVariable("id") int id) {
        return "views/level2/" + id;
    }

    @RequestMapping("/level3/{id}")
    public String level3(@PathVariable("id") int id) {
        return "views/level3/" + id;
    }
}

通过@PathVariable来减少controller的个数

  • 测试

image.png

==目前可以对页面进行查看,还没有添加任何安全相关的代码,我们要通过AOP横切来实现安全控制==

2. 用户认证和授权

Spring Security是针对Spring项目的安全框架,也是Spring Boot底层安全的技术选型,可以实现强大的Web安全控制,仅仅需要引入spring-boot-starter-security模块,进行少量配置即可。

几个常用类:

  • WebSecurityConfigurerAdapter:自定义Security策略
  • AuthenticationManagerBuilder:自定义认证策略(==适配器模式和建造者模式==)
  • @EnableWebSecurity:开启Web Security模式

Spring Security的两个主要目标是==认证(Authentication)==和==授权(访问控制,Authorization)==

要使用认证和授权,需要在项目中添加config类,然后继承WebSecurityConfigurerAdapter,并使用注解@EnableWebSecurity

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

(1)授权

重写configure(HttpSecurity http)

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 首页所有人可以访问,但是功能也只有对应权限的用户可以访问
    // 请求授权的规则
    http.authorizeRequests().antMatchers("/").permitAll()
        .antMatchers("/level1/**").hasRole("vip1")
        .antMatchers("/level2/**").hasRole("vip2")
        .antMatchers("/level3/**").hasRole("vip3");

    // 没有权限默认会到登录页,需要开启登录的页面
    // 会进入Login页面
    http.formLogin();
    
    // 防止网站攻击,关闭csrf功能
    http.csrf().disable();
    
    // 开启注销,跳到首页
    http.logout().logoutSuccessUrl("/");
}
  • 登录:localhost:8080/login
  • 注销:localhost:8080/logout

(2)认证

重写configure(AuthenticationManagerBuilder auth)

/**
 * 认证
 * 密码需要编码:PasswordEncoder
 * 在Spring Security中新增很多加密方法:BCryptPasswordEncoder
 * @param auth
 * @throws Exception
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 这些数据正常应该从数据库中读取,这里直接从内存中读
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
        .withUser("nick").password(new BCryptPasswordEncoder().encode("wang2995")).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");
}

(3)验证

  • 使用nick登录,只有vip2vip3的权限

image.png

  • 点击vip1

image.png

  • 点击vip2

image.png

  • 登出

image.png

(4)针对不用认证权限显示不同页面

  • 需要导入thymeleaf和security的整合包
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>
  • 修改thymeleaf的html
<!--如果未登录,显示登录按钮-->
<div sec:authorize="!isAuthenticated()">
    <a class="item" th:href="@{/toLogin}">
        <i class="address card icon"></i> 登录
    </a>
</div>

<!--如果已登录,显示用户名和注销按钮-->
<div sec:authorize="isAuthenticated()">
    <a class="item">
        用户名:<span sec:authentication="name"></span>
        角色:<span sec:authentication="authorities"></span>
    </a>
    <a class="item" th:href="@{/logout}">
        <i class="sign-out icon"></i> 注销
    </a>
</div>

<!--根据不同用户实现动态菜单效果-->
<div class="column" sec:authorize="hasRole('vip1')">
    <div class="ui raised segment">
        <div class="ui">
            <div class="content">
                <h5 class="content">Level 1</h5>
                <hr>
                <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
            </div>
        </div>
    </div>
</div>

==要是用sec:,需要添加约束:***xmlns:sec="www.thymeleaf.org/thymeleaf-e…

  • sec:authorize="isAuthenticated()":判断当前是否用户登录
  • sec:authentication="name":获取登录用户的用户名
  • sec:authentication="authorities":获取登录用户的权限
  • sec:authorize="hasRole('xxx')":判断当前用户是否拥有某个具体权限
  • 未登录

image.png

  • 登录后

image.png

3. 记住我以及首页定制

  • 在安全的配置类中,在重写的方法configure中添加记住我功能
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 首页所有人可以访问,但是功能也只有对应权限的用户可以访问
    // 请求授权的规则
    http.authorizeRequests().antMatchers("/").permitAll()
        .antMatchers("/level1/**").hasRole("vip1")
        .antMatchers("/level2/**").hasRole("vip2")
        .antMatchers("/level3/**").hasRole("vip3");

    // 没有权限默认会到登录页,需要开启登录的页面
    // 会进入Login页面,登录
    http.formLogin().loginPage("/toLogin").usernameParameter("username").passwordParameter("password");

    // 防止网站攻击,关闭csrf功能
    http.csrf().disable();

    // 开启注销,跳到首页
    http.logout().logoutSuccessUrl("/");

    // 开启记住我功能
    http.rememberMe().rememberMeParameter("remember");
}
  • 效果

image.png

勾选后,关闭浏览器然后重新进入主页,仍然可以保持登录装状态,其底层原理是利用cookie实现,默认保存两周

image.png

  • 定制登录页

在安全配置类中configure(HttpSecurity http)方法内,做如下修改

这里通过.usernameParameter.passwordParameter来指定html中<input name="xxx">所指定的名称

// 没有权限默认会到登录页,需要开启登录的页面
// 会进入Login页面,登录
http.formLogin().loginPage("/toLogin").usernameParameter("username").passwordParameter("password");

对应的需要修改login.html文件:

<form th:action="@{/toLogin}" method="post">
  • 修正rememberMe功能

之前在configure(HttpSecurity http)中通过http.rememberMe();设置了该功能,但是所关联的页面是默认实现的/login页面,因此切换成自己实现的/toLogin后,该功能失效了,需要修改模板进行重新添加

<div class="ui form">
    <form th:action="@{/toLogin}" method="post">
        <div class="field">
            <label>Username</label>
            <div class="ui left icon input">
                <input type="text" placeholder="Username" name="username">
                <i class="user icon"></i>
            </div>
        </div>
        <div class="field">
            <label>Password</label>
            <div class="ui left icon input">
                <input type="password" name="password">
                <i class="lock icon"></i>
            </div>
        </div>
        <div class="field">
            <input type="checkbox" name="remember">记住我
        </div>
        <input type="submit" class="ui blue submit button"/>
    </form>
</div>

在安全配置类中configure(HttpSecurity http)方法内,做如下修改:

http.rememberMe().rememberMeParameter("remember");

10.2 Shiro

1. 什么是Shiro

  • Apache Shiro是一个Java的安全(权限)框架
  • Shiro可以非常容易开发出足够好的应用,其不仅可用于JavaSE,也可用于JavaEE
  • Shiro可以完成:认证、授权、加密、会话管理、Web集成、缓存等
  • 下载地址

2. 有哪些功能

image.png

  • Authentication:身份认证、登录,验证用户是不是拥有相应的身份
  • Authorization:授权,即权限认证,验证某个已认证的用户是否拥有某个权限,及判断用户能够进行什么操作
  • Session Management:会话管理,及用户登录后就是第一次会话,在没有退出之前,所有信息都在会话中
  • Cryptography:加密,保护数据安全性
  • Web Support:Web支持,可以非常容易集成到Web环境
  • Caching:缓存,比如用户登录后,其用户信息、拥有角色、权限不必每次都去查
  • Concurrency:Shiro支持多线程应用的并发验证
  • Testing:提供测试支持
  • Run As:允许一个用户假装另一个用户的身份进行访问
  • Remember Me:记住我

3. Shiro架构

image.png

  • Subject:应用代码直接交互的对象,对外API核心,代表当前用户(不一定是具体的人),与当前用户交互的任何东西,它是一个交互的门面,具体执行是依靠SecurityManager
  • SecurityManager:安全管理器,所有与安全相关的操作都与其交互,它管理着所有的Subject,是Shiro的核心,负责与其他组件交互,相当于SpringMVC中DispatcherServlet
  • Realm:Shiro从Realm中获取安全数据(用户、角色、权限),SecurityManager要验证用户身份,它需要从Realm获取响应的用户进行比较,来确认用户的身份是否合法,得到用响应的角色、权限,进行验证用户的操作是否能够进行。可以看成是DataSource

image.png

  • Authenticator:负责Subject认证,可以使用认证策略

  • Authorizer:授权器,及访问控制器,用来决定主体是否有权限进行相应操作

  • Session Manager:管理Session生命周期组件

  • Cache Manager:缓存控制器,管理用户、角色、权限等缓存

  • Cryptography:密码模块

4. 示例

  • 导入依赖
<dependencies>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.8.0</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>1.7.35</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.17.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.17.1</version>
    </dependency>
</dependencies>
  • 配置文件
<Configuration name="ConfigTest" status="ERROR" monitorInterval="5">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Logger name="org.springframework" level="warn" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <Logger name="org.apache" level="warn" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <Logger name="net.sf.ehcache" level="warn" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <Logger name="org.apache.shiro.util.ThreadContext" level="warn" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <Root level="info">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5
  • 快速开始
/**
 * Simple Quickstart application showing how to use Shiro's API.
 *
 * @author Shiro
 * @since 0.9 RC2
 */
public class Quickstart {
    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {

        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);

        // 获取当前的用户对象Subject
        Subject currentUser = SecurityUtils.getSubject();

        // 通过当前对象拿到Shiro的Session,如何拿到session并存值和取值
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Subject中session取到了值: [" + value + "]");
        }

        // currentUser.isAuthenticated()判断当前用户是否被认证
        if (!currentUser.isAuthenticated()) {
            // 创建用户令牌
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            // 设置记住我
            token.setRememberMe(true);
            try {
                // 执行登录操作
                currentUser.login(token);
            } catch (UnknownAccountException uae) {
                log.info("用户名不存在:" + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                log.info("密码错误:" + token.getPrincipal());
            } catch (LockedAccountException lae) {
                log.info("账号被锁定:" + token.getPrincipal());
            } catch (AuthenticationException ae) {
            }
        }

        log.info("用户 [" + currentUser.getPrincipal() + "] 登录成功");

        // 测试当前用户是否有某个角色
        if (currentUser.hasRole("schwartz")) {
            log.info("当前用户拥有角色:schwartz");
        } else {
            log.info("当前用户没有角色:schwartz");
        }

        // 验证权限
        if (currentUser.isPermitted("lightsaber:wield")) {
            log.info("当前用户拥有lightsaber权限");
        } else {
            log.info("当前用户没有lightsaber权限");
        }

        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
            log.info("当前用户拥有winnebago权限");
        } else {
            log.info("当前用户没有winnebago权限");
        }

        // 登出
        currentUser.logout();
        System.exit(0);
    }
}
  • 结果
15:10:09.727 [main] INFO  Quickstart - Subject中session取到了值: [aValue]
15:10:09.730 [main] INFO  Quickstart - 用户 [lonestarr] 登录成功
15:10:09.730 [main] INFO  Quickstart - 当前用户拥有角色:schwartz
15:10:09.730 [main] INFO  Quickstart - 当前用户拥有lightsaber权限
15:10:09.730 [main] INFO  Quickstart - 当前用户拥有winnebago权限

==总结==

  • 获得SubjectSubject currentUser = SecurityUtils.getSubject();
  • 获得sessionSession session = currentUser.getSession();
  • 判断当前用户是否被认证:currentUser.isAuthenticated()
  • 获得当前用户认证:currentUser.getPrincipal()
  • 判断当前用户是否拥有某角色:currentUser.hasRole("xxx")
  • 判断当前用户是否拥有某权限:currentUser.isPermitted("xxx")
  • 登录:currentUser.login(token);
  • 注销:currentUser.logout();

5. SpringBoot继承Shiro

  • Shiro整合SpringBoot包
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.8.0</version>
</dependency>
  • 创建自定义Realm(config->UserRealm
public class UserRealm extends AuthorizingRealm {
    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了授权=>doGetAuthorizationInfo");
        return null;
    }

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了认证=>doGetAuthenticationInfo");
        return null;
    }
}
  • 创建ShiroConfig
  1. 创建userRealm
  2. 通过userRealm创建DefaultWebSecurityManager
  3. 通过DefaultWebSecurityManager创建ShiroFilterFactoryBean
  4. 然后根据需求设置bean

==注意:在创建ShiroFilterFactoryBean时,方法名称必须是shiroFilterFactoryBean==

@Configuration
public class ShiroConfig {
    /**
     * 1. 创建realm对象,需要自定义
     */
    @Bean(name = "userRealm")
    public UserRealm userRealm() {
        return new UserRealm();
    }

    /**
     * 2. DefaultWebSecurityManager
     */
    @Bean(name="defaultWebSecurityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        // 关联reaml
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    /**
     * 3. ShiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);

        /**
         * 添加Shiro内置过滤器
         * anon: 无需认证就可访问
         * authc: 必须认证才能访问
         * user: 必须记住我才能使用
         * perms: 拥有对某个资源的权限才能访问
         * role: 拥有某个角色才能访问
         */
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/user/add", "anon");
        filterChainDefinitionMap.put("/user/update", "authc");

        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return bean;
    }
}
  • 测试

image.png

6. 拦截

  • 登录界面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>登录</h1>
<form action="">
    <p>用户名:<input type="text" name="username"></p>
    <p>密 码:<input type="text" name="password"></p>
    <p><input type="submit"></p>
</form>
</body>
</html>
  • controller
@RequestMapping("/toLogin")
public String toLogin() {
    return "login";
}
  • Shiro配置
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    // 设置安全管理器
    bean.setSecurityManager(defaultWebSecurityManager);

    /**
         * 添加Shiro内置过滤器
         * anon: 无需认证就可访问
         * authc: 必须认证才能访问
         * user: 必须记住我才能使用
         * perms: 拥有对某个资源的权限才能访问
         * role: 拥有某个角色才能访问
         */
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    filterChainDefinitionMap.put("/user/add", "anon");
    filterChainDefinitionMap.put("/user/update", "authc");
    bean.setFilterChainDefinitionMap(filterChainDefinitionMap);

    // 设置登录的请求
    bean.setLoginUrl("/toLogin");
    return bean;
}

7. 用户认证

用户认证工作应该放在Realm中,即自定义Realm类UserRealm

  • 编写登录的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", "用户名不存在:" + token.getPrincipal());
        return "login";
    } catch (IncorrectCredentialsException ice) {
        model.addAttribute("msg", "密码错误:" + token.getPrincipal());
        return "login";
    }
}
  • UserRealm中添加用户认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("执行了认证=>doGetAuthenticationInfo");

    // 用户名密码 数据库中取(暂时写死)
    String name = "root";
    String password = "123456";
    UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
    if (!userToken.getUsername().equals(name)) {
        // 抛出异常 UnknownAccountException
        return null;
    }

    // 密码认证Shiro接管
    return new SimpleAuthenticationInfo("", password, "");
}
  • html输出错误信息
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>登录</h1>
<p th:text="${msg}" style="color: red"></p>
<form th:action="@{/login}">
    <p>用户名:<input type="text" name="username"></p>
    <p>密 码:<input type="text" name="password"></p>
    <p><input type="submit"></p>
</form>
</body>
</html>

8. Shiro整合Mybatis

  • 导入相关依赖
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
    <scope>provided</scope>
</dependency>
  • pojo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users {
    private int id;
    private String name;
    private String password;
    private String email;
    private Date birthday;
}
  • mapper接口
@Mapper
@Repository
public interface UsersMapper {
    public Users queryUserByName(@Param("name") String name);
}
  • 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.nick.mapper.UsersMapper">
    <select id="queryUserByName" parameterType="String" resultType="Users">
        select *
        from users
        where name = #{name}
    </select>
</mapper>
  • service
public interface UserService {
    public Users queryUserByName(@Param("name") String name);
}
  • serviceImpl
@Service
public class UserServiceImpl implements UserService{
    @Autowired
    UsersMapper usersMapper;

    @Override
    public Users queryUserByName(String name) {
        return usersMapper.queryUserByName(name);
    }
}
  • 修改自定义Realm
public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了授权=>doGetAuthorizationInfo");

        return null;
    }

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了认证=>doGetAuthenticationInfo");

        UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

        // 用户名密码 数据库中取
        Users user = userService.queryUserByName(userToken.getUsername());


        if (user == null) {
            // 抛出异常 UnknownAccountException
            return null;
        }

        // 密码认证Shiro接管
        return new SimpleAuthenticationInfo("", user.getPassword(), "");
    }
}

9. Shiro请求授权

  • shiroFilterFactoryBean中添加权限限制
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    // 设置安全管理器
    bean.setSecurityManager(defaultWebSecurityManager);

    /**
         * 添加Shiro内置过滤器
         * anon: 无需认证就可访问
         * authc: 必须认证才能访问
         * user: 必须记住我才能使用
         * perms: 拥有对某个资源的权限才能访问
         * role: 拥有某个角色才能访问
         */
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    // 拦截
    filterChainDefinitionMap.put("/user/add", "anon");
    filterChainDefinitionMap.put("/user/update", "authc");
    // 授权,要有user:add权限才能访问/add
    filterChainDefinitionMap.put("/user/add", "perms[user:add]");
    filterChainDefinitionMap.put("/user/update", "perms[user:update]");

    bean.setFilterChainDefinitionMap(filterChainDefinitionMap);

    // 设置登录的请求
    bean.setLoginUrl("/toLogin");
    // 设置未授权的请求
    bean.setUnauthorizedUrl("/noauth");
    return bean;
}
  1. user:add权限才能访问/add

  2. user:update权限才能访问/update

  • 修改数据库表结构(增加权限字段)

image.png

  • 对应修改pojo
@Data
@AllArgsConstructor
@NoArgsConstructor
@Alias("Users")
public class Users {
    private int id;
    private String name;
    private String password;
    private String email;
    private Date birthday;
    private String perms;
}
  • 修改自定义Realm类
public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了授权=>doGetAuthorizationInfo");

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        // 拿到当前对象
        Subject subject = SecurityUtils.getSubject();
        Users currentUser = (Users) subject.getPrincipal();
        // 设置当前用户权限(从数据库中获取)
        info.addStringPermission(currentUser.getPerms());

        return info;
    }

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了认证=>doGetAuthenticationInfo");

        UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

        // 用户名密码 数据库中取
        Users user = userService.queryUserByName(userToken.getUsername());


        if (user == null) {
            // 抛出异常 UnknownAccountException
            return null;
        }

        // 密码认证Shiro接管
        return new SimpleAuthenticationInfo(user, user.getPassword(), "");
    }
}
  1. 首先在认证部分,在进行密码认证的时候,将user对象作为第一个参数传入,public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)
  2. 在授权的时候,可以通过Users currentUser = (Users) subject.getPrincipal();获取user对象
  3. 调用info.addStringPermission来添加权限,参数权限是从数据库中获取的
  • Controller中添加未授权处理
@RequestMapping("/noauth")
@ResponseBody
public String unauthorized() {
    return "未经授权,无法访问此页面";
}
  • shiroFilterFactoryBean中添加未授权处理的请求跳转
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
    // ......省略
    // 设置未授权的请求
    bean.setUnauthorizedUrl("/noauth");
    return bean;
}
  • 测试

账号rootuser:update的权限,使用该账号登录后分别点击update和add

image.png

10. Shiro整合Thymeleaf

为了处理不同权限用户看到不同界面的问题

  • 导入Shiro和Thymeleaf整合的包
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.1.0</version>
</dependency>
  • 添加相关配置(在ShiroConfig类中)
@Bean
public ShiroDialect getShiroDialect() {
    return new ShiroDialect();
}
  • 修改前端
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>首页</h1>

    <p><a th:href="@{/toLogin}">登录</a></p>

    <div th:text="${msg}"></div>
    <div shiro:hasPermission="user:add">
        <a th:href="@{/user/add}">add</a>
    </div>
    <div shiro:hasPermission="user:update">
        <a th:href="@{/user/update}">update</a>
    </div>
</body>
</html>
  • 测试

使用root账号登录后只显示update,没有add

image.png

  • 登录成功后不显示登录按钮

doGetAuthenticationInfo认证的时候,将user存入session

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("执行了认证=>doGetAuthenticationInfo");

    UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

    // 用户名密码 数据库中取
    Users user = userService.queryUserByName(userToken.getUsername());


    if (user == null) {
        // 抛出异常 UnknownAccountException
        return null;
    }

    // 如果登录成功
    Subject subject = SecurityUtils.getSubject();
    Session session = subject.getSession();
    session.setAttribute("loginUser", user);

    // 密码认证Shiro接管
    return new SimpleAuthenticationInfo(user, user.getPassword(), "");
}

在前端添加判断:

<div th:if="session.loginUser == null">
    <p><a th:href="@{/toLogin}">登录</a></p>
</div>

十一、异步任务

11.1 异步任务

1. 项目搭建

  • service
@Service
public class AsyncService {
    public void hello() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("数据正在处理......");
    }
}
  • controller
@RestController
public class AsyncController {
    @Autowired
    AsyncService asyncService;

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

image.png

由于service耗时三秒,期间网页不会响应并且转圈

三秒后显示OK

image.png

2. 异步处理

开启异步处理需要两步

  1. 告诉Srping这是一个异步处理

需要在Service方法上使用注解@Async

@Async
public void hello() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("数据正在处理......");
}
  1. 开启异步功能

在SpringBoot启动类上添加注解@EnableAsync

@SpringBootApplication
@EnableAsync
public class Springboot09AsynchtaskApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot09AsynchtaskApplication.class, args);
    }
}
  • 测试

访问/hello后立马可以看到OK

11.2 邮件发送

邮件发送协议:

  • ==SMTP==:Simple Mail Transfer Protocol,是建立在FTP文件传输服务上的一种邮件服务,主要用于系统之间的邮件信息传递,并提供有关来信的通知。
  • ==POP3==:邮局协议负责从邮件服务器中检索电子邮件,Post Office Protocol 3即邮局协议的第3个版本,是因特网电子邮件的第一个离线协议标准。

1. 导入依赖包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
    <version>2.6.3</version>
</dependency>

2. 配置

  • 自动配置类:MailSenderAutoConfiguration
  • 相关配置:MailProperties
spring:
  mail:
    username: wenkun_wang@163.com
    # 这是开启STMP协议时提供的
    password: LWFVDTUZGNZDGVHP
    # 不同平台中间不同
    host: smtp.163.com

3. 测试类

  • 简单邮件发送

主要使用的类JavaMailSenderImpl

发送简单内容使用SimpleMailMessage

@SpringBootTest
class Springboot09AsynchtaskApplicationTests {
    @Autowired
    JavaMailSenderImpl mailSender;

    /**
     * 邮件发送测试
     */
    @Test
    void contextLoads() {
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        mailMessage.setSubject("一封来自SpringBoot主机的邮件");
        mailMessage.setText("你好,这里是SpringBoot主机,该消息通过STMP协议进行传输,感谢收看,请勿回复。" +
                "如果你看到发件人是自己,不要疑惑,消息有POP3服务器转发");
        mailMessage.setTo("wenkun_wang@163.com");
        mailMessage.setFrom("wenkun_wang@163.com");
        mailSender.send(mailMessage);
    }

}

image.png

  • 复杂邮件发送

主要使用的类JavaMailSenderImpl

发送简单内容使用MimeMessageMimeMessageHelper

 @Test
void miniMsg() throws MessagingException {
    MimeMessage mimeMessage = mailSender.createMimeMessage();
    MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "utf-8");
    helper.setSubject("一封来自SpringBoot主机的MimeMessage类型邮件");
    helper.setText("<h1 style=\"color: red;\">你好,这里是HTML段落" +
                   "<h3>这里是SpringBoot主机,该消息通过STMP协议进行传输,感谢收看,请勿回复。</h3>" +
                   "</h1>", true);
    helper.addAttachment("1.jpg", new File("F:\\Link++\\images\\git.png"));
    helper.setFrom("wenkun_wang@163.com");
    helper.setTo("wenkun_wang@163.com");
    mailSender.send(mimeMessage);
}

image.png

11.3 定时任务

相关接口:

  • TaskExecutor:任务执行
  • TaskScheduler:任务调度

相关注解:

  • @Scheduled:表示什么时候执行

  • @EnableScheduling:开启定时任务支持

Cron表达式:

  • 计划任务,是任务在约定时间执行已经计划好的工作

  • 格式:M H D m d cmd

  • M:分钟(0-59)

  • H:小时(0-23)

  • D:日(1-31)

  • m:月(1-12)

  • d:一星期内的天(0-7,07都代表星期天)

  • cmd:要执行的命令

  • *:匹配任何值

  • ?:只能在DayOfMonth和DayOfWeek两个域使用,可以理解为匹配任何且忽略

  • -:表示起始时间开始触发,每隔固定时间触发,5/20表示5分钟触发一次,25,45分别触发一次

  • 10,18:表示列出枚举值

0 15 10 ? * 1-6:==表示每个月的周一到周六,10:15:00分执行==

  1. 开启定时任务支持
@SpringBootApplication
@EnableAsync
@EnableScheduling
public class Springboot09AsynchtaskApplication {

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

}
  1. 编写定时任务
@Service
public class ScheduledService {
    /**
     * 在特定时间执行该方法
     * 秒   分 时 月 天
     * 0秒  *  *  *  0-7
     * 每天的任何时候的第0秒就会执行
     */
    @Scheduled(cron = "0 * * * * 0-7")
    public void hello() {
        System.out.println("Hello, ScheduledService.hello被执行了");
    }
}
  1. 测试

直接启动SpringBoot项目,该Service是异步的

任何小时的任何分钟0秒的时候就会执行该方法:

image.png