本篇是Web开发整合的下篇内容,具体包括、启动系统任务、整合Servlet、Filter和Listener路径映射、配置AOP、自定义欢迎页、自定义favicon和除去某个自动配置等相关知识。

启动系统任务

众所周知,有一些特殊的任务需要在系统启动时执行,如配置文件加载、数据库初始化等操作。如果没有使用SpringBoot,这些问题可以在Listener中得到解决。SpringBoot对此提供了两种方式:CommandLineRunnerApplicationRunner。这两种基本上差不多,只是使用时的参数不同而已。

CommandLineRunner

SpringBoot项目在启动时会遍历所有CommandLineRunner的实现类,并调用其中的run方法,如果整个系统中存在多个CommandLineRunner的实现类,那么可以使用@Order注解来对这些实现类的调用顺序进行排序。

查看CommandLineRunner源码可知为一个函数式接口,其中只包含一个run方法:

1
2
3
4
@FunctionalInterface
public interface CommandLineRunner {
void run(String... args) throws Exception;
}

新建一个SpringBoot的Web项目(我依旧使用前面corsspringboot项目),然后新建一个command包,里面新建两个CommandLineRunner的实现类MyCommandLineRunner1MyCommandLineRunner2,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@Order(1)
public class MyCommandLineRunner1 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("This is the Runner1>>>>>"+ Arrays.toString(args));
}
}


@Component
@Order(2)
public class MyCommandLineRunner2 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("This is the Runner2>>>>>"+ Arrays.toString(args));
}
}

解释一下上述代码的含义:自定义两个MyCommandLineRunner类,实现CommandLineRunner接口,并重写其中的run方法,请注意需要使用@Component注解将自定义的类注入到Spring容器中。@Order注解用来描述CommandLineRunner对象的执行顺序,数字越小越先执行。其中的run方法是调用的核心逻辑,参数是系统启动时传入的参数,也就是入口类中main方法的参数。(在调用SpringApplication.run(CorsspringbootApplication.class, args)这个方法时,才被传入SpringBoot项目中。)

那么问题来了如何在系统启动时,传入参数呢?很简单,按照图示操作即可:

然后启动项目,可以发现控制台就会输出以下信息:

1
2
This is the Runner1>>>>>[西游记, 吴承恩, 红楼梦, 曹雪芹]
This is the Runner2>>>>>[西游记, 吴承恩, 红楼梦, 曹雪芹]

请注意多个参数之间是以空格分隔的,这一点尤其要引起重视。

ApplicationRunner

前面也说过ApplicationRunnerCommandLineRunner基本一致,区别主要体现在run方法的参数上。

SpringBoot项目在启动时会遍历所有ApplicationRunner的实现类,并调用其中的run方法,如果整个系统中存在多个ApplicationRunner的实现类,那么可以使用@Order注解来对这些实现类的调用顺序进行排序。

查看ApplicationRunner源码可知它也是一个函数式接口,其中也只是包含一个run方法,但是参数是ApplicationArguments类型,这一点与CommandLineRunner不同的:

1
2
3
4
@FunctionalInterface
public interface ApplicationRunner {
void run(ApplicationArguments args) throws Exception;
}

新建一个SpringBoot的Web项目(我依旧使用前面corsspringboot项目),然后新建一个application包,里面新建两个ApplicationRunner的实现类MyApplicationRunner1MyApplicationRunner2。现在问题在于传入的参数是ApplicationArguments类型,查看一下它的源码:

1
2
3
4
5
6
7
public interface ApplicationArguments {
String[] getSourceArgs();
Set<String> getOptionNames();
boolean containsOption(String name);
List<String> getOptionValues(String name);
List<String> getNonOptionArgs();
}

一共有5个方法,这个具体怎样使用也不太清楚,老规矩找一下它的实现类,发现了一个DefaultApplicationArguments类,它这里面就实现了全部方法。所以可以借鉴其中的代码来书写MyApplicationRunner1MyApplicationRunner2这两个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component
@Order(1)
public class MyApplicationRunner1 implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
List<String> nonOptionArgs = args.getNonOptionArgs();
System.out.println("1-nonOptionArgs>>>>>> "+nonOptionArgs);
Set<String> optionNames = args.getOptionNames();
for(String optionName:optionNames){
System.out.println("1-key:"+optionName+";value:"+args.getOptionValues(optionName));
}
}
}

@Component
@Order(2)
public class MyApplicationRunner2 implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
List<String> nonOptionArgs = args.getNonOptionArgs();
System.out.println("2-nonOptionArgs>>>>>> "+nonOptionArgs);
Set<String> optionNames = args.getOptionNames();
for(String optionName:optionNames){
System.out.println("2-key:"+optionName+";value:"+args.getOptionValues(optionName));
}
}
}

解释一下上述代码的含义:自定义两个MyApplicationRunner类,实现ApplicationRunner接口,并重写其中的run方法,请注意需要使用@Component注解将自定义的类注入到Spring容器中。@Order注解用来描述MyApplicationRunner对象的执行顺序,数字越小越先执行。

不同于CommandLineRunner中run方法的String数组参数,这里的run方法的参数是一个ApplicationArguments对象,如果你想从ApplicationArguments对象中获取入口类main方法中接收的参数,那么需要调用ApplicationArguments对象的getNonOptionArgs。当然如果你想获取项目启动命令行中参数的key,那么需要调用ApplicationArguments对象的getOptionNames方法。将本项目打包成一个jar包,然后使用java -jar xxx.jar -name=Envy命令来启动该项目,此时getOptionNames方法获取到的就是name,而getOptionValues方法则是获取到相应的value。

接下来使用mvn package命令来打包项目,然后找到打包生成的jar同级目录下,执行下面的命令来启动项目:

1
java -jar corsspringboot-0.0.1-SNAPSHOT.jar --name=Envy --age=24 西游记 吴承恩 红楼梦 曹雪芹

解释一下上面代码的含义:--name=Envy --age=24都属于getOptionNames/getOptionValues的范围。而后面的“西游记 吴承恩 红楼梦 曹雪芹”可以通过getNonOptionArgs方法来获取,获取到的是一个数组,相当于前面运行时配置的ProgramArguments

整合Servlet、Filter和Listener路径映射

一般来说,当你使用Spring、SpringMVC等这些框架以后,基本上就不会再使用Servlet、Filter以及Listener,但是当你整合一些第三方框架的时候,可能不得不使用Servlet,这种例子太多了,像报表插件、统计插件、登录插件等等,这是就需要使用Servlet了。SpringBoot对于整合这些基本的Web组件也提供了很好的支持。

还是以前面的corsspringboot项目为例进行说明。新建一个union包,接着在里面自定义三个类:MyServlet、MyFilter和MyListener类,分别去继承/实现Servlet(一般用HttpServlet)、Filter以及Listener(一般用ServletRequestListener)类/接口,并重写相应的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@WebServlet("/myname")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("name >>>>>> "+req.getParameter("name"));
}
}



@WebFilter("/*")
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("MyFilter >>>>>> init");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("MyFilter >>>>>> doFilter");
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {
System.out.println("MyFilter >>>>>> destroy");
}
}



@WebListener
public class MyListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("MyListener >>>>>> requestDestroyed");
}

@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("MyListener >>>>>> requestInitialized");
}
}

简单解释一下上述代码的含义:定义了三个基本的组件,分别使用@WebServlet@WebFilter@WebListener三个注解进行标记。注意一下这里的Listener是以ServletRequestListener,但是对于其他的Listener,如HttpSessionListenerServletContextListener等也是支持的。

请注意由于这里的Servlet、Filter和Listener不是,因此仅仅使用@SpringBootApplication注解是无法完成扫描的,需要使用@ServletComponentScan注解才行,这一点需要注意,这个注解需要添加到项目的入口类上。

然后启动项目,在浏览器地址栏中输入http://localhost:8080/myname?name=envy(如果不行就使用postman进行测试,注意需要将前面拦截器的代码注释掉,否则请求过不来),就可以看到控制台已经输出相关日志信息:

路径映射

一般情况下, 使用了页面模板后,用户需要通过控制器才能访问页面。有一些页面需要在控制器中加载数据,然后渲染才能显示出来。还有一些页面在控制器中不需要加载数据,只是完成简单的跳转,对于这种页面可以直接配置路径映射,以提高访问速度。举个例子来说,现在有两个使用Thymeleaf作为模板引擎时的页面login.htmlindex.html,这两个页面不需要加载任何数据,所以最好的解决办法就是不放入控制器中 ,直接在MVC配置中重写addViewControllers方法,用来配置映射关系。

在config包内新建一个MyControllerWebMcvConfig类,实现WebMvcConfigurer接口,并重写addViewControllers方法,又看到了WebMvcConfigurer接口,这个接口非常重要,里面实现了很多方法,如果你想自定义配置,最简单的方式就是实现这个接口,然后实现它对应的方法即可。MyControllerWebMcvConfig类中的代码为:

1
2
3
4
5
6
7
8
@Configuration
public class MyControllerWebMcvConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/index").setViewName("index");
}
}

这样你不需要在controller中定义相应的访问路由,只需要在resources/templates目录下新建对应的html页面,然后就可以使用诸如http://localhost:8080/index等地址来访问页面。

配置AOP

AOP简介

在介绍AOP(Aspect Oriented pragramming)面向切面编程之前,先来考虑这么一个场景:公司有一个人力资源管理系统目前已经上线,但是系统运行不稳定时快时慢,为了检测出到底是哪个环节出问题了,开发人员想要监控每一个方法的执行时间,再根据这些执行时间来判断问题所在。当问题解决后,需要将这些监控移除掉,以避免影响正常的系统运行。如果手动修改系统中成千上万个方法,那么工作量未免太大,且这些监控方法之后还要移除,显然是一个非明智之举。

我们希望有这么一种技术,能够在系统运行过程中动态地添加代码,这样问题就迎刃而解了。这种在系统运行时动态添加代码的方式称为面向切面编程(AOP)。Spring框架对AOP提供了很好的支持,AOP 中有一些常见的概念需要事先了解一下。我在《Spring学习(3):Spring AOP》和《Spring学习(4):基于AspectJ的AOP开发》这两篇文章中对AOP进行了介绍和使用,有兴趣的可以去看一下。不过这里还是简单提一下相关的概念。

Joinpoint(连接点):类里面可以被增强的方法就是连接点,Spring只支持方法增强。如想修改某个方法的功能,那么该方法就是一个连接点。Pointcut(切入点):对Joinpoint(连接点)进行拦截的定义即为切入点。如拦截所有以insert开始的方法,这个定义即为切入点。Advice(通知/增强):拦截到Joinpoint(连接点)之后所要做的事情就是通知。如打印日志监控就分为:前置通知、后置通知、异常通知、最终通知和环绕通知。Aspect(切面):其实就是Pointcut(切入点)和Advice(通知/增强)的结合。Target(目标对象):指需要增强的类。接下来介绍如何在SpringBoot中实现AOP。

SpringBoot支持

SpringBoot在Spring的基础上对AOP的配置提供了自动化配置解决方案spring-boot-starter-aop,极大的方便了开发者在SpringBoot中使用AOP。第一步,使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为aopspringboot,然后添加spring-boot-starter-webspring-boot-starter-aop依赖:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--添加AOP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

第二步,新建service包,并在里面新建UserService类,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserService {
public String getUserById(Integer id){
System.out.println("this is getUserById method......");
return "user";
}

public void deleteUserById(Integer id){
System.out.println("this is deleteUserById method......");
}
}

第三步,新建一个aspect包,并在里面新建一个LogAspect类,用于记录日志信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(* com.envy.aopspringboot.service.*.*(..))")
public void test(){}

/**前置通知**/
@Before(value = "test()")
public void before(JoinPoint joinpoint){
String name =joinpoint.getSignature().getName();
System.out.println("【前置通知】"+name+ " 方法开始执行...");
}

