一、回顾
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项目
2.2 准备工作
- 导入web依赖
<!--导入web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.3</version>
</dependency>
- 启动项目
- 查看结果
只有当添加了web启动器依赖时,springBoot启动后才会集成Tomcat,否则启动后会直接停止
2.3 编写Controller
@Controller
:要返回一个视图名称@ResponseBody
:返回字符串,不使用视图解析器
@Controller
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("/h1")
@ResponseBody
public String hello() {
return "hello";
}
}
测试
2.4 配置文件
- 修改默认端口号
# 修改项目的默认端口号
server.port=8081
三、SpringBoot自动装配原理
3.1 pom.xml
dependencies
spring-boot-dependencies:核心依赖在父工程中
- 启动器
spring-boot-starter-xxx:是springBoot的启动器,说白了就是springBoot的启动场景。
springBoot会有很多已经内置好的启动场景,如果要使用什么功能,只需要找到对应的启动器就可以了。
Name | Description |
---|---|
spring-boot-starter | Core starter, including auto-configuration support, logging and YAML(==默认主启动器==) |
spring-boot-starter-activemq | Starter for JMS messaging using Apache ActiveMQ |
spring-boot-starter-amqp | Starter for using Spring AMQP and Rabbit MQ |
spring-boot-starter-aop | Starter for aspect-oriented programming with Spring AOP and AspectJ(==AOP==) |
spring-boot-starter-artemis | Starter for JMS messaging using Apache Artemis |
spring-boot-starter-batch | Starter for using Spring Batch |
spring-boot-starter-cache | Starter for using Spring Framework’s caching support(==缓存==) |
spring-boot-starter-data-cassandra | Starter for using Cassandra distributed database and Spring Data Cassandra |
spring-boot-starter-data-cassandra-reactive | Starter for using Cassandra distributed database and Spring Data Cassandra Reactive |
spring-boot-starter-data-couchbase | Starter for using Couchbase document-oriented database and Spring Data Couchbase |
spring-boot-starter-data-couchbase-reactive | Starter for using Couchbase document-oriented database and Spring Data Couchbase Reactive |
spring-boot-starter-data-elasticsearch | Starter for using Elasticsearch search and analytics engine and Spring Data Elasticsearch |
spring-boot-starter-data-jdbc | Starter for using Spring Data JDBC |
spring-boot-starter-data-jpa | Starter for using Spring Data JPA with Hibernate |
spring-boot-starter-data-ldap | Starter for using Spring Data LDAP |
spring-boot-starter-data-mongodb | Starter for using MongoDB document-oriented database and Spring Data MongoDB |
spring-boot-starter-data-mongodb-reactive | Starter for using MongoDB document-oriented database and Spring Data MongoDB Reactive |
spring-boot-starter-data-neo4j | Starter for using Neo4j graph database and Spring Data Neo4j |
spring-boot-starter-data-r2dbc | Starter for using Spring Data R2DBC |
spring-boot-starter-data-redis | Starter for using Redis key-value data store with Spring Data Redis and the Lettuce client(==Redis==) |
spring-boot-starter-data-redis-reactive | Starter for using Redis key-value data store with Spring Data Redis reactive and the Lettuce client |
spring-boot-starter-data-rest | Starter for exposing Spring Data repositories over REST using Spring Data REST |
spring-boot-starter-freemarker | Starter for building MVC web applications using FreeMarker views |
spring-boot-starter-groovy-templates | Starter for building MVC web applications using Groovy Templates views |
spring-boot-starter-hateoas | Starter for building hypermedia-based RESTful web application with Spring MVC and Spring HATEOAS |
spring-boot-starter-integration | Starter for using Spring Integration |
spring-boot-starter-jdbc | Starter for using JDBC with the HikariCP connection pool(==JDBC==) |
spring-boot-starter-jersey | Starter for building RESTful web applications using JAX-RS and Jersey. An alternative to spring-boot-starter-web |
spring-boot-starter-jooq | Starter 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-json | Starter for reading and writing json |
spring-boot-starter-jta-atomikos | Starter for JTA transactions using Atomikos |
spring-boot-starter-mail | Starter for using Java Mail and Spring Framework’s email sending support |
spring-boot-starter-mustache | Starter for building web applications using Mustache views |
spring-boot-starter-oauth2-client | Starter for using Spring Security’s OAuth2/OpenID Connect client features |
spring-boot-starter-oauth2-resource-server | Starter for using Spring Security’s OAuth2 resource server features |
spring-boot-starter-quartz | Starter for using the Quartz scheduler |
spring-boot-starter-rsocket | Starter for building RSocket clients and servers |
spring-boot-starter-security | Starter for using Spring Security |
spring-boot-starter-test | Starter for testing Spring Boot applications with libraries including JUnit Jupiter, Hamcrest and Mockito(==测试==) |
spring-boot-starter-thymeleaf | Starter for building MVC web applications using Thymeleaf views |
spring-boot-starter-validation | Starter for using Java Bean Validation with Hibernate Validator |
spring-boot-starter-web | Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container(==web==) |
spring-boot-starter-web-services | Starter for using Spring Web Services |
spring-boot-starter-webflux | Starter for building WebFlux applications using Spring Framework’s Reactive Web support |
spring-boot-starter-websocket | Starter 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;
}
META-INF/spring.factories
文件
其中
META-INF/spring.factories
是自动配置的核心文件,其位置在:
在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;
}
loadFactoryNames
方法:所有的资源加载到配置类中
其中FACTORIES_RESOURCE_LOCATION
的位置就是autoConfig
包下的META-INF/spring.factories
文件:
3.3 自动配置原理总结
结论:SpringBoot所有的自动配置都在启动类中被扫描并加载,所有自动配置类都在
spring.factories
里面,但是不一定生效,要判断条件是否成立,只要导入对应的starter,就有对应的启动器,自动装配就会生效,配置就成功了。一切从
@SpringBootApplication
开始,该注解中有一个自动导入配置的注解@EnableAutoConfiguration
,该注解引入了一个自动配置引入选择器AutoConfigurationImportSelector
,该类负责导入候选配置,首先调用getAutoConfigurationEntry
方法获得所有实体,该方法又调用了getCandidateCofigrations
方法获取候选配置,该方法从标注了@EnableAutoConfiguration
注解的类中加载配置,具体方法是从META-INF/spring.factories
中读取资源,将其封装成为配置
- SpringBoot在启动的时候,从类路径下
/META-INF/spring.factories
获取指定的值- 将这些自动配置的类导入容器,自动配置类就会生效
- 以前需要自动配置的东西,现在SpringBoot帮我们做了
- 整合javaEE解决方案和自动配置的东西都在
spring-boot-autoconfigure-x.x.x.RELEASE.jar
下- 它会将所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器中
- 容器中会存在很多
xxxAutoConfiguration
的类,这些类就是给容器导入这个场景需要的所有组件(@Configutation
)- 有了自动配置类,免去我们手动编写配置文件工作
四、SpringBoot主启动类如何运行
4.1 SpringApplication类
这个类主要做了一下四件事情:
- 推断应用的类型是普通项目还是web项目
- 查找并加载所有可用初始化器,设置到
initializers
属性中 - 找出所有的应用程序监听器,设置到
listeners
属性中 - 推断并设置main方法的定义类,找到运行的主类
4.2 run方法
五、SpringBoot配置
5.1 配置文件
SpringBoot使用一个全局的配置文件,配置文件的名称是固定的:
application.properties
- 语法结构:
key = value
- 语法结构:
application.yml
- 语法结构:
key:(空格)value
- 语法结构:
配置文件的作用:修改SpringBoot自动配置的默认值,因为SpringBoot在底层给我们自动配置好了
5.2 YAML
YAML
是Yet 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
下面的类:
当我们使用@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后执行成功:
5.5 配置文件位置及多环境配置
1. 配置文件位置
默认支持的配置文件位置有(默认的加载优先级如下):
file:./config/
file:./
classpath:/config/
classpath:/
(默认的配置文件位置)
其中
file:
指的是项目路径
classpath:
指的是类路径
2. 多环境配置
- properties方式
我们一般可以在项目中配置多个环境:
==使用多环境时,需要在配置环境中添加激活配置文件:==
如上所示,三个环境分别是application.properties
、application-dev.properties
、application-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
注解说明:
总结:在我们配置文件中,能够配置的东西,都存在一个规律,首先在spring.factories
中存在一个xxxAutoConfiguration
类存放默认值,然后有一个类xxxProperties
用于改变默认值,该类通过注解@ConfigurationProperties
与配置文件绑定
六、Web开发探究
6.1 SpringBoot Web开发
要解决的问题:
- 导入静态资源
- 首页
- jsp,模板引擎
- 装配扩展mvc
- 增删改查
- 拦截器
- 国际化
6.2 静态资源导入
创建一个新的spring-boot项目,添加web支持,目录结构如下所示:
可以看出,我们从名字上可以大致推断静态资源应该放在==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);
}
});
}
}
}
- 从webjars中获取静态资源
在==WebMvcAutoConfiguration==类中,有一个静态内部类==WebMvcAutoConfigurationAdapter==,该类中有一个==addResourceHandlers==方法,可以看到会从==classpath:/META-INF/resources/webjars/==中去加载静态资源。那么什么是
webjars
呢?
webjars
:网站链接
此时,在网页中输入的/webjars/**
就会被映射到classpath:/META-INF/resources/webjars/
如上图所示,
localhost:8080/webjars
会被映射到classpath:/META-INF/resources/webjars/
,因此访问/jquery/3.4.1/jquery.js
就可以访问到js资源
- 从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/**
访问)的位置。
访问结果如下:
==这三个路径的优先级为: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:/public
、classpath:/resources
、classpath:/static
下放index.html
但是有一种更好的方式是放在 templates
下面,需要注意的是==templates下面的网页只能通过controller来访问==(需要模板引擎的支持)
6.4 模板引擎
前端交给我们的页面是html,如果我们是以前的开发,需要把它们转成jsp,jsp好处就是当我们查出一些数据转发到jsp页面以后,可以利用jsp轻松实现数据的显示以及交互。但是spring-boot项目是一个jar包,不是war包,默认不支持jsp。
SpringBoot推荐使用Thymeleaf模板引擎来替代jsp。
模板引擎的作用就是写一个页面的模板,它会按照我们提供的数据帮你把表达式进行解析、填充到指定位置,然后把这个数据生成一个我们想要的内容。
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
文件。
我们可以使用上一小节创建的index.html
,编写对应的controller:
@Controller
public class IndexController {
@RequestMapping("/index")
public String index() {
return "index";
}
}
访问测试可以正常跳转:
通过模板引擎,可以从controller跳转到html页面
6.5 Thymeleaf使用
- 首先要导入thymeleaf的约束
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head></head>
<body></body>
</html>
- 基础语法
-
Simple expressions:
- Variable Expressions:
${...}
- Selection Variable Expressions:
*{...}
- Message Expressions:
#{...}
- Link URL Expressions:
@{...}
- Fragment Expressions:
~{...}
- Variable 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 literals:
-
Text operations:
- String concatenation:
+
- Literal substitutions:
|The name is ${name}|
- String concatenation:
-
Boolean operations:
- Binary operators:
and
,or
- Boolean negation (unary operator):
!
,not
- Binary operators:
-
Conditional operators:
- If-then:
(if) ? (then)
- If-then-else:
(if) ? (then) : (else)
- Default:
(value) ?: (defaultvalue)
- If-then:
- 测试
- 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>
- 结果
- 特性语法
- 转义
@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>
- 遍历
@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>
6.6 MVC装配原理
Spring MVC Auto-configuration
The auto-configuration adds the following features on top of Spring’s defaults:
- Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans.- Support for serving static resources, including support for WebJars (covered later in this document).
- Automatic registration of
Converter
,GenericConverter
, andFormatter
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;
}
}
}
如何确定自己编写的视图解析器成功了呢?由于所有的网络请求都会走DispatcherServlet
的doDispatch
方法,因此在上面打断点,然后访问网络请求。
结论:要想自定义视图解析器,只需要自己实现
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
如果勾选上图两个,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-jdbc
和mysql-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
可以看到我们能够配置的所有变量:
查看对应配置类:
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
,使用时需要传入DataSource
和JdbcProperties
.
接下来我们通过示例来使用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;
}
}
- 测试结果
八、整合Druid数据源
8.1 介绍
Druid是阿里巴巴开源平台上一个数据源连接池实现,结合C3P0、DBCP、PROXOOL等DB池的优点,同时加入了日志监控。
Druid可以很好监控DB连接池和SQL执行情况,天生就是针对监控而生的DB连接池。
SpringBoot 2.0以上默认使用Hikari数据源,可以说Hikari与Druid都是当前Java web上最优秀的数据源。
8.2 引入
- 导入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
- 查看源码结构
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/*
- 登录查看界面
其中在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 创建项目
- 添加对应扩展
- 导入依赖
<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();
}
}
- 测试
十、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>
- 导入静态资源
- 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的个数
- 测试
==目前可以对页面进行查看,还没有添加任何安全相关的代码,我们要通过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
登录,只有vip2
和vip3
的权限
- 点击
vip1
- 点击
vip2
- 登出
(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')"
:判断当前用户是否拥有某个具体权限
- 未登录
- 登录后
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");
}
- 效果
勾选后,关闭浏览器然后重新进入主页,仍然可以保持登录装状态,其底层原理是利用cookie实现,默认保存两周
- 定制登录页
在安全配置类中
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. 有哪些功能
Authentication
:身份认证、登录,验证用户是不是拥有相应的身份Authorization
:授权,即权限认证,验证某个已认证的用户是否拥有某个权限,及判断用户能够进行什么操作Session Management
:会话管理,及用户登录后就是第一次会话,在没有退出之前,所有信息都在会话中Cryptography
:加密,保护数据安全性Web Support
:Web支持,可以非常容易集成到Web环境Caching
:缓存,比如用户登录后,其用户信息、拥有角色、权限不必每次都去查Concurrency
:Shiro支持多线程应用的并发验证Testing
:提供测试支持Run As
:允许一个用户假装另一个用户的身份进行访问Remember Me
:记住我
3. Shiro架构
Subject
:应用代码直接交互的对象,对外API核心,代表当前用户(不一定是具体的人),与当前用户交互的任何东西,它是一个交互的门面,具体执行是依靠SecurityManager
SecurityManager
:安全管理器,所有与安全相关的操作都与其交互,它管理着所有的Subject
,是Shiro的核心,负责与其他组件交互,相当于SpringMVC中DispatcherServlet
Realm
:Shiro从Realm
中获取安全数据(用户、角色、权限),SecurityManager
要验证用户身份,它需要从Realm
获取响应的用户进行比较,来确认用户的身份是否合法,得到用响应的角色、权限,进行验证用户的操作是否能够进行。可以看成是DataSource
-
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权限
==总结==
- 获得
Subject
:Subject currentUser = SecurityUtils.getSubject();
- 获得
session
:Session 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
- 创建
userRealm
- 通过
userRealm
创建DefaultWebSecurityManager
- 通过
DefaultWebSecurityManager
创建ShiroFilterFactoryBean
- 然后根据需求设置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;
}
}
- 测试
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;
}
有
user:add
权限才能访问/add
有
user:update
权限才能访问/update
- 修改数据库表结构(增加权限字段)
- 对应修改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(), "");
}
}
- 首先在认证部分,在进行密码认证的时候,将
user
对象作为第一个参数传入,public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)
- 在授权的时候,可以通过
Users currentUser = (Users) subject.getPrincipal();
获取user
对象- 调用
info.addStringPermission
来添加权限,参数权限是从数据库中获取的
- Controller中添加未授权处理
@RequestMapping("/noauth")
@ResponseBody
public String unauthorized() {
return "未经授权,无法访问此页面";
}
- 在
shiroFilterFactoryBean
中添加未授权处理的请求跳转
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
// ......省略
// 设置未授权的请求
bean.setUnauthorizedUrl("/noauth");
return bean;
}
- 测试
账号
root
有user:update
的权限,使用该账号登录后分别点击update和add
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
- 登录成功后不显示登录按钮
在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";
}
}
- 测试
由于service耗时三秒,期间网页不会响应并且转圈
三秒后显示OK
2. 异步处理
开启异步处理需要两步
- 告诉Srping这是一个异步处理
需要在Service方法上使用注解@Async
@Async
public void hello() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据正在处理......");
}
- 开启异步功能
在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);
}
}
- 复杂邮件发送
主要使用的类
JavaMailSenderImpl
发送简单内容使用
MimeMessage
和MimeMessageHelper
@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);
}
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,0
和7
都代表星期天)
cmd
:要执行的命令
*
:匹配任何值
?
:只能在DayOfMonth和DayOfWeek两个域使用,可以理解为匹配任何且忽略
-
:表示起始时间开始触发,每隔固定时间触发,5/20
表示5分钟触发一次,25
,45
分别触发一次
10,18
:表示列出枚举值
0 15 10 ? * 1-6
:==表示每个月的周一到周六,10:15:00分执行==
- 开启定时任务支持
@SpringBootApplication
@EnableAsync
@EnableScheduling
public class Springboot09AsynchtaskApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot09AsynchtaskApplication.class, args);
}
}
- 编写定时任务
@Service
public class ScheduledService {
/**
* 在特定时间执行该方法
* 秒 分 时 月 天
* 0秒 * * * 0-7
* 每天的任何时候的第0秒就会执行
*/
@Scheduled(cron = "0 * * * * 0-7")
public void hello() {
System.out.println("Hello, ScheduledService.hello被执行了");
}
}
- 测试
直接启动SpringBoot项目,该Service是异步的
任何小时的任何分钟0秒的时候就会执行该方法: