打赌!绝对有你不知道的 SpringBoot 使用技巧

559 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

日期时间格式化处理

两个注解

  • 注解 @JsonFormat 主要是 后端到前端 的时间格式的转换, @JsonFormat 注解需要添加 jackson 相关的依赖包. 如果是 SpringBoot 项目就不需要自己手动添加依赖,因为在 spring-boot-start-web 下已经包含了 jackson 相关依赖.
  • 注解 @DateTimeFormat 主要是 前端到后端 的时间格式的转换,@DateTimeFormat 属于 Spring 的注解.

后端 -> 前端

当后端接口给前端返回数据时, 如果实体类中有 Date 类型的字段.

如果不做处理, 那么返回给前端的是 2020-06-28T08:43:56.719+0000 这种类型的时间, 不符合要求.

可以在 application.yml 中统一对日期的格式进行配置.

#时间格式配置
spring:
  jackson:
  date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

上面配置是统一的处理, 如果某个实体类有自己的特定格式, 可以通过在实体类的 Date 字段上使用 @JsonFormat 注解单独处理.

@JsonFormat(pattern="yyyy-MM-dd ",timezone="GMT+8")
private Date lastLoginTime; //最后登录时间

优先级 : @JsonFormat 注解 > 统一配置 > 未处理

前端 -> 后端

在 controller 层自动封装映射对象时,在对应的接收前台数据的对象的 Date 属性上加 @DateTimeFormat 注解即可.

@DateTimeFormat(pattern = "yyyy-MM-dd")  // 表示前端需要传入 yyyy-MM-dd 格式的日期
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date symstarttime;
 
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date symendtime;

需要注意的是: @DateTimeFormat 注解的 pattern 属性指定的日期时间格式, 是前端传入的日期时间格式相对应.

\

获取项目全部URL

Spring boot 项目在做URL权限控制的时候需要获得全部的URL,一个一个去controller中找费时费力。而且有的权限点的命名和URL有一定的对应关系。如果能用程序获得全部URL,将会省去很多事。下面就介绍一种获取URL的方法。在项目中添加如下 Controller,请求 /getAllUrl,即可看到项目所有的URL。当然也可以根据项目将URL写入数据库或写入配置文件。

方式1:

@Controller
@RequestMapping("/*")
public class UrlController {

    @Autowired
    WebApplicationContext applicationContext;
    
    @GetMapping("/getAllUrl")
    @ResponseBody
    public List<String> getAllUrl(){
        RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
        //获取url与类和方法的对应信息
        Map<RequestMappingInfo,HandlerMethod> map = mapping.getHandlerMethods();
        List<String> urlList = new ArrayList<>();
        for (RequestMappingInfo info : map.keySet()){
            //获取url的Set集合,一个方法可能对应多个url
            Set<String> patterns = info.getPatternsCondition().getPatterns();
            for (String url : patterns){
                urlList.add(url);
            }
        }
        return urlList;
    }
}

方式2:

	@Autowired
	WebApplicationContext applicationContext;
 
	@RequestMapping(value = "v1/getAllUrl", method = RequestMethod.POST)
	public Object getAllUrl() {
		RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
		// 获取url与类和方法的对应信息
		Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
		List<Map<String, String>> list = new ArrayList<Map<String, String>>();
		for (Entry<RequestMappingInfo, HandlerMethod> m : map.entrySet()) {
			Map<String, String> map1 = new HashMap<String, String>();
			RequestMappingInfo info = m.getKey();  
            HandlerMethod method = m.getValue();  
            PatternsRequestCondition p = info.getPatternsCondition();  
            for (String url : p.getPatterns()) {  
            	map1.put("url", url);
            }  
            map1.put("className", method.getMethod().getDeclaringClass().getName()); // 类名 
            map1.put("method", method.getMethod().getName()); // 方法名 
            RequestMethodsRequestCondition methodsCondition = info.getMethodsCondition();
            for (RequestMethod requestMethod : methodsCondition.getMethods()) {
            	map1.put("type", requestMethod.toString());
			}
            list.add(map1);
		}
		JSONArray jsonArray = JSONArray.fromObject(list);
		return jsonArray;
	}

对 BigDecimal 的处理

BigDecimal 类型的字段如果值为类似 0.000001 这种精度很高的小数, 直接返回给前端, 则会转为科学记数法去展示. 想在只需要其展示小数点后两位即可.

自定义 Jackson 处理类

1.自定义处理类 :

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

public class BigDecimalJsonSerializer extends JsonSerializer<BigDecimal>{
    @Override
    public void serialize(BigDecimal arg0, JsonGenerator arg1, SerializerProvider arg2) throws IOException {
        if (arg0 != null) {
            BigDecimal newValue = arg0.setScala(2, BigDecimal.Round_DOWN);
            arg1.writeString(arg0.toPlainString());
        }
    }
}

2.在实体类上使用 :

@JsonSerialize(using = BigDecimalJsonSerializer.class)
private BigDecimal amount;