/**后置通知**/
@After(value = "test()")
public void after(JoinPoint joinpoint){
String name =joinpoint.getSignature().getName();
System.out.println("【后置通知】"+name+ " 方法执行完毕...");
}

/**返回通知**/
@AfterReturning(value = "test()",returning = "result")
public void afterReturning(JoinPoint joinpoint,Object result){
String name =joinpoint.getSignature().getName();
System.out.println("【返回通知】"+name+ " 方法的返回值为:"+result);
}

/**异常通知**/
@AfterThrowing(value = "test()",throwing = "e")
public void afterThrowing(JoinPoint joinpoint,Exception e){
String name =joinpoint.getSignature().getName();
System.out.println("【异常通知】"+name+ " 方法运行抛异常了,异常信息为:"+e.getMessage());
}

/**环绕通知**/
@Around(value = "test()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
return proceedingJoinPoint.proceed();
}
}

解释一下上述代码的含义:首先使用@Component注解表明让Spring容器来管理它,@Aspect注解表示这是一个切面类。test方法使用了@Pointcut注解,说明这是一个切入点定义。execution中需要书写拦截规则,第一个表示方法任意返回值,第二个表示service包下的任意类,第三个*表示任意类中的任意方法,括号中的两个点表示方法参数任意,也就是说这里描述的切入点为com.envy.aopspringboot.service包下的所有类的所有方法。请注意JoinPoint选择使用org.aspectj.lang.JoinPoint;包内的,不要弄错。

before方法上使用了@Before注解,表示这是一个前置通知,该方法在目标方法执行前执行,通过JoinPoint参数可以获取目标方法的方法名、修饰符等信息。

after方法上使用了@After注解,表示这是一个后置通知,该方法在目标方法执行之后执行,通过JoinPoint参数可以获取目标方法的方法名、修饰符等信息。

afterReturning方法上使用了@AfterReturning注解,表示这是一个返回通知,在该方法中可以获取目标方法的返回值。@AfterReturning注解的returning参数是指返回值的变量名,对应方法的参数。请注意在方法参数中定义了result的类型为Object,表示目标方法的返回值可以是任意类型,若result参数的类型为Long,则该方法只能处理目标方法返回值为Long的情况。

afterThrowing方法上使用了@AfterThrowing注解,表示这是一个异常通知,即只有当目标方法发生异常时,该方法才会被调用,异常类型为Exception,表示所有的异常都会进入该方法中执行,若异常类型为ArithmeticException,则表示只有目标方法抛出的ArithmeticException异常才会进入该方法中处理。

around方法上使用了@Around注解,表示这是一个环绕通知。环绕通知是所有通知中功能最为强大的通知,可以实现前置通知、后置通知、异常通知以及返回通知的功能。目标方法进入环绕通知后,通过调用ProceedingJoinPoint对象proceed方法使目标方法继续执行,开发者可以在此修改目标方法的执行参数、返回值等,且可以在此处理目标方法的异常。

第四步,配置完上述信息后,接下来新建一个controller包,在里面新建一个UserController类,分别调用UserService中定义的两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/getUserById")
public String getUserById(Integer id){
return userService.getUserById(id);
}

@GetMapping("/deleteUserById")
public void deleteUserById(Integer id){
userService.deleteUserById(id);
}
}

然后运行项目,访问这两个链接,看控制台的输出信息:

1
2
3
4
【前置通知】getUserById 方法开始执行...
this is getUserById method......
【后置通知】getUserById 方法执行完毕...
【返回通知】getUserById 方法的返回值为:user

可以看到在LogAspect类中定义的信息已经动态嵌入到目标方法中,且按照既定的顺序执行了。

其他配置

接下来介绍一些非常有意思的配置,当然受目前知识的影响,后续会补充其他内容。

自定义欢迎页

通过前面的介绍已经知道SpringBoot项目启动后,首先会去静态资源路径(resources/static)目录下查找index.html文件作为首页文件,如果找不到则会去查找动态的index文件作为首页文件。

