自动装配:
pom.xml
启动器
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>
starter
。主程序
//@SpringBootApplication 来标注一个主程序类 //说明这是一个Spring Boot应用 @SpringBootApplication public class SpringbootApplication { public static void main(String[] args) { //以为是启动了一个方法,没想到启动了一个服务 SpringApplication.run(SpringbootApplication.class, args); } }
所以,自动配置真正实现是从 classpath 中搜寻所有的 META-INF/spring.factories 配置文件
并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项
通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类
然后将这些都汇总成为一个实例并加载到IOC容器中。
结论:
run方法流程分析:
application.yml
# 选择要激活哪个环境块 spring: profiles: active: prod # 用三条杠来分隔块 --- spring: profiles: dev # 配置环境的名称 server: port: 8081 --- spring: profiles: prod # 配置环境的名称 server: port: 8082
SpringBoot启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:
优先级1:项目路径下的config文件夹配置文件 优先级2:项目路径下配置文件 优先级3:资源路径下的config文件夹配置文件 优先级4:资源路径下配置文件
优先级由高到底,高优先级的配置会覆盖低优先级的配置;
SpringBoot会从这四个位置全部加载主配置文件;互补配置;
以HttpEncodingAutoConfiguration(Http编码自动配置)为例解释自动配置原理。
// 表示这是一个配置类,和以前编写的配置文件<Beans>一样,也可以给容器中添加组件; @Configuration // 启动指定类的ConfigurationProperties功能; // 进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来; // 并把HttpProperties加入到ioc容器中 @EnableConfigurationProperties({HttpProperties.class}) // Spring底层@Conditional注解 // 根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效; // 这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效 @ConditionalOnWebApplication( type = Type.SERVLET ) // 判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码处理的过滤器; @ConditionalOnClass({CharacterEncodingFilter.class}) // 判断配置文件中是否存在某个配置:spring.http.encoding.enabled; // 如果不存在,判断也是成立的 // 即使我们配置文件中不配置spring.http.encoding.enabled=true,也是默认生效的; @ConditionalOnProperty( prefix = "spring.http.encoding", value = {"enabled"}, matchIfMissing = true ) public class HttpEncodingAutoConfiguration { // 他已经和SpringBoot的配置文件映射了 private final Encoding properties; // 只有一个有参构造器的情况下,参数的值就会从容器中拿 public HttpEncodingAutoConfiguration(HttpProperties properties) { this.properties = properties.getEncoding(); } // 给容器中添加一个组件,这个组件的某些值需要从properties中获取 @Bean @ConditionalOnMissingBean //判断容器没有这个组件? public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); filter.setEncoding(this.properties.getCharset().name()); // ...... return filter; } }
一句话总结 :根据当前不同的条件判断,决定这个配置类是否生效!
//从配置文件中获取指定的值和bean的属性进行绑定 @ConfigurationProperties(prefix = "spring.http") public class HttpProperties { // ..... }
精髓
SpringBoot启动会加载大量的自动配置类
我们看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;
我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)
给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;
xxxxAutoConfigurartion:自动配置类;给容器中添加组件
xxxxProperties:封装配置文件中相关属性;供自动配置类使用
了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;
@Conditional派生注解(Spring注解版原生的@Conditional作用)
作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;
@Conditional扩展注解 | 作用(判断是否满足当前指定条件) |
---|---|
@ConditionalOnJava | 系统的Java版本是否符合要求 |
@ConditionalOnBean | 容器中存在指定Bean |
@ConditionalOnMissingBean | 容器中不存在指定Bean |
@ConditionalOnExpression | 满足SpEL表达式指定 |
@ConditionalOnClass | 系统中有指定的类 |
@ConditionalOnMissingClass | 系统中没有指定的类 |
@ConditionalOnSingleCandidate | 容器中只有一个指定的Bean,或者这个Bean是首选Bean |
@ConditionalOnProperty | 系统中指定的属性是否有指定的值 |
@ConditionalOnResource | 类路径下是否存在指定资源文件 |
@ConditionalOnWebApplication | 当前是Web环境 |
@ConditionalOnNotWebApplication | 当前不是Web环境 |
@ConditionalOnJndi | JNDI存在指定项 |
通过启用 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;
#开启springboot的调试类 debug: true
# k=v # 对空格的要求十分高! # 普通的key-value name: qinjiang #对象 student: name: qinjiang age: 3 #行内写法 student: {name: qinjiang, age: 3} #数组 pets: - cat - dog - pig pets: [cat,dog,pig]
@SpringBootTest class DemoApplicationTests { @Autowired Person person; //将person自动注入进来 @Test public void contextLoads() { System.out.println(person); //打印person信息 } }
@Component //注册bean @PropertySource(value = "classpath:user.properties") public class User { //直接使用@value @Value("${user.name}") //从配置文件中取值 private String name; @Value("#{9*2}") // #{SPEL} Spring表达式 private int age; @Value("男") // 字面量 private String sex; }
application.yaml
person: name: qinjiangage:3 happy: false birth: 2019/11/e2 maps: {k1:v1,k2:v2} lists: - code - music - girl dog: name: 旺财 age: 3
Person.java
@Component @ConfigurationProperties(prefix = "person") public class Person { private String name; private Integer age; private Boolean happy; private Date birth; private Map<String,object> maps; private List<object> lists; private Dog dog; }
@ConfigurationProperties作用:
将配置文件中配置的每一个属性的值,映射到这个组件中;
告诉springBoot将本类中的所有属性和配置文件中相关的配置进行绑定;
参数 prefix = "person”:将配置文件中的person下面的所有属性一一对应;
只有这个组件是容器中的组件,才能使用容器提供的@ConfigurationProperties功能。
配置文件占位符:
配置文件还可以编写占位符生成随机数
person: name: qinjiang${random.uuid} # 随机uuid age: ${random.int} # 随机int happy: false birth: 2000/01/01 maps: {k1: v1,k2: v2} lists: - code - girl - music dog: name: ${person.hello:other}_旺财 age: 1
如何使用
@Component //注册bean @ConfigurationProperties(prefix = "person") @Validated //数据校验 public class Person { @Email(message="邮箱格式错误") //name必须是邮箱格式 private String name; }
常见参数
@NotNull(message="名字不能为空") private String userName; @Max(value=120,message="年龄最大不能查过120") private int age; @Email(message="邮箱格式错误") private String email; 空检查 @Null 验证对象是否为null @NotNull 验证对象是否不为null, 无法查检长度为0的字符串 @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格. @NotEmpty 检查约束元素是否为NULL或者是EMPTY. Boolean检查 @AssertTrue 验证 Boolean 对象是否为 true @AssertFalse 验证 Boolean 对象是否为 false 长度检查 @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内 @Length(min=, max=) string is between min and max included. 日期检查 @Past 验证 Date 和 Calendar 对象是否在当前时间之前 @Future 验证 Date 和 Calendar 对象是否在当前时间之后 @Pattern 验证 String 对象是否符合正则表达式的规则 .......等等 除此以外,我们还可以自定义一些数据校验规则
在SpringBoot,我们可以使用以下方式处理静态资源:
webjars localhost:8080/webjars/
public,static,/**, resource localhost:8080/
优先级: resource > static (默认) > public
resources目录下有四个子文件夹:
pom.xml
<!-- Thymeleaf --> <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring5</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-java8time</artifactId> </dependency>
TestController.java
@Controller public class TestController { @RequestMapping("/t1") public String test1(){ //classpath:/templates/test.html return "test"; } }
引入提示:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
${...}
*{...}
#{...}
(来自于properties、yml文件的变量)@{...}
~{...}
以自定义用户视图解析器为例
// 因为类型要求为WebMvcConfigurer,所以我们实现其接口 // 可以使用自定义类扩展MVC的功能 @Configuration public class MyMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { // 浏览器发送/test , 就会跳转到test页面; registry.addViewController("/test").setViewName("test"); } }
@Configuration public class ServletConfig { @Bean public ServletRegistrationBean DIYServlet() { ServletRegistrationBean bean = new ServletRegistrationBean(new DIYServlet(), "/xxx"); bean.setxxxx(); ...... return bean; } }
@Bean public FilterRegistrationBean DIYFilter() { FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new DIYFilter()); //exclusions:设置哪些请求进行过滤排除掉,从而不进行统计 Map<String, String> initParams = new HashMap<>(); initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*"); bean.setInitParameters(initParams); //"/*" 表示过滤所有请求 bean.setUrlPatterns(Arrays.asList("/*")); return bean; }
依赖导入(pom.xml)
<!-- JDBC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MySQL --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
配置参数(application.yml)
spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.jdbc.Driver
使用
@RestController public class JDBCController { @Autowired JdbcTemplate jdbcTemplate; @GetMapping("/userList") public List<Map<String,object>> userList(){ String sql = "select * from user"; List<Map<String,object>> list_maps = jdbcTemplate.queryForList(sql); return list_maps; } }
依赖导入(pom.xml)
<!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.21</version> </dependency>
配置参数(application.yml)
spring: datasource: username: root password: 123456 #?serverTimezone=UTC解决时区的报错 url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
Log4j依赖
<!-- https://mvnrepository.com/artifact/log4j/log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
编写DruidConfig配置类
@Configuration public class DruidConfig { /* 将自定义的 Druid数据源添加到容器中,不再让 Spring Boot 自动创建 绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource 从而让它们生效 @ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中 前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中 */ @ConfigurationProperties(prefix = "spring.datasource") @Bean public DataSource druidDataSource() { return new DruidDataSource(); } }
配置Druid数据源监控
//配置 Druid 监控管理后台的Servlet; //内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式 @Bean public ServletRegistrationBean statViewServlet() { // 设置进入后台页面的url ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); // 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet // 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到 Map<String, String> initParams = new HashMap<>(); initParams.put("loginUsername", "admin"); //后台管理界面的登录账号 initParams.put("loginPassword", "123456"); //后台管理界面的登录密码 //后台允许谁可以访问 //initParams.put("allow", "localhost"):表示只有本机可以访问 //initParams.put("allow", ""):为空或者为null时,表示允许所有访问 initParams.put("allow", ""); //deny:Druid 后台拒绝谁访问 //initParams.put("kuangshen", "192.168.1.20");表示禁止此ip访问 //设置初始化参数 bean.setInitParameters(initParams); return bean; }
配置 Druid web 监控 filter 过滤器
//WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计 @Bean public FilterRegistrationBean webStatFilter() { FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new WebStatFilter()); //exclusions:设置哪些请求进行过滤排除掉,从而不进行统计 Map<String, String> initParams = new HashMap<>(); initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*"); bean.setInitParameters(initParams); //"/*" 表示过滤所有请求 bean.setUrlPatterns(Arrays.asList("/*")); return bean; }
导入依赖
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency>
配置数据库连接信息
spring: datasource: username: root password: 123456 #?serverTimezone=UTC解决时区的报错 url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 mybatis: mapper-locations: classpath:mappers/**/*.xml
创建Mapper接口
//@Mapper:表示本类是一个 MyBatis 的 Mapper @Mapper @Repository public interface DepartmentMapper { // 获取所有部门信息 List<Department> getDepartments(); // 通过id获得部门 Department getDepartment(Integer id); }
对应Mapper映射文件xml
DepartmentMapper.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.kuang.mapper.DepartmentMapper"> <select id="getDepartments" resultType="Department"> select * from department; </select> <select id="getDepartment" resultType="Department" parameterType="int"> select * from department where id = #{id}; </select> </mapper>
maven配置资源过滤问题
<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> </resources>
这里使用开源项目:Dynamic DataSource
依赖导入
<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.3.6</version> </dependency>
配置数据源
spring: datasource: dynamic: primary: master #设置默认的数据源或者数据源组,默认值即为master strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源 datasource: master: url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置 slave_1: url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver slave_2: url: ENC(xxxxx) # 内置加密,使用请查看详细文档 username: ENC(xxxxx) password: ENC(xxxxx) driver-class-name: com.mysql.jdbc.Driver #......省略 #以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
使用 @DS 切换数据源。
@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。一般在Service层使用。
注解 | 结果 |
---|---|
没有@DS | 默认数据源 |
@DS("dsName") | dsName可以为组名也可以为具体某个库的名称 |
@Service @DS("slave") public class UserServiceImpl implements UserService { @Autowired private JdbcTemplate jdbcTemplate; public List selectAll() { return jdbcTemplate.queryForList("select * from user"); } @Override @DS("slave_1") public List selectByCondition() { return jdbcTemplate.queryForList("select * from user where age >10"); } }
给方法添加@Async注解
//告诉Spring这是一个异步方法 @Async public void hello(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("业务进行中...."); }
SpringBoot 就会自己开一个线程池,进行调用!
但是要让这个注解生效,我们还需要在主程序上添加一个注解@EnableAsync ,开启异步注解功能;
@EnableAsync //开启异步注解功能 @SpringBootApplication public class SpringbootTaskApplication { public static void main(String[] args) { SpringApplication.run(SpringbootTaskApplication.class, args); } }
1、创建一个ScheduledService
@Service public class ScheduledService { //秒 分 时 日 月 周几 //0 * * * * MON-FRI //注意cron表达式的用法; @Scheduled(cron = "0 * * * * 0-7") public void hello(){ System.out.println("hello....."); } }
2、这里写完定时任务之后,我们需要在主程序上增加@EnableScheduling 开启定时任务功能
@EnableAsync //开启异步注解功能 @EnableScheduling //开启基于注解的定时任务 @SpringBootApplication public class SpringbootTaskApplication { public static void main(String[] args) { SpringApplication.run(SpringbootTaskApplication.class, args); } }
3、详细了解cron表达式
http://www.bejson.com/othertools/cron/
4、常用的cron表达式
(1)0/2 * * * * ? 表示每2秒 执行任务 (1)0 0/2 * * * ? 表示每2分钟 执行任务 (1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务 (2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业 (3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作 (4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 (5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时 (6)0 0 12 ? * WED 表示每个星期三中午12点 (7)0 0 12 * * ? 每天中午12点触发 (8)0 15 10 ? * * 每天上午10:15触发 (9)0 15 10 * * ? 每天上午10:15触发 (10)0 15 10 * * ? 每天上午10:15触发 (11)0 15 10 * * ? 2005 2005年的每天上午10:15触发 (12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发 (13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发 (14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 (15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 (16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发 (17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发 (18)0 15 10 15 * ? 每月15日上午10:15触发 (19)0 15 10 L * ? 每月最后一日的上午10:15触发 (20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发 (21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发 (22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
引入pom依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
看它引入的依赖,可以看到 jakarta.mail
<dependency> <groupId>com.sun.mail</groupId> <artifactId>jakarta.mail</artifactId> <version>1.6.4</version> <scope>compile</scope> </dependency>
配置文件
spring.mail.username=24736743@qq.com spring.mail.password=你的qq授权码 spring.mail.host=smtp.qq.com # qq需要配置ssl spring.mail.properties.mail.smtp.ssl.enable=true
获取授权码:在QQ邮箱中的设置->账户->开启pop3和smtp服务
Spring单元测试
@Autowired JavaMailSenderImpl mailSender; @Test public void contextLoads() { //邮件设置1:一个简单的邮件 SimpleMailMessage message = new SimpleMailMessage(); message.setSubject("通知-明天来狂神这听课"); message.setText("今晚7:30开会"); message.setTo("24736743@qq.com"); message.setFrom("24736743@qq.com"); mailSender.send(message); } @Test public void contextLoads2() throws MessagingException { //邮件设置2:一个复杂的邮件 MimeMessage mimeMessage = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); helper.setSubject("通知-明天来狂神这听课"); helper.setText("<b style='color:red'>今天 7:30来开会</b>",true); //发送附件 helper.addAttachment("1.jpg",new File("")); helper.addAttachment("2.jpg",new File("")); helper.setTo("24736743@qq.com"); helper.setFrom("24736743@qq.com"); mailSender.send(mimeMessage); }
https://links.jianshu.com/go?to=https%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzI4OTA3NDQ0Nw%3D%3D%26mid%3D2455549015%26idx%3D2%26sn%3D44fa3621a8df3bd88e8c7064f1832105%26chksm%3Dfb9ca837cceb2121b9c15064f0a5f6c87f610c4b19c030cf385e0b297abe91cd177895c9c0fa%26scene%3D126%26sessionid%3D1600250050%26key%3Dcf1b11cd59da7ce7d9f9d510eb01a11d336a0136e7a8daea4a88ce757d5d899300b20616201de2a8a81284c4de25ea9a694a218fd06da8b4a0f3be80f7e34e06c63adefb7ffb7124ff8cc0e4833f8bbfbde4da7120c547c133d899108caf4876dd6c5d6d379884c956fcf7d5a503af6a97513175a8d5fb4a3421f55d51d41a0e%26ascene%3D14%26uin%3DOTMyNDYxMzQw%26devicetype%3DWindows%2B10%2Bx64%26version%3D6300002f%26lang%3Dzh_CN%26exportkey%3DAUOK0L%252F5IlqEYWRwGtxG%252FwI%253D%26pass_ticket%3DfEs1HHbuT5JrkDds5Mn8cg1NzokPvax7ngMP5blOa%252FDonl2n0B0odFiegTD8NGcg%26wx_header%3D0
@Data @AllArgsConstructor @JSONType(orders = {"code", "message", "data"}) public class Result { private int code; private String message; private Object data; public static Result error(int code, String message) { return new Result(code, message, null); } public static Result error(ResultStatus info) { return new Result(info.getCode(), info.getMessage(), null); } public static Result success(Object obj) { return new Result(200, "SUCCESS", obj); } }
@Getter public enum ResultStatus { PERMISSION_DENIED(1001, "Unable to authenticate"), TOKEN_EXPIRE(1002, "token expire"), INTERNAL_SERVER_ERROR(1003, "simple error"); // ... private final int code; private final String message; ResultStatus(int code, String message) { this.code = code; this.message = message; } }
@RestController @Slf4j public class BasicErrorController implements ErrorController { @RequestMapping(value = {"/error"}) public Object error(HttpServletResponse response,Exception ex) { log.error(ex.getMessage(), ex); int code = response.getStatus(); String message = HttpStatus.valueOf(response.getStatus()).getReasonPhrase(); return Result.error(code, message); } }
@Data @AllArgsConstructor public class ResultException extends Exception{ ResultStatus status; }
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) @Documented @ResponseBody public @interface JsonResponseBody { }
@RestControllerAdvice @Slf4j public class JsonResponseBodyAdvice implements ResponseBodyAdvice<Object> { private static final Class<? extends Annotation> ANNOTATION_TYPE = JsonResponseBody.class; /** * 判断类或者方法是否使用了 @JsonResponseBody */ @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE) || returnType.hasMethodAnnotation(ANNOTATION_TYPE); } /** * 当类或者方法使用了 @JsonResponseBody 就会调用这个方法 */ @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof String) { response.getHeaders().add("Content-Type", "application/json"); return JSON.toJSONString(Result.success(body)); } if (body instanceof Result) { return body; } return Result.success(body); } /** * 提供对标准Spring MVC异常的处理 * * @param ex the target exception * @param request the current request */ @ExceptionHandler(Exception.class) public final ResponseEntity<Result> exceptionHandler(Exception ex, WebRequest request) { log.error("ExceptionHandler: {}", ex.getMessage()); if (ex instanceof ResultException) { return handleResultException((ResultException) ex); } // TODO: 2019/10/05 galaxy 这里可以自定义其他的异常拦截 return handleException(ex, new HttpHeaders(), request); } /** * 对ResultException类返回返回结果的处理 */ protected ResponseEntity<Result> handleResultException(ResultException ex) { Result body = Result.error(ex.getStatus()); return new ResponseEntity<>(body, HttpStatus.OK); } /** * 异常类的统一处理 */ protected ResponseEntity<Result> handleException(Exception ex, HttpHeaders headers, WebRequest request) { Result body = Result.error(500,"Internal Server Error"); HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); return new ResponseEntity<>(body, headers, status); } }
方式一
就是我们在我们自己写的beforeBodyWrite
的方法中,将返回值用 Result 包装过后再将Result 转为 String 类型进行返回。
@Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof String) { response.getHeaders().add("Content-Type", "application/json"); return JSON.toJSONString(Result.success(body)); } if (body instanceof Result) { return body; } return Result.success(body); }
方式二
处理 spring 框架提供的转换器,如下代码中的方式,任选其一即可。
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { // 第一种方式是将 json 处理的转换器放到第一位,使得先让 json 转换器处理返回值,这样 String转换器就处理不了了。 converters.add(0, new MappingJackson2HttpMessageConverter()); // 第二种就是把String类型的转换器去掉,不使用String类型的转换器 converters.removeIf(httpMessageConverter -> httpMessageConverter.getClass() == StringHttpMessageConverter.class); } }