Spring boot框架
1、什么是Spring Boot?
Spring Boot是Spring开源组织下的子项目,是Spring组件一站式解决方案,主要是简化了使用Spring的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。
Spring Boot官方文档:https://spring.io/projects/spring-boot/
2、Spring Boot的优点与特点
优点:
- 独立运行
- Spring Boot内嵌了各种servlet容器,Tomcat、Jetty等,不再需要打成war包部署到容器中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内
- 简化配置
- spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。
- 自动配置
- Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starter-web启动器就能拥有web的功能,无需其他配置
- 无代码生成和XML配置
- Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是Spring4.x的核心功能之一。
- 应用监控
- Spring Boot提供一系列端点可以监控服务及应用,做健康检测。
- 上手容易
特点:
- 为 Spring 开发提供一个更快、更广泛的入门体验。
- 开箱即用,远离繁琐的配置。
- 提供了一系列大型项目通用的非业务性功能,例如:内嵌服务器、安全管理、运行数据监控、运行状况检查和外部化配置等。
- 绝对没有代码生成,也不需要XML配置。
3、Spring Boot的配置文件以及之间的区别
.properties 和 .yml,它们的区别主要是书写格式不同。
1).properties
app.user.name = javastack
2).yml
app:user:name: javastack
注:.yml 格式不支持 @PropertySource
注解导入配置。
4、Spring Boot的常用注解
@SpringBootApplication
:
申明让spring boot自动给程序进行必要的配置,这个配置等同于:@Configuration @EnableAutoConfiguration 和 @ComponentScan 三个配置。
@ResponseBody
:
表示该方法的返回结果直接写入HTTP response body中,一般在异步获取数据时使用,用于构建RESTful的api。在使用@RequestMapping后,返回值通常解析为跳转路径,加上@esponsebody后返回结果不会被解析为跳转路径,而是直接写入HTTP response body中。比如异步获取json数据,加上@Responsebody后,会直接返回json数据。该注解一般会配合@RequestMapping一起使用。
@Controller
:
用于定义控制器类,在spring项目中由控制器负责将用户发来的URL请求转发到对应的服务接口(service层),一般这个注解在类中,通常方法需要配合注解@RequestMapping。
@RestController
:
用于标注控制层组件(如struts中的action),@ResponseBody和@Controller的合集。
@RequestMapping
:
提供路由信息,负责URL到Controller中的具体函数的映射。
@EnableAutoConfiguration
:
SpringBoot自动配置(auto-configuration):尝试根据你添加的jar依赖自动配置你的Spring应用。例如,如果你的classpath下存在HSQLDB,并且你没有手动配置任何数据库连接beans,那么我们将自动配置一个内存型(in-memory)数据库”。你可以将@EnableAutoConfiguration或者@SpringBootApplication :添加到一个@Configuration类上来选择自动配置。如果发现应用了你不想要的特定自动配置类,你可以使用@EnableAutoConfiguration注解的排除属性来禁用它们。
@ComponentScan
:
表示将该类自动发现扫描组件。个人理解相当于,如果扫描到有@Component、@Controller、@Service等这些注解的类,并注册为Bean,可以自动收集所有的Spring组件,包括@Configuration类。我们经常使用@ComponentScan注解搜索beans,并结合@Autowired注解导入。可以自动收集所有的Spring组件,包括@Configuration类。我们经常使用@ComponentScan注解搜索beans,并结合@Autowired注解导入。如果没有配置的话,Spring Boot会扫描启动类所在包下以及子包下的使用了@Service,@Repository等注解的类。
@Configuration
:
相当于传统的xml配置文件,如果有些第三方库需要用到xml文件,建议仍然通过@Configuration类作为项目的配置主类——可以使用@ImportResource注解加载xml配置文件。
@Import
:用来导入其他配置类。
@ImportResource
:用来加载xml配置文件。
@Service
:一般用于修饰service层的组件
@Repository
:使用@Repository注解可以确保DAO或者repositories提供异常转译,这个注解修饰的DAO或者repositories类会被ComponetScan发现并配置,同时也不需要为它们提供XML配置项。
@Value
:注入Spring boot application.properties配置的属性的值。示例代码:
@Inject
:等价于默认的@Autowired,只是没有required属性;
@Component
:泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。
@Bean
:相当于XML中的,放在方法的上面,而不是类,意思是产生一个bean,并交给spring管理。
@AutoWired
:
自动导入依赖的bean。byType方式。把配置好的Bean拿来用,完成属性、方法的组装,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。当加上(required=false)时,就算找不到bean也不报错。
@Qualifier
:
当有多个同一类型的Bean时,可以用@Qualifier(“name”)来指定。与@Autowired配合使用。@Qualifier限定描述符除了能根据名字进行注入,但能进行更细粒度的控制如何选择候选者,具体使用方式如下:
@Resource(name=”name”,type=”type”)
:没有括号内内容的话,默认byName。与@Autowired干类似的事。
5、spring boot开启的两种方式
① 继承spring-boot-starter-parent项目
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>1.5.6.RELEASE</version>xml</parent>
② 导入spring-boot-dependencies项目依赖
<dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>1.5.6.RELEASE</version><type>pom</type><scope>import</scope></dependency></dependencyManagement>
Spring Boot依赖注意点
1、属性覆盖只对继承有效
Spring Boot依赖包里面的组件的版本都是和当前Spring Boot绑定的,如果要修改里面组件的版本,只需要添加如下属性覆盖即可,但这种方式只对继承有效,导入的方式无效。
<properties><slf4j.version>1.7.25<slf4j.version></properties>
如果导入的方式要实现版本的升级,达到上面的效果,这样也可以做到,把要升级的组件依赖放到Spring Boot之前。
<dependencyManagement><dependencies><!-- Override Spring Data release train provided by Spring Boot --><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-releasetrain</artifactId><version>Fowler-SR2</version><scope>import</scope><type>pom</type></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>1.5.6.RELEASE</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
需要注意,要修改Spring Boot的依赖组件版本可能会造成不兼容的问题。
2、资源文件过滤问题
使用继承Spring Boot时,如果要使用Maven resource filter过滤资源文件时,资源文件里面的占位符为了使${}和Spring Boot区别开来,此时要用@...@包起来,不然无效。另外,@...@占位符在yaml文件编辑器中编译报错,所以使用继承方式有诸多问题,坑要慢慢趟。
6、Spring Boot的几种运行方式
- 打包用命令或者放到容器中运行
- 用 Maven/ Gradle 插件运行
- 直接执行 main 方法运行
7、Spring Boot实现热部署
第一种引用devtools依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional></dependency>
第二种自定义配置热部署
# 热部署开关,false即不启用热部署spring.devtools.restart.enabled: true# 指定热部署的目录#spring.devtools.restart.additional-paths: src/main/java# 指定目录不更新spring.devtools.restart.exclude: test/**
第三种Intellij Idea修改
1、勾上自动编译或者手动重新编译
File > Settings > Compiler-Build Project automatically
2、注册
ctrl + shift + alt + / > Registry > 勾选Compiler autoMake allow when app running
注意事项:
1、生产环境devtools将被禁用,如java -jar方式或者自定义的类加载器等都会识别为生产环境。
2、打包应用默认不会包含devtools,除非你禁用SpringBoot Maven插件的 excludeDevtools
属性。
3、Thymeleaf无需配置 spring.thymeleaf.cache:false
,devtools默认会自动设置,参考完整属性。
4、devtools会在windows资源管理器占用java进程,在开发工具里面杀不掉,只能手动kill掉,不然重启会选成端口重复绑定报错。
更多详细用法,参考发官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html
8、保护Spring Boot的应用方法
- 在生产中使用HTTPS
- 使用Snyk检查你的依赖关系
- 升级到最新版本
- 启动CSRF保护
- 使用内容安全策略防止XSS攻击
- 使用OpenID Connect进行身份验证
- 管理密码使用面密码哈希
- 安全地存储秘密
- 使用OWASP的ZAP测试应用程序
- 安全团队进行代码审查
9、Spring Boot配置加载顺序(2.0)
1、properties文件;
2、YAML文件;
3、系统环境变量;
4、命令行参数;
等等……
配置属性加载的顺序如下:
1、开发者工具 `Devtools` 全局配置参数;2、单元测试上的 `@TestPropertySource` 注解指定的参数;3、单元测试上的 `@SpringBootTest` 注解指定的参数;4、命令行指定的参数,如 `java -jar springboot.jar --name="Java技术栈"`;5、命令行中的 `SPRING_APPLICATION_JSONJSON` 指定参数, 如 `java -Dspring.application.json='{"name":"Java技术栈"}' -jar springboot.jar`6、`ServletConfig` 初始化参数;7、`ServletContext` 初始化参数;8、JNDI参数(如 `java:comp/env/spring.application.json`);9、Java系统参数(来源:`System.getProperties()`);10、操作系统环境变量参数;11、`RandomValuePropertySource` 随机数,仅匹配:`ramdom.*`;12、JAR包外面的配置文件参数(`application-{profile}.properties(YAML)`)13、JAR包里面的配置文件参数(`application-{profile}.properties(YAML)`)14、JAR包外面的配置文件参数(`application.properties(YAML)`)15、JAR包里面的配置文件参数(`application.properties(YAML)`)16、`@Configuration`配置文件上 `@PropertySource` 注解加载的参数;17、默认参数(通过 `SpringApplication.setDefaultProperties` 指定);
10、Spring Boot日志集成
Spring Boot日志框架
Spring Boot支持Java Util Logging,Log4j2,Lockback作为日志框架,如果你使用starters启动器,Spring Boot将使用Logback作为默认日志框架。无论使用哪种日志框架,Spring Boot都支持配置将日志输出到控制台或者文件中。
spring-boot-starter启动器包含spring-boot-starter-logging启动器并集成了slf4j日志抽象及Logback日志框架。
11、Spring Boot读取配置的几种方式
读取application文件
在application.yml或者properties文件中添加:
info.address=USA
info.company=Spring
info.degree=high
@Value注解读物方式
import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;@Componentpublic class InfoConfig1 {@Value("${info.address}")private String address;@Value("${info.company}")private String company;@Value("${info.degree}")private String degree;public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public String getCompany() {return company;}public void setCompany(String company) {this.company = company;}public String getDegree() {return degree;}public void setDegree(String degree) {this.degree = degree;}}
@ConfigurationProperties注解读取方式
@Component@ConfigurationProperties(prefix = "info")public class InfoConfig2 {private String address;private String company;private String degree;public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public String getCompany() {return company;}public void setCompany(String company) {this.company = company;}public String getDegree() {return degree;}public void setDegree(String degree) {this.degree = degree;}}
读取指定文件
资源目录下建立config/db-config.properties:
db.username=root
db.password=123456
@PropertySource+@Value注解读取方式
@Component@PropertySource(value = { "config/db-config.properties" })public class DBConfig1 {@Value("${db.username}")private String username;@Value("${db.password}")private String password;public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}}
注意:@PropertySource不支持yml文件读取。
@PropertySource+@ConfigurationProperties注解读取方式
@Component@ConfigurationProperties(prefix = "db")@PropertySource(value = { "config/db-config.properties" })public class DBConfig2 {private String username;private String password;public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}}
Environment读取方式
以上所有加载出来的配置都可以通过Environment注入获取到。
@Autowiredprivate Environment env;// 获取参数String getProperty(String key);
总结:从以上示例来看,Spring Boot可以通过@PropertySource,@Value,@Environment,@ConfigurationProperties来绑定变量。
12、Spring Boot自动配置原理
① SpringBoot启动的时候加载主配置类,开启了自动配置功能@EnableAutoConfiguration。
② @EnableAutoConfiguration的作用是利用AutoConfigurationImportSelector给容器中导入一些组件。
③ 可以查看public String[] selectImports(AnnotationMetadata annotationMetadata)方法的内容。
④ 通过protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes)获取候选的配置,这个是扫描所有jar包类路径下"META-INF/spring.factories";
⑤ 然后把扫描到的这些文件包装成Properties对象。
⑥ 从properties中获取到EnableAutoConfiguration.class类名对应的值,然后把他们添加在容器中。
⑦ 整个过程就是将类路径下"META-INF/spring.factories"里面配置的所有EnableAutoConfiguration的值加入到 容器中。
⑧ 每一个这样XXAutoConfiguration类都是容器中的一个组件都加入到容器中,用他们来做自动配置。
每一个自动配置类进行自动配置功能,以HttpEncodingAutoConfiguration为例解释自动配置原理
⑨根据当前不同的条件判断,决定这个配置是否生效。
13、Spring Boot中的starter
首先,这个 Starter 并非什么新的技术点,基本上还是基于 Spring 已有功能来实现的。首先它提供了一个自动化配置类,一般命名为 XXXAutoConfiguration ,在这个配置类中通过条件注解来决定一个配置是否生效(条件注解就是 Spring 中原本就有的),然后它还会提供一系列的默认配置,也允许开发者根据实际情况自定义相关配置,然后通过类型安全的属性注入将这些配置属性注入进来,新注入的属性会代替掉默认属性。正因为如此,很多第三方框架,我们只需要引入依赖就可以直接使用了。 当然,开发者也可以自定义 Starter,自定义 Starter 可以参考:徒手撸一个 Spring Boot 中的 Starter ,解密自动化配置黑魔法!
14、Spring Boot跨域问题
跨域可以在前端通过 JSONP 来解决,但是 JSONP 只可以发送 GET 请求,无法发送其他类型的请求,在 RESTful 风格的应用中,就显得非常鸡肋,因此我们推荐在后端通过 (CORS,Cross-origin resource sharing) 来解决跨域问题。这种解决方案并非 Spring Boot 特有的,在传统的 SSM 框架中,就可以通过 CORS 来解决跨域问题,只不过之前我们是在 XML 文件中配置 CORS ,现在则是通过 @CrossOrigin 注解来解决跨域问题。关于 CORS ,小伙伴们可以参考:Spring Boot 中通过 CORS 解决跨域问题
15、Spring Boot定时任务
使用spring boot创建定时任务主要有以下三种创建方式:
一、基于注解@Scheduled
默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时 间的影响。
① 创建定时器
@Configuration //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling // 2.开启定时任务
public class SaticScheduleTask {//3.添加定时任务@Scheduled(cron = "0/5 * * * * ?")//或直接指定时间间隔,例如:5秒//@Scheduled(fixedRate=5000)private void configureTasks() {System.err.println("执行静态定时任务时间: " + LocalDateTime.now());}
}
②启动测试
显然,使用@Scheduled 注解很方便,但缺点是当我们调整了执行周期的时候,需要重启应用才能生效,这多少有些不方便。为了达到实时生效的效果,可以使用接口来完成定时任务。
二、基于接口(SchedulingConfigurer)
1、导入依赖包:
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version>2.0.4.RELEASE</version></parent><dependencies><dependency><!--添加Web依赖 --><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><!--添加MySql依赖 --><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><!--添加Mybatis依赖 配置mybatis的一些初始化的东西--><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.1</version></dependency><dependency><!-- 添加mybatis依赖 --><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.4.5</version><scope>compile</scope></dependency></dependencies>
2、添加数据库记录
开启本地数据库mysql,随便打开查询窗口,然后执行脚本内容,代码如下:
DROP DATABASE IF EXISTS `socks`;
CREATE DATABASE `socks`;
USE `SOCKS`;
DROP TABLE IF EXISTS `cron`;
CREATE TABLE `cron` (`cron_id` varchar(30) NOT NULL PRIMARY KEY,`cron` varchar(30) NOT NULL
);
INSERT INTO `cron` VALUES ('1', '0/5 * * * * ?');
然后在项目中的application.yml添加数据源:
spring:datasource:url: jdbc:mysql://localhost:3306/socksusername: rootpassword: 123456
3、创建定时器
数据库准备好数据之后,我们编写定时任务,注意这里添加的是TriggerTask,目的是循环读取我们在数据库设置好的执行周期,以及执行相关定时任务的内容。
具体代码如下:
@Configuration //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling // 2.开启定时任务
public class DynamicScheduleTask implements SchedulingConfigurer {@Mapperpublic interface CronMapper {@Select("select cron from cron limit 1")public String getCron();}@Autowired //注入mapper@SuppressWarnings("all")CronMapper cronMapper;/*** 执行定时任务.*/@Overridepublic void configureTasks(ScheduledTaskRegistrar taskRegistrar) {taskRegistrar.addTriggerTask(//1.添加任务内容(Runnable)() -> System.out.println("执行动态定时任务: " + LocalDateTime.now().toLocalTime()),//2.设置执行周期(Trigger)triggerContext -> {//2.1 从数据库获取执行周期String cron = cronMapper.getCron();//2.2 合法性校验.if (StringUtils.isEmpty(cron)) {// Omitted Code ..}//2.3 返回执行周期(Date)return new CronTrigger(cron).nextExecutionTime(triggerContext);});}}
4、启动测试
启动应用后,查看控制台,打印时间是我们预期的每10秒一次:
然后打开Navicat ,将执行周期修改为每6秒执行一次,如图:
查看控制台,发现执行周期已经改变,并且不需要我们重启应用,十分方便。如图:
注意: 如果在数据库修改时格式出现错误,则定时任务会停止,即使重新修改正确;此时只能重新启动项目才能恢复。
三、基于注解设定多线程定时任务
1、创建多线程定时任务
//@Component注解用于对那些比较中立的类进行注释;
//相对与在持久层、业务层和控制层分别采用 @Repository、@Service 和 @Controller 对分层中的类进行注释
@Component
@EnableScheduling // 1.开启定时任务
@EnableAsync // 2.开启多线程
public class MultithreadScheduleTask {@Async@Scheduled(fixedDelay = 1000) //间隔1秒public void first() throws InterruptedException {System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "rn线程 : " + Thread.currentThread().getName());System.out.println();Thread.sleep(1000 * 10);}@Async@Scheduled(fixedDelay = 2000)public void second() {System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "rn线程 : " + Thread.currentThread().getName());System.out.println();}}
2、启动测试查看控制台
从控制台可以看出,第一个定时任务和第二个定时任务互不影响;
并且,由于开启了多线程,第一个任务的执行时间也不受其本身执行时间的限制,所以需要注意可能会出现重复操作导致数据异常
16、Spring Boot 全局异常处理
1、基于@ControllerAdvice注解的Controller层
创建 MyControllerAdvice,并添加 @ControllerAdvice注解。
package com.shsxt.demo.controller;import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;/*** controller 增强器* @author sam* @since 2017/7/17*/
@ControllerAdvice
public class MyControllerAdvice {/*** 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器* @param binder*/@InitBinderpublic void initBinder(WebDataBinder binder) {}/*** 把值绑定到Model中,使全局@RequestMapping可以获取到该值* @param model*/@ModelAttributepublic void addAttributes(Model model) {model.addAttribute("author", "Magical Sam");}/*** 全局异常捕捉处理* @param ex* @return*/@ResponseBody@ExceptionHandler(value = Exception.class)public Map errorHandler(Exception ex) {Map map = new HashMap();map.put("code", 100);map.put("msg", ex.getMessage());return map;}}
启动应用后,被 @ExceptionHandler、@InitBinder、@ModelAttribute 注解的方法,都会作用在 被 @RequestMapping 注解的方法上。
@ModelAttribute:在Model上设置的值,对于所有被 @RequestMapping 注解的方法中,都可以通过 ModelMap 获取,如下:
@RequestMapping("/home")
public String home(ModelMap modelMap) {System.out.println(modelMap.get("author"));
}//或者 通过@ModelAttribute获取@RequestMapping("/home")
public String home(@ModelAttribute("author") String author) {System.out.println(author);
}
@ExceptionHandler 拦截了异常,我们可以通过该注解实现自定义异常处理。其中,@ExceptionHandler 配置的 value 指定需要拦截的异常类型,上面拦截了 Exception.class 这种异常。
2、基于Springboot自身的全局异常统一处理,主要是实现ErrorController接口或者继承AbstractErrorController抽象类或者继承BasicErrorController类
Controller层代码:
@Controller
@RequestMapping(value = "error")
@EnableConfigurationProperties({ServerProperties.class})
public class ExceptionController implements ErrorController {private ErrorAttributes errorAttributes;@Autowiredprivate ServerProperties serverProperties;/*** 初始化ExceptionController* @param errorAttributes*/@Autowiredpublic ExceptionController(ErrorAttributes errorAttributes) {Assert.notNull(errorAttributes, "ErrorAttributes must not be null");this.errorAttributes = errorAttributes;}/*** 定义404的ModelAndView* @param request* @param response* @return*/@RequestMapping(produces = "text/html",value = "404")public ModelAndView errorHtml404(HttpServletRequest request,HttpServletResponse response) {response.setStatus(getStatus(request).value());Map<String, Object> model = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));return new ModelAndView("error/404", model);}/*** 定义404的JSON数据* @param request* @return*/@RequestMapping(value = "404")@ResponseBodypublic ResponseEntity<Map<String, Object>> error404(HttpServletRequest request) {Map<String, Object> body = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));HttpStatus status = getStatus(request);return new ResponseEntity<Map<String, Object>>(body, status);}/*** 定义500的ModelAndView* @param request* @param response* @return*/@RequestMapping(produces = "text/html",value = "500")public ModelAndView errorHtml500(HttpServletRequest request,HttpServletResponse response) {response.setStatus(getStatus(request).value());Map<String, Object> model = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));return new ModelAndView("error/500", model);}/*** 定义500的错误JSON信息* @param request* @return*/@RequestMapping(value = "500")@ResponseBodypublic ResponseEntity<Map<String, Object>> error500(HttpServletRequest request) {Map<String, Object> body = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));HttpStatus status = getStatus(request);return new ResponseEntity<Map<String, Object>>(body, status);}/*** Determine if the stacktrace attribute should be included.* @param request the source request* @param produces the media type produced (or {@code MediaType.ALL})* @return if the stacktrace attribute should be included*/protected boolean isIncludeStackTrace(HttpServletRequest request,MediaType produces) {ErrorProperties.IncludeStacktrace include = this.serverProperties.getError().getIncludeStacktrace();if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {return true;}if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) {return getTraceParameter(request);}return false;}/*** 获取错误的信息* @param request* @param includeStackTrace* @return*/private Map<String, Object> getErrorAttributes(HttpServletRequest request,boolean includeStackTrace) {RequestAttributes requestAttributes = new ServletRequestAttributes(request);return this.errorAttributes.getErrorAttributes(requestAttributes,includeStackTrace);}/*** 是否包含trace* @param request* @return*/private boolean getTraceParameter(HttpServletRequest request) {String parameter = request.getParameter("trace");if (parameter == null) {return false;}return !"false".equals(parameter.toLowerCase());}/*** 获取错误编码* @param request* @return*/private HttpStatus getStatus(HttpServletRequest request) {Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");if (statusCode == null) {return HttpStatus.INTERNAL_SERVER_ERROR;}try {return HttpStatus.valueOf(statusCode);}catch (Exception ex) {return HttpStatus.INTERNAL_SERVER_ERROR;}}/*** 实现错误路径,暂时无用* @see ExceptionMvcAutoConfiguration#containerCustomizer()* @return*/@Overridepublic String getErrorPath() {return "";}}
3、基于AOP也可以实现异常的全局处理
在执行切点中配置的路径中的方法有异常时,可以被捕获到
建议使用该方式:选用AOP方式主要是因为AOP不只可以做全局异常统一处理还可以统一打印接口请求入参和返回结果日志,打印接口访问性能日志,处理sql注入攻击以及处理入参特殊字符等问题
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;/*** @Author: smy* @Description: 调用接口打印性能日志以及接口报错之后记录错误日志* @Date: 2018/9/20* @Time: 15:16*/
@Component
@Aspect
public class InterfaceRequestErrrorAndPerformanceLog {public static final Logger logger = LoggerFactory.getLogger(InterfaceRequestErrrorAndPerformanceLog.class);@Value("${dc.log.bad.value:3000}")private int performanceBadValue;@Resourceprivate RabbitMQService rabbitMQService;@Resourceprivate InterfaceErrorService interfaceErrorService;@Pointcut("execution(* test.test.test.test.test.controller.*.*.*(..))")public void pointCut(){}@Around("pointCut()")public APIResponse handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable{Stopwatch stopwatch = Stopwatch.createStarted();APIResponse apiResponse;try {logger.info("执行Controller开始: " + pjp.getSignature() + " 参数:" + Lists.newArrayList(pjp.getArgs()).toString());//处理入参特殊字符和sql注入攻击checkRequestParam(pjp);//执行访问接口操作apiResponse = (APIResponse) pjp.proceed(pjp.getArgs());try{logger.info("执行Controller结束: " + pjp.getSignature() + ", 返回值:" + JSONObject.toJSONString(apiResponse));//此处将日志打印放入try-catch是因为项目中有些对象实体bean过于复杂,导致序列化为json的时候报错,但是此处报错并不影响主要功能使用,只是返回结果日志没有打印,所以catch中也不做抛出异常处理}catch (Exception ex){logger.error(pjp.getSignature()+" 接口记录返回结果失败!,原因为:{}",ex.getMessage());}Long consumeTime = stopwatch.stop().elapsed(TimeUnit.MILLISECONDS);logger.info("耗时:" + consumeTime + "(毫秒).");//当接口请求时间大于3秒时,标记为异常调用时间,并记录入库if(consumeTime > performanceBadValue){DcPerformanceEntity dcPerformanceEntity = new DcPerformanceEntity();dcPerformanceEntity.setInterfaceName(pjp.getSignature().toString());dcPerformanceEntity.setRequestParam(Lists.newArrayList(pjp.getArgs()).toString());dcPerformanceEntity.setConsumeTime(consumeTime + "毫秒");RabbitMQMessageTarget mqTarget = RabbitMQMessageTarget.createFanoutTarget(ProjectConstants.DC_KEY_EXCHANGE_PERFORMANCE, new String[] { ProjectConstants.DC_KEY_QUEUE_PERFORMANCE});rabbitMQService.send(mqTarget, JSON.toJSONString(dcPerformanceEntity));}} catch (Exception throwable) {apiResponse = handlerException(pjp, throwable);}return apiResponse;}/*** @Author: smy* @Description: 处理接口调用异常* @Date: 15:13 2018/10/25*/private APIResponse handlerException(ProceedingJoinPoint pjp, Throwable e) {APIResponse apiResponse;if(e.getClass().isAssignableFrom(ProjectException.class) ){//ProjectException为自定义异常类,项目中Controller层会把所有的异常都catch掉,并手工封装成ProjectException抛出来,这样做的目的是ProjectException会记录抛出异常接口的路径,名称以及请求参数等等,有助于错误排查ProjectException projectException = (ProjectException)e;logger.error("捕获到ProjectException异常:",JSONObject.toJSONString(projectException.getDcErrorEntity()));RabbitMQMessageTarget mqTarget = RabbitMQMessageTarget.createFanoutTarget(ProjectConstants.DC_KEY_EXCHANGE_INTERFACE_ERROR, new String[] { ProjectConstants.DC_KEY_QUEUE_INTERFACE_ERROR});rabbitMQService.send(mqTarget, JSON.toJSONString(dataCenterException.getDcErrorEntity()));apiResponse = new APIResponse(APIResponse.FAIL,null,projectException.getDcErrorEntity().getErrorMessage());} else if (e instanceof RuntimeException) {logger.error("RuntimeException{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + e.getMessage() + "}", e);apiResponse = new APIResponse(APIResponse.FAIL,null,e.getMessage());} else {logger.error("异常{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + e.getMessage() + "}", e);apiResponse = new APIResponse(APIResponse.FAIL,null,e.getMessage());}return apiResponse;}/*** @Author: gmy* @Description: 处理入参特殊字符和sql注入攻击* @Date: 15:37 2018/10/25*/private void checkRequestParam(ProceedingJoinPoint pjp){String str = String.valueOf(pjp.getArgs());if (!IllegalStrFilterUtil.sqlStrFilter(str)) {logger.info("访问接口:" + pjp.getSignature() + ",输入参数存在SQL注入风险!参数为:" + Lists.newArrayList(pjp.getArgs()).toString());DcErrorEntity dcErrorEntity = interfaceErrorService.processDcErrorEntity(pjp.getSignature() + "",Lists.newArrayList(pjp.getArgs()).toString(),"输入参数存在SQL注入风险!");throw new DataCenterException(dcErrorEntity);}if (!IllegalStrFilterUtil.isIllegalStr(str)) {logger.info("访问接口:" + pjp.getSignature() + ",输入参数含有非法字符!,参数为:" + Lists.newArrayList(pjp.getArgs()).toString());DcErrorEntity dcErrorEntity = interfaceErrorService.processDcErrorEntity(pjp.getSignature() + "",Lists.newArrayList(pjp.getArgs()).toString(),"输入参数含有非法字符!");throw new DataCenterException(dcErrorEntity);}}}
IllegalStrFilterUtil代码:
import org.slf4j.LoggerFactory;import java.util.regex.Matcher;
import java.util.regex.Pattern;/*** @Author: gmy* @Description: 特殊字符检测工具(防止传入非法字符和sql注入攻击)* @Date: 2018/10/25* @Time: 15:08*/
public class IllegalStrFilterUtil {private static final org.slf4j.Logger Logger = LoggerFactory.getLogger(IllegalStrFilterUtil.class);private static final String REGX = "!|!|@|◎|#|#|($)|¥|%|%|(^)|……|(&)|※|(*)|×|(()|(|())|)|_|——|(+)|+|(|)|§ ";/*** 对常见的sql注入攻击进行拦截** @param sInput* @return* true 表示参数不存在SQL注入风险* false 表示参数存在SQL注入风险*/public static Boolean sqlStrFilter(String sInput) {if (sInput == null || sInput.trim().length() == 0) {return false;}sInput = sInput.toUpperCase();if (sInput.indexOf("DELETE") >= 0 || sInput.indexOf("ASCII") >= 0 || sInput.indexOf("UPDATE") >= 0 || sInput.indexOf("SELECT") >= 0|| sInput.indexOf("'") >= 0 || sInput.indexOf("SUBSTR(") >= 0 || sInput.indexOf("COUNT(") >= 0 || sInput.indexOf(" OR ") >= 0|| sInput.indexOf(" AND ") >= 0 || sInput.indexOf("DROP") >= 0 || sInput.indexOf("EXECUTE") >= 0 || sInput.indexOf("EXEC") >= 0|| sInput.indexOf("TRUNCATE") >= 0 || sInput.indexOf("INTO") >= 0 || sInput.indexOf("DECLARE") >= 0 || sInput.indexOf("MASTER") >= 0) {Logger.error("该参数怎么SQL注入风险:sInput=" + sInput);return false;}Logger.info("通过sql检测");return true;}/*** 对非法字符进行检测** @param sInput* @return* true 表示参数不包含非法字符* false 表示参数包含非法字符*/public static Boolean isIllegalStr(String sInput) {if (sInput == null || sInput.trim().length() == 0) {return false;}sInput = sInput.trim();Pattern compile = Pattern.compile(REGX, Pattern.CASE_INSENSITIVE);Matcher matcher = compile.matcher(sInput);Logger.info("通过字符串检测");return matcher.find();}
}
17、spring-boot-starter-parent 的作用
- 定义了 Java 编译版本为 1.8 。
- 使用 UTF-8 格式编码。
- 继承自 spring-boot-dependencies,这个里边定义了依赖的版本,也正是因为继承了这个依赖,所以我们在写依赖时才不需要写版本号。
- 执行打包操作的配置。
- 自动化的资源过滤。
- 自动化的插件配置。
- 针对 application.properties 和 application.yml 的资源过滤,包括通过 profile 定义的不同环境的配置文件,例如 application-dev.properties 和 application-dev.yml。