使用 @NumberFormat 注解

还没使用过, 不过好像是可以格式化的.

接口启用压缩

(1) 优化场景

在优化接口时间的过程中,发现很多接口的 Content Download 时间较长,除了网络问题,就是接口请求的数据太大了,有的达到了几百 kb。

控制返回参数的收效甚微,这时开启 gzip 就非常有用了,可以压缩接口请求的数据,一般的 json 文本压缩比率很大,开启之后接口时间大幅下降!

(2) 启用步骤

Spring Boot 项目配置比较简单,如下配置 Gzip 就开启了!

server:
  #如果请求需要通过网关转发和返回时,则以下配置需要配置在网关上,而不是目标微服务上
  compression:
    enabled: true
    #响应内容长度超过设置大小时进行压缩,默认值为2048(2KB,不带单位时默认为字节)
    min-response-size: 5kb
    #对指定的响应类型进行压缩,值是数组,用逗号隔开
	  mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml

(3) 验证 Gzip 是否开启成功

Google 浏览器打开 F12,切换到 NetWork 下,右键表头选择 Response Headers 下的 Content-Encoding,如果开启了 Gzip,对应接口中的 Content-Encoding 中会有显示。

注意: 在同时开启 https 和 http 的工程中,Gzip 配置只对主端口生效!

开启前:数据 Size 为 511KB,右侧时间蓝色部分(Content Download)较长。开启后:数据 Size 为 21.3KB,Content-Encoding 中显示 gzip,右侧时间蓝色部分消失。对比效果非常明显,接口时间由 1.03s 降到 164ms,完美!

配置接口的统一请求前缀

如下配置:

server:
  servlet:
    context-path: /api/v1.0

那么之前 http://localhost:8080/user/1 就需要变为 http://localhost:8080/api/v1.0/user/1 才能访问.

自定义错误页面

当请求发生了 404 或者 500 错误时, Spring Boot 默认会在 error 目录下查找 4xx, 5xx 的文件作为错误视图。当找不到时会用 error 作为默认的错误页面视图名, 如果还是找不到, 则来到 SpringBoot 默认的错误页面.

自定义错误页面其实很简单, 提供 4xx 和 5xx 页面即可. 如果开发者不需要向用户展示详细的错误信息, 那么可以把错误信息定义成静态页面, 直接在resources/static目录下创建error目录, 然后在 error 目录中创建错误展示页面.

错误展示页面的命名规则有两种:一种是4xx.html, 5xx.html;另一种是直接使用响应码命名文件, 例如 404.html, 405.html, 500.html. 第二种命名方式划分得更细, 当出错时, 不同的错误会展示不同的错误页面.

路径映射

有时候我们想要发出一个请求, 只是跳转到指定的页面, 并不获取数据. 如果使用 SpringMVC, 就必须要写一个 Controller 类, 再写一个方法进行页面跳转, 比较麻烦. 使用 SpringBoot 之后, 只要实现WebMvcConfigurer接口中的addViewControllers方法即可.

@Configuration
public class WebConfig implements WebMvcConfigurer{
    
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {   
        registry.addViewController("/admin").setViewName("forward:/admin.html");
    }
}

重写 WebMvcConfigurer 的 addViewControllers 方法, 并不会覆盖 WebMvcAutoConfiguration 中的 addViewControllers(在此方法中, Spring Boot 将 “/” 映射至 index.html), 即自己的配置和 SpringBoot 的自动配置同时有效. 同样在该方法中还可以修改默认的首页.

数据预加载

想要在项目启动时就去加载一些数据或做一些操作时. SpringBoot 中只需要编写一个 Java 类, 实现CommandLineRunner接口, 重写 run() 方法就可以, 无需其他配置.

SpringBoot 在启动后, 会自动遍历 CommandLineRunner 接口的实例, 并运行它们的 run() 方法. 如果接口 CommandLineRunner 的实现类有多个, 可以在实现类上, 使用@Order注解来设置所有 CommandLineRunner 实现类的先后运行顺序. value 值越小, 越先执行.

@Component
@Order(value = 100)
public class MyStartupRunner implements CommandLineRunner {
     @Override
     public void run(String... args) throws Exception {
           System.out.println("MyStartupRunner.run()");
           System.out.println("服务启动执行,预先加载数据");
     }
}

静态资源映射

默认静态资源映射

Spring Boot 默认提供了静态资源处理, 映射优先级顺序为:META-INF/resources > resources > static > public

可以在上面 4 个路径下都放一张同名但内容不同的图片, 访问一下即可验证. 访问也很简单, 直接加资源名即可 : localhost:8080/ + 资源名. 比如: localhost:8080/123.jpg

在 Eclipse 工程里面路径是这样:

在 IDEA 中, 工程目录如下:

自定义静态资源映射

在实际开发中, 可能需要自定义静态资源访问路径, 那么可以继承 WebMvcConfigurerAdapter 来实现.

//配置静态资源映射
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //将所有/sanjingye/** 访问都映射到 classpath:/sanjingye/ 目录下
        registry.addResourceHandler("/sanjingye/**")
                .addResourceLocations("classpath:/sanjingye/");
    }
}

重启项目, 访问:http://lcoalhost:8080/sanjingye/123.jpg 能正常访问 sanjingye 目录下的 123.jpg 图片资源.

也可以指定外部的目录 :

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    //将所有/sanjingye/** 访问都映射到 D:/sanjingye/ 目录下
    registry.addResourceHandler("/sanjingye/**")
        .addResourceLocations("file:D:/sanjingye/");
}

接管 Web 配置

通过在一个配置类(有 @Configuration 注解的类)上添加 @EnableWebMvc 注解,就可以实现完全自定义的 MVC 配置.

不过, 既要保留 Spring Boot 提供的配置, 又需要增加自己的额外的配置时, 可以定义一个配置类并继承 WebMvcConfigurerAdapter, 来实现.

注意 : 在 SpringBoot 1.5 中是使用这个类, 但在 SpringBoot2.0 中, 该类已被废弃, 使用 WebMvcConfigurationSupport 类, 这个类是 WebMvcConfigurerAdapter 的扩展和替代, 它们的使用方法都是一样的.

以下是 WebMvcConfigurerAdapter 比较常用的重写方法 :

/** 解决跨域问题 **/
public void addCorsMappings(CorsRegistry registry) ;
/** 添加拦截器 **/
void addInterceptors(InterceptorRegistry registry);
/** 这里配置视图解析器 **/
void configureViewResolvers(ViewResolverRegistry registry);
/** 配置内容裁决的一些选项 **/
void configureContentNegotiation(ContentNegotiationConfigurer configurer);
/** 视图跳转控制器 **/
void addViewControllers(ViewControllerRegistry registry);
/** 静态资源处理 **/
void addResourceHandlers(ResourceHandlerRegistry registry);
/** 默认静态资源处理器 **/
void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer);

使用 Servlet / Filter / Listener

SpringBoot 默认是以 jar 包的方式启动嵌入式的 Servlet 容器来启动 web 应用, 所以没有 web.xml 文件. 但有时候确实需要 Servlet , Filter, Listener 来实现一些功能.

在 SpringBoot 中使用 Servlet 有两种方法(Filter 和 Listener 也是同样的方式)

方式一 : 通过代码注册 Servlet

通过 ServletRegistrationBean, FilterRegistrationBean ServletListenerRegistrationBean分别可以获取 Servlet , Filter, Listener.

① 配置类中配置 bean.

@Configure
public class ServletConfig {
    //使用代码注册Servlet(不需要@ServletComponentScan注解)
    @Bean
    public ServletRegistrationBean firstServlet() {
        //参数1:servlet对象, 参数2:映射路径.
        ServletRegistrationBean firstServlet = new ServletRegistrationBean(new FirstServlet(), "/firstServlet");
        //可以进行相关属性的设置.
        firstServlet.setLoadOnStartup(1);
        return firstServlet;
    }
}

② 自定义的 Servlet

public class FirstServlet extends HttpServlet {
     @Override
     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
           doPost(req, resp);
     }
     @Override
     protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
           ...
     }
}

方式二 : 使用注解注册 Servlet

在启动类上使用@ServletComponentScan 注解扫描需要注册的 Servlet.

需要注册的 Servlet, Filter, Listener 可以直接通过 @WebServlet, @WebFilter, @WebListener 注解实现自动注册, 无需其他代码.

① SpringBoot 启动类

//使用注解注册Servlet
@ServletComponentScan(basePackages="it.com.servlet")
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

② 自定义 Servlet

package it.com.servlet;

@WebServlet(urlPatterns="/firstServlet")
public class FirstServlet extends HttpServlet {
     @Override
     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
           doPost(req, resp);
     }
     @Override
     protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          ...
     }
}

如上是使用 Servlet 的两种方式, 使用 Filter 和 Listener 也是同样的方式.

代码演示 : 使用 Filter

@Bean
public FilterRegistrationBean myFilter(){
    FilterRegistrationBean registrationBean = new FilterRegistrationBean();
    registrationBean.setFilter(new MyFilter());
    registrationBean.setUrlPatterns(Arrays.asList("/hello","/myServlet"));    //过滤路径
    Map<String, String> initParameters = new HashMap();
    //excludes用于配置不需要参数过滤的请求url
    initParameters.put("excludes", "/favicon.ico,/img/*,/js/*,/css/*");
    registrationBean.setInitParameters(initParameters);            //设置init值
    return registrationBean;
}

代码演示 : 使用 Listener

@Bean
public ServletListenerRegistrationBean myListener(){
    ServletListenerRegistrationBean<MyListener> registrationBean = new ServletListenerRegistrationBean<>(new MyListener());
    return registrationBean;
}