原文地址:Spring Boot 入門之 Web 篇(二)
博客地址:http://www.extlight.comjavascript
上一篇《Spring Boot 入門之基礎篇(一)》介紹了 Spring Boot 的環境搭建以及項目啓動打包等基礎內容,本篇繼續深刻介紹 Spring Boot 與 Web 開發相關的知識。css
因爲 jsp 不被 SpringBoot 推薦使用,因此模板引擎主要介紹 Freemarker 和 Thymeleaf。html
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency>
在 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
上述配置都是默認值。java
在 controller 包中建立 FreemarkerController:jquery
@Controller @RequestMapping("freemarker") public class FreemarkerController { @RequestMapping("hello") public String hello(Map<String,Object> map) { map.put("msg", "Hello Freemarker"); return "hello"; } }
在 templates 目錄中建立名爲 hello.ftl 文件,內容以下:git
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>Document</title> <link href="/css/index.css" rel="stylesheet"/> </head> <body> <div class="container"> <h2>${msg}</h2> </div> </body> </html>
結果以下:github
在 pom.xml 文件中添加:web
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
在 application.properties 中添加以下內容:ajax
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
上述配置都是默認值。
在 controller 包中建立 ThymeleafController:
@Controller @RequestMapping("thymeleaf") public class ThymeleafController { @RequestMapping("hello") public String hello(Map<String,Object> map) { map.put("msg", "Hello Thymeleaf"); return "hello"; } }
在 template 目錄下建立名爲 hello.html 的文件,內容以下:
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>Document</title> <link href="/css/index.css" rel="stylesheet"/> </head> <body> <div class="container"> <h2 th:text="${msg}"></h2> </div> </body> </html>
結果以下:
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.35</version> </dependency>
建立一個配置管理類 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); } }
建立一個實體類 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 整合成功。
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"); } }
將 Servelt 註冊成 Bean。在上文建立的 WebConfig 類中添加以下代碼:
@Bean public ServletRegistrationBean servletRegistrationBean() { return new ServletRegistrationBean(new ServletTest(),"/servletTest"); }
結果以下:
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("=======銷燬過濾器========="); } }
要是該過濾器生效,有兩種方式:
使用 @Component 註解
添加到過濾器鏈中,此方式適用於使用第三方的過濾器。將過濾器寫到 WebConfig 類中,以下:
@Bean public FilterRegistrationBean timeFilter() { FilterRegistrationBean registrationBean = new FilterRegistrationBean(); TimeFilter timeFilter = new TimeFilter(); registrationBean.setFilter(timeFilter); List<String> urls = new ArrayList<>(); urls.add("/*"); registrationBean.setUrlPatterns(urls); return registrationBean; }
結果以下:
public class ListenerTest implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { System.out.println("監聽器初始化..."); } @Override public void contextDestroyed(ServletContextEvent sce) { } }
註冊監聽器爲 Bean,在 WebConfig 配置類中添加以下代碼:
@Bean public ServletListenerRegistrationBean<ListenerTest> servletListenerRegistrationBean() { return new ServletListenerRegistrationBean<ListenerTest>(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); } }
使用 @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); } }
編寫攔截器後,咱們還須要將其註冊到攔截器鏈中,以下配置:
@Configuration public class WebConfig extends WebMvcConfigurerAdapter{ @Autowired private TimeInterceptor timeInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(timeInterceptor); } }
請求一個 controller ,結果以下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
使用 @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 控制器的方法,結果以下:
先演示非友好頁面,修改 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 文件,該頁面的內容就是當系統報錯時返回給用戶瀏覽的內容:
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>系統錯誤</title> <link href="/css/index.css" rel="stylesheet"/> </head> <body> <div class="container"> <h2>系統內部錯誤</h2> </div> </body> </html>
路徑時固定的,Spring Boot 會在系統報錯時將返回視圖指向該目錄下的文件。
以下圖:
上邊處理的 5xx 狀態碼的問題,接下來解決 404 狀態碼的問題。
當出現 404 的狀況時,用戶瀏覽的頁面也不夠友好,所以咱們也須要自定義一個友好的頁面給用戶展現。
在 /public/error 目錄下再建立一個名爲 404.html 的文件:
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>訪問異常</title> <link href="/css/index.css" rel="stylesheet"/> </head> <body> <div class="container"> <h2>找不到頁面</h2> </div> </body> </html>
咱們請求一個不存在的資源,如:http://localhost:8080/fastjson/test2,結果以下圖:
若是項目先後端是經過 JSON 進行數據通訊,則當出現異常時能夠經常使用以下方式處理異常信息。
編寫一個類充當全局異常的處理類,須要使用 @ControllerAdvice 和 @ExceptionHandler 註解:
@ControllerAdvice public class GlobalDefaultExceptionHandler { /** * 處理 Exception 類型的異常 * @param e * @return */ @ExceptionHandler(Exception.class) @ResponseBody public Map<String,Object> defaultExceptionHandler(Exception e) { Map<String,Object> map = new HashMap<String,Object>(); map.put("code", 500); map.put("msg", e.getMessage()); return map; } }
其中,方法名爲任意名,入參通常使用 Exception 異常類,方法返回值可自定義。
啓動項目,訪問 http://localhost:8080/fastjson/test,結果以下圖:
咱們還能夠自定義異常,在全局異常的處理類中捕獲和判斷,從而對不一樣的異常作出不一樣的處理。
<!-- 工具 --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency>
編寫一個實體類,用於封裝返回信息:
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。
文件上傳測試結果以下圖:
前端頁面:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>跨域測試</title> </head> <body> <button id="test">測試</button> <script type="text/javascript" src="jquery-1.12.3.min.js"></script> <script type="text/javascript"> $(function() { $("#test").on("click", function() { $.ajax({ "url": "http://localhost:8080/fastjson/test", "type": "get", "dataType": "json", "success": function(data) { console.log(data); } }) }); }); </script> </body> </html>
經過 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。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
方式一:
該方式只適用於經過 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<WebSocketServer> 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); } } } }
前端頁面:
<!DOCTYPE html> <html> <head lang="zh"> <meta charset="UTF-8"> <link rel="stylesheet" href="css/bootstrap.min.css"> <link rel="stylesheet" href="css/bootstrap-theme.min.css"> <script src="js/jquery-1.12.3.min.js"></script> <script src="js/bootstrap.js"></script> <style type="text/css"> #msg { height: 400px; overflow-y: auto; } #userName { width: 200px; } #logout { display: none; } </style> <title>webSocket測試</title> </head> <body> <div class="container"> <div class="page-header" id="tou">webSocket及時聊天Demo程序</div> <p class="text-right" id="logout"> <button class="btn btn-danger" id="logout-btn">退出</button> </p> <div class="well" id="msg"></div> <div class="col-lg"> <div class="input-group"> <input type="text" class="form-control" placeholder="發送信息..." id="message"> <span class="input-group-btn"> <button class="btn btn-default" type="button" id="send" disabled="disabled">發送</button> </span> </div> <div class="input-group"> <input id="userName" type="text" class="form-control" name="userName" placeholder="輸入您的用戶名" /> <button class="btn btn-default" type="button" id="connection-btn">創建鏈接</button> </div> <!-- /input-group --> </div> <!-- /.col-lg-6 --> </div> <!-- /.row --> </div> <script type="text/javascript"> $(function() { var websocket; $("#connection-btn").bind("click", function() { var userName = $("#userName").val(); if (userName == null || userName == "") { alert("請輸入您的用戶名"); return; } connection(userName); }); function connection(userName) { var host = window.location.host; if ('WebSocket' in window) { websocket = new WebSocket("ws://" + host + "/webSocketServer/" + userName); } else if ('MozWebSocket' in window) { websocket = new MozWebSocket("ws://" + host + "/webSocketServer/" + userName); } websocket.onopen = function(evnt) { $("#tou").html("連接服務器成功!") $("#send").prop("disabled", ""); $("#connection-btn").prop("disabled", "disabled"); $("#logout").show(); }; websocket.onmessage = function(evnt) { $("#msg").html($("#msg").html() + "<br/>" + evnt.data); }; websocket.onerror = function(evnt) { $("#tou").html("報錯!") }; websocket.onclose = function(evnt) { $("#tou").html("與服務器斷開了連接!"); $("#send").prop("disabled", "disabled"); $("#connection-btn").prop("disabled", ""); $("#logout").hide(); } } function send() { if (websocket != null) { var $message = $("#message"); var data = $message.val(); if (data == null || data == "") { return; } websocket.send(data); $message.val(""); } else { alert('未與服務器連接.'); } } $('#send').bind('click', function() { send(); }); $(document).on("keypress", function(event) { if (event.keyCode == "13") { send(); } }); $("#logout-btn").on("click", function() { websocket.close(); //關閉TCP鏈接 }); }); </script> </body> </html>
演示圖以下:
若是使用該方式實現 WebSocket 功能並打包成 war 運行會報錯:
javax.websocket.DeploymentException: Multiple Endpoints may not be deployed to the same path
方式二:
該方式適用於 jar 包方式運行和 war 方式運行。
WebSocket 配置類:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(webSocketServer(), "/webSocketServer/*"); } @Bean public WebSocketHandler webSocketServer() { return new WebSocketServer(); } }
WebSocket 處理類:
public class WebSocketServer extends TextWebSocketHandler { private static final Map<WebSocketSession, String> connections = new ConcurrentHashMap<>(); private static String getDatetime(Date date) { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return format.format(date); } /** * 創建鏈接 */ @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String uri = session.getUri().toString(); String userName = uri.substring(uri.lastIndexOf("/") + 1); String nickname = URLDecoder.decode(userName, "utf-8"); connections.put(session, nickname); String message = String.format("* %s %s", nickname, "加入聊天!"); broadcast(new TextMessage(message)); } /** * 斷開鏈接 */ @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { String nickname = connections.remove(session); String message = String.format("* %s %s", nickname, "退出聊天!"); broadcast(new TextMessage(message)); } /** * 處理消息 */ @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String msg = "【" + connections.get(session) + "】" + getDatetime(new Date()) + " : " + message.getPayload(); broadcast(new TextMessage(msg)); } private static void broadcast(TextMessage msg) { // 廣播形式發送消息 for (WebSocketSession session : connections.keySet()) { try { synchronized (session) { session.sendMessage(msg); } } catch (Exception e) { connections.remove(session); try { session.close(); } catch (Exception e2) { e2.printStackTrace(); } String message = String.format("* %s %s", connections.get(session), "斷開鏈接"); broadcast(new TextMessage(message)); } } } }
運行結果與上圖一致。
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.7.0</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.7.0</version> </dependency>
從新建立一個配置類,以下:
@Configuration @EnableSwagger2 public class Swagger2Configuration { @Bean public Docket accessToken() { return new Docket(DocumentationType.SWAGGER_2) .groupName("api")// 定義組 .select() // 選擇那些路徑和 api 會生成 document .apis(RequestHandlerSelectors.basePackage("com.light.springboot.controller")) // 攔截的包路徑 .paths(PathSelectors.regex("/*/.*"))// 攔截的接口路徑 .build() // 建立 .apiInfo(apiInfo()); // 配置說明 } private ApiInfo apiInfo() { return new ApiInfoBuilder()// .title("Spring Boot 之 Web 篇")// 標題 .description("spring boot Web 相關內容")// 描述 .termsOfServiceUrl("http://www.extlight.com")// .contact(new Contact("moonlightL", "http://www.extlight.com", "445847261@qq.com"))// 聯繫 .version("1.0")// 版本 .build(); } }
爲了能更好的說明接口信息,咱們還能夠在 Controller 類上使用 Swagger2 相關注解說明信息。
咱們以 FastJsonController 爲例:
@Api(value = "FastJson測試", tags = { "測試接口" }) @RestController @RequestMapping("fastjson") public class FastJsonController { @ApiOperation("獲取用戶信息") @ApiImplicitParam(name = "name", value = "用戶名", dataType = "string", paramType = "query") @GetMapping("/test/{name}") public User test(@PathVariable("name") String name) { User user = new User(); user.setId(1); user.setUsername(name); user.setPassword("jack123"); user.setBirthday(new Date()); return user; } }
注意,上邊的方法是用 @GetMapping 註解,若是隻是使用 @RequestMapping 註解,不配置 method 屬性,那麼 API 文檔會生成 7 種請求方式。
啓動項目,打開瀏覽器訪問 http://localhost:8080/swagger-ui.html。結果以下圖: