原文地址:Spring Boot 入门之 Web 篇(二)
博客地址:http://www.extlight.com
一、前言
上一篇《Spring Boot 入门之基础篇(一)》介绍了 Spring Boot 的环境搭建以及项目启动打包等基础内容,本篇继续深入介绍 Spring Boot 与 Web 开发相关的知识。
二、整合模板引擎
由于 jsp 不被 SpringBoot 推荐使用,所以模板引擎主要介绍 Freemarker 和 Thymeleaf。
2.1 整合 Freemarker
2.1.1 添加 Freemarker 依赖
org.springframework.boot
spring-boot-starter-freemarker
2.1.2 添加 Freemarker 模板配置
在 application.properties 中添加如下内容:
spring.freemarker.allow-request-override=false
spring.freemarker.cache=true
spring.freemarker.check-template-location=true
spring.freemarker.charset=UTF-8
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.expose-spring-macro-helpers=false
spring.freemarker.prefix=
spring.freemarker.suffix=.ftl
上述配置都是默认值。
2.1.3 Freemarker 案例演示
在 controller 包中创建 FreemarkerController:
@Controller
@RequestMapping("freemarker")
public class FreemarkerController {
@RequestMapping("hello")
public String hello(Map map) {
map.put("msg", "Hello Freemarker");
return "hello";
}
}
在 templates 目录中创建名为 hello.ftl 文件,内容如下:
Document
${msg}
结果如下:
2.2 整合 Thymeleaf
2.2.1 添加 Thymeleaf 依赖
在 pom.xml 文件中添加:
org.springframework.boot
spring-boot-starter-thymeleaf
2.2.2 添加 Thymeleaf 模板配置
在 application.properties 中添加如下内容:
spring.thymeleaf.cache=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html
上述配置都是默认值。
2.2.3 Thymeleaf 案例演示
在 controller 包中创建 ThymeleafController:
@Controller
@RequestMapping("thymeleaf")
public class ThymeleafController {
@RequestMapping("hello")
public String hello(Map map) {
map.put("msg", "Hello Thymeleaf");
return "hello";
}
}
在 template 目录下创建名为 hello.html 的文件,内容如下:
Document
结果如下:
三、整合 Fastjson
3.1 添加依赖
com.alibaba
fastjson
1.2.35
3.2 整合 Fastjson
创建一个配置管理类 WebConfig ,如下:
@Configuration
public class WebConfig {
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter> converter = fastJsonHttpMessageConverter;
return new HttpMessageConverters(converter);
}
}
3.3 演示案例:
创建一个实体类 User:
public class User {
private Integer id;
private String username;
private String password;
private Date birthday;
}
getter 和 setter 此处省略。
创建控制器类 FastjsonController :
@Controller
@RequestMapping("fastjson")
public class FastJsonController {
@RequestMapping("/test")
@ResponseBody
public User test() {
User user = new User();
user.setId(1);
user.setUsername("jack");
user.setPassword("jack123");
user.setBirthday(new Date());
return user;
}
}
打开浏览器,访问 http://localhost:8080/fastjson/test,结果如下图:
此时,还不能看出 Fastjson 是否正常工作,我们在 User 类中使用 Fastjson 的注解,如下内容:
@JSONField(format="yyyy-MM-dd")
private Date birthday;
再次访问 http://localhost:8080/fastjson/test,结果如下图:
日期格式与我们修改的内容格式一致,说明 Fastjson 整合成功。
四、自定义 Servlet
4.1 编写 Servlet
public class ServletTest 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 {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("自定义 Servlet");
}
}
4.2 注册 Servlet
将 Servelt 注册成 Bean。在上文创建的 WebConfig 类中添加如下代码:
@Bean
public ServletRegistrationBean servletRegistrationBean() {
return new ServletRegistrationBean(new ServletTest(),"/servletTest");
}
结果如下:
五、自定义过滤器/第三方过滤器
5.1 编写过滤器
public class TimeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("=======初始化过滤器=========");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
long start = System.currentTimeMillis();
filterChain.doFilter(request, response);
System.out.println("filter 耗时:" + (System.currentTimeMillis() - start));
}
@Override
public void destroy() {
System.out.println("=======销毁过滤器=========");
}
}
5.2 注册过滤器
要是该过滤器生效,有两种方式:
1) 使用 @Component 注解
2) 添加到过滤器链中,此方式适用于使用第三方的过滤器。将过滤器写到 WebConfig 类中,如下:
@Bean
public FilterRegistrationBean timeFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
TimeFilter timeFilter = new TimeFilter();
registrationBean.setFilter(timeFilter);
List urls = new ArrayList<>();
urls.add("/*");
registrationBean.setUrlPatterns(urls);
return registrationBean;
}
结果如下:
六、自定义监听器
6.1 编写监听器
public class ListenerTest implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("监听器初始化...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
6.2 注册监听器
注册监听器为 Bean,在 WebConfig 配置类中添加如下代码:
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
return new ServletListenerRegistrationBean(new ListenerTest());
}
当启动容器时,结果如下:
针对自定义 Servlet、Filter 和 Listener 的配置,还有另一种方式:
@SpringBootApplication
public class SpringbootWebApplication implements ServletContextInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// 配置 Servlet
servletContext.addServlet("servletTest",new ServletTest())
.addMapping("/servletTest");
// 配置过滤器
servletContext.addFilter("timeFilter",new TimeFilter())
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST),true,"/*");
// 配置监听器
servletContext.addListener(new ListenerTest());
}
public static void main(String[] args) {
SpringApplication.run(SpringbootWebApplication.class, args);
}
}
七、自定义拦截器
7.1 编写拦截器
使用 @Component 让 Spring 管理其生命周期:
@Component
public class TimeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("========preHandle=========");
System.out.println(((HandlerMethod)handler).getBean().getClass().getName());
System.out.println(((HandlerMethod)handler).getMethod().getName());
request.setAttribute("startTime", System.currentTimeMillis());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
System.out.println("========postHandle=========");
Long start = (Long) request.getAttribute("startTime");
System.out.println("耗时:"+(System.currentTimeMillis() - start));
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception)
throws Exception {
System.out.println("========afterCompletion=========");
Long start = (Long) request.getAttribute("startTime");
System.out.println("耗时:"+(System.currentTimeMillis() - start));
System.out.println(exception);
}
}
7.2 注册拦截器
编写拦截器后,我们还需要将其注册到拦截器链中,如下配置:
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
private TimeInterceptor timeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInterceptor);
}
}
请求一个 controller ,结果如下:
八、配置 AOP 切面
8.1 添加依赖
org.springframework.boot
spring-boot-starter-aop
8.2 编写切面类
使用 @Component,@Aspect 标记到切面类上:
@Aspect
@Component
public class TimeAspect {
@Around("execution(* com.light.springboot.controller.FastJsonController..*(..))")
public Object method(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("=====Aspect处理=======");
Object[] args = pjp.getArgs();
for (Object arg : args) {
System.out.println("参数为:" + arg);
}
long start = System.currentTimeMillis();
Object object = pjp.proceed();
System.out.println("Aspect 耗时:" + (System.currentTimeMillis() - start));
return object;
}
}
请求 FastJsonController 控制器的方法,结果如下:
九、错误处理
9.1 友好页面
先演示非友好页面,修改 FastJsonController 类中的 test 方法:
@RestController
@RequestMapping("fastjson")
public class FastJsonController {
@RequestMapping("/test")
public User test() {
User user = new User();
user.setId(1);
user.setUsername("jack");
user.setPassword("jack123");
user.setBirthday(new Date());
// 模拟异常
int i = 1/0;
return user;
}
}
浏览器请求:http://localhost:8080/fastjson/test,结果如下:
当系统报错时,返回到页面的内容通常是一些杂乱的代码段,这种显示对用户来说不友好,因此我们需要自定义一个友好的提示系统异常的页面。
在 src/main/resources 下创建 /public/error,在该目录下再创建一个名为 5xx.html 文件,该页面的内容就是当系统报错时返回给用户浏览的内容:
系统错误
系统内部错误
路径时固定的,Spring Boot 会在系统报错时将返回视图指向该目录下的文件。
如下图:
上边处理的 5xx 状态码的问题,接下来解决 404 状态码的问题。
当出现 404 的情况时,用户浏览的页面也不够友好,因此我们也需要自定义一个友好的页面给用户展示。
在 /public/error 目录下再创建一个名为 404.html 的文件:
访问异常
找不到页面
我们请求一个不存在的资源,如:http://localhost:8080/fastjson/test2,结果如下图:
9.2 全局异常捕获
如果项目前后端是通过 JSON 进行数据通信,则当出现异常时可以常用如下方式处理异常信息。
编写一个类充当全局异常的处理类,需要使用 @ControllerAdvice 和 @ExceptionHandler 注解:
@ControllerAdvice
public class GlobalDefaultExceptionHandler {
/**
* 处理 Exception 类型的异常
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Map defaultExceptionHandler(Exception e) {
Map map = new HashMap();
map.put("code", 500);
map.put("msg", e.getMessage());
return map;
}
}
其中,方法名为任意名,入参一般使用 Exception 异常类,方法返回值可自定义。
启动项目,访问 http://localhost:8080/fastjson/test,结果如下图:
我们还可以自定义异常,在全局异常的处理类中捕获和判断,从而对不同的异常做出不同的处理。
十、文件上传和下载
10.1 添加依赖
commons-io
commons-io
2.4
10.2 实现
编写一个实体类,用于封装返回信息:
public class FileInfo {
private String path;
public FileInfo(String path) {
this.path = path;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}
编写 Controller,用于处理文件上传下载:
@RestController
@RequestMapping("/file")
public class FileController {
private String path = "d:\\";
@PostMapping
public FileInfo upload(MultipartFile file) throws Exception {
System.out.println(file.getName());
System.out.println(file.getOriginalFilename());
System.out.println(file.getSize());
File localFile = new File(path, file.getOriginalFilename());
file.transferTo(localFile);
return new FileInfo(localFile.getAbsolutePath());
}
@GetMapping("/{id}")
public void download(@PathVariable String id, HttpServletRequest request, HttpServletResponse response) {
try (InputStream inputStream = new FileInputStream(new File(path, id + ".jpg"));
OutputStream outputStream = response.getOutputStream();) {
response.setContentType("application/x-download");
response.addHeader("Content-Disposition", "attachment;filename=" + id + ".jpg");
IOUtils.copy(inputStream, outputStream);
} catch (Exception e) {
e.printStackTrace();
}
}
}
基本上都是在学习 javaweb 时用到的 API。
文件上传测试结果如下图:
十一、CORS 支持
前端页面:
跨域测试
通过 http 容器启动前端页面代码,笔者使用 Sublime Text 的插件启动的,测试结果如下:
从图中可知,前端服务器启动端口为 8088 与后端服务器 8080 不同源,因此出现跨域的问题。
现在开始解决跨域问题,可以两种维度控制客户端请求。
粗粒度控制:
方式一
@Configuration
public class WebConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/fastjson/**")
.allowedOrigins("http://localhost:8088");// 允许 8088 端口访问
}
};
}
}
方式二
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/fastjson/**")
.allowedOrigins("http://localhost:8088");// 允许 8088 端口访问
}
}
配置后,重新发送请求,结果如下:
细粒度控制:
在 FastJsonController 类中的方法上添加 @CrossOrigin(origins="xx") 注解:
@RequestMapping("/test")
@CrossOrigin(origins="http://localhost:8088")
public User test() {
User user = new User();
user.setId(1);
user.setUsername("jack");
user.setPassword("jack123");
user.setBirthday(new Date());
return user;
}
在使用该注解时,需要注意 @RequestMapping 使用的请求方式类型,即 GET 或 POST。
十二、整合 WebSocket
12.1 添加依赖
org.springframework.boot
spring-boot-starter-websocket
12.2 实现方式
方式一:
该方式只适用于通过 jar 包直接运行项目的情况。
WebSocket 配置类:
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocket 处理类:
@ServerEndpoint(value = "/webSocketServer/{userName}")
@Component
public class WebSocketServer {
private static final Set connections = new CopyOnWriteArraySet<>();
private String nickname;
private Session session;
private static String getDatetime(Date date) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return format.format(date);
}
@OnOpen
public void start(@PathParam("userName") String userName, Session session) {
this.nickname = userName;
this.session = session;
connections.add(this);
String message = String.format("* %s %s", nickname, "加入聊天!");
broadcast(message);
}
@OnClose
public void end() {
connections.remove(this);
String message = String.format("* %s %s", nickname, "退出聊天!");
broadcast(message);
}
@OnMessage
public void pushMsg(String message) {
broadcast("【" + this.nickname + "】" + getDatetime(new Date()) + " : " + message);
}
@OnError
public void onError(Throwable t) throws Throwable {
}
private static void broadcast(String msg) {
// 广播形式发送消息
for (WebSocketServer client : connections) {
try {
synchronized (client) {
client.session.getBasicRemote().sendText(msg);
}
} catch (IOException e) {
connections.remove(client);
try {
client.session.close();
} catch (IOException e1) {
e.printStackTrace();
}
String message = String.format("* %s %s", client.nickname, "断开连接");
broadcast(message);
}
}
}
}
前端页面:
webSocket测试
webSocket及时聊天Demo程序