如果想使用静态的index.html页面作为项目首页那么只需在静态资源路径(resources/static)目录下创建index.html文件即可;如果想使用动态页面作为项目首页,则需要在动态资源路径(resources/templates)目录下创建index.html文件(使用Thymeleaf作为模板)或者index.ftlh(使用FreeMaker模板),然后在Controller中返回逻辑视图即可:

1
2
3
4
@RequestMapping("/index")
public String index(){
return "index";
}

最后启动项目,在浏览器地址栏中输入http://localhost:8080/即可看到项目首页的内容了:

自定义favicon

favicon是浏览器选项卡左上角的图标,可以放在静态资源路径下或者类路径下,但是静态资源路径下的favicon优先级高于类路径下的favicon。

可以使用一些将png或者jpg转为favicon的网站,这里推荐一个图片转换,图片转换成功后需要将名称修改为favicon.ico,然后复制到静态资源路径(resources/static)目录下即可:

最后启动项目,在浏览器地址栏中输入http://localhost:8080/即可在浏览器选项卡左上角看到自定义的favicon:

除去某个自动配置

SpringBoot中提供了大量的自动化配置类,如前面提到的ErrorMvcAutoConfigurationWebMvcAutoConfigurationThymeleafAutoConfigurationFreeMakerAutoConfigurationMutilpartAutoConfiguration等,这些自动化配置可以减少相应操作的配置,达到开箱即用的效果。在SpringBoot的入口类上有一个@SpringBootApplication注解,前面多次提到它是一个组合注解,由@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan三个注解组成,其中的@EnableAutoConfiguration注解开启自动化配置,相关的自动化配置类就会被使用。如果开发者不想使用某个自动化配置,可以按照如下方式除去相关配置即可:

1
2
3
4
5
6
7
@SpringBootApplication
@EnableAutoConfiguration(exclude = {ErrorMvcAutoConfiguration.class})
public class OtherApplication {
public static void main(String[] args){
SpringApplication.run(OtherApplication.class,args);
}
}

但实际上按照上面的写法可能会抛错误:

点开可以发现具体错误信息为:

1
2
3
4
5
6
Spring Boot Application in default package less… (Ctrl+F1)
Inspection info: Checks Spring Boot Application Setup.
@SpringBootApplication used in default package
Redundant @ComponentScan declaration
Redundant @EnableAutoConfiguration declaration
New in 2018.2

原因在于这里定义的OtherApplication类是直接在项目目录下,应该将其放在项目目录下的包目录:

当然如果你不想移动位置,也是可以的,既然@SpringbootApplication注解失效了,那可以使用它三个注解来代替即可:

1
2
3
4
5
6
7
8
@SpringBootConfiguration
@ComponentScan
@EnableAutoConfiguration(exclude = {ErrorMvcAutoConfiguration.class})
public class OtherApplication {
public static void main(String[] args){
SpringApplication.run(OtherApplication.class,args);
}
}

这样也没有问题。在@EnableAutoConfiguration注解中使用exclude属性除去了Error的自动化配置类,这时如果在resources/static/error目录下创建了4xx.html、5xx.html文件,那么当访问出错时就不会自动跳转到该处。由于@EnableAutoConfiguration注解的exclude属性值是一个数组,因此有多个要排除的自动化配置类时只需要继续添加即可。

当然除了这种配置方式外,开发者还可以在application.properties配置文件中进行设置:

1
2
# 除去ErrorMVCAutoConfiguration
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration

这样也是可以的,当然还是需要结合实际情况来进行选择。

Web开发整合小结

此处利用了三篇文章来介绍SpringBoot整合Web开发时的一些常见的、有用配置。如果开发者之前使用过SpringMVC,就会发现这些大部分是SpringMVC的功能,只是在SpringBoot中做了自动化配置,少部分是SpringBoot自身提供的功能,像CommandLineRunner。这里三篇文章的学习其实完全不够,后续会针对具体的情况再进一步学习。