springboot入门到入坟
课程目录
- 导读:课程概览
- 第01课:Spring Boot开发环境搭建和项目启动
- 第02课:Spring Boot返回Json数据及数据封装
- 第03课:Spring Boot使用slf4j进行日志记录
- 第04课:Spring Boot中的项目属性配置
- 第05课:Spring Boot中的MVC支持
- 第06课:Spring Boot集成Swagger2展现在线接口文档
- 第07课:Spring Boot集成Thymeleaf模板引擎
- 第08课:Spring Boot中的全局异常处理
- 第09课:Spring Boot中的切面AOP处理
- 第10课:Spring Boot中集成MyBatis
- 第11课:Spring Boot事务配置管理
- 第12课:Spring Boot中使用监听器
- 第13课:Spring Boot中使用拦截器
- 第14课:Spring Boot中集成Redis
- 第15课:Spring Boot中集成ActiveMQ
- 第16课:Spring Boot中集成Shiro
- 第17课:Spring Boot中结成Lucence
- 第18课:Spring Boot搭建实际项目开发中的架构
1. Spring Boot是什么
我认为 Spring 的 Web 应用体系结构可以大大简化,如果它提供了从上到下利用 Spring 组件和配置模型的工具和参考体系结构。在简单的
main()
方法引导的 Spring 容器内嵌入和统一这些常用Web 容器服务的配置。
这一要求促使了 2013 年初开始的 Spring Boot 项目的研发,到今天,Spring Boot 的版本已经到了 2.0.3 RELEASE。Spring Boot 并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。
它集成了大量常用的第三方库配置,Spring Boot应用中这些第三方库几乎可以是零配置的开箱即用(out-of-the-box),大部分的 Spring Boot 应用都只需要非常少量的配置代码(基于 Java 的配置),开发者能够更加专注于业务逻辑。
2. 为什么学习Spring Boot
2.1 从Spring官方来看
SpringCloud:Coordinate Anything,协调任何事情;
SpringCloud Data Flow:Connect everything,连接任何东西。
2.2 从Spring Boot的优点来看
Spring Boot 有哪些优点?主要给我们解决了哪些问题呢?我们以下图来说明:
2.2.1 良好的基因
Spring Boot 是伴随着 Spring 4.0 诞生的,从字面理解,Boot是引导的意思,因此 Spring Boot 旨在帮助开发者快速搭建 Spring 框架。Spring Boot 继承了原有 Spring 框架的优秀基因,使 Spring 在使用中更加方便快捷。
2.2.2 简化编码
举个例子,比如我们要创建一个 web 项目,使用 Spring 的朋友都知道,在使用 Spring 的时候,需要在 pom 文件中添加多个依赖,而 Spring Boot 则会帮助开发着快速启动一个 web 容器,在 Spring Boot 中,我们只需要在 pom 文件中添加如下一个 starter-web 依赖即可。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
我们点击进入该依赖后可以看到,Spring Boot 这个 starter-web 已经包含了多个依赖,包括之前在 Spring 工程中需要导入的依赖,我们看一下其中的一部分,如下:
<!-- .....省略其他依赖 -->
<dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId><version>5.0.7.RELEASE</version><scope>compile</scope>
</dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.0.7.RELEASE</version><scope>compile</scope>
</dependency>
由此可以看出,Spring Boot 大大简化了我们的编码,我们不用一个个导入依赖,直接一个依赖即可。
2.2.3 简化配置
Spring 虽然使Java EE轻量级框架,但由于其繁琐的配置,一度被人认为是“配置地狱”。各种XML、Annotation配置会让人眼花缭乱,而且配置多的话,如果出错了也很难找出原因。Spring Boot更多的是采用 Java Config 的方式,对 Spring 进行配置。举个例子:
我新建一个类,但是我不用 @Service
注解,也就是说,它是个普通的类,那么我们如何使它也成为一个 Bean 让 Spring 去管理呢?只需要@Configuration
和@Bean
两个注解即可,如下:
public class TestService {public String sayHello () {return "Hello Spring Boot!";}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class JavaConfig {@Beanpublic TestService getTestService() {return new TestService();}
}
@Configuration
表示该类是个配置类,@Bean
表示该方法返回一个 Bean。这样就把TestService
作为 Bean 让 Spring 去管理了,在其他地方,我们如果需要使用该 Bean,和原来一样,直接使用@Resource
注解注入进来即可使用,非常方便。
@Resource
private TestService testService;
另外,部署配置方面,原来 Spring 有多个 xml 和 properties配置,在 Spring Boot 中只需要个 application.yml即可。
2.2.4 简化部署
在使用 Spring 时,项目部署时需要我们在服务器上部署 tomcat,然后把项目打成 war 包扔到 tomcat里,在使用 Spring Boot 后,我们不需要在服务器上去部署 tomcat,因为 Spring Boot 内嵌了 tomcat,我们只需要将项目打成 jar 包,使用 java -jar xxx.jar
一键式启动项目。
另外,也降低对运行环境的基本要求,环境变量中有JDK即可。
2.2.5 简化监控
我们可以引入 spring-boot-start-actuator 依赖,直接使用 REST 方式来获取进程的运行期性能参数,从而达到监控的目的,比较方便。但是 Spring Boot 只是个微框架,没有提供相应的服务发现与注册的配套功能,没有外围监控集成方案,没有外围安全管理方案,所以在微服务架构中,还需要 Spring Cloud 来配合一起使用。
2.3 从未来发展的趋势来看
3. 本课程能学到什么
5. 本课程开发环境和插件
第01课:Spring Boot开发环境搭建和项目启动
上一节对 SpringBoot 的特性做了一个介绍,本节主要对 jdk 的配置、Spring Boot工程的构建和项目的启动、Spring Boot 项目工程的结构做一下讲解和分析。
1. jdk 的配置
本课程是使用 IDEA 进行开发,在IDEA 中配置 jdk 的方式很简单,打开File->Project Structure
,如下图所:
通过以上三步骤,即可导入本地安装的 jdk。如果是使用 STS 或者 eclipse 的朋友,可以通过两步骤添加:
window->preference->java->Instralled JRES
来添加本地 jdk。window-->preference-->java-->Compiler
选择 jre,和 jdk 保持一致。
2. Spring Boot 工程的构建
2.1 IDEA 快速构建
- Group:填企业域名,本课程使用com.itcodai
- Artifact:填项目名称,本课程中每一课的工程名以
course+课号
命令,这里使用 course01 - Dependencies:可以添加我们项目中所需要的依赖信息,根据实际情况来添加,本课程只需要选择 Web 即可。
2.2 官方构建
- 访问 http://start.spring.io/。
- 在页面上输入相应的 Spring Boot 版本、Group 和 Artifact 信息以及项目依赖,然后创建项目。
- 解压后,使用 IDEA 导入该 maven 工程:
File->New->Model from Existing Source
,然后选择解压后的项目文件夹即可。如果是使用 eclipse 的朋友,可以通过Import->Existing Maven Projects->Next
,然后选择解压后的项目文件夹即可。
2.3 maven配置
创建了 Spring Boot 项目之后,需要进行 maven 配置。打开File->settings
,搜索 maven,配置一下本地的 maven 信息。如下:
<mirror><id>nexus-aliyun</id><mirrorOf>*</mirrorOf><name>Nexus aliyun</name><url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
如果是使用 eclipse 的朋友,可以通过window-->preference-->Maven-->User Settings
来配置,配置方式和上面一致。
2.4 编码配置
同样地,新建项目后,我们一般都需要配置编码,这点非常重要,很多初学者都会忘记这一步,所以要养成良好的习惯。
IDEA 中,仍然是打开File->settings
,搜索 encoding,配置一下本地的编码信息。如下:
如果是使用 eclipse 的朋友,有两个地方需要设置一下编码:
- window–> perferences–>General–>Workspace,将Text file encoding改成utf-8
- window–>perferences–>General–>content types,选中Text,将Default encoding填入utf-8
3. Spring Boot 项目工程结构
到此为止,Spring Boot 就启动成功了,为了比较清楚的看到效果,我们写一个 Controller 来测试一下,如下:
package com.itcodai.course01.controller;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/start")
public class StartController {@RequestMapping("/springboot")public String startSpringBoot() {return "Welcome to the world of Spring Boot!";}
}
server:port: 8001
4. 总结
第02课:Spring Boot返回Json数据及数据封装
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {String value() default "";
}
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-json</artifactId><version>2.0.3.RELEASE</version><scope>compile</scope>
</dependency>
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.6</version><scope>compile</scope>
</dependency>
<dependency><groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-jdk8</artifactId><version>2.9.6</version><scope>compile</scope>
</dependency>
<dependency><groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-jsr310</artifactId><version>2.9.6</version><scope>compile</scope>
</dependency>
<dependency><groupId>com.fasterxml.jackson.module</groupId><artifactId>jackson-module-parameter-names</artifactId><version>2.9.6</version><scope>compile</scope>
</dependency>
到此为止,我们知道了 Spring Boot 中默认使用的 json 解析框架是 jackson。下面我们看一下默认的 jackson 框架对常用数据类型的转 Json 处理。
1. Spring Boot 默认对Json的处理
在实际项目中,常用的数据结构无非有类对象、List对象、Map对象,我们看一下默认的 jackson 框架对这三个常用的数据结构转成 json 后的格式如何。
1.1 创建 User 实体类
为了测试,我们需要创建一个实体类,这里我们就用 User 来演示。
public class User {private Long id;private String username;private String password;/* 省略get、set和带参构造方法 */
}
1.2 创建Controller类
然后我们创建一个 Controller,分别返回 User
对象、List<User>
和 Map<String, Object>
。
import com.itcodai.course02.entity.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@RestController
@RequestMapping("/json")
public class JsonController {@RequestMapping("/user")public User getUser() {return new User(1, "彭于晏", "123456");}@RequestMapping("/list")public List<User> getUserList() {List<User> userList = new ArrayList<>();User user1 = new User(1, "彭于晏", "123456");User user2 = new User(2, "达人课", "123456");userList.add(user1);userList.add(user2);return userList;}@RequestMapping("/map")public Map<String, Object> getMap() {Map<String, Object> map = new HashMap<>(3);User user = new User(1, "彭于晏", "123456");map.put("作者信息", user);map.put("博客地址", "http://blog.itcodai.com");map.put("CSDN地址", "http://blog.csdn.net/eson_15");map.put("粉丝数量", 4153);return map;}
}
1.3 测试不同数据类型返回的json
OK,写好了接口,分别返回了一个 User 对象、一个 List 集合和一个 Map 集合,其中 Map 集合中的 value 存的是不同的数据类型。接下来我们依次来测试一下效果。
在浏览器中输入:localhost:8080/json/user
返回 json 如下:
{"id":1,"username":"倪升武","password":"123456"}
在浏览器中输入:localhost:8080/json/list
返回 json 如下:
[{"id":1,"username":"倪升武","password":"123456"},{"id":2,"username":"达人课","password":"123456"}]
在浏览器中输入:localhost:8080/json/map
返回 json 如下:
{"作者信息":{"id":1,"username":"倪升武","password":"123456"},"CSDN地址":"http://blog.csdn.net/eson_15","粉丝数量":4153,"博客地址":"http://blog.itcodai.com"}
可以看出,map 中不管是什么数据类型,都可以转成相应的 json 格式,这样就非常方便。
1.4 jackson 中对null的处理
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;import java.io.IOException;@Configuration
public class JacksonConfig {@Bean@Primary@ConditionalOnMissingBean(ObjectMapper.class)public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {ObjectMapper objectMapper = builder.createXmlMapper(false).build();objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {@Overridepublic void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {jsonGenerator.writeString("");}});return objectMapper;}
}
然后我们修改一下上面返回 map 的接口,将几个值改成 null 测试一下:
@RequestMapping("/map")
public Map<String, Object> getMap() {Map<String, Object> map = new HashMap<>(3);User user = new User(1, "倪升武", null);map.put("作者信息", user);map.put("博客地址", "http://blog.itcodai.com");map.put("CSDN地址", null);map.put("粉丝数量", 4153);return map;
}
重启项目,再次输入:localhost:8080/json/map
,可以看到 jackson 已经将所有 null 字段转成了空字符串了。
{"作者信息":{"id":1,"username":"倪升武","password":""},"CSDN地址":"","粉丝数量":4153,"博客地址":"http://blog.itcodai.com"}
2. 使用阿里巴巴FastJson的设置
2.1 jackson 和 fastJson 的对比
选项 | fastJson | jackson |
---|---|---|
上手难易程度 | 容易 | 中等 |
高级特性支持 | 中等 | 丰富 |
官方文档、Example支持 | 中文 | 英文 |
处理json速度 | 略快 | 快 |
2.2 fastJson依赖导入
使用 fastJson 需要导入依赖,本课程使用 1.2.35 版本,依赖如下:
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.35</version>
</dependency>
2.2 使用 fastJson 处理 null
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;@Configuration
public class fastJsonConfig extends WebMvcConfigurationSupport {/*** 使用阿里 FastJson 作为JSON MessageConverter* @param converters*/@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();FastJsonConfig config = new FastJsonConfig();config.setSerializerFeatures(// 保留map空的字段SerializerFeature.WriteMapNullValue,// 将String类型的null转成""SerializerFeature.WriteNullStringAsEmpty,// 将Number类型的null转成0SerializerFeature.WriteNullNumberAsZero,// 将List类型的null转成[]SerializerFeature.WriteNullListAsEmpty,// 将Boolean类型的null转成falseSerializerFeature.WriteNullBooleanAsFalse,// 避免循环引用SerializerFeature.DisableCircularReferenceDetect);converter.setFastJsonConfig(config);converter.setDefaultCharset(Charset.forName("UTF-8"));List<MediaType> mediaTypeList = new ArrayList<>();// 解决中文乱码问题,相当于在Controller上的@RequestMapping中加了个属性produces = "application/json"mediaTypeList.add(MediaType.APPLICATION_JSON);converter.setSupportedMediaTypes(mediaTypeList);converters.add(converter);}
}
3. 封装统一返回的数据结构
3.1 定义统一的 json 结构
public class JsonResult<T> {private T data;private String code;private String msg;/*** 若没有数据返回,默认状态码为0,提示信息为:操作成功!*/public JsonResult() {this.code = "0";this.msg = "操作成功!";}/*** 若没有数据返回,可以人为指定状态码和提示信息* @param code* @param msg*/public JsonResult(String code, String msg) {this.code = code;this.msg = msg;}/*** 有数据返回时,状态码为0,默认提示信息为:操作成功!* @param data*/public JsonResult(T data) {this.data = data;this.code = "0";this.msg = "操作成功!";}/*** 有数据返回,状态码为0,人为指定提示信息* @param data* @param msg*/public JsonResult(T data, String msg) {this.data = data;this.code = "0";this.msg = msg;}// 省略get和set方法
}
3.2 修改 Controller 中的返回值类型及测试
@RestController
@RequestMapping("/jsonresult")
public class JsonResultController {@RequestMapping("/user")public JsonResult<User> getUser() {User user = new User(1, "倪升武", "123456");return new JsonResult<>(user);}@RequestMapping("/list")public JsonResult<List> getUserList() {List<User> userList = new ArrayList<>();User user1 = new User(1, "倪升武", "123456");User user2 = new User(2, "达人课", "123456");userList.add(user1);userList.add(user2);return new JsonResult<>(userList, "获取用户列表成功");}@RequestMapping("/map")public JsonResult<Map> getMap() {Map<String, Object> map = new HashMap<>(3);User user = new User(1, "倪升武", null);map.put("作者信息", user);map.put("博客地址", "http://blog.itcodai.com");map.put("CSDN地址", null);map.put("粉丝数量", 4153);return new JsonResult<>(map);}
}
我们重新在浏览器中输入:localhost:8080/jsonresult/user
返回 json 如下:
{"code":"0","data":{"id":1,"password":"123456","username":"倪升武"},"msg":"操作成功!"}
输入:localhost:8080/jsonresult/list
,返回 json 如下:
{"code":"0","data":[{"id":1,"password":"123456","username":"倪升武"},{"id":2,"password":"123456","username":"达人课"}],"msg":"获取用户列表成功"}
输入:localhost:8080/jsonresult/map
,返回 json 如下:
{"code":"0","data":{"作者信息":{"id":1,"password":"","username":"倪升武"},"CSDN地址":null,"粉丝数量":4153,"博客地址":"http://blog.itcodai.com"},"msg":"操作成功!"}
通过封装,我们不但将数据通过 json 传给前端或者其他接口,还带上了状态码和提示信息,这在实际项目场景中应用非常广泛。
4. 总结
第03课:Spring Boot使用slf4j进行日志记录
1. slf4j 介绍
SLF4J,即简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统。按照官方的说法,SLF4J是一个用于日志系统的简单Facade,允许最终用户在部署其应用时使用其所希望的日志系统。
这段的大概意思是:你只需要按统一的方式写记录日志的代码,而无需关心日志是通过哪个日志系统,以什么风格输出的。因为它们取决于部署项目时绑定的日志系统。例如,在项目中使用了 slf4j 记录日志,并且绑定了 log4j(即导入相应的依赖),则日志会以 log4j 的风格输出;后期需要改为以 logback 的风格输出日志,只需要将 log4j 替换成 logback 即可,不用修改项目中的代码。这对于第三方组件的引入的不同日志系统来说几乎零学习成本,况且它的优点不仅仅这一个而已,还有简洁的占位符的使用和日志级别的判断。
正因为 sfl4j 有如此多的优点,阿里巴巴已经将 slf4j 作为他们的日志框架了。在《阿里巴巴Java开发手册(正式版)》中,日志规约一项第一条就强制要求使用 slf4j:
1.【强制】应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
“强制”两个字体现出了 slf4j 的优势,所以建议在实际项目中,使用 slf4j 作为自己的日志框架。使用 slf4j 记录日志非常简单,直接使用 LoggerFactory 创建即可。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class Test {private static final Logger logger = LoggerFactory.getLogger(Test.class);// ……
}
2. application.yml 中对日志的配置
我们看一下 application.yml 文件中对日志的配置:
logging:config: logback.xmllevel:com.itcodai.course03.dao: trace
常用的日志级别按照从高到低依次为:ERROR、WARN、INFO、DEBUG。
3. logback.xml 配置文件解析
3.1 定义日志输出格式和存储路径
<configuration><property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" /><property name="FILE_PATH" value="D:/logs/course03/demo.%d{yyyy-MM-dd}.%i.log" />
</configuration>
3.2 定义控制台输出
<configuration><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><!-- 按照上面配置的LOG_PATTERN来打印日志 --><pattern>${LOG_PATTERN}</pattern></encoder></appender>
</configuration>
3.3 定义日志文件的相关参数
<configuration><appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!-- 按照上面配置的FILE_PATH路径来保存日志 --><fileNamePattern>${FILE_PATH}</fileNamePattern><!-- 日志保存15天 --><maxHistory>15</maxHistory><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><!-- 单个日志文件的最大,超过则新建日志文件存储 --><maxFileSize>10MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy></rollingPolicy><encoder><!-- 按照上面配置的LOG_PATTERN来打印日志 --><pattern>${LOG_PATTERN}</pattern></encoder></appender>
</configuration>
使用 <appender>
定义一个名为 “FILE” 的文件配置,主要是配置日志文件保存的时间、单个日志文件存储的大小、以及文件保存的路径和日志的输出格式。
3.4 定义日志输出级别
<configuration><logger name="com.itcodai.course03" level="INFO" /><root level="INFO"><appender-ref ref="CONSOLE" /><appender-ref ref="FILE" /></root>
</configuration>
4. 使用Logger在项目中打印日志
在代码中,我们一般使用 Logger 对象来打印出一些 log 信息,可以指定打印出的日志级别,也支持占位符,很方便。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/test")
public class TestController {private final static Logger logger = LoggerFactory.getLogger(TestController.class);@RequestMapping("/log")public String testLog() {logger.debug("=====测试日志debug级别打印====");logger.info("======测试日志info级别打印=====");logger.error("=====测试日志error级别打印====");logger.warn("======测试日志warn级别打印=====");// 可以使用占位符打印出一些参数信息String str1 = "blog.itcodai.com";String str2 = "blog.csdn.net/eson_15";logger.info("======倪升武的个人博客:{};倪升武的CSDN博客:{}", str1, str2);return "success";}
}
启动该项目,在浏览器中输入 localhost:8080/test/log
后可以看到控制台的日志记录:
======测试日志info级别打印=====
=====测试日志error级别打印====
======测试日志warn级别打印=====
======倪升武的个人博客:blog.itcodai.com;倪升武的CSDN博客:blog.csdn.net/eson_15
5. 总结
第04课:Spring Boot中的项目属性配置
1. 少量配置信息的情形
server:port: 8001# 配置微服务的地址
url:# 订单微服务的地址orderUrl: http://localhost:8002
然后在业务代码中如何获取到这个配置的订单服务地址呢?我们可以使用 @Value
注解来解决。在对应的类中加上一个属性,在属性上使用 @Value
注解即可获取到配置文件中的配置信息,如下:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/test")
public class ConfigController {private static final Logger LOGGER = LoggerFactory.getLogger(ConfigController.class);@Value("${url.orderUrl}")private String orderUrl;@RequestMapping("/config")public String testConfig() {LOGGER.info("=====获取的订单服务地址为:{}", orderUrl);return "success";}
}
=====获取的订单服务地址为:http://localhost:8002
说明我们成功获取到了配置文件中的订单微服务地址,在实际项目中也是这么用的,后面如果因为服务器部署的原因,需要修改某个服务的地址,那么只要在配置文件中修改即可。
2. 多个配置信息的情形
# 配置多个微服务的地址
url:# 订单微服务的地址orderUrl: http://localhost:8002# 用户微服务的地址userUrl: http://localhost:8003# 购物车微服务的地址shoppingUrl: http://localhost:8004
也许实际业务中,远远不止这三个微服务,甚至十几个都有可能。对于这种情况,我们可以先定义一个 MicroServiceUrl
类来专门保存微服务的 url,如下:
@Component
@ConfigurationProperties(prefix = "url")
public class MicroServiceUrl {private String orderUrl;private String userUrl;private String shoppingUrl;// 省去get和set方法
}
需要注意的是,使用 @ConfigurationProperties
注解需要导入它的依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional>
</dependency>
@RestController
@RequestMapping("/test")
public class TestController {private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);@Resourceprivate MicroServiceUrl microServiceUrl;@RequestMapping("/config")public String testConfig() {LOGGER.info("=====获取的订单服务地址为:{}", microServiceUrl.getOrderUrl());LOGGER.info("=====获取的用户服务地址为:{}", microServiceUrl.getUserUrl());LOGGER.info("=====获取的购物车服务地址为:{}", microServiceUrl.getShoppingUrl());return "success";}
}
再次启动项目,请求一下可以看到,控制台打印出如下信息,说明配置文件生效,同时正确获取配置文件内容:
=====获取的订单服务地址为:http://localhost:8002
=====获取的订单服务地址为:http://localhost:8002
=====获取的用户服务地址为:http://localhost:8003
=====获取的购物车服务地址为:http://localhost:8004
3. 指定项目配置文件
最好的解决方法就是开发环境和生产环境都有一套对用的配置信息,然后当我们在开发时,指定读取开发环境的配置,当我们将项目部署到服务器上之后,再指定去读取生产环境的配置。
# 开发环境配置文件
server:port: 8001
# 开发环境配置文件
server:port: 8002
然后在 application.yml
文件中指定读取哪个配置文件即可。比如我们在开发环境下,指定读取 applicationn-dev.yml
文件,如下:
spring:profiles:active:- dev
4. 总结
第05课:Spring Boot中的MVC支持
1. @RestController
@RestController
是 Spring Boot 新增的一个注解,我们看一下该注解都包含了哪些东西。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {String value() default "";
}
public String getUser() {return "user";
}
2. @RequestMapping
该注解有6个属性,一般在项目中比较常用的有三个属性:value、method 和 produces。
- value 属性:指定请求的实际地址,value 可以省略不写
- method 属性:指定请求的类型,主要有 GET、PUT、POST、DELETE,默认为 GET
- produces属性:指定返回内容类型,如 produces = “application/json; charset=UTF-8”
@RestController
@RequestMapping(value = "/test", produces = "application/json; charset=UTF-8")
public class TestController {@RequestMapping(value = "/get", method = RequestMethod.GET)public String testGet() {return "success";}
}
这个很简单,启动项目在浏览器中输入 localhost:8080/test/get
测试一下即可。
3. @PathVariable
@GetMapping("/user/{id}")
public String testPathVariable(@PathVariable Integer id) {System.out.println("获取到的id为:" + id);return "success";
}
@GetMapping("/user/{idd}/{name}")public String testPathVariable(@PathVariable(value = "idd") Integer id, @PathVariable String name) {System.out.println("获取到的id为:" + id);System.out.println("获取到的name为:" + name);return "success";}
运行项目,在浏览器中请求 localhost:8080/test/user/2/zhangsan
可以看到控制台输出如下信息:
获取到的id为:2
获取到的name为:zhangsan
所以支持多个参数的接收。同样地,如果 url 中的参数和方法中的参数名称不同的话,也需要使用 value 属性来绑定两个参数。
4. @RequestParam
@GetMapping("/user")
public String testRequestParam(@RequestParam Integer id) {System.out.println("获取到的id为:" + id);return "success";
}
@PostMapping("/form1")public String testForm(@RequestParam String username, @RequestParam String password) {System.out.println("获取到的username为:" + username);System.out.println("获取到的password为:" + password);return "success";}
我们使用 postman 来模拟一下表单提交,测试一下接口:
public class User {private String username;private String password;// set get
}
使用实体接收的话,我们不能在前面加 @RequestParam
注解了,直接使用即可。
@PostMapping("/form2")public String testForm(User user) {System.out.println("获取到的username为:" + user.getUsername());System.out.println("获取到的password为:" + user.getPassword());return "success";}
使用 postman 再次测试一下表单提交,观察一下返回值和控制台打印出的日志即可。在实际项目中,一般都是封装一个实体类来接收表单数据,因为实际项目中表单数据一般都很多。
5. @RequestBody
public class User {private String username;private String password;// set get
}
@PostMapping("/user")
public String testRequestBody(@RequestBody User user) {System.out.println("获取到的username为:" + user.getUsername());System.out.println("获取到的password为:" + user.getPassword());return "success";
}
我们使用 postman 工具来测试一下效果,打开 postman,然后输入请求地址和参数,参数我们用 json 来模拟,如下图所有,调用之后返回 success。
获取到的username为:倪升武
获取到的password为:123456
6. 总结
第06课:Spring Boot集成 Swagger2 展现在线接口文档
1. Swagger 简介
1.1 解决的问题
1.2 Swagger 官方
我们打开 Swagger 官网,官方对 Swagger 的定义为:
The Best APIs are Built with Swagger Tools
翻译成中文是:“最好的 API 是使用 Swagger 工具构建的”。由此可见,Swagger 官方对其功能和所处的地位非常自信,由于其非常好用,所以官方对其定位也合情合理。如下图所示:
本文主要讲解在 Spring Boot 中如何导入 Swagger2 工具来展现项目中的接口文档。本节课使用的 Swagger 版本为 2.2.2。下面开始进入 Swagger2 之旅。
2. Swagger2 的 maven 依赖
<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.2.2</version>
</dependency>
<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.2.2</version>
</dependency>
3. Swagger2 的配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;/*** @author shengwu ni*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {@Beanpublic Docket createRestApi() {return new Docket(DocumentationType.SWAGGER_2)// 指定构建api文档的详细信息的方法:apiInfo().apiInfo(apiInfo()).select()// 指定要生成api接口的包路径,这里把controller作为包路径,生成controller中的所有接口.apis(RequestHandlerSelectors.basePackage("com.itcodai.course06.controller")).paths(PathSelectors.any()).build();}/*** 构建api文档的详细信息* @return*/private ApiInfo apiInfo() {return new ApiInfoBuilder()// 设置页面标题.title("Spring Boot集成Swagger2接口总览")// 设置接口描述.description("跟武哥一起学Spring Boot第06课")// 设置联系方式.contact("倪升武," + "CSDN:http://blog.csdn.net/eson_15")// 设置版本.version("1.0")// 构建.build();}
}
结合该图,对照上面的 Swagger2 配置文件中的配置,可以很明确的知道配置类中每个方法的作用。这样就很容易理解和掌握 Swagger2 中的配置了,也可以看出,其实 Swagger2 配置很简单。
【友情提示】可能有很多朋友在配置 Swagger 的时候会遇到下面的情况,而且还关不掉的,这是因为浏览器缓存引起的,清空一下浏览器缓存即可解决问题。
4. Swagger2 的使用
4.1 实体类注解
本节我们建一个 User 实体类,主要介绍一下 Swagger2 中的 @ApiModel
和 @ApiModelProperty
注解,同时为后面的测试做准备。
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;@ApiModel(value = "用户实体类")
public class User {@ApiModelProperty(value = "用户唯一标识")private Long id;@ApiModelProperty(value = "用户姓名")private String username;@ApiModelProperty(value = "用户密码")private String password;// 省略set和get方法
}
解释下 @ApiModel
和 @ApiModelProperty
注解:
@ApiModel
注解用于实体类,表示对类进行说明,用于参数用实体类接收。
@ApiModelProperty
注解用于类中属性,表示对 model 属性的说明或者数据操作更改。
该注解在在线 API 文档中的具体效果在下文说明。
4.2 Controller 类中相关注解
我们写一个 TestController,再写几个接口,然后学习一下 Controller 中和 Swagger2 相关的注解。
import com.itcodai.course06.entiy.JsonResult;
import com.itcodai.course06.entiy.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/swagger")
@Api(value = "Swagger2 在线接口文档")
public class TestController {@GetMapping("/get/{id}")@ApiOperation(value = "根据用户唯一标识获取用户信息")public JsonResult<User> getUserInfo(@PathVariable @ApiParam(value = "用户唯一标识") Long id) {// 模拟数据库中根据id获取User信息User user = new User(id, "倪升武", "123456");return new JsonResult(user);}
}
我们来学习一下 @Api
、 @ApiOperation
和 @ApiParam
注解。
@Api
注解用于类上,表示标识这个类是 swagger 的资源。
@ApiOperation
注解用于方法,表示一个 http 请求的操作。
@ApiParam
注解用于参数上,用来标明参数信息。
这里返回的是 JsonResult,是第02课中学习返回 json 数据时封装的实体。以上是 Swagger 中最常用的 5 个注解,接下来运行一下项目工程,在浏览器中输入 localhost:8080/swagger-ui.html
看一下 Swagger 页面的接口状态。
可以看出,Swagger 页面对该接口的信息展示的非常全面,每个注解的作用以及展示的地方在上图中已经标明,通过页面即可知道该接口的所有信息,那么我们直接在线测试一下该接口返回的信息,输入id为1,看一下返回数据:
可以看出,直接在页面返回了 json 格式的数据,开发人员可以直接使用该在线接口来测试数据的正确与否,非常方便。上面是对于单个参数的输入,如果输入参数为某个对象这种情况,Swagger 是什么样子呢?我们再写一个接口。
@PostMapping("/insert")@ApiOperation(value = "添加用户信息")public JsonResult<Void> insertUser(@RequestBody @ApiParam(value = "用户信息") User user) {// 处理添加逻辑return new JsonResult<>();}
重启项目,在浏览器中输入 localhost:8080/swagger-ui.html
看一下效果:
5. 总结
第07课:Spring Boot集成Thymeleaf模板引擎
1. Thymeleaf 介绍
Thymeleaf 是适用于 Web 和独立环境的现代服务器端 Java 模板引擎。
Thymeleaf 的主要目标是为您的开发工作流程带来优雅的自然模板 - 可以在浏览器中正确显示的HTML,也可以用作静态原型,从而在开发团队中实现更强大的协作。
以上翻译自 Thymeleaf 官方网站。传统的 JSP+JSTL 组合是已经过去了,Thymeleaf 是现代服务端的模板引擎,与传统的 JSP 不同,Thymeleaf 可以使用浏览器直接打开,因为可以忽略掉拓展属性,相当于打开原生页面,给前端人员也带来一定的便利。
什么意思呢?就是说在本地环境或者有网络的环境下,Thymeleaf 均可运行。由于 thymeleaf 支持 html 原型,也支持在 html 标签里增加额外的属性来达到 “模板+数据” 的展示方式,所以美工可以直接在浏览器中查看页面效果,当服务启动后,也可以让后台开发人员查看带数据的动态页面效果。比如:
<div class="ui right aligned basic segment"><div class="ui orange basic label" th:text="${blog.flag}">静态原创信息</div>
</div>
<h2 class="ui center aligned header" th:text="${blog.title}">这是静态标题</h2>
- 1
- 2
- 3
- 4
类似与上面这样,在静态页面时,会展示静态信息,当服务启动后,动态获取数据库中的数据后,就可以展示动态数据,th:text
标签是用来动态替换文本的,这会在下文说明。该例子说明浏览器解释 html 时会忽略 html 中未定义的标签属性(比如 th:text
),所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示数据。
2. 依赖导入
在 Spring Boot 中使用 thymeleaf 模板需要引入依赖,可以在创建项目工程时勾选 Thymeleaf,也可以创建之后再手动导入,如下:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
另外,在 html 页面上如果要使用 thymeleaf 模板,需要在页面标签中引入:
<html xmlns:th="http://www.thymeleaf.org">
3. Thymeleaf相关配置
因为 Thymeleaf 中已经有默认的配置了,我们不需要再对其做过多的配置,有一个需要注意一下,Thymeleaf 默认是开启页面缓存的,所以在开发的时候,需要关闭这个页面缓存,配置如下。
spring:thymeleaf:cache: false #关闭缓存
否则会有缓存,导致页面没法及时看到更新后的效果。 比如你修改了一个文件,已经 update 到 tomcat 了,但刷新页面还是之前的页面,就是因为缓存引起的。
4. Thymeleaf 的使用
4.1 访问静态页面
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>这是404页面
</body>
</html>
我们再写一个 controller 来测试一下 404 和 500 页面:
@Controller
@RequestMapping("/thymeleaf")
public class ThymeleafController {@RequestMapping("/test404")public String test404() {return "index";}@RequestMapping("/test500")public String test500() {int i = 1 / 0;return "index";}
}
当我们在浏览器中输入
localhost:8080/thymeleaf/test400
时,故意输入错误,找不到对应的方法,就会跳转到 404.html 显示。
当我们在浏览器中输入localhost:8088/thymeleaf/test505
时,会抛出异常,然后会自动跳转到 500.html 显示。
【注】这里有个问题需要注意一下,前面的课程中我们说了微服务中会走向前后端分离,我们在 Controller 层上都是使用的 @RestController
注解,自动会把返回的数据转成 json 格式。但是在使用模板引擎时,Controller 层就不能用 @RestController
注解了,因为在使用 thymeleaf 模板时,返回的是视图文件名,比如上面的 Controller 中是返回到 index.html 页面,如果使用 @RestController
的话,会把 index 当作 String 解析了,直接返回到页面了,而不是去找 index.html 页面,大家可以试一下。所以在使用模板时要用 @Controller
注解。
4.2 Thymeleaf 中处理对象
我们来看一下 thymeleaf 模板中如何处理对象信息,假如我们在做个人博客的时候,需要给前端传博主相关信息来展示,那么我们会封装成一个博主对象,比如:
public class Blogger {private Long id;private String name;private String pass;// 省去set和get
}
@GetMapping("/getBlogger")
public String getBlogger(Model model) {Blogger blogger = new Blogger(1L, "倪升武", "123456");model.addAttribute("blogger", blogger);return "blogger";
}
我们先初始化一个 Blogger 对象,然后将该对象放到 Model 中,然后返回到 blogger.html 页面去渲染。接下来我们再写一个 blogger.html 来渲染 blogger 信息:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head><meta charset="UTF-8"><title>博主信息</title>
</head>
<body>
<form action="" th:object="${blogger}" >用户编号:<input name="id" th:value="${blogger.id}"/><br>用户姓名:<input type="text" name="username" th:value="${blogger.getName()}" /><br>登陆密码:<input type="text" name="password" th:value="*{pass}" />
</form>
</body>
</html>
可以看出,在 thymeleaf 模板中,使用 th:object="${}"
来获取对象信息,然后在表单里面可以有三种方式来获取对象属性。如下:
使用
th:value="*{属性名}"
使用th:value="${对象.属性名}"
,对象指的是上面使用th:object
获取的对象
使用th:value="${对象.get方法}"
,对象指的是上面使用th:object
获取的对象
可以看出,在 Thymeleaf 中可以像写 java 一样写代码,很方便。我们在浏览器中输入 localhost:8080/thymeleaf/getBlogger
来测试一下数据:
4.3 Thymeleaf 中处理 List
处理 List 的话,和处理上面介绍的对象差不多,但是需要在 thymeleaf 中进行遍历。我们先在 Controller 中模拟一个 List。
@GetMapping("/getList")
public String getList(Model model) {Blogger blogger1 = new Blogger(1L, "倪升武", "123456");Blogger blogger2 = new Blogger(2L, "达人课", "123456");List<Blogger> list = new ArrayList<>();list.add(blogger1);list.add(blogger2);model.addAttribute("list", list);return "list";
}
接下来我们写一个 list.html 来获取该 list 信息,然后在 list.html 中遍历这个list。如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head><meta charset="UTF-8"><title>博主信息</title>
</head>
<body>
<form action="" th:each="blogger : ${list}" >用户编号:<input name="id" th:value="${blogger.id}"/><br>用户姓名:<input type="text" name="password" th:value="${blogger.name}"/><br>登录密码:<input type="text" name="username" th:value="${blogger.getPass()}"/>
</form>
</body>
</html>
4.4 其他常用 thymeleaf 操作
我们来总结一下 thymeleaf 中的一些常用的标签操作,如下:
标签 | 功能 | 例子 |
---|---|---|
th:value
|
给属性赋值 |
<input th:value="${blog.name}" />
|
th:style
|
设置样式 |
th:style="'display:'+@{(${sitrue}?'none':'inline-block')} + ''"
|
th:onclick
|
点击事件 |
th:onclick="'getInfo()'"
|
th:if
|
条件判断 |
<a th:if="${userId == collect.userId}" >
|
th:href
|
超链接 |
<a th:href="@{/blogger/login}">Login</a> />
|
th:unless
|
条件判断和th:if 相反
|
<a th:href="@{/blogger/login}" th:unless=${session.user != null}>Login</a>
|
th:switch
|
配合th:case
|
<div th:switch="${user.role}">
|
th:case
|
配合th:switch
|
<p th:case="'admin'">administator</p>
|
th:src
|
地址引入 |
<img alt="csdn logo" th:src="@{/img/logo.png}" />
|
th:action
|
表单提交的地址 |
<form th:action="@{/blogger/update}">
|
5. 总结
第08课:Spring Boot中的全局异常处理
1. 定义返回的统一 json 结构
public class JsonResult {/*** 异常码*/protected String code;/*** 异常信息*/protected String msg;public JsonResult() {this.code = "200";this.msg = "操作成功";}public JsonResult(String code, String msg) {this.code = code;this.msg = msg;}// get set
}
2. 处理系统异常
新建一个 GlobalExceptionHandler 全局异常处理类,然后加上 @ControllerAdvice
注解即可拦截项目中抛出的异常,如下:
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {// 打印logprivate static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);// ……
}
2.1 处理参数缺失异常
参数缺失的时候,会抛出 HttpMessageNotReadableException
,我们可以拦截该异常,做一个友好处理,如下:
/**
* 缺少请求参数异常
* @param ex HttpMessageNotReadableException
* @return
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public JsonResult handleHttpMessageNotReadableException(MissingServletRequestParameterException ex) {logger.error("缺少请求参数,{}", ex.getMessage());return new JsonResult("400", "缺少必要的请求参数");
}
我们来写个简单的 Controller 测试一下该异常,通过 POST 请求方式接收两个参数:姓名和密码。
@RestController
@RequestMapping("/exception")
public class ExceptionController {private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);@PostMapping("/test")public JsonResult test(@RequestParam("name") String name,@RequestParam("pass") String pass) {logger.info("name:{}", name);logger.info("pass:{}", pass);return new JsonResult();}
}
然后使用 Postman 来调用一下该接口,调用的时候,只传姓名,不传密码,就会抛缺少参数异常,该异常被捕获之后,就会进入我们写好的逻辑,给调用方返回一个友好信息,如下:
2.2 处理空指针异常
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);/*** 空指针异常* @param ex NullPointerException* @return*/@ExceptionHandler(NullPointerException.class)@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)public JsonResult handleTypeMismatchException(NullPointerException ex) {logger.error("空指针异常,{}", ex.getMessage());return new JsonResult("500", "空指针异常了");}
}
{"code":"500","msg":"空指针异常了"}
2.3 一劳永逸?
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);/*** 系统异常 预期以外异常* @param ex* @return*/@ExceptionHandler(Exception.class)@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)public JsonResult handleUnexpectedServer(Exception ex) {logger.error("系统异常:", ex);return new JsonResult("500", "系统发生异常,请联系管理员");}
}
3. 拦截自定义异常
3.1 定义异常信息
由于在业务中,有很多异常,针对不同的业务,可能给出的提示信息不同,所以为了方便项目异常信息管理,我们一般会定义一个异常信息枚举类。比如:
/*** 业务异常提示信息枚举类* @author shengwu ni*/
public enum BusinessMsgEnum {/** 参数异常 */PARMETER_EXCEPTION("102", "参数异常!"),/** 等待超时 */SERVICE_TIME_OUT("103", "服务调用超时!"),/** 参数过大 */PARMETER_BIG_EXCEPTION("102", "输入的图片数量不能超过50张!"),/** 500 : 一劳永逸的提示也可以在这定义 */UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!");// 还可以定义更多的业务异常/*** 消息码*/private String code;/*** 消息内容*/private String msg;private BusinessMsgEnum(String code, String msg) {this.code = code;this.msg = msg;}// set get方法
}
3.2 拦截自定义异常
然后我们可以定义一个业务异常,当出现业务异常时,我们就抛这个自定义的业务异常即可。比如我们定义一个 BusinessErrorException 异常,如下:
/*** 自定义业务异常* @author shengwu ni*/
public class BusinessErrorException extends RuntimeException {private static final long serialVersionUID = -7480022450501760611L;/*** 异常码*/private String code;/*** 异常提示信息*/private String message;public BusinessErrorException(BusinessMsgEnum businessMsgEnum) {this.code = businessMsgEnum.code();this.message = businessMsgEnum.msg();}// get set方法
}
在构造方法中,传入我们上面自定义的异常枚举类,所以在项目中,如果有新的异常信息需要添加,我们直接在枚举类中添加即可,很方便,做到统一维护,然后再拦截该异常时获取即可。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);/*** 拦截业务异常,返回业务异常信息* @param ex* @return*/@ExceptionHandler(BusinessErrorException.class)@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)public JsonResult handleBusinessError(BusinessErrorException ex) {String code = ex.getCode();String message = ex.getMessage();return new JsonResult(code, message);}
}
@RestController
@RequestMapping("/exception")
public class ExceptionController {private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);@GetMapping("/business")public JsonResult testException() {try {int i = 1 / 0;} catch (Exception e) {throw new BusinessErrorException(BusinessMsgEnum.UNEXPECTED_EXCEPTION);}return new JsonResult();}
}
运行一下项目,测试一下,返回 json 如下,说明我们自定义的业务异常捕获成功:
{"code":"500","msg":"系统发生异常,请联系管理员!"}
4. 总结
第09课:Spring Boot中的切面AOP处理
1. 什么是AOP
2. Spring Boot 中的 AOP 处理
2.1 AOP 依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 实现 AOP 切面
@Aspect
@Component
public class LogAspectHandler {}
1.@Pointcut:定义一个切面,即上面所描述的关注的某件事入口。
2.@Before:在做某件事之前做的事。
3.@After:在做某件事之后做的事。
4.@AfterReturning:在做某件事之后,对其返回值做增强处理。
5.@AfterThrowing:在做某件事抛出异常时,处理。
2.2.1 @Pointcut 注解
@Pointcut
注解:用来定义一个切面(切入点),即上文中所关注的某件事情的入口。切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。
@Aspect
@Component
public class LogAspectHandler {/*** 定义一个切面,拦截com.itcodai.course09.controller包和子包下的所有方法*/@Pointcut("execution(* com.itcodai.course09.controller..*.*(..))")public void pointCut() {}
}
@Pointcut
注解指定一个切面,定义需要拦截的东西,这里介绍两个常用的表达式:一个是使用 execution()
,另一个是使用 annotation()
。
以 execution(* com.itcodai.course09.controller..*.*(..)))
表达式为例,语法如下:
execution()
为表达式主体
第一个*
号的位置:表示返回值类型,*
表示所有类型
包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.itcodai.course09.controller
包、子包下所有类的方法
第二个*
号的位置:表示类名,*
表示所有类
*(..)
:这个星号表示方法名,*
表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
annotation()
方式是针对某个注解来定义切面,比如我们对具有@GetMapping
注解的方法做切面,可以如下定义切面:
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void annotationCut() {}
然后使用该切面的话,就会切入注解是 @GetMapping
的方法。因为在实际项目中,可能对于不同的注解有不同的逻辑处理,比如 @GetMapping
、@PostMapping
、@DeleteMapping
等。所以这种按照注解的切入方式在实际项目中也很常用。
2.2.2 @Before 注解
@Before
注解指定的方法在切面切入目标方法之前执行,可以做一些 log 处理,也可以做一些信息的统计,比如获取用户的请求 url 以及用户的 ip 地址等等,这个在做个人站点的时候都能用得到,都是常用的方法。例如:
@Aspect
@Component
public class LogAspectHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());/*** 在上面定义的切面方法之前执行该方法* @param joinPoint jointPoint*/@Before("pointCut()")public void doBefore(JoinPoint joinPoint) {logger.info("====doBefore方法进入了====");// 获取签名Signature signature = joinPoint.getSignature();// 获取切入的包名String declaringTypeName = signature.getDeclaringTypeName();// 获取即将执行的方法名String funcName = signature.getName();logger.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);// 也可以用来记录一些信息,比如获取请求的url和ipServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();// 获取请求urlString url = request.getRequestURL().toString();// 获取请求ipString ip = request.getRemoteAddr();logger.info("用户请求的url为:{},ip地址为:{}", url, ip);}
}
JointPoint 对象很有用,可以用它来获取一个签名,然后利用签名可以获取请求的包名、方法名,包括参数(通过 joinPoint.getArgs()
获取)等等。
2.2.3 @After 注解
@After
注解和 @Before
注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 log 处理。
@Aspect
@Component
public class LogAspectHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());/*** 定义一个切面,拦截com.itcodai.course09.controller包下的所有方法*/@Pointcut("execution(* com.itcodai.course09.controller..*.*(..))")public void pointCut() {}/*** 在上面定义的切面方法之后执行该方法* @param joinPoint jointPoint*/@After("pointCut()")public void doAfter(JoinPoint joinPoint) {logger.info("====doAfter方法进入了====");Signature signature = joinPoint.getSignature();String method = signature.getName();logger.info("方法{}已经执行完", method);}
}
到这里,我们来写一个 Controller 来测试一下执行结果,新建一个 AopController 如下:
@RestController
@RequestMapping("/aop")
public class AopController {@GetMapping("/{name}")public String testAop(@PathVariable String name) {return "Hello " + name;}
}
启动项目,在浏览器中输入 localhost:8080/aop/CSDN
,观察一下控制台的输出信息:
====doBefore方法进入了====
即将执行方法为: testAop,属于com.itcodai.course09.controller.AopController包
用户请求的url为:http://localhost:8080/aop/name,ip地址为:0:0:0:0:0:0:0:1
====doAfter方法进入了====
方法testAop已经执行完
从打印出来的 log 中可以看出程序执行的逻辑与顺序,可以很直观的掌握 @Before
和 @After
两个注解的实际作用。
2.2.4 @AfterReturning 注解
@AfterReturning
注解和 @After
有些类似,区别在于 @AfterReturning
注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理,例如:
@Aspect
@Component
public class LogAspectHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());/*** 在上面定义的切面方法返回后执行该方法,可以捕获返回对象或者对返回对象进行增强* @param joinPoint joinPoint* @param result result*/@AfterReturning(pointcut = "pointCut()", returning = "result")public void doAfterReturning(JoinPoint joinPoint, Object result) {Signature signature = joinPoint.getSignature();String classMethod = signature.getName();logger.info("方法{}执行完毕,返回参数为:{}", classMethod, result);// 实际项目中可以根据业务做具体的返回值增强logger.info("对返回参数进行业务上的增强:{}", result + "增强版");}
}
需要注意的是:在 @AfterReturning
注解 中,属性 returning
的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值,在 doAfterReturning
方法中可以对返回值进行增强,可以根据业务需要做相应的封装。我们重启一下服务,再测试一下(多余的 log 我不贴出来了):
方法testAop执行完毕,返回参数为:Hello CSDN
对返回参数进行业务上的增强:Hello CSDN增强版
- 1
- 2
2.2.5 @AfterThrowing 注解
顾名思义,@AfterThrowing
注解是当被切方法执行时抛出异常时,会进入 @AfterThrowing
注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是 throwing
属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。
/*** 使用AOP处理log* @author shengwu ni* @date 2018/05/04 20:24*/
@Aspect
@Component
public class LogAspectHandler {private final Logger logger = LoggerFactory.getLogger(this.getClass());/*** 在上面定义的切面方法执行抛异常时,执行该方法* @param joinPoint jointPoint* @param ex ex*/@AfterThrowing(pointcut = "pointCut()", throwing = "ex")public void afterThrowing(JoinPoint joinPoint, Throwable ex) {Signature signature = joinPoint.getSignature();String method = signature.getName();// 处理异常的逻辑logger.info("执行方法{}出错,异常为:{}", method, ex);}
}
该方法我就不测试了,大家可以自行测试一下。
3. 总结
第10课:Spring Boot集成MyBatis
1. MyBatis 介绍
2. MyBatis 的配置
2.1 依赖导入
Spring Boot 集成 MyBatis,需要导入 mybatis-spring-boot-starter
和 mysql 的依赖,这里我们使用的版本时 1.3.2,如下:
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.2</version>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope>
</dependency>
<!-- 省去其他 -->
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId>
</dependency>
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId>
</dependency>
2.2 properties.yml配置
我们再来看一下,集成 MyBatis 时需要在 properties.yml 配置文件中做哪些基本配置呢?
# 服务端口号
server:port: 8080# 数据库地址
datasource:url: localhost:3306/blog_testspring:datasource: # 数据库配置driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://${datasource.url}?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10username: rootpassword: 123456hikari:maximum-pool-size: 10 # 最大连接池数max-lifetime: 1770000mybatis:# 指定别名设置的包为所有entitytype-aliases-package: com.itcodai.course10.entityconfiguration:map-underscore-to-camel-case: true # 驼峰命名规范mapper-locations: # mapper映射文件位置- classpath:mapper/*.xml
3. 基于 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.itcodai.course10.dao.UserMapper"><resultMap id="BaseResultMap" type="com.itcodai.course10.entity.User"><id column="id" jdbcType="BIGINT" property="id" /><result column="user_name" jdbcType="VARCHAR" property="username" /><result column="password" jdbcType="VARCHAR" property="password" /></resultMap><select id="getUserByName" resultType="User" parameterType="String">select * from user where user_name = #{username}</select>
</mapper>
实体类中有 id,username 和 password,我不在这贴代码,大家可以下载源码查看。UserMapper.java 文件中写一个接口即可:
User getUserByName(String username);
中间省略 service 的代码,我们写一个 Controller 来测试一下:
@RestController
public class TestController {@Resourceprivate UserService userService;@RequestMapping("/getUserByName/{name}")public User getUserByName(@PathVariable String name) {return userService.getUserByName(name);}
}
启动项目,在浏览器中输入:http://localhost:8080/getUserByName/CSDN
即可查询到数据库表中用户名为 CSDN 的用户信息(事先搞两个数据进去即可):
{"id":2,"username":"CSDN","password":"123456"}
@SpringBootApplication
@MapperScan("com.itcodai.course10.dao")
public class Course10Application {public static void main(String[] args) {SpringApplication.run(Course10Application.class, args);}
}
这样的话,com.itcodai.course10.dao
包下的所有 mapper 都会被扫描到了。
4. 基于注解的整合
@Select("select * from user where id = #{id}")
User getUser(Long id);
@Select("select * from user where id = #{id} and user_name=#{name}")
User getUserByIdAndName(@Param("id") Long id, @Param("name") String username);
可以看出,@Param
指定的参数应该要和 sql 中 #{}
取的参数名相同,不同则取不到。可以在 controller 中自行测试一下,接口都在源码中,文章中我就不贴测试代码和结果了。
@Select("select * from user where id = #{id}")
@Results({@Result(property = "username", column = "user_name"),@Result(property = "password", column = "password")
})
User getUser(Long id);
@Results
中的 @Result
注解是用来指定每一个属性和字段的对应关系,这样的话就可以解决上面说的这个问题了。
@Select("select * from user where id = #{id}")
@ResultMap("BaseResultMap")
User getUser(Long id);
@ResultMap
注解中的值从哪来呢?对应的是 UserMapper.xml 文件中定义的 <resultMap>
时对应的 id 值:
<resultMap id="BaseResultMap" type="com.itcodai.course10.entity.User">
这种 xml 和注解结合着使用的情况也很常见,而且也减少了大量的代码,因为 xml 文件可以使用自动生成工具去生成,也不需要人为手动敲,所以这种使用方式也很常见。
5. 总结
第11课:Spring Boot事务配置管理
1. 事务相关
2. Spring Boot 事务配置
2.1 依赖导入
在 Spring Boot 中使用事务,需要导入 mysql 依赖:
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.2</version>
</dependency>
2.2 事务的测试
id | user_name | password |
---|---|---|
1 | 倪升武 | 123456 |
public interface UserMapper {@Insert("insert into user (user_name, password) values (#{username}, #{password})")Integer insertUser(User user);
}
@Service
public class UserServiceImpl implements UserService {@Resourceprivate UserMapper userMapper;@Override@Transactionalpublic void isertUser(User user) {// 插入用户信息userMapper.insertUser(user);// 手动抛出异常throw new RuntimeException();}
}
@RestController
public class TestController {@Resourceprivate UserService userService;@PostMapping("/adduser")public String addUser(@RequestBody User user) throws Exception {if (null != user) {userService.isertUser(user);return "success";} else {return "false";}}
}
3. 常见问题总结
这一小节,我专门针对实际项目中经常出现的,和事务相关的细节做一下总结,希望读者在读完之后,能够落实到自己的项目中,能有所受益。
3.1 异常并没有被 ”捕获“ 到
@Service
public class UserServiceImpl implements UserService {@Resourceprivate UserMapper userMapper;@Override@Transactionalpublic void isertUser2(User user) throws Exception {// 插入用户信息userMapper.insertUser(user);// 手动抛出异常throw new SQLException("数据库异常");}
}
3.2 异常被 ”吃“ 掉
那这种怎么解决呢?直接往上抛,给上一层来处理即可,千万不要在事务中把异常自己 ”吃“ 掉。
3.3 事务的范围
@Service
public class UserServiceImpl implements UserService {@Resourceprivate UserMapper userMapper;@Override@Transactional(rollbackFor = Exception.class)public synchronized void isertUser4(User user) {// 实际中的具体业务……userMapper.insertUser(user);}
}
但是在压测时,就会出现上面的问题,数据库中确实有两条同一用户的信息,分析其原因,在于事务的范围和锁的范围问题。
这个问题可以避免,第一,把事务去掉即可(不推荐);第二,在调用该 service 的地方加锁,保证锁的范围比事务的范围大即可。
4. 总结
第12课:Spring Boot中使用监听器
1. 监听器介绍
2. Spring Boot中监听器的使用
2.1 监听Servlet上下文对象
下面我们针对这个功能,来写一个 demo,在实际中,读者可以完全套用该代码,来实现自己项目中的相关逻辑。首先写一个 Service,模拟一下从数据库查询数据:
@Service
public class UserService {/*** 获取用户信息* @return*/public User getUser() {// 实际中会根据具体的业务场景,从数据库中查询对应的信息return new User(1L, "倪升武", "123456");}
}
/*** 使用ApplicationListener来初始化一些数据到application域中的监听器* @author shengni ni* @date 2018/07/05*/
@Component
public class MyServletContextListener implements ApplicationListener<ContextRefreshedEvent> {@Overridepublic void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {// 先获取到application上下文ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();// 获取对应的serviceUserService userService = applicationContext.getBean(UserService.class);User user = userService.getUser();// 获取application域对象,将查到的信息放到application域中ServletContext application = applicationContext.getBean(ServletContext.class);application.setAttribute("user", user);}
}
@RestController
@RequestMapping("/listener")
public class TestController {@GetMapping("/user")public User getUser(HttpServletRequest request) {ServletContext application = request.getServletContext();return (User) application.getAttribute("user");}
}
2.2 监听HTTP会话 Session对象
/*** 使用HttpSessionListener统计在线用户数的监听器* @author shengwu ni* @date 2018/07/05*/
@Component
public class MyHttpSessionListener implements HttpSessionListener {private static final Logger logger = LoggerFactory.getLogger(MyHttpSessionListener.class);/*** 记录在线的用户数量*/public Integer count = 0;@Overridepublic synchronized void sessionCreated(HttpSessionEvent httpSessionEvent) {logger.info("新用户上线了");count++;httpSessionEvent.getSession().getServletContext().setAttribute("count", count);}@Overridepublic synchronized void sessionDestroyed(HttpSessionEvent httpSessionEvent) {logger.info("用户下线了");count--;httpSessionEvent.getSession().getServletContext().setAttribute("count", count);}
}
@RestController
@RequestMapping("/listener")
public class TestController {/*** 获取当前在线人数,该方法有bug* @param request* @return*/@GetMapping("/total")public String getTotalUser(HttpServletRequest request) {Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");return "当前在线人数:" + count;}
}
@GetMapping("/total2")
public String getTotalUser(HttpServletRequest request, HttpServletResponse response) {Cookie cookie;try {// 把sessionId记录在浏览器中cookie = new Cookie("JSESSIONID", URLEncoder.encode(request.getSession().getId(), "utf-8"));cookie.setPath("/");//设置cookie有效期为2天,设置长一点cookie.setMaxAge( 48*60 * 60);response.addCookie(cookie);} catch (UnsupportedEncodingException e) {e.printStackTrace();}Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");return "当前在线人数:" + count;
}
2.3 监听客户端请求Servlet Request对象
使用监听器获取用户的访问信息比较简单,实现 ServletRequestListener 接口即可,然后通过 request 对象获取一些信息。如下:
/*** 使用ServletRequestListener获取访问信息* @author shengwu ni* @date 2018/07/05*/
@Component
public class MyServletRequestListener implements ServletRequestListener {private static final Logger logger = LoggerFactory.getLogger(MyServletRequestListener.class);@Overridepublic void requestInitialized(ServletRequestEvent servletRequestEvent) {HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();logger.info("session id为:{}", request.getRequestedSessionId());logger.info("request url为:{}", request.getRequestURL());request.setAttribute("name", "倪升武");}@Overridepublic void requestDestroyed(ServletRequestEvent servletRequestEvent) {logger.info("request end");HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();logger.info("request域中保存的name值为:{}", request.getAttribute("name"));}}
这个比较简单,不再赘述,接下来写一个 Controller 测试一下即可。
@GetMapping("/request")
public String getRequestInfo(HttpServletRequest request) {System.out.println("requestListener中的初始化的name数据:" + request.getAttribute("name"));return "success";
}
3. Spring Boot中自定义事件监听
3.1 自定义事件
自定义事件需要继承 ApplicationEvent 对象,在事件中定义一个 User 对象来模拟数据,构造方法中将 User 对象传进来初始化。如下:
/*** 自定义事件* @author shengwu ni* @date 2018/07/05*/
public class MyEvent extends ApplicationEvent {private User user;public MyEvent(Object source, User user) {super(source);this.user = user;}// 省去get、set方法
}
3.2 自定义监听器
接下来,自定义一个监听器来监听上面定义的 MyEvent 事件,自定义监听器需要实现 ApplicationListener
接口即可。如下:
OK,定义好了事件和监听器之后,需要手动发布事件,这样监听器才能监听到,这需要根据实际业务场景来触发,针对本文的例子,我写个触发逻辑,如下:
@GetMapping("/request")
public String getRequestInfo(HttpServletRequest request) {System.out.println("requestListener中的初始化的name数据:" + request.getAttribute("name"));return "success";
}
在浏览器中输入 http://localhost:8080/listener/publish
,然后观察一下控制台打印的用户名和密码,即可说明自定义监听器已经生效。
4. 总结
第13课:Spring Boot中使用拦截器
1. 拦截器的快速使用
使用拦截器很简单,只需要两步即可:定义拦截器和配置拦截器。在配置拦截器中,Spring Boot 2.0 以后的版本和之前的版本有所不同,我会重点讲解一下这里可能出现的坑。
1.1 定义拦截器
preHandle(……)
方法:该方法的执行时机是,当某个 url 已经匹配到对应的 Controller 中的某个方法,且在这个方法执行之前。所以preHandle(……)
方法可以决定是否将请求放行,这是通过返回值来决定的,返回 true 则放行,返回 false 则不会向后执行。
postHandle(……)
方法:该方法的执行时机是,当某个 url 已经匹配到对应的 Controller 中的某个方法,且在执行完了该方法,但是在 DispatcherServlet 视图渲染之前。所以在这个方法中有个 ModelAndView 参数,可以在此做一些修改动作。
afterCompletion(……)
方法:顾名思义,该方法是在整个请求处理完成后(包括视图渲染)执行,这时做一些资源的清理工作,这个方法只有在preHandle(……)
被成功执行后并且返回 true 才会被执行。
了解了该接口,接下来自定义一个拦截器。
/*** 自定义拦截器* @author shengwu ni* @date 2018/08/03*/
public class MyInterceptor implements HandlerInterceptor {private static final Logger logger = LoggerFactory.getLogger(MyInterceptor.class);@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();String methodName = method.getName();logger.info("====拦截到了方法:{},在该方法执行之前执行====", methodName);// 返回true才会继续执行,返回false则取消当前请求return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {logger.info("执行完方法之后进执行(Controller方法调用之后),但是此时还没进行视图渲染");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {logger.info("整个请求都处理完咯,DispatcherServlet也渲染了对应的视图咯,此时我可以做一些清理的工作了");}
}
OK,到此为止,拦截器已经定义完成,接下来就是对该拦截器进行拦截配置。
1.2 配置拦截器
@Configuration
public class MyInterceptorConfig extends WebMvcConfigurationSupport {@Overrideprotected void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");super.addInterceptors(registry);}
}
@Controller
@RequestMapping("/interceptor")
public class InterceptorController {@RequestMapping("/test")public String test() {return "hello";}
}
====拦截到了方法:test,在该方法执行之前执行====
执行完方法之后进执行(Controller方法调用之后),但是此时还没进行视图渲染
整个请求都处理完咯,DispatcherServlet也渲染了对应的视图咯,此时我可以做一些清理的工作了
1.3 解决静态资源被拦截问题
如何放开呢?除了在 MyInterceptorConfig 配置类中重写 addInterceptors
方法外,还需要再重写一个方法:addResourceHandlers
,将静态资源放开:
/*** 用来指定静态资源不被拦截,否则继承WebMvcConfigurationSupport这种方式会导致静态资源无法直接访问* @param registry*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");super.addResourceHandlers(registry);
}
这样配置好之后,重启项目,静态资源也可以正常访问了。如果你是个善于学习或者研究的人,那肯定不会止步于此,没错,上面这种方式的确能解决静态资源无法访问的问题,但是,还有更方便的方式来配置。
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 实现WebMvcConfigurer不会导致静态资源被拦截registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");}
}
这样就非常方便了,实现 WebMvcConfigure 接口的话,不会拦截 Spring Boot 默认的静态资源。
2. 拦截器使用实例
2.1 判断用户有没有登录
2.2 取消拦截操作
是可以的,我们可以定义一个注解,该注解专门用来取消拦截操作,如果某个 Controller 中的方法我们不需要拦截掉,即可在该方法上加上我们自定义的注解即可,下面先定义一个注解:
/*** 该注解用来指定某个方法不用拦截*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UnInterception {
}
然后在 Controller 中的某个方法上添加该注解,在拦截器处理方法中添加该注解取消拦截的逻辑,如下:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();String methodName = method.getName();logger.info("====拦截到了方法:{},在该方法执行之前执行====", methodName);// 通过方法,可以获取该方法上的自定义注解,然后通过注解来判断该方法是否要被拦截// @UnInterception 是我们自定义的注解UnInterception unInterception = method.getAnnotation(UnInterception.class);if (null != unInterception) {return true;}// 返回true才会继续执行,返回false则取消当前请求return true;
}
3. 总结
第14课:Spring Boot 中集成Redis
1. Redis 介绍
2. Redis 安装
因为后面安装redis的时候需要编译,所以事先得先安装gcc编译。阿里云主机已经默认安装了 gcc,如果是自己安装的虚拟机,那么需要先安装一下 gcc:
yum install gcc-c++
有两种方式下载安装包,一种是去官网上下载(https://redis.io),然后将安装包考到 centos 中,另种方法是直接使用 wget 来下载:
wget http://download.redis.io/releases/redis-3.2.8.tar.gz
yum install wget
tar –vzxf redis-3.2.8.tar.gz
make MALLOC=libc
make install
bind 0.0.0.0
使用同样的方法,将 daemonize 改成 yes (默认为 no),允许 redis 在后台执行。
将 requirepass 注释打开,并设置密码为 123456(密码自己设置)。
在 redis-3.2.8 目录下,指定刚刚修改好的配置文件 redis.conf 来启动 redis:
redis-server ./redis.conf
redis-cli
由于我们设置了密码,在启动客户端之后,输入 auth 123456
即可登录进入客户端。
然后我们来测试一下,往 redis 中插入一个数据:
set name CSDN
get name
3. Spring Boot 集成 Redis
3.1 依赖导入
Spring Boot 集成 redis 很方便,只需要导入一个 redis 的 starter 依赖即可。如下:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--阿里巴巴fastjson -->
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.35</version>
</dependency>
这里也导入阿里巴巴的 fastjson 是为了在后面我们要存一个实体,为了方便把实体转换成 json 字符串存进去。
3.2 Redis 配置
导入了依赖之后,我们在 application.yml 文件里配置 redis:
server:port: 8080
spring:#redis相关配置redis:database: 5# 配置redis的主机地址,需要修改成自己的host: 192.168.48.190port: 6379password: 123456timeout: 5000jedis:pool:# 连接池中的最大空闲连接,默认值也是8。max-idle: 500# 连接池中的最小空闲连接,默认值也是0。min-idle: 50# 如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)max-active: 1000# 等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionExceptionmax-wait: 2000
3.3 常用 api 介绍
Spring Boot 对 redis 的支持已经非常完善了,丰富的 api 已经足够我们日常的开发,这里我介绍几个最常用的供大家学习,其他 api 希望大家自己多学习,多研究。用到会去查即可。
3.3.1 redis:string 类型
新建一个 RedisService,注入 StringRedisTemplate,使用 stringRedisTemplate.opsForValue()
可以获取 ValueOperations<String, String>
对象,通过该对象即可读写 redis 数据库了。如下:
public class RedisService {@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** set redis: string类型* @param key key* @param value value*/public void setString(String key, String value){ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();valueOperations.set(key, value);}/*** get redis: string类型* @param key key* @return*/public String getString(String key){return stringRedisTemplate.opsForValue().get(key);}
该对象操作的是 string,我们也可以存实体类,只需要将实体类转换成 json 字符串即可。下面来测试一下:
@RunWith(SpringRunner.class)
@SpringBootTest
public class Course14ApplicationTests {private static final Logger logger = LoggerFactory.getLogger(Course14ApplicationTests.class);@Resourceprivate RedisService redisService;@Testpublic void contextLoads() {//测试redis的string类型redisService.setString("weichat","我是彭于晏");logger.info("打印的日志:{}", redisService.getString("weichat"));// 如果是个实体,我们可以使用json工具转成json字符串,User user = new User("CSDN", "123456");redisService.setString("userInfo", JSON.toJSONString(user));logger.info("用户信息:{}", redisService.getString("userInfo"));}
}
先启动 redis,然后运行这个测试用例,观察控制台打印的日志如下:3.3.2 redis:hash 类型
hash 类型其实原理和 string 一样的,但是有两个 key,使用 stringRedisTemplate.opsForHash()
可以获取 HashOperations<String, Object, Object>
对象。比如我们要存储订单信息,所有订单信息都放在 order 下,针对不同用户的订单实体,可以通过用户的 id 来区分,这就相当于两个 key 了。
可以看出,hash 和 string 没啥两样,只不过多了个参数,Spring Boot 中操作 redis 非常简单方便。来测试一下:
@SpringBootTest
public class Course14ApplicationTests {private static final Logger logger = LoggerFactory.getLogger(Course14ApplicationTests.class);@Resourceprivate RedisService redisService;@Testpublic void contextLoads() {//测试redis的hash类型redisService.setHash("user", "name", JSON.toJSONString(user));logger.info("用户姓名:{}", redisService.getHash("user","name"));}
}
3.3.3 redis:list 类型
使用 stringRedisTemplate.opsForList()
可以获取 ListOperations<String, String> listOperations
redis 列表对象,该列表是个简单的字符串列表,可以支持从左侧添加,也可以支持从右侧添加,一个列表最多包含 2 ^ 32 -1 个元素。
@Service
public class RedisService {@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** set redis:list类型* @param key key* @param value value* @return*/public long setList(String key, String value){ListOperations<String, String> listOperations = stringRedisTemplate.opsForList();return listOperations.leftPush(key, value);}/*** get redis:list类型* @param key key* @param start start* @param end end* @return*/public List<String> getList(String key, long start, long end){return stringRedisTemplate.opsForList().range(key, start, end);}
}
可以看出,这些 api 都是一样的形式,方便记忆也方便使用。具体的 api 细节我就不展开了,大家可以自己看 api 文档。其实,这些 api 根据参数和返回值也能知道它们是做什么用的。来测试一下:
@RunWith(SpringRunner.class)
@SpringBootTest
public class Course14ApplicationTests {private static final Logger logger = LoggerFactory.getLogger(Course14ApplicationTests.class);@Resourceprivate RedisService redisService;@Testpublic void contextLoads() {//测试redis的list类型redisService.setList("list", "football");redisService.setList("list", "basketball");List<String> valList = redisService.getList("list",0,-1);for(String value :valList){logger.info("list中有:{}", value);}}
}
4. 总结
第15课: Spring Boot中集成ActiveMQ
1. JMS 和 ActiveMQ 介绍
1.1 JMS 是啥
JMS 即 Java 消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的 API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java 消息服务是一个与具体平台无关的 API,绝大多数 MOM 提供商都对 JMS 提供支持。
JMS 只是接口,不同的提供商或者开源组织对其有不同的实现,ActiveMQ 就是其中之一,它支持JMS,是 Apache 推出的。JMS 中有几个对象模型:
连接工厂:ConnectionFactory
JMS连接:Connection
JMS会话:Session
JMS目的:Destination
JMS生产者:Producer
JMS消费者:Consumer
JMS消息两种类型:点对点和发布/订阅。
可以看出 JMS 实际上和 JDBC 有点类似,JDBC 是可以用来访问许多不同关系数据库的 API,而 JMS 则提供同样与厂商无关的访问方法,以访问消息收发服务。本文主要使用 ActiveMQ。
1.2 ActiveMQ
2. ActiveMQ安装
我们可以看到有 Queues 和 Topics 这两个选项,这两个选项分别是点对点消息和发布/订阅消息的查看窗口。何为点对点消息和发布/订阅消息呢?
发布/订阅消息:消息生产者(发布)将消息发布到 topic 中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到 topic 的消息会被所有订阅者消费。下面分析具体的实现方式。
3. ActiveMQ集成
3.1 依赖导入和配置
在 Spring Boot 中集成 ActiveMQ 需要导入如下 starter 依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
然后在 application.yml 配置文件中,对 activemq 做一下配置:
3.2 Queue 和 Topic 的创建
首先我们需要创建两种消息 Queue 和 Topic,这两种消息的创建,我们放到 ActiveMqConfig 中来创建,如下:
3.3 消息的发送接口
在 Spring Boot 中,我们只要注入 JmsMessagingTemplate 模板即可快速发送消息,如下:
/*** 消息发送者* @author shengwu ni*/
@Service
public class MsgProducer {@Resourceprivate JmsMessagingTemplate jmsMessagingTemplate;public void sendMessage(Destination destination, String msg) {jmsMessagingTemplate.convertAndSend(destination, msg);}
}
convertAndSend
方法中第一个参数是消息发送的目的地,第二个参数是具体的消息内容。
3.4 点对点消息生产与消费
3.4.1 点对点消息的生产
消息的生产,我们放到 Controller 中来做,由于上面已经生成了 Queue 消息的组件,所以在 Controller 中我们直接注入进来即可。然后调用上文的消息发送方法 sendMessage
即可成功生产一条消息。
/*** ActiveMQ controller* @author shengwu ni*/
@RestController
@RequestMapping("/activemq")
public class ActiveMqController {private static final Logger logger = LoggerFactory.getLogger(ActiveMqController.class);@Resourceprivate MsgProducer producer;@Resourceprivate Destination queue;@GetMapping("/send/queue")public String sendQueueMessage() {logger.info("===开始发送点对点消息===");producer.sendMessage(queue, "Queue: hello activemq!");return "success";}
}
3.4.2 点对点消息的消费
点对点消息的消费很简单,只要我们指定目的地即可,jms 监听器一直在监听是否有消息过来,如果有,则消费。
可以看出,使用 @JmsListener
注解来指定要监听的目的地,在消息接收方法内部,我们可以根据具体的业务需求做相应的逻辑处理即可。
3.4.3 测试一下
启动项目,在浏览器中输入:http://localhost:8081/activemq/send/queue
,观察控制台的输出日志,出现下面的日志说明消息发送和消费成功。
收到的消息为:Queue: hello activemq!
- 1
3.5 发布/订阅消息的生产和消费
3.5.1 发布/订阅消息的生产
和点对点消息一样,我们注入 topic 并调用 producer 的 sendMessage
方法即可发送订阅消息,如下,不再赘述:
@RestController
@RequestMapping("/activemq")
public class ActiveMqController {private static final Logger logger = LoggerFactory.getLogger(ActiveMqController.class);@Resourceprivate MsgProducer producer;@Resourceprivate Destination topic;@GetMapping("/send/topic")public String sendTopicMessage() {logger.info("===开始发送订阅消息===");producer.sendMessage(topic, "Topic: hello activemq!");return "success";}
}
3.5.2 发布/订阅消息的消费
发布/订阅消息的消费和点对点不同,订阅消息支持多个消费者一起消费。其次,Spring Boot 中默认的时点对点消息,所以在使用 topic 时,会不起作用,我们需要在配置文件 application.yml 中添加一个配置:
spring:jms:pub-sub-domain: true
该配置是 false 的话,则为点对点消息,也是 Spring Boot 默认的。这样是可以解决问题,但是如果这样配置的话,上面提到的点对点消息又不能正常消费了。所以二者不可兼得,这并非一个好的解决办法。
比较好的解决办法是,我们定义一个工厂,@JmsListener
注解默认只接收 queue 消息,如果要接收 topic 消息,需要设置一下 containerFactory。我们还在上面的那个 ActiveMqConfig 配置类中添加:
/*** activemq的配置** @author shengwu ni*/
@Configuration
public class ActiveMqConfig {// 省略其他内容/*** JmsListener注解默认只接收queue消息,如果要接收topic消息,需要设置containerFactory*/@Beanpublic JmsListenerContainerFactory topicListenerContainer(ConnectionFactory connectionFactory) {DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();factory.setConnectionFactory(connectionFactory);// 相当于在application.yml中配置:spring.jms.pub-sub-domain=truefactory.setPubSubDomain(true);return factory;}
}
经过这样的配置之后,我们在消费的时候,在 @JmsListener
注解中指定这个容器工厂即可消费 topic 消息。如下:
指定 containerFactory 属性为上面我们自己配置的 topicListenerContainer 即可。由于 topic 消息可以多个消费,所以该消费的类可以拷贝几个一起测试一下,这里我就不贴代码了,可以参考我的源码测试。
3.5.3 测试一下
启动项目,在浏览器中输入:http://localhost:8081/activemq/send/topic
,观察控制台的输出日志,出现下面的日志说明消息发送和消费成功。
收到的消息为:Topic: hello activemq!
收到的消息为:Topic: hello activemq!
- 1
- 2
4. 总结
第16课:Spring Boot中集成 Shiro
Shiro 是一个强大、简单易用的 Java 安全框架,主要用来更便捷的认证,授权,加密,会话管等等,可为任何应用提供安全保障。本课程主要来介绍 Shiro 的认证和授权功能。
1. Shiro 三大核心组件
Shiro 有三大核心的组件:Subject
、SecurityManager
和 Realm
。先来看一下它们之间的关系。
Principals:身份。可以是用户名,邮件,手机号码等等,用来标识一个登录主体身份;
Credentials:凭证。常见有密码,数字证书等等。
说白了,就是需要认证的东西,最常见的就是用户名密码了,比如用户在登录的时候,Shiro 需要去进行身份认证,就需要 Subject 认证主体。
SecurityManager:安全管理员。这是 Shiro 架构的核心,它就像 Shiro 内部所有原件的保护伞一样。我们在项目中一般都会配置 SecurityManager,开发人员大部分精力主要是在 Subject 认证主体上面。我们在与 Subject 进行交互的时候,实际上是 SecurityManager 在背后做一些安全操作。
Realms:Realms 是一个域,它是连接 Shiro 和具体应用的桥梁,当需要与安全数据交互的时候,比如用户账户、访问控制等,Shiro 就会从一个或多个 Realms 中去查找。我们一般会自己定制 Realm,这在下文会详细说明。
1. Shiro 身份和权限认证
1.2 Shiro 身份认证
我们来分析一下 Shiro 身份认证的过程,看一下官方的一个认证图:
Step1:应用程序代码在调用 Subject.login(token)
方法后,传入代表最终用户的身份和凭证的 AuthenticationToken 实例 token。
Step2:将 Subject 实例委托给应用程序的 SecurityManager(Shiro的安全管理)来开始实际的认证工作。这里开始真正的认证工作了。
Step3,4,5:然后 SecurityManager 就会根据具体的 realm 去进行安全认证了。 从图中可以看出,realm 可以自定义(Custom Realm)。
1.3 Shiro 权限认证
权限认证,也就是访问控制,即在应用中控制谁能访问哪些资源。在权限认证中,最核心的三个要素是:权限,角色和用户。
权限(permission):即操作资源的权利,比如访问某个页面,以及对某个模块的数据的添加,修改,删除,查看的权利;
角色(role):指的是用户担任的的角色,一个角色可以有多个权限;
用户(user):在 Shiro 中,代表访问系统的用户,即上面提到的 Subject 认证主体。
它们之间的的关系可以用下图来表示:
一个用户可以有多个角色,而不同的角色可以有不同的权限,也可由有相同的权限。比如说现在有三个角色,1是普通角色,2也是普通角色,3是管理员,角色1只能查看信息,角色2只能添加信息,管理员都可以,而且还可以删除信息,类似于这样。
2. Spring Boot 集成 Shiro 过程
2.1 依赖导入
Spring Boot 2.x 集成 Shiro 需要导入如下 starter 依赖:
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.4.0</version>
</dependency>
2.2 数据库表数据初始化
这里主要涉及到三张表:用户表、角色表和权限表,其实在 demo 中,我们完全可以自己模拟一下,不用建表,但是为了更加接近实际情况,我们还是加入 mybatis,来操作数据库。下面是数据库表的脚本。
CREATE TABLE `t_role` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',`rolename` varchar(20) DEFAULT NULL COMMENT '角色名称',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8CREATE TABLE `t_user` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户主键',`username` varchar(20) NOT NULL COMMENT '用户名',`password` varchar(20) NOT NULL COMMENT '密码',`role_id` int(11) DEFAULT NULL COMMENT '外键关联role表',PRIMARY KEY (`id`),KEY `role_id` (`role_id`),CONSTRAINT `t_user_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8CREATE TABLE `t_permission` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',`permissionname` varchar(50) NOT NULL COMMENT '权限名',`role_id` int(11) DEFAULT NULL COMMENT '外键关联role',PRIMARY KEY (`id`),KEY `role_id` (`role_id`),CONSTRAINT `t_permission_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
其中,t_user,t_role 和 t_permission,分别存储用户信息,角色信息和权限信息,表建立好了之后,我们往表里插入一些测试数据。
t_user 表:
id | username | password | role_id |
---|---|---|---|
1 | csdn1 | 123456 | 1 |
2 | csdn2 | 123456 | 2 |
3 | csdn3 | 123456 | 3 |
id | rolename |
---|---|
1 | admin |
2 | teacher |
3 | student |
id | permissionname | role_id |
---|---|---|
1 |
user:*
|
1 |
2 |
student:*
|
2 |
解释一下这里的权限:user:*
表示权限可以是 user:create
或者其他,*
处表示一个占位符,我们可以自己定义,具体的会在下文 Shiro 配置那里说明。
2.2 自定义 Realm
doGetAuthenticationInfo()
方法:用来验证当前登录的用户,获取认证信息
doGetAuthorizationInfo()
方法:用来为当前登陆成功的用户授予权限和角色
具体实现如下,相关的解释我放在代码的注释中,这样更加方便直观:
/*** 自定义realm* @author shengwu ni*/
public class MyRealm extends AuthorizingRealm {@Resourceprivate UserService userService;@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {// 获取用户名String username = (String) principalCollection.getPrimaryPrincipal();SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();// 给该用户设置角色,角色信息存在t_role表中取authorizationInfo.setRoles(userService.getRoles(username));// 给该用户设置权限,权限信息存在t_permission表中取authorizationInfo.setStringPermissions(userService.getPermissions(username));return authorizationInfo;}@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {// 根据token获取用户名,如果您不知道该该token怎么来的,先可以不管,下文会解释String username = (String) authenticationToken.getPrincipal();// 根据用户名从数据库中查询该用户User user = userService.getByUsername(username);if(user != null) {// 把当前用户存到session中SecurityUtils.getSubject().getSession().setAttribute("user", user);// 传入用户名和密码进行身份认证,并返回认证信息AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), "myRealm");return authcInfo;} else {return null;}}
}
从上面两个方法中可以看出:验证身份的时候是根据用户输入的用户名先从数据库中查出该用户名对应的用户,这时候并没有涉及到密码,也就是说到这一步的时候,即使用户输入的密码不对,也是可以查出来该用户的,然后将该用户的正确信息封装到 authcInfo 中返回给 Shiro,接下来就是Shiro的事了,它会根据这里面的真实信息与用户前台输入的用户名和密码进行校验, 这个时候也要校验密码了,如果校验通过就让用户登录,否则跳转到指定页面。同理,权限验证的时候也是先根据用户名从数据库中获取与该用户名有关的角色和权限,然后封装到 authorizationInfo 中返回给 Shiro。
2.3 Shiro 配置
自定义的 realm 写好了,接下来需要对 Shiro 进行配置了。我们主要配置三个东西:自定义 realm、安全管理器 SecurityManager 和 Shiro 过滤器。如下:
@Configuration
public class ShiroConfig {private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);/*** 注入自定义的realm* @return MyRealm*/@Beanpublic MyRealm myAuthRealm() {MyRealm myRealm = new MyRealm();logger.info("====myRealm注册完成=====");return myRealm;}
}
@Configuration
public class ShiroConfig {private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);/*** 注入安全管理器* @return SecurityManager*/@Beanpublic SecurityManager securityManager() {// 将自定义realm加进来DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(myAuthRealm());logger.info("====securityManager注册完成====");return securityManager;}
}
配置 SecurityManager 时,需要将上面的自定义 realm 添加进来,这样的话 Shiro 才会走到自定义的 realm 中。
@Configuration
public class ShiroConfig {private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);/*** 注入Shiro过滤器* @param securityManager 安全管理器* @return ShiroFilterFactoryBean*/@Beanpublic ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {// 定义shiroFactoryBeanShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();// 设置自定义的securityManagershiroFilterFactoryBean.setSecurityManager(securityManager);// 设置默认登录的url,身份认证失败会访问该urlshiroFilterFactoryBean.setLoginUrl("/login");// 设置成功之后要跳转的链接shiroFilterFactoryBean.setSuccessUrl("/success");// 设置未授权界面,权限认证失败会访问该urlshiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");// LinkedHashMap是有序的,进行顺序拦截器配置Map<String,String> filterChainMap = new LinkedHashMap<>();// 配置可以匿名访问的地址,可以根据实际情况自己添加,放行一些静态资源等,anon表示放行filterChainMap.put("/css/**", "anon");filterChainMap.put("/imgs/**", "anon");filterChainMap.put("/js/**", "anon");filterChainMap.put("/swagger-*/**", "anon");filterChainMap.put("/swagger-ui.html/**", "anon");// 登录url 放行filterChainMap.put("/login", "anon");// “/user/admin” 开头的需要身份认证,authc表示要身份认证filterChainMap.put("/user/admin*", "authc");// “/user/student” 开头的需要角色认证,是“admin”才允许filterChainMap.put("/user/student*/**", "roles[admin]");// “/user/teacher” 开头的需要权限认证,是“user:create”才允许filterChainMap.put("/user/teacher*/**", "perms[\"user:create\"]");// 配置logout过滤器filterChainMap.put("/logout", "logout");// 设置shiroFilterFactoryBean的FilterChainDefinitionMapshiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);logger.info("====shiroFilterFactoryBean注册完成====");return shiroFilterFactoryBean;}
}
默认登录的 url:身份认证失败会访问该 url
认证成功之后要跳转的 url
权限认证失败会访问该 url
需要拦截或者放行的 url:这些都放在一个 map 中
从上述代码中可以看出,在 map 中,针对不同的 url,有不同的权限要求,这里总结一下常用的几个权限。
Filter | 说明 |
---|---|
anon | 开放权限,可以理解为匿名用户或游客,可以直接访问的 |
authc | 需要身份认证的 |
logout |
注销,执行后会直接跳转到 shiroFilterFactoryBean.setLoginUrl(); 设置的 url,即登录页面
|
roles[admin] | 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles[“admin,user”],当有多个参数时必须每个参数都通过才算通过 |
perms[user] | 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms[“user, admin”],当有多个参数时必须每个参数都通过才算通过 |
2.4 使用 Shiro 进行认证
到这里,我们对 Shiro 的准备工作都做完了,接下来开始使用 Shiro 进行认证工作。我们首先来设计几个接口:
接口一: 使用
http://localhost:8080/user/admin
来验证身份认证
接口二: 使用http://localhost:8080/user/student
来验证角色认证
接口三: 使用http://localhost:8080/user/teacher
来验证权限认证
接口四: 使用http://localhost:8080/user/login
来实现用户登录
然后来一下认证的流程:
流程一: 直接访问接口一(此时还未登录),认证失败,跳转到 login.html 页面让用户登录,登录会请求接口四,实现用户登录功能,此时 Shiro 已经保存了用户信息了。
流程二: 再次访问接口一(此时用户已经登录),认证成功,跳转到 success.html 页面,展示用户信息。
流程三: 访问接口二,测试角色认证是否成功。
流程四: 访问接口三,测试权限认证是否成功。
2.4.1 身份、角色、权限认证接口
@Controller
@RequestMapping("/user")
public class UserController {/*** 身份认证测试接口* @param request* @return*/@RequestMapping("/admin")public String admin(HttpServletRequest request) {Object user = request.getSession().getAttribute("user");return "success";}/*** 角色认证测试接口* @param request* @return*/@RequestMapping("/student")public String student(HttpServletRequest request) {return "success";}/*** 权限认证测试接口* @param request* @return*/@RequestMapping("/teacher")public String teacher(HttpServletRequest request) {return "success";}
}
这三个接口很简单,直接返回到指定页面展示即可,只要认证成功就会正常跳转,如果认证失败,就会跳转到上文 ShrioConfig 中配置的页面进行展示。
2.4.2 用户登录接口
@Controller
@RequestMapping("/user")
public class UserController {/*** 用户登录接口* @param user user* @param request request* @return string*/@PostMapping("/login")public String login(User user, HttpServletRequest request) {// 根据用户名和密码创建tokenUsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());// 获取subject认证主体Subject subject = SecurityUtils.getSubject();try{// 开始认证,这一步会跳到我们自定义的realm中subject.login(token);request.getSession().setAttribute("user", user);return "success";}catch(Exception e){e.printStackTrace();request.getSession().setAttribute("user", user);request.setAttribute("error", "用户名或密码错误!");return "login";}}
}
我们重点分析一下这个登录接口,首先会根据前端传过来的用户名和密码,创建一个 token,然后使用 SecurityUtils 来创建一个认证主体,接下来开始调用 subject.login(token)
开始进行身份认证了,注意这里传了刚刚创建的 token,就如注释中所述,这一步会跳转到我们自定义的 realm 中,进入 doGetAuthenticationInfo
方法,所以到这里,您就会明白该方法中那个参数 token 了。然后就是上文分析的那样,开始进行身份认证。
2.4.3 测试一下
最后,启动项目,测试一下:
浏览器请求 http://localhost:8080/user/admin
会进行身份认证,因为此时未登录,所以会跳转到 IndexController 中的 /login
接口,然后跳转到 login.html
页面让我们登录,使用用户名密码为 csdn/123456 登录之后,我们在浏览器中请求 http://localhost:8080/user/student
接口,会进行角色认证,因为数据库中 csdn1 的用户角色是 admin,所以和配置中的吻合,认证通过;我们再请求 http://localhost:8080/user/teacher
接口,会进行权限认证,因为数据库中 csdn1 的用户权限为 user:*
,满足配置中的 user:create
,所以认证通过。
接下来,我们点退出,系统会注销重新让我们登录,我们使用 csdn2 这个用户来登录,重复上述操作,当在进行角色认证和权限认证这两步时,就认证不通过了,因为数据库中 csdn2 这个用户存的角色和权限与配置中的不同,所以认证不通过。
3. 总结
第17课:Spring Boot中集成Lucence
1. Lucence 和全文检索
Lucene是一套用于全文检索和搜寻的开源程式库,由 Apache 软件基金会支持和提供。Lucene 提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在 Java 开发环境里 Lucene 是一个成熟的免费开源工具。就其本身而言,Lucene 是当前以及最近几年最受欢迎的免费 Java 信息检索程序库。——《百度百科》
1.1 全文检索
这里提到了全文检索的概念,我们先来分析一下什么是全文检索,理解了全文检索之后,再理解 Lucene 的原理就非常简单了。
1.2 Lucene 建立索引的方式
那么 Lucene 中是如何建立索引的呢?假设现在有两篇文章,内容如下:
文章1的内容为:Tom lives in Guangzhou, I live in Guangzhou too.
文章2的内容为:He once lived in Shanghai.
首先第一步是将文档传给分词组件(Tokenizer),分词组件会将文档分成一个个单词,并去除标点符号和停词。所谓的停词指的是没有特别意义的词,比如英文中的 a,the,too 等。经过分词后,得到词元(Token) 。如下:
文章1经过分词后的结果:
[Tom]
[lives]
[Guangzhou]
[I]
[live]
[Guangzhou]
文章2经过分词后的结果:[He]
[lives]
[Shanghai]
然后将词元传给语言处理组件(Linguistic Processor),对于英语,语言处理组件一般会将字母变为小写,将单词缩减为词根形式,如 ”lives” 到 ”live” 等,将单词转变为词根形式,如 ”drove” 到 ”drive” 等。然后得到词(Term)。如下:
文章1经过处理后的结果:
[tom]
[live]
[guangzhou]
[i]
[live]
[guangzhou]
文章2经过处理后的结果:[he]
[live]
[shanghai]
最后将得到的词传给索引组件(Indexer),索引组件经过处理,得到下面的索引结构:
关键词 | 文章号[出现频率] | 出现位置 |
---|---|---|
guangzhou | 1[2] | 3,6 |
he | 2[1] | 1 |
i | 1[1] | 4 |
live | 1[2],2[1] | 2,5,2 |
shanghai | 2[1] | 3 |
tom | 1[1] | 1 |
以上就是Lucene 索引结构中最核心的部分。它的关键字是按字符顺序排列的,因此 Lucene 可以用二元搜索算法快速定位关键词。实现时 Lucene 将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)和位置文件(positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。
搜索的过程是先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果,然后就可以在具体的文章中根据出现位置找到该词了。所以 Lucene 在第一次建立索引的时候可能会比较慢,但是以后就不需要每次都建立索引了,就快了。
理解了 Lucene 的分词原理,接下来我们在 Spring Boot 中集成 Lucene 并实现索引和搜索的功能。
2. Spring Boot 中集成 Lucence
2.1 依赖导入
首先需要导入 Lucene 的依赖,它的依赖有好几个,如下:
<!-- Lucence核心包 -->
<dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-core</artifactId><version>5.3.1</version>
</dependency><!-- Lucene查询解析包 -->
<dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-queryparser</artifactId><version>5.3.1</version>
</dependency><!-- 常规的分词(英文) -->
<dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-analyzers-common</artifactId><version>5.3.1</version>
</dependency><!--支持分词高亮 -->
<dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-highlighter</artifactId><version>5.3.1</version>
</dependency><!--支持中文分词 -->
<dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-analyzers-smartcn</artifactId><version>5.3.1</version>
</dependency>
最后一个依赖是用来支持中文分词的,因为默认是支持英文的。那个高亮的分词依赖是最后我要做一个搜索,然后将搜到的内容高亮显示,模拟当前互联网上的做法,大家可以运用到实际项目中去。
2.2 快速入门
根据上文的分析,全文检索有两个步骤,先建立索引,再检索。所以为了测试这个过程,我新建两个 java 类,一个用来建立索引的,另一个用来检索。
2.2.1 建立索引
我们自己弄几个文件,放到 D:\lucene\data
目录下,新建一个 Indexer 类来实现建立索引功能。首先在构造方法中初始化标准分词器和写索引实例。
public class Indexer {/*** 写索引实例*/private IndexWriter writer;/*** 构造方法,实例化IndexWriter* @param indexDir* @throws Exception*/public Indexer(String indexDir) throws Exception {Directory dir = FSDirectory.open(Paths.get(indexDir));//标准分词器,会自动去掉空格啊,is a the等单词Analyzer analyzer = new StandardAnalyzer();//将标准分词器配到写索引的配置中IndexWriterConfig config = new IndexWriterConfig(analyzer);//实例化写索引对象writer = new IndexWriter(dir, config);}
}
在构造放发中传一个存放索引的文件夹路径,然后构建标准分词器(这是英文的),再使用标准分词器来实例化写索引对象。接下来就开始建立索引了,我将解释放到代码注释里,方便大家跟进。
/*** 索引指定目录下的所有文件* @param dataDir* @return* @throws Exception*/
public int indexAll(String dataDir) throws Exception {// 获取该路径下的所有文件File[] files = new File(dataDir).listFiles();if (null != files) {for (File file : files) {//调用下面的indexFile方法,对每个文件进行索引indexFile(file);}}//返回索引的文件数return writer.numDocs();
}/*** 索引指定的文件* @param file* @throws Exception*/
private void indexFile(File file) throws Exception {System.out.println("索引文件的路径:" + file.getCanonicalPath());//调用下面的getDocument方法,获取该文件的documentDocument doc = getDocument(file);//将doc添加到索引中writer.addDocument(doc);
}/*** 获取文档,文档里再设置每个字段,就类似于数据库中的一行记录* @param file* @return* @throws Exception*/
private Document getDocument(File file) throws Exception {Document doc = new Document();//开始添加字段//添加内容doc.add(new TextField("contents", new FileReader(file)));//添加文件名,并把这个字段存到索引文件里doc.add(new TextField("fileName", file.getName(), Field.Store.YES));//添加文件路径doc.add(new TextField("fullPath", file.getCanonicalPath(), Field.Store.YES));return doc;
}
这样就建立好索引了,我们在该类中写一个 main 方法测试一下:
public static void main(String[] args) {//索引保存到的路径String indexDir = "D:\\lucene";//需要索引的文件数据存放的目录String dataDir = "D:\\lucene\\data";Indexer indexer = null;int indexedNum = 0;//记录索引开始时间long startTime = System.currentTimeMillis();try {// 开始构建索引indexer = new Indexer(indexDir);indexedNum = indexer.indexAll(dataDir);} catch (Exception e) {e.printStackTrace();} finally {try {if (null != indexer) {indexer.close();}} catch (Exception e) {e.printStackTrace();}}//记录索引结束时间long endTime = System.currentTimeMillis();System.out.println("索引耗时" + (endTime - startTime) + "毫秒");System.out.println("共索引了" + indexedNum + "个文件");}
我搞了两个 tomcat 相关的文件放到 D:\lucene\data
下了,执行完之后,看到控制台输出:
索引文件的路径:D:\lucene\data\catalina.properties
索引文件的路径:D:\lucene\data\logging.properties
索引耗时882毫秒
共索引了2个文件
然后我们去 D:\lucene\
目录下可以看到一些索引文件,这些文件不能删除,删除了就需要重新构建索引,否则没了索引,就无法去检索内容了。
####2.2.2 检索内容
上面把这两个文件的索引建立好了,接下来我们就可以写检索程序了,在这两个文件中查找特定的词。
public class Searcher {public static void search(String indexDir, String q) throws Exception {//获取要查询的路径,也就是索引所在的位置Directory dir = FSDirectory.open(Paths.get(indexDir));IndexReader reader = DirectoryReader.open(dir);//构建IndexSearcherIndexSearcher searcher = new IndexSearcher(reader);//标准分词器,会自动去掉空格啊,is a the等单词Analyzer analyzer = new StandardAnalyzer();//查询解析器QueryParser parser = new QueryParser("contents", analyzer);//通过解析要查询的String,获取查询对象,q为传进来的待查的字符串Query query = parser.parse(q);//记录索引开始时间long startTime = System.currentTimeMillis();//开始查询,查询前10条数据,将记录保存在docs中TopDocs docs = searcher.search(query, 10);//记录索引结束时间long endTime = System.currentTimeMillis();System.out.println("匹配" + q + "共耗时" + (endTime-startTime) + "毫秒");System.out.println("查询到" + docs.totalHits + "条记录");//取出每条查询结果for(ScoreDoc scoreDoc : docs.scoreDocs) {//scoreDoc.doc相当于docID,根据这个docID来获取文档Document doc = searcher.doc(scoreDoc.doc);//fullPath是刚刚建立索引的时候我们定义的一个字段,表示路径。也可以取其他的内容,只要我们在建立索引时有定义即可。System.out.println(doc.get("fullPath"));}reader.close();}
}
ok,这样我们检索的代码就写完了,每一步解释我写在代码中的注释上了,下面写个 main 方法来测试一下:
public static void main(String[] args) {String indexDir = "D:\\lucene";//查询这个字符串String q = "security";try {search(indexDir, q);} catch (Exception e) {e.printStackTrace();}
}
查一下 security
这个字符串,执行一下看控制台打印的结果:
匹配security共耗时23毫秒
查询到1条记录
D:\lucene\data\catalina.properties
可以看出,耗时了23毫秒在两个文件中找到了 security 这个字符串,并输出了文件的名称。上面的代码我写的很详细,这个代码已经比较全了,可以用在生产环境上。
2.3 中文分词检索高亮实战
2.3.1 中文分词
我们新建一个 ChineseIndexer 类来建立中文索引,建立过程和英文索引一样的,不同的地方在于使用的是中文分词器。除此之外,这里我们不用通过读取文件去建立索引,我们模拟一下用字符串来建立,因为在实际项目中,绝大部分情况是获取到一些文本字符串,然后根据一些关键字去查询相关内容等等。代码如下:
public class ChineseIndexer {/*** 存放索引的位置*/private Directory dir;//准备一下用来测试的数据//用来标识文档private Integer ids[] = {1, 2, 3};private String citys[] = {"上海", "南京", "青岛"};private String descs[] = {"上海是个繁华的城市。","南京是一个文化的城市南京,简称宁,是江苏省会,地处中国东部地区,长江下游,濒江近海。全市下辖11个区,总面积6597平方公里,2013年建成区面积752.83平方公里,常住人口818.78万,其中城镇人口659.1万人。[1-4] “江南佳丽地,金陵帝王州”,南京拥有着6000多年文明史、近2600年建城史和近500年的建都史,是中国四大古都之一,有“六朝古都”、“十朝都会”之称,是中华文明的重要发祥地,历史上曾数次庇佑华夏之正朔,长期是中国南方的政治、经济、文化中心,拥有厚重的文化底蕴和丰富的历史遗存。[5-7] 南京是国家重要的科教中心,自古以来就是一座崇文重教的城市,有“天下文枢”、“东南第一学”的美誉。截至2013年,南京有高等院校75所,其中211高校8所,仅次于北京上海;国家重点实验室25所、国家重点学科169个、两院院士83人,均居中国第三。[8-10] 。","青岛是一个美丽的城市。"};/*** 生成索引* @param indexDir* @throws Exception*/public void index(String indexDir) throws Exception {dir = FSDirectory.open(Paths.get(indexDir));// 先调用 getWriter 获取IndexWriter对象IndexWriter writer = getWriter();for(int i = 0; i < ids.length; i++) {Document doc = new Document();// 把上面的数据都生成索引,分别用id、city和desc来标识doc.add(new IntField("id", ids[i], Field.Store.YES));doc.add(new StringField("city", citys[i], Field.Store.YES));doc.add(new TextField("desc", descs[i], Field.Store.YES));//添加文档writer.addDocument(doc);}//close了才真正写到文档中writer.close();}/*** 获取IndexWriter实例* @return* @throws Exception*/private IndexWriter getWriter() throws Exception {//使用中文分词器SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();//将中文分词器配到写索引的配置中IndexWriterConfig config = new IndexWriterConfig(analyzer);//实例化写索引对象IndexWriter writer = new IndexWriter(dir, config);return writer;}public static void main(String[] args) throws Exception {new ChineseIndexer().index("D:\\lucene2");}
}
这里我们用 id、city、desc 分别代表 id、城市名称和城市描述,用他们作为关键字来建立索引,后面我们获取内容的时候,主要来获取城市描述。南京的描述我故意写的长一点,因为下文检索的时候,根据不同的关键字会检索到不同部分的信息,有个权重的概念在里面。
然后执行一下 main 方法,将索引保存到 D:\lucene2\
中。
2.3.2 中文分词查询
中文分词查询代码逻辑和默认的查询差不多,有一些区别在于,我们需要将查询出来的关键字标红加粗等需要处理,需要计算出一个得分片段,这是什么意思呢?比如我搜索 “南京文化” 跟搜索 “南京文明”,这两个搜索结果应该根据关键字出现的位置,返回的结果不一样才对,这在下文会测试。我们先看一下代码和注释:
public class ChineseSearch {private static final Logger logger = LoggerFactory.getLogger(ChineseSearch.class);public static List<String> search(String indexDir, String q) throws Exception {//获取要查询的路径,也就是索引所在的位置Directory dir = FSDirectory.open(Paths.get(indexDir));IndexReader reader = DirectoryReader.open(dir);IndexSearcher searcher = new IndexSearcher(reader);//使用中文分词器SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();//由中文分词器初始化查询解析器QueryParser parser = new QueryParser("desc", analyzer);//通过解析要查询的String,获取查询对象Query query = parser.parse(q);//记录索引开始时间long startTime = System.currentTimeMillis();//开始查询,查询前10条数据,将记录保存在docs中TopDocs docs = searcher.search(query, 10);//记录索引结束时间long endTime = System.currentTimeMillis();logger.info("匹配{}共耗时{}毫秒", q, (endTime - startTime));logger.info("查询到{}条记录", docs.totalHits);//如果不指定参数的话,默认是加粗,即<b><b/>SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color=red>","</font></b>");//根据查询对象计算得分,会初始化一个查询结果最高的得分QueryScorer scorer = new QueryScorer(query);//根据这个得分计算出一个片段Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);//将这个片段中的关键字用上面初始化好的高亮格式高亮Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);//设置一下要显示的片段highlighter.setTextFragmenter(fragmenter);//取出每条查询结果List<String> list = new ArrayList<>();for(ScoreDoc scoreDoc : docs.scoreDocs) {//scoreDoc.doc相当于docID,根据这个docID来获取文档Document doc = searcher.doc(scoreDoc.doc);logger.info("city:{}", doc.get("city"));logger.info("desc:{}", doc.get("desc"));String desc = doc.get("desc");//显示高亮if(desc != null) {TokenStream tokenStream = analyzer.tokenStream("desc", new StringReader(desc));String summary = highlighter.getBestFragment(tokenStream, desc);logger.info("高亮后的desc:{}", summary);list.add(summary);}}reader.close();return list;}
}
每一步的注释我写的很详细,在这就不赘述了。接下来我们来测试一下效果。
2.3.3 测试一下
这里我们使用 thymeleaf 来写个简单的页面来展示获取到的数据,并高亮展示。在 controller 中我们指定索引的目录和需要查询的字符串,如下:
@Controller
@RequestMapping("/lucene")
public class IndexController {@GetMapping("/test")public String test(Model model) {// 索引所在的目录String indexDir = "D:\\lucene2";// 要查询的字符
// String q = "南京文明";String q = "南京文化";try {List<String> list = ChineseSearch.search(indexDir, q);model.addAttribute("list", list);} catch (Exception e) {e.printStackTrace();}return "result";}
}
直接返回到 result.html 页面,该页面主要来展示一下 model 中的数据即可。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<div th:each="desc : ${list}"><div th:utext="${desc}"></div>
</div>
</body>
</html>
这里注意一下,不能使用 th:test
,否则字符串中的 html 标签都会被转义,不会被渲染到页面。下面启动服务,在浏览器中输入 http://localhost:8080/lucene/test
,测试一下效果,我们搜索的是 “南京文化”。
再将 controller 中的搜索关键字改成 “南京文明”,看下命中的效果。
可以看出,不同的关键词,它会计算一个得分片段,也就是说不同的关键字会命中不同位置的内容,然后将关键字根据我们自己设定的形式高亮显示。从结果中可以看出,Lucene 也可以很智能的将关键字拆分命中,这在实际项目中会很好用。
3. 总结
第18课:Spring Boot搭建实际项目开发中的架构
结合前面的课程和以上的这些点,本节课手把手带领大家搭建一个实际项目开发中可用的 Spring Boot 架构。整个项目工程如下图所示,学习的时候,可以结合我的源码,这样效果会更好。
1. 统一的数据封装
大家可以根据自己项目中所需要的一些东西,合理的修改统一结构中的字段信息。
2. json的处理
/*** jacksonConfig* @author shengwu ni*/
@Configuration
public class JacksonConfig {@Bean@Primary@ConditionalOnMissingBean(ObjectMapper.class)public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {ObjectMapper objectMapper = builder.createXmlMapper(false).build();objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {@Overridepublic void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {jsonGenerator.writeString("");}});return objectMapper;}
}
这里先不测试,等下面 swagger2 配置好了之后,我们一起来测试一下。
3. swagger2在线可调式接口
/*** swagger配置* @author shengwu ni*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {@Beanpublic Docket createRestApi() {return new Docket(DocumentationType.SWAGGER_2)// 指定构建api文档的详细信息的方法:apiInfo().apiInfo(apiInfo()).select()// 指定要生成api接口的包路径,这里把controller作为包路径,生成controller中的所有接口.apis(RequestHandlerSelectors.basePackage("com.itcodai.course18.controller")).paths(PathSelectors.any()).build();}/*** 构建api文档的详细信息* @return*/private ApiInfo apiInfo() {return new ApiInfoBuilder()// 设置页面标题.title("Spring Boot搭建实际项目中开发的架构")// 设置接口描述.description("跟武哥一起学Spring Boot第18课")// 设置联系方式.contact("倪升武," + "微信公众号:程序员私房菜")// 设置版本.version("1.0")// 构建.build();}
}
到这里,可以先测试一下,写一个 Controller,弄一个静态的接口测试一下上面集成的内容。
@RestController
@Api(value = "用户信息接口")
public class UserController {@Resourceprivate UserService userService;@GetMapping("/getUser/{id}")@ApiOperation(value = "根据用户唯一标识获取用户信息")public JsonResult<User> getUserInfo(@PathVariable @ApiParam(value = "用户唯一标识") Long id) {User user = new User(id, "倪升武", "123456");return new JsonResult<>(user);}
}
然后启动项目,在浏览器中输入 localhost:8080/swagger-ui.html
即可看到 swagger 接口文档页面,调用一下上面这个接口,即可看到返回的 json 数据。
4. 持久层集成
每个项目中是必须要有持久层的,与数据库交互,这里我们主要来集成 mybatis,集成 mybatis 首先要在 application.yml 中进行配置。
# 服务端口号
server:port: 8080# 数据库地址
datasource:url: localhost:3306/blog_testspring:datasource: # 数据库配置driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://${datasource.url}?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10username: rootpassword: 123456hikari:maximum-pool-size: 10 # 最大连接池数max-lifetime: 1770000mybatis:# 指定别名设置的包为所有entitytype-aliases-package: com.itcodai.course18.entityconfiguration:map-underscore-to-camel-case: true # 驼峰命名规范mapper-locations: # mapper映射文件位置- classpath:mapper/*.xml
public interface UserMapper {@Select("select * from user where id = #{id}")@Results({@Result(property = "username", column = "user_name"),@Result(property = "password", column = "password")})User getUser(Long id);@Select("select * from user where id = #{id} and user_name=#{name}")User getUserByIdAndName(@Param("id") Long id, @Param("name") String username);@Select("select * from user")List<User> getAll();
}
5. 拦截器
public class MyInterceptor implements HandlerInterceptor {private static final Logger logger = LoggerFactory.getLogger(MyInterceptor.class);@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {logger.info("执行方法之前执行(Controller方法调用之前)");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {logger.info("执行完方法之后进执行(Controller方法调用之后),但是此时还没进行视图渲染");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {logger.info("整个请求都处理完咯,DispatcherServlet也渲染了对应的视图咯,此时我可以做一些清理的工作了");}
}
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 实现WebMvcConfigurer不会导致静态资源被拦截registry.addInterceptor(new MyInterceptor())// 拦截所有url.addPathPatterns("/**")// 放行swagger.excludePathPatterns("/swagger-resources/**");}
}
在 Spring Boot 中,我们通常会在如下目录里存放一些静态资源:
classpath:/static
classpath:/public
classpath:/resources
classpath:/META-INF/resources
上面代码中配置的 /**
是对所有 url 都进行了拦截,但我们实现了 WebMvcConfigurer 接口,不会导致 Spring Boot 对上面这些目录下的静态资源实施拦截。但是我们平时访问的 swagger 会被拦截,所以要将其放行。swagger 页面在 swagger-resources 目录下,放行该目录下所有文件即可。
然后在浏览器中输入一下 swagger 页面,若能正常显示 swagger,说明放行成功。同时可以根据后台打印的日志判断代码执行的顺序。
6. 全局异常处理
public enum BusinessMsgEnum {/** 参数异常 */PARMETER_EXCEPTION("102", "参数异常!"),/** 等待超时 */SERVICE_TIME_OUT("103", "服务调用超时!"),/** 参数过大 */PARMETER_BIG_EXCEPTION("102", "输入的图片数量不能超过50张!"),/** 500 : 发生异常 */UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!");/*** 消息码*/private String code;/*** 消息内容*/private String msg;private BusinessMsgEnum(String code, String msg) {this.code = code;this.msg = msg;}public String code() {return code;}public String msg() {return msg;}}
在全局统一异常处理类中,我们一般会对自定义的业务异常最先处理,然后去处理一些常见的系统异常,最后会来一个一劳永逸(Exception 异常)。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);/*** 拦截业务异常,返回业务异常信息* @param ex* @return*/@ExceptionHandler(BusinessErrorException.class)@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)public JsonResult handleBusinessError(BusinessErrorException ex) {String code = ex.getCode();String message = ex.getMessage();return new JsonResult(code, message);}/*** 空指针异常* @param ex NullPointerException* @return*/@ExceptionHandler(NullPointerException.class)@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)public JsonResult handleTypeMismatchException(NullPointerException ex) {logger.error("空指针异常,{}", ex.getMessage());return new JsonResult("500", "空指针异常了");}/*** 系统异常 预期以外异常* @param ex* @return*/@ExceptionHandler(Exception.class)@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)public JsonResult handleUnexpectedServer(Exception ex) {logger.error("系统异常:", ex);return new JsonResult(BusinessMsgEnum.UNEXPECTED_EXCEPTION);}}
7. 总结
springboot入门到入坟相关推荐
- canvas 从入门到入坟
文章目录 canvas 从入门到入坟 1. 概述 2. 使用场景 2.1 图表的绘制 2.2 canvas游戏 2.3 说明与作用 3. Canvas 入门 3.1 完成目标 3.2 初始化 3.3 ...
- FHQ-Treap(非旋treap/平衡树)——从入门到入坟
作者:hsez_yyh 链接: FHQ-Treap--从入门到入坟_hsez_yyh的博客-CSDN博客 来源:湖北省黄石二中信息竞赛组 著作权归作者所有.商业转载请联系作者获得授权,非 ...
- 从入门到入坟搭建FreeNAS服务器并配置NextCloud_NAS存储
从入门到入坟搭建FreeNAS服务器并配置NextCloud 2021-01-06 23:15:38 173点赞 963收藏 100评论 创作立场声明:本文所有商品皆自费购入,第一次发表文章,若有不当 ...
- 深聊MySQL,从入门到入坟之:MySQL竟然也有后悔药!!!
MySQL后悔药,防止看铁窗 1.引言 2.5种后悔药 2.1 limit 2.2 先测试后生产 2.3 删除前,先备份 2.4 删除前,先查询 2.5 修改时, begin+commit 3. 总结 ...
- 【教程汇总+持续更新】Unity游戏开发从入门到入坟
新的一年,本该在年前整理的年终总结被拖到了年后开工.去年大量时间投入在Catlike教程的翻译上,截止目前位置,教程的进度已经完全追平原作者. 去年还有一部分是断断续续的更新SLG实战教程,但遗憾的是 ...
- 初阶指针---从入门到入坟
今天我们来见识一下c语言里让万千少年少女从入门到入坟的一道大门槛--指针 目录 1.指针是什么? 2.指针和指针类型 3.野指针 4. 指针运算 5. 指针和数组 6. 二级指针 7. 指针数组 1. ...
- c语言深度剖析第三版pdf_入门到入坟,蕴含全网最强知识点3283页笔记、pdf教程,活到老,学到老...
又到了"金九银十"面试求职高峰期,在金三银四时也参与过不少面试,2020都说工作不好找,也是对开发人员的要求变高.前段时间自己有整理了一些Java后端开发面试常问的高频考点问题做成 ...
- unity ui框架_[教程汇总+持续更新]Unity从入门到入坟——收藏这一篇就够了
----------------塔防(更新中),作者重写了基础篇(下方目录为:1.1(新) 基础)目前还在持续连载了5篇,因为不多我们更新完就能追到原作者的进度了------------------- ...
- 网络安全学习路线,入门到入坟,史上最全网络安全学习路线整理
很多小伙伴在网上搜索网络安全时,会出来网络安全工程师这样一个职位,它的范围很广,只要是与网络安全挂钩的技术人员都算网络安全工程师,一些小伙伴就有疑问了,网络安全现在真的很火吗? 那么寒哥就带大家看看, ...
最新文章
- SQL查询结果集对注入的影响及利用
- Veeam Backup Replication v7 安装配置手册
- 003_数据模型一览
- 思考ANDROID架构(4):HOW-TO, 如何从API洞悉软件的话语权
- 关于switch-case问题
- arma matlab函数,MATLAB中ARMA模型预测差分问题
- Pentium II Pentium III架构/微架构/流水线 (3) - P6执行核详解 (指令时延吞吐量,执行单元/发射口)
- 基于JAVA+SpringMVC+MYSQL的便利店运营管理系统
- OLAP-druid-大数据Week13-DAY2-druid
- linux下查看已经安装的jdk 并卸载jdk的方法
- opencore 0.6.3 华硕_Apex英雄支持NVIDIA Reflex 华硕显卡低延迟利器_
- MongoDB 文档操作之插入、查询
- pdf怎么压缩文件到最小?pdf文件怎么变小内存?
- vue 项目打印时去掉页眉页脚
- Linux常用命令学习-base64
- 【AI视野·今日CV 计算机视觉论文速览 第215期】Tue, 8 Jun 2021
- 红月所有物品完全代码
- ADuC7026简单开发过程
- 战神引挚手游数据库解析mysql/mir
- 偶遇Trojan.AVKill.19646
热门文章
- windows7英文版,变为中文版
- 为什么我选择csdn写blog
- java 函数名相同_下列方法不属于java.lang.Math类的有(方法名相同即可)【 】...
- Springmvc开发流程(入门)
- Little Red Riding Hood(动态规划)
- android原生widget 电量控制(PowerSave)设计浅析
- java中static什么意思_java中static什么意思
- 《功夫》“经典”台词
- 个体工商户核名查询_网上核名怎么查询
- 黄健翔,请你像个男人一样去向全国的球迷道歉!(摘自网络)