SpringBoot:Web開發

西部開源-秦疆老師:基于SpringBoot 2.1.6 的博客教程 , 基于atguigu 1.5.x 視頻優化

秦老師交流Q群號: 664386224

未授權禁止轉載!編輯不易 , 轉發請注明出處!防君子不防小人,共勉!

簡介

好的,同學們,那么接下來呢,我們開始學習SpringBoot與Web開發,從這一章往后,就屬于我們實戰部分的內容了;

其實SpringBoot的東西用起來非常簡單,因為SpringBoot最大的特點就是自動裝配。

使用SpringBoot的步驟:

  1. 創建一個SpringBoot應用,選擇我們需要的???,SpringBoot就會默認將我們的需要的??樽遠渲煤?/li>
  2. 手動在配置文件中配置部分配置項目就可以運行起來了
  3. 專注編寫業務代碼,不需要考慮以前那樣一大堆的配置了。

要熟悉掌握開發,之前學習的自動配置的原理一定要搞明白!

比如SpringBoot到底幫我們配置了什么?我們能不能修改?我們能修改哪些配置?我們能不能擴展?

  • 向容器中自動配置組件 : *** Autoconfiguration
  • 自動配置類,封裝配置文件的內容:***Properties

沒事就找找類,看看自動裝配原理,我們之后來進行一個CRUD的實驗測試!

靜態資源映射規則

首先,我們搭建一個普通的SpringBoot項目,回顧一下HelloWorld程序!【演示】

那我們要引入我們小實驗的測試資源,我們項目中有許多的靜態資源,比如,css,js等文件,這個SpringBoot怎么處理呢?

 如果我們是一個web應用,我們的main下會有一個webapp,我們以前都是將所有的頁面導在這里面的,對吧!

但是我們現在的pom呢,打包方式是為jar的方式,那么這種方式SpringBoot能不能來給我們寫頁面呢?當然是可以的,但是SpringBoot對于靜態資源放置的位置,是有規定的!

我們先來聊聊這個靜態資源映射規則;

SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration  這個配置里面,我們可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;

比如:addResourceHandlers

  public void addResourceHandlers(ResourceHandlerRegistry registry) {
            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");
            } else {
                Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
                CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
                if (!registry.hasMappingForPattern("/webjars/**")) {
                    this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
                }

                String staticPathPattern = this.mvcProperties.getStaticPathPattern();
                if (!registry.hasMappingForPattern(staticPathPattern)) {
                    this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
                }

            }
        }

讀一下源代碼:比如所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找對應的資源,那什么是webjars呢?

webjars本質就是以jar包的方式引入我們的靜態資源 , 我們以前要導入一個靜態資源文件,直接導入即可。使用SpringBoot需要使用webjars,我們可以去搜索一下

網站:https://www.webjars.org/ 【網站帶看,并引入jQuery測試】

要使用jQuery,我們只要要引入jQuery對應版本的pom依賴即可!【導入完畢,查看webjars目錄結構,并訪問Jquery.js文件】

導入依賴:

        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.3.1</version>
        </dependency>

查看目錄結構:

訪問:只要是靜態資源,SpringBoot就會去對應的路徑尋找資源,我們這里訪問 ://localhost:8080/webjars/jquery/3.3.1/jquery.js

 

那我們項目中要是使用自己的靜態資源該怎么導入呢?我們看下一行代碼;

我們去找staticPathPattern發現第二種映射規則 : /** , 訪問當前的項目任意資源,它會去找 resourceProperties 這個類,我們可以點進去看一下,

@ConfigurationProperties(
    prefix = "spring.resources",
    ignoreUnknownFields = false
)
public class ResourceProperties {
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};

 

resourceProperties 可以設置和我們靜態資源有關的參數,this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS; 這里面指向了它會去尋找資源的文件夾;即上面數組的內容。再向下看一個方法;它還會在根路徑下去尋找資源!

    private String[] appendSlashIfNecessary(String[] staticLocations) {
        String[] normalized = new String[staticLocations.length];

        for(int i = 0; i < staticLocations.length; ++i) {
            String location = staticLocations[i];
            normalized[i] = location.endsWith("/") ? location : location + "/";
        }

        return normalized;
    }

所以得出結論:

"classpath:/META-INF/resources/", 
"classpath:/resources/",
 "classpath:/static/", 
"classpath:/public/",
"/" :當前項目的根目錄

我們可以在resources根目錄下新建對應的文件夾,都可以存放我們的靜態文件;

比如我們訪問 localhost:8080/1.js, 他就會去這些文件夾中尋找對應的靜態資源文件;

靜態資源文件夾說完后,我們繼續看源碼!可以看到一個歡迎頁的映射,就是我們的首頁!

        @Bean
        public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext) {
            return new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
        }

點進去繼續看

        private Optional<Resource> getWelcomePage() {
            String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
            return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
        }

        private Resource getIndexHtml(String location) {
            return this.resourceLoader.getResource(location + "index.html");
        }

歡迎頁,靜態資源文件夾下的所有index.html頁面;被 /** 映射。

比如我訪問 localhost:8080/    ,就會找靜態資源文件夾下的 index.html 【測試一下】

我們繼續向下閱讀源碼,可以看到一個好玩的功能,配置我們喜歡的圖標!

 @Configuration
        @ConditionalOnProperty(
            value = {"spring.mvc.favicon.enabled"},
            matchIfMissing = true
        )
        public static class FaviconConfiguration implements ResourceLoaderAware {
            private final ResourceProperties resourceProperties;
            private ResourceLoader resourceLoader;

            public FaviconConfiguration(ResourceProperties resourceProperties) {
                this.resourceProperties = resourceProperties;
            }

            public void setResourceLoader(ResourceLoader resourceLoader) {
                this.resourceLoader = resourceLoader;
            }

            @Bean
            public SimpleUrlHandlerMapping faviconHandlerMapping() {
                SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
                mapping.setOrder(-2147483647);
                mapping.setUrlMap(Collections.singletonMap("**/favicon.ico", this.faviconRequestHandler()));
                return mapping;
            }

            @Bean
            public ResourceHttpRequestHandler faviconRequestHandler() {
                ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
                requestHandler.setLocations(this.resolveFaviconLocations());
                return requestHandler;
            }

            private List<Resource> resolveFaviconLocations() {
                String[] staticLocations = WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter.getResourceLocations(this.resourceProperties.getStaticLocations());
                List<Resource> locations = new ArrayList(staticLocations.length + 1);
                Stream var10000 = Arrays.stream(staticLocations);
                ResourceLoader var10001 = this.resourceLoader;
                this.resourceLoader.getClass();
                var10000.map(var10001::getResource).forEach(locations::add);
                locations.add(new ClassPathResource("/"));
                return Collections.unmodifiableList(locations);
            }
        }

"**/favicon.ico" , 就是我們的圖標定義的格式!也是在我們的靜態資源文件夾下定義,我們可以自定義來測試一下 【測試】

自己放一個圖標進去,然后在配置文件中關閉SpringBoot默認的圖標!

#關閉默認圖標
spring.mvc.favicon.enabled=false

清除瀏覽器緩存!刷新網頁,發現圖標已經變成自己的了!

我們也可以自己通過配置文件來指定一下,哪些文件夾是需要我們放靜態資源文件的,在application.properties中配置;

spring.resources.static-locations=classpath:/hello/,classpath:/kuang/

一旦自己定義了靜態文件夾的路徑,原來的就都會失效了!

模板引擎

前端交給我們的頁面,是html頁面。如果是我們以前開發,我們需要把他們轉成jsp頁面,jsp好處就是當我們查出一些數據轉發到JSP頁面以后,我們可以用jsp輕松實現數據的顯示,及交互等。jsp支持非常強大的功能,包括能寫Java代碼,但是呢,我們現在的這種情況,SpringBoot這個項目首先是以jar的方式,不是war,像第二,我們用的還是嵌入式的Tomcat,所以呢,他現在默認是不支持jsp的。

那不支持jsp,如果我們直接用純靜態頁面的方式,那給我們開發會帶來非常大的麻煩,那怎么辦呢,SpringBoot推薦你可以來使用模板引擎。

那么這模板引擎,我們其實大家聽到很多,其實jsp就是一個模板引擎,還有以用的比較多的freemarker,包括SpringBoot給我們推薦的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他們的思想都是一樣的,什么樣一個思想呢我們來看一下這張圖。

模板引擎的作用就是我們來寫一個頁面模板,比如有些值呢,是動態的,我們寫一些表達式。而這些值,從哪來呢,我們來組裝一些數據,我們把這些數據找到。然后把這個模板和這個數據交給我們模板引擎,模板引擎按照我們這個數據幫你把這表達式解析、填充到我們指定的位置,然后把這個數據最終生成一個我們想要的內容給我們寫出去,這就是我們這個模板引擎,不管是jsp還是其他模板引擎,都是這個思想。只不過呢,就是說不同模板引擎之間,他們可能這個語法有點不一樣。其他的我就不介紹了,我主要來介紹一下SpringBoot給我們推薦的Thymeleaf模板引擎,這模板引擎呢,是一個高級語言的模板引擎,他的這個語法更簡單。而且呢,功能更強大。

我們呢,就來看一下這個模板引擎,那既然要看這個模板引擎。首先,我們來看SpringBoot里邊怎么用。

第一步:引入thymeleaf , 怎么引入呢,對于springboot來說,什么事情不都是一個start的事情嘛,我們去在項目中引入一下。給大家三個網址:

1、Thymeleaf 官網:https://www.thymeleaf.org/

2、Thymeleaf 在Github 的主頁:https://github.com/thymeleaf/thymeleaf

3、Spring官方文檔:“https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/htmlsingle/#using-boot-starter” , 找到我們對應的版本

        <!--thymeleaf模板-->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-java8time</artifactId>
        </dependency>

maven會自動下載jar包,我們可以去看下下載的東西;

使用Thymeleaf

前面呢,我們已經引入了Thymeleaf,那這個要怎么使用呢?
我們首先得按照SpringBoot的自動配置原理看一下我們這個Thymeleaf的自動配置規則,在按照那個規則,我們進行使用。
我們去找一下Thymeleaf的自動配置類;

這個包下有我們的thymeleaf,看我們的配置類,我們選擇部分

@ConfigurationProperties(
    prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING;
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";
    private boolean checkTemplate = true;
    private boolean checkTemplateLocation = true;
    private String prefix = "classpath:/templates/";
    private String suffix = ".html";
    private String mode = "HTML";
    private Charset encoding;
}

我們可以在其中看到默認的前綴和后綴!我們只需要把我們的html頁面放在類路徑下的templates下,thymeleaf就可以幫我們自動渲染了。

我們可以去測試一下 , 寫一個Controller,跳轉到一個指定頁面,這個指定頁面需要在 類路徑下的模板目錄下 【演示】

使用thymeleaf什么都不需要配置,只需要將他放在指定的文件夾下即可!

Thymeleaf語法

 要學習語法,還是參考官網文檔最為準確,我們找到對應的版本看一下;

Thymeleaf 官網:https://www.thymeleaf.org/ , 簡單看一下官網!我們去下載Thymeleaf的官方文檔!

我們做個最簡單的練習 : 我們需要查出一些數據,在頁面中展示

我們去在controller編寫一個請求,放進去一些數據;

    @RequestMapping("/success")
    public String success(Model model){
        //存入數據
        model.addAttribute("msg","Hello,Thymeleaf");
        //classpath:/templates/success.html
        return "success";
    }

 

我們要使用thymeleaf,需要在html文件中導入命名空間的約束,方便提示。我們可以去官方文檔的#3中看一下命名空間拿來過來

 xmlns:th="//www.thymeleaf.org"

 

我們去編寫下前端頁面

<!DOCTYPE html>
<html lang="en" xmlns:th="//www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>狂神說</title>
</head>
<body>
<h1>Success</h1>

<!--th:text就是將div中的內容設置為它指定的值,和之前學習的Vue一樣-->
<div th:text="${msg}"></div>
</body>
</html>

 

OK,入門搞定,我們來認真學習一下Thymeleaf的使用語法!

一、我們可以使用任意的 th:attr 來替換Html中原生屬性的值!【測試】  全部屬性可以參考官網文檔#10; th語法

二、我們能寫那些表達式呢?我們可以看到官方文檔 #4

Simple expressions:(表達式語法)
Variable Expressions: ${...}:獲取變量值;OGNL;
1)、獲取對象的屬性、調用方法 2)、使用內置的基本對象: #18 #ctx : the context object. #vars: the context variables. #locale : the context locale. #request : (only in Web Contexts) the HttpServletRequest object. #response : (only in Web Contexts) the HttpServletResponse object. #session : (only in Web Contexts) the HttpSession object. #servletContext : (only in Web Contexts) the ServletContext object. ${session.foo} 3)、內置的一些工具對象:       #execInfo : information about the template being processed.       #messages : methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.       #uris : methods for escaping parts of URLs/URIs       #conversions : methods for executing the configured conversion service (if any).       #dates : methods for java.util.Date objects: formatting, component extraction, etc.       #calendars : analogous to #dates , but for java.util.Calendar objects.       #numbers : methods for formatting numeric objects.       #strings : methods for String objects: contains, startsWith, prepending/appending, etc.       #objects : methods for objects in general.       #bools : methods for boolean evaluation.       #arrays : methods for arrays.       #lists : methods for lists.       #sets : methods for sets.       #maps : methods for maps.       #aggregates : methods for creating aggregates on arrays or collections.       #ids : methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).
============================================================================================== Selection Variable Expressions:
*{...}:選擇表達式:和${}在功能上是一樣; 補充:配合 th:object="${session.user}:
<div th:object="${session.user}"> <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p> <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p> <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p> </div> Message Expressions: #{...}:獲取國際化內容 Link URL Expressions: @{...}:定義URL; @{/order/process(execId=${execId},execType='FAST')}
Fragment Expressions:
~{...}:片段引用表達式 <div th:insert="~{commons :: main}">...</div> Literals(字面量) Text literals: 'one text' , 'Another one!' ,… Number literals: 0 , 34 , 3.0 , 12.3 ,… Boolean literals: true , false Null literal: null Literal tokens: one , sometext , main ,… Text operations:(文本操作) String concatenation: + Literal substitutions: |The name is ${name}| Arithmetic operations:(數學運算) Binary operators: + , - , * , / , % Minus sign (unary operator): - Boolean operations:(布爾運算) Binary operators: and , or Boolean negation (unary operator): ! , not Comparisons and equality:(比較運算) Comparators: > , < , >= , <= ( gt , lt , ge , le ) Equality operators: == , != ( eq , ne ) Conditional operators:條件運算(三元運算符) If-then: (if) ? (then) If-then-else: (if) ? (then) : (else) Default: (value) ?: (defaultvalue) Special tokens: No-Operation: _

 

 測試練習

 我們編寫一個Controller,放一些數據

    @RequestMapping("/success2")
    public String success2(Map<String,Object> map){
        //存入數據
        map.put("msg","<h1>Hello</h1>");
        map.put("users", Arrays.asList("qinjiang","kuangshen"));
        //classpath:/templates/success.html
        return "success";
    }

 

前端

<!DOCTYPE html>
<html lang="en" xmlns:th="//www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>狂神說</title>
</head>
<body>
<h1>Success</h1>

<div th:text="${msg}"></div>
<!--不轉義-->
<div th:utext="${msg}"></div>

<!--遍歷數據-->
<!--th:each每次遍歷都會生成當前這個標簽:官網#9-->
<h4 th:each="user :${users}" th:text="${user}"></h4>

<h4>
    <!--行內寫法:官網#12-->
    <span th:each="user:${users}">[[${user}]]</span>
</h4>

</body>
</html>

很多樣式,我們即使現在學習了,也會忘記,所以我們在學習過程中,需要使用什么,根據官方文檔來查詢,才是最重要的,要熟練使用官方文檔!

SpringMVC自動配置

在進行測試前,我們還需要知道一個東西,就是SpringBoot 對我們的SpringMVC還做了哪些配置,包括如何擴展,如何定制。只有把這些都搞清楚了,我們在之后使用才會更加得心應手。 途徑一:源碼分析,途徑二:官方文檔

地址 : https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/html/boot-features-developing-web-applications.html

我們來閱讀一段官方文檔:

29.1.1 Spring MVC Auto-configuration
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.

The auto-configuration adds the following features on top of Spring’s defaults:

//包含視圖解析器 Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
//支持靜態資源文件夾的路徑,以及webjars Support
for serving static resources, including support for WebJars (covered later in this document)).
//自動注冊了Converter:【轉換器,這就是我們網頁提交數據到后臺自動封裝成為對象的東西,比如把18字符串自動轉換為int類型】
//Formatter:【格式化器,比如頁面給我們了一個2019-8-10,它會給我們自動格式化為Date對象】 Automatic registration of Converter, GenericConverter, and Formatter beans.
//HttpMessageConverters:SpringMVC用來轉換Http請求和響應的的,比如我們要把一個User對象轉換為JSON字符串,可以去看官網文檔解釋; Support
for HttpMessageConverters (covered later in this document).
//定義錯誤代碼生成規則的 Automatic registration of MessageCodesResolver (covered later
in this document).
//首頁定制 Static index.html support.
//圖標定制 Custom Favicon support (covered later
in this document).
//初始化數據綁定器:幫我們把請求數據綁定到JavaBean中! Automatic use of a ConfigurableWebBindingInitializer bean (covered later
in this document).
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration
class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components. If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

我們來仔細對照,看一下它怎么實現的,它告訴我們SpringBoot已經幫我們自動配置好了SpringMVC,然后自動配置了哪些東西呢?

ContentNegotiatingViewResolver

自動配置了ViewResolver,就是我們之前學習的SpringMVC的視圖解析器:即根據方法的返回值取得視圖對象(View),然后由視圖對象決定如何渲染(轉發,重定向)。我們去看看這里的源碼:我們找到 WebMvcAutoConfiguration , 然后搜索ContentNegotiatingViewResolver。找到如下方法!

        @Bean //我們在這里確實看到已經給容器中注冊了一個bean
        @ConditionalOnBean({ViewResolver.class})
        @ConditionalOnMissingBean(
            name = {"viewResolver"},
            value = {ContentNegotiatingViewResolver.class}
        )
        public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
            ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
            resolver.setContentNegotiationManager((ContentNegotiationManager)beanFactory.getBean(ContentNegotiationManager.class));
            resolver.setOrder(-2147483648);
            return resolver;
        }

我們可以點進這類看看!找到對應的解析視圖的代碼

注解說明:@Nullable 即參數可為null

    @Nullable
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
        List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
        if (requestedMediaTypes != null) {
//獲取候選的視圖對象 List
<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
//選擇一個最適合的視圖對象,然后把這個對象返回 View bestView
= this.getBestView(candidateViews, requestedMediaTypes, attrs); if (bestView != null) { return bestView; } } String mediaTypeInfo = this.logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : ""; if (this.useNotAcceptableStatusCode) { if (this.logger.isDebugEnabled()) { this.logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo); } return NOT_ACCEPTABLE_VIEW; } else { this.logger.debug("View remains unresolved" + mediaTypeInfo); return null; } }

我們繼續點進去看,他是怎么獲得候選的視圖的呢? getCandidateViews中看到他是把所有的視圖解析器拿來,進行while循環,挨個解析!

 Iterator var5 = this.viewResolvers.iterator();

所以得出結論:ContentNegotiatingViewResolver 這個視圖解析器就是用來組合所有的視圖解析器的 

我們再去研究下他的組合邏輯,看到有個屬性viewResolvers,看看它是在哪里進行賦值的!

  protected void initServletContext(ServletContext servletContext) {
//這里它是從beanFactory工具中獲取容器中的所有視圖解析器,ViewRescolver.class , 把所有的視圖解析器來組合的 Collection
<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values(); ViewResolver viewResolver; if (this.viewResolvers == null) { this.viewResolvers = new ArrayList(matchingBeans.size());

既然它是在容器中去找視圖解析器,我們是否可以猜想,我們就可以去實現定制了呢?

我們可以自己給容器中去添加一個視圖解析器;這個類就會幫我們自動的將它組合進來;我們去實現一下

我們在我們的主程序中去寫一個視圖解析器來試試;

    @Bean //放到bean中
    public ViewResolver myViewResolver(){
        return new MyViewResolver();
    }

    //我們寫一個靜態內部類,視圖解析器就需要實現ViewResolver接口
    private static class MyViewResolver implements ViewResolver{
        @Override
        public View resolveViewName(String s, Locale locale) throws Exception {
            return null;
        }
    }

怎么看我們自己寫的視圖解析器有沒有起作用呢?我們給dispatcherServlet中的 doDispatch方法加個斷點進行調試一下,因為所有的請求都會走到這個方法中

我們啟動我們的項目,然后隨便訪問一個頁面,看一下Debug信息;

找到this;

找到視圖解析器,我們看到我們自己定義的就在這里了;

所以說,我們如果想要使用自己定制化的東西,我們只需要給容器中添加這個組件就好了!剩下的事情SpringBoot就會幫我們做了

轉換器和格式化器

找到格式化轉換器

        @Bean
        public FormattingConversionService mvcConversionService() {
//拿到配置文件中的格式化規則 WebConversionService conversionService
= new WebConversionService(this.mvcProperties.getDateFormat()); this.addFormatters(conversionService); return conversionService; }

點擊去

    public String getDateFormat() {
        return this.dateFormat;
    }

可以看到在我們的Properties文件中,我們可以進行自動配置它!如果注冊了自己的格式化方式,就會注冊到Bean中,否則不會注冊

我們可以在配置文件中配置日期格式化的規則:

修改SpringBoot的默認配置

方式一

這么多的自動配置,原理都是一樣的,通過這個WebMVC的自動配置原理分析,我們要學會一種學習方式,通過源碼探究,得出結論;這個結論一定是屬于自己的,而且一通百通。

SpringBoot的底層,大量用到了這些設計細節思想,所以,沒事需要多閱讀源碼!得出結論;

SpringBoot在自動配置很多組件的時候,先看容器中有沒有用戶自己配置的(如果用戶自己配置@bean),如果有就用用戶配置的,如果沒有就用自動配置的;如果有些組件可以存在多個,比如我們的視圖解析器,就將用戶配置的和自己默認的組合起來!

擴展使用SpringMVC

If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

我們要做的就是編寫一個@Configuration注解類,并且類型要為WebMvcConfigurer,還不能標注@EnableWebMvc注解;我們去自己寫一個;

我們新建一個包叫config,寫一個類MyMvcConfig;

package com.kuang.myproject.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

//應為類型要求為WebMvcConfigurer,所以我們實現其接口
//可以使用自定義類擴展MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //瀏覽器發送/kuang , 就會跳轉到success頁面;
        registry.addViewController("/kuang").setViewName("success");
    }
}

我們去瀏覽器訪問一下:

確實也跳轉過來了!所以說,我們要擴展SpringMVC,官方就推薦我們這么去使用,既保SpringBoot留所有的自動配置,也能用我們擴展的配置!

 我們可以去分析一下原理:

  1. WebMvcAutoConfiguration 是 SpringMVC的自動配置類,里面有一個類WebMvcAutoConfigurationAdapter
  2. 這個類上有一個注解,在做其他自動配置時會導入:@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
  3. 我們點進EnableWebMvcConfiguration這個類看一下,它繼承了一個父類:DelegatingWebMvcConfiguration,這個父類中有這樣一段代碼
  4.     private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    
       //從容器中獲取所有的webmvcConfigurer @Autowired(required
    = false) public void setConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { this.configurers.addWebMvcConfigurers(configurers); } }

    我們可以在這個類中去尋找一個我們剛才設置的viewController當做參考,發現它調用了一個

    this.configurers.addViewControllers(registry);

    我們點進去看一下

        public void addViewControllers(ViewControllerRegistry registry) {
            Iterator var2 = this.delegates.iterator();
    
            while(var2.hasNext()) {
    //將所有的WebMvcConfigurer相關配置來一起調用!包括我們自己配置的和Spring給我們配置的 WebMvcConfigurer delegate
    = (WebMvcConfigurer)var2.next(); delegate.addViewControllers(registry); } }
  5. 所以得出結論:所有的WebMvcConfiguration都會被作用,不止Spring自己的配置類,我們自己的配置類當然也會被調用;

全面接管SpringMVC

 If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

全面接管即:SpringBoot對SpringMVC的自動配置不需要了,所有都是我們自己去配置!只需在我們的配置類中要加一個@EnableWebMvc.

我們看下如果我們全面接管了SpringMVC了,我們之前SpringBoot給我們配置的靜態資源映射一定會無效,我們可以去測試一下;

 不加注解之前,訪問首頁

給配置類加上注解:@EnableWebMvc

我們發現所有的SpringMVC自動配置都失效了!回歸到了最初的樣子;

當然,我們開發中,不推薦使用全面接管SpringMVC

思考問題?為什么加了一個注解,自動配置就失效了!我們看下源碼:

1.這里發現它是導入了一個類,我們可以繼續進去看

@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}

2.它繼承了一個父類

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
}

3.我們來回顧一下Webmvc自動配置類

@Configuration
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
//這個注解的意思就是:容器中沒有這個組件的時候,這個自動配置類才生效 @ConditionalOnMissingBean({WebMvcConfigurationSupport.
class}) @AutoConfigureOrder(-2147483638) @AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class}) public class WebMvcAutoConfiguration { }

4. 總結一句話:@EnableWebMvc將WebMvcConfigurationSupport組件導入進來了;而導入的WebMvcConfigurationSupport只是SpringMVC最基本的功能!

在SpringBoot中會有非常多的***Configurer幫助我們進行擴展配置,只要看見了這個,我們就應該多留心注意~

RestfulCRUD

準備工作

我們現在可以來導入我們的所有提供的資源!

pojo 及 dao 放到項目對應的路徑下:

pojo實體類

package com.kuang.myproject.pojo;

public class Department {

    private Integer id;
    private String departmentName;

    public Department() {
    }
    
    public Department(int i, String string) {
        this.id = i;
        this.departmentName = string;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getDepartmentName() {
        return departmentName;
    }

    public void setDepartmentName(String departmentName) {
        this.departmentName = departmentName;
    }

    @Override
    public String toString() {
        return "Department [id=" + id + ", departmentName=" + departmentName + "]";
    }
    
}
Department.java
package com.kuang.myproject.pojo;

import java.util.Date;

public class Employee {

    private Integer id;
    private String lastName;

    private String email;
    //1 male, 0 female
    private Integer gender;
    private Department department;
    private Date birth;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getGender() {
        return gender;
    }

    public void setGender(Integer gender) {
        this.gender = gender;
    }

    public Department getDepartment() {
        return department;
    }

    public void setDepartment(Department department) {
        this.department = department;
    }

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }
    public Employee(Integer id, String lastName, String email, Integer gender,
                    Department department) {
        super();
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
        this.department = department;
        this.birth = new Date();
    }

    public Employee() {
    }

    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", lastName='" + lastName + '\'' +
                ", email='" + email + '\'' +
                ", gender=" + gender +
                ", department=" + department +
                ", birth=" + birth +
                '}';
    }
    
    
}
Employee.java

 

dao層

package com.kuang.myproject.dao;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import com.kuang.myproject.pojo.Department;
import org.springframework.stereotype.Repository;

@Repository
public class DepartmentDao {

    private static Map<Integer, Department> departments = null;
    
    static{
        departments = new HashMap<Integer, Department>();
        
        departments.put(101, new Department(101, "D-AA"));
        departments.put(102, new Department(102, "D-BB"));
        departments.put(103, new Department(103, "D-CC"));
        departments.put(104, new Department(104, "D-DD"));
        departments.put(105, new Department(105, "D-EE"));
    }
    
    public Collection<Department> getDepartments(){
        return departments.values();
    }
    
    public Department getDepartment(Integer id){
        return departments.get(id);
    }
    
}
DepartmentDao
package com.kuang.myproject.dao;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import com.kuang.myproject.pojo.Department;
import com.kuang.myproject.pojo.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;


@Repository
public class EmployeeDao {

    private static Map<Integer, Employee> employees = null;
    
    @Autowired
    private DepartmentDao departmentDao;
    
    static{
        employees = new HashMap<Integer, Employee>();

        employees.put(1001, new Employee(1001, "E-AA", "[email protected]", 1, new Department(101, "D-AA")));
        employees.put(1002, new Employee(1002, "E-BB", "[email protected]", 1, new Department(102, "D-BB")));
        employees.put(1003, new Employee(1003, "E-CC", "[email protected]", 0, new Department(103, "D-CC")));
        employees.put(1004, new Employee(1004, "E-DD", "[email protected]", 0, new Department(104, "D-DD")));
        employees.put(1005, new Employee(1005, "E-EE", "[email protected]", 1, new Department(105, "D-EE")));
    }
    
    private static Integer initId = 1006;
    
    public void save(Employee employee){
        if(employee.getId() == null){
            employee.setId(initId++);
        }
        
        employee.setDepartment(departmentDao.getDepartment(employee.getDepartment().getId()));
        employees.put(employee.getId(), employee);
    }
    
    public Collection<Employee> getAll(){
        return employees.values();
    }
    
    public Employee get(Integer id){
        return employees.get(id);
    }
    
    public void delete(Integer id){
        employees.remove(id);
    }
}
EmployeeDao

 

導入完畢這些之后,我們還需要導入我們的前端頁面,及靜態資源文件!

  • css,js等放在static文件夾下
  • html放在templates文件夾下

準備工作:OK?。?!

首頁實現

要求一:默認訪問首頁

方式一:寫一個controller實現!

    //會解析到templates目錄下的index.html頁面
    @RequestMapping({"/","/index.html"})
    public String index(){
        return "index";
    }

 

方式二:自己編寫MVC的擴展配置

package com.kuang.myproject.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }
}

 

解決了這個問題,我們還需要解決一個資源導入的問題;

我們講我們項目的啟動名改掉

server.servlet.context-path=/kuang

 

現在你訪問localhost:8080  就不行了,需要訪問localhost:8080/kuang

為了保證資源導入穩定,我們建議在所有資源導入時候使用 th:去替換原有的資源路徑!

<link href="asserts/css/bootstrap.min.css" th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">

 

比如以上這樣,無論我們項目名稱如何變化,它都可以自動的尋找到!看源代碼就可以看出來區別

 

頁面國際化

第一步 :編寫國際化配置文件,抽取頁面需要顯示的國際化頁面消息。我們可以去登錄頁面查看一下

先在IDEA中統一設置properties的編碼問題!

 

我們在resources資源文件下新建一個i18n目錄,建立一個login.propetries文件,還有一個login_zh_CN.properties,發現IDEA自動識別了我們要做國際化操作;文件夾變了

我們可以在這上面去新建一個文件;

彈出如下頁面:我們再添加一個英文的;

這樣就快捷多了!

 

接下來,我們就來編寫配置,我們可以看到idea下面有另外一個視圖;

這個視圖我們點擊 + 號就可以直接添加屬性了;我們新建一個login.tip,可以看到邊上有三個文件框可以輸入

我們添加一下首頁的內容!

然后依次添加其他頁面內容即可!

 

然后去查看我們的配置文件;

login.properties : 默認

login.btn=登錄
login.password=密碼
login.remember=記住我
login.tip=請登錄
login.username=用戶名

 

 英文:

login.btn=Sign in
login.password=Password
login.remember=Remember me
login.tip=Please sign in
login.username=Username

 

中文:

login.btn=登錄
login.password=密碼
login.remember=記住我
login.tip=請登錄
login.username=用戶名

 

 OK,搞定!

 

第二步 :我們去看一下SpringBoot對國際化的自動配置!

這里又涉及到一個類: MessageSourceAutoConfiguration ,里面有一個方法,這里發現SpringBoot已經自動配置好了管理我們國際化資源文件的組件 ResourceBundleMessageSource;

public class MessageSourceAutoConfiguration {
    private static final Resource[] NO_RESOURCES = new Resource[0];

    public MessageSourceAutoConfiguration() {
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.messages") //我們的配置文件可以直接放在類路徑下叫: messages.properties, 就可以進行國際化操作了
    public MessageSourceProperties messageSourceProperties() {
        return new MessageSourceProperties();
    }

    @Bean
    public MessageSource messageSource(MessageSourceProperties properties) {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        if (StringUtils.hasText(properties.getBasename())) {
        //設置國際化文件的基礎名(去掉語言國家代碼的) messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename()))); } if (properties.getEncoding() != null) { messageSource.setDefaultEncoding(properties.getEncoding().name()); } messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale()); Duration cacheDuration = properties.getCacheDuration(); if (cacheDuration != null) { messageSource.setCacheMillis(cacheDuration.toMillis()); } messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat()); messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage()); return messageSource; } }

 

 

 我們真實 的情況是放在了i18n目錄下,所以我們要去配置這個messages的路徑;

spring.messages.basename=i18n.login

第三步 : 去頁面獲取國際化的值;

查看Thymeleaf的文檔,找到message取值操作為: #{...}。

我們去頁面測試下;

其余同理!IDEA還有提示,非常智能的!

我們可以去打開項目,訪問一下,發現已經自動識別為中文的了!

但是我們想要更好!可以根據按鈕自動切換中文英文!

在Spring中有一個國際化的Locale (區域信息對象);里面有一個叫做LocaleResolver (獲取區域信息對象)的解析器

我們去我們webmvc自動配置文件,尋找一下!看到SpringBoot默認配置了

        @Bean
        @ConditionalOnMissingBean
        @ConditionalOnProperty(
            prefix = "spring.mvc",
            name = {"locale"}
        )
        public LocaleResolver localeResolver() {
//容器中沒有就自己配,有的話就用用戶配置的
if (this.mvcProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.LocaleResolver.FIXED) { return new FixedLocaleResolver(this.mvcProperties.getLocale()); } else { AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); localeResolver.setDefaultLocale(this.mvcProperties.getLocale()); return localeResolver; } }

 

 AcceptHeaderLocaleResolver 這個類中有一個方法

    public Locale resolveLocale(HttpServletRequest request) {
        Locale defaultLocale = this.getDefaultLocale();
//默認的就是根據請求頭帶來的區域信息獲取Locale進行國際化
if (defaultLocale != null && request.getHeader("Accept-Language") == null) { return defaultLocale; } else { Locale requestLocale = request.getLocale(); List<Locale> supportedLocales = this.getSupportedLocales(); if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) { Locale supportedLocale = this.findSupportedLocale(request, supportedLocales); if (supportedLocale != null) { return supportedLocale; } else { return defaultLocale != null ? defaultLocale : requestLocale; } } else { return requestLocale; } } }

 

那假如我們現在想點擊鏈接讓我們的國際化資源生效,就需要讓我們自己的locale生效!

我們去自己寫一個自己的LocaleResolver,可以在鏈接上攜帶區域信息!

修改一下前端頁面的跳轉連接;

//這里傳入參數不需要使用 ?  使用 (key=value)
<
a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a> <a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>

 

我們去寫一個處理的組件類

package com.kuang.myproject.component;

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

//可以在鏈接上攜帶區域信息
public class MyLocaleResolver implements LocaleResolver {

    //解析請求
    @Override
    public Locale resolveLocale(HttpServletRequest request) {

        String language = request.getParameter("l");
        Locale locale = Locale.getDefault(); //如果沒有獲取到就使用系統默認的
        //如果請求鏈接不為空
        if (!StringUtils.isEmpty(language)){
            //分割請求參數
            String[] split = language.split("_");
            //國家,地區
            locale = new Locale(split[0],split[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {

    }
}

 

為了讓我們的區域化信息能夠生效,我們需要再配置一下這個組件!在我們自己的MvcConofig下添加bean;

    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }

 

我們重啟項目,來訪問一下,發現點擊按鈕可以實現成功切換!

登錄 + 攔截器

我們這里就先不連接數據庫了,輸入任意用戶名都可以登錄成功!

聲明一個之前沒有提到的問題:

templates下的頁面只能通過Controller跳轉實現,而static下的頁面是能直接被外界訪問的,就能正常訪問了。

我們把登錄頁面的表單提交地址寫一個controller!

<form class="form-signin" action="dashboard.html" th:action="@{/user/login}" method="post">
//這里面的所有表單標簽都需要加上一個name屬性
</form>

 

去編寫對應的controller

package com.kuang.myproject.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class LoginController {

    //@RequestMapping(value = "/user/login",method = RequestMethod.POST)
    @PostMapping("/user/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Model model){

        if (!StringUtils.isEmpty(username) && "123456".equals(password)){
            //登錄成功!
            return "dashboard";
        }else {
            //登錄失敗!存放錯誤信息
            model.addAttribute("msg","用戶名密碼錯誤");
            return "index";
        }

    }

}

頁面存在緩存,所以我們需要禁用模板引擎的緩存

#禁用模板緩存
spring.thymeleaf.cache=false

 模板引擎修改后,想要實時生效!頁面修改完畢后,IDEA小技巧 : Ctrl + F9  重新編譯!

OK ,測試登錄成功!如果模板出現500錯誤,參考處理連接:https://blog.csdn.net/fengzyf/article/details/83341479

登錄成功,發現了靜態資源導入問題!

登錄失敗的話,我們需要將后臺信息輸出到前臺,可以在首頁標題下面加上判斷!

<!--判斷是否顯示,使用if, ${}可以使用工具類,可以看thymeleaf的中文文檔-->
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

刷新測試 :

優化,登錄成功后,由于是轉發,鏈接不變,我們可以重定向到首頁!

我們再添加一個視圖控制映射,在我們的自己的MyMvcConfig中

registry.addViewController("/main.html").setViewName("dashboard");

將 Controller 的代碼改為重定向;

//登錄成功!防止表單重復提交,我們重定向
return "redirect:/main.html";

重定向成功之后!我們解決了之前資源沒有加載進來的問題!后臺主頁正常顯示!

但是又發現新的問題,我們可以直接登錄到后臺主頁,不用登錄也可以實現!

怎么處理這個問題呢?我們可以使用攔截器機制,實現登錄檢查!

我們先自定義一個攔截器

package com.kuang.myproject.component;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginHandlerInterceptor implements HandlerInterceptor {
    //目標方法執行之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object user = request.getSession().getAttribute("loginUser");
        if (user == null){//未登錄,返回登錄頁面
            request.setAttribute("msg","沒有權限,請先登錄");
            request.getRequestDispatcher("/index.html").forward(request,response);
            return false;
        }else {
            //登錄,放行
            return true;
        }
    }
}

然后將攔截器注冊到我們的SpringMVC配置類當中!

    //注冊攔截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注冊攔截器,及攔截請求和要剔除哪些請求!
        //我們還需要過濾靜態資源文件,否則樣式顯示不出來
        registry.addInterceptor(new LoginHandlerInterceptor())
            .addPathPatterns("/**").excludePathPatterns("/index.html","/","/user/login","/asserts/**");
    }

 

我們然后在后臺主頁,獲取用戶登錄的信息

<!--后臺主頁顯示登錄用戶的信息-->
[[${session.loginUser}]]

然后我們登錄測試!完美!

員工列表功能

要求 : 我們需要使用 Restful風格實現我們的crud操作!

看看一些具體的要求,就是我們小實驗的架構;

我們根據這些要求,來完成第一個功能,就是我們的員工列表功能!

我們在主頁點擊Customers,就顯示列表頁面;我們去修改下

1.將首頁的側邊欄Customers改為員工管理

2.a鏈接添加請求

<a class="nav-link" href="#" th:href="@{/emps}">員工管理</a>

 

3.將list放在emp文件夾下

 

4.編寫處理請求的controller

@Controller
public class EmployeeController {

    @Autowired
    EmployeeDao employeeDao;

    //查詢所有員工,返回列表頁面
    @GetMapping("/emps")
    public String list(Model model){
        Collection<Employee> employees = employeeDao.getAll();
        //將結果放在請求中
        model.addAttribute("emps",employees);
        return "emp/list";
    }

}

 

我們啟動項目,測試一下看是否能夠跳轉,測試OK!我們只需要將數據渲染進去即可!

但是發現一個問題,側邊欄和頂部都相同,我們是不是應該將它抽取出來呢?

Thymeleaf 公共頁面元素抽取

1.抽取公共片段 th:fragment  定義模板名

2.引入公共片段  th:insert  插入模板名

我們來抽取一下,使用list列表做演示!我們要抽取頭部,nav標簽

我們在dashboard中將nav部分定義一個模板名;

        <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar" >
            <!--后臺主頁顯示登錄用戶的信息-->
            <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="//getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>
            <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
            <ul class="navbar-nav px-3">
                <li class="nav-item text-nowrap">
                    <a class="nav-link" href="//getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a>
                </li>
            </ul>
        </nav>

 

然后我們在list頁面中去引入,可以刪掉原來的nav

        <!--引入抽取的topbar-->
        <!--模板名 : 會使用thymeleaf的前后綴配置規則進行解析
        使用~{模板::標簽名}-->
        <div th:insert="~{dashboard::topbar}"></div>

 

效果:可以看到已經成功加載過來了!

 

 除了使用insert插入,還可以使用replace替換,或者include包含,三種方式會有一些小區別,可以見名知義;

我們使用replace替換,可以解決div多余的問題,可以查看thymeleaf的文檔學習

側邊欄也是同理,當做練手,可以也同步一下!

保證這一步做完!

我們發現一個小問題,側邊欄激活的問題,它總是激活第一個;按理來說,這應該是動態的才對!

為了重用更清晰,我們建立一個commons文件夾,專門存放公共頁面;

我們去頁面中引入一下

<div th:replace="~{commons/bar::topbar}"></div>
<div th:replace="~{commons/bar::sitebar}"></div>

 

我們先測試一下,保證所有的頁面沒有出問題!ok!

我們來解決我們側邊欄激活問題!

1.將首頁的超鏈接地址改到項目中

2.我們在a標簽中加一個判斷,使用class改變標簽的值;

 <a class="nav-link active" th:class="${activeUrl=='main.html'?'nav-link active':'nav-link'}" href="#" th:href="@{/main.html}">
其余同理,判斷請求攜帶的參數,進行激活測試

 

3.修改請求鏈接

<div th:replace="~{commons/bar::sitebar(activeUrl='main.html')}"></div>
<div th:replace="~{commons/bar::sitebar(activeUrl='emps')}"></div>
其余要用都是同理。

 

我們刷新頁面,去測試一下,OK,動態激活搞定!

現在我們來遍歷我們的員工信息!順便美化一些頁面,增加添加,修改,刪除的按鈕

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                    <!--添加員工按鈕-->
                    <h2> <button class="btn btn-sm btn-success">添加員工</button></h2>
                    <div class="table-responsive">
                        <table class="table table-striped table-sm">
                            <thead>
                                <tr>
                                    <th>id</th>
                                    <th>lastName</th>
                                    <th>email</th>
                                    <th>gender</th>
                                    <th>department</th>
                                    <th>birth</th>
                                    <!--我們還可以在顯示的時候帶一些操作按鈕-->
                                    <th>操作</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr th:each="emp:${emps}">
                                    <td th:text="${emp.id}"></td>
                                    <td>[[${emp.lastName}]]</td>
                                    <td th:text="${emp.email}"></td>
                                    <td th:text="${emp.gender==0?'女':'男'}"></td>
                                    <td th:text="${emp.department.departmentName}"></td>
                                    <!--<td th:text="${emp.birth}"></td>-->
                                    <!--使用時間格式化工具-->
                                    <td th:text="${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}"></td>

                                    <!--操作-->
                                    <td>
                                        <button class="btn btn-sm btn-primary">編輯</button>
                                        <button class="btn btn-sm btn-danger">刪除</button>
                                    </td>
                                </tr>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </main>

OK,顯示全部員工OK!

添加員工信息

1. 將添加員工信息改為超鏈接

<!--添加員工按鈕-->
<h2> <a class="btn btn-sm btn-success" href="emp" th:href="@{/emp}">添加員工</a></h2>

 

2.編寫對應的controller

    //to員工添加頁面
    @GetMapping("/emp")
    public String toAddPage(){
        return "emp/add";
    }

 

3.添加前端頁面;復制list頁面,修改即可

bootstrap官網文檔 : https://v4.bootcss.com/docs/4.0/components/forms/ , 我們去可以里面找自己喜歡的樣式!

我這里給大家提供了編輯好的

<form>
    <div class="form-group">
        <label>LastName</label>
        <input type="text" class="form-control" placeholder="kuangshen">
    </div>
    <div class="form-group">
        <label>Email</label>
        <input type="email" class="form-control" placeholder="[email protected]">
    </div>
    <div class="form-group">
        <label>Gender</label><br/>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="gender"  value="1">
            <label class="form-check-label"></label>
        </div>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="gender"  value="0">
            <label class="form-check-label"></label>
        </div>
    </div>
    <div class="form-group">
        <label>department</label>
        <select class="form-control">
            <option>1</option>
            <option>2</option>
            <option>3</option>
            <option>4</option>
            <option>5</option>
        </select>
    </div>
    <div class="form-group">
        <label>Birth</label>
        <input type="text" class="form-control" placeholder="kuangstudy">
    </div>
    <button type="submit" class="btn btn-primary">添加</button>
</form>

 

 4.部門信息下拉框應該選擇的是我們提供的數據,所以我們要修改一下前端和后端

controller

    //to員工添加頁面
    @GetMapping("/emp")
    public String toAddPage(Model model){
        //查出所有的部門,提供選擇
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments",departments);
        return "emp/add";
    }

 

前端

<div class="form-group">
    <label>department</label>
    <!--提交的是部門的ID-->
    <select class="form-control">
        <option th:each="dept:${departments}" th:text="${dept.departmentName}" th:value="${dept.id}">1</option>
    </select>
</div>

 

OK,修改了controller,重啟項目測試!

 我們來具體實現添加功能;

1.修改add頁面form表單提交地址和方式

<form th:action="@{/emp}" method="post">

 2.編寫controller;

    //員工添加功能,使用post接收
    @PostMapping("/emp")
    public String addEmp(){

        //回到員工列表頁面,可以使用redirect或者forward,就不會被視圖解析器解析
        return "redirect:/emps";
    }

 

回憶:重定向和轉發 以及 /的問題?

原理探究 : ThymeleafViewResolver

    public static final String REDIRECT_URL_PREFIX = "redirect:";
    public static final String FORWARD_URL_PREFIX = "forward:";

    protected View createView(String viewName, Locale locale) throws Exception {
        if (!this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
            vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
            return null;
        } else {
            String forwardUrl;
            if (viewName.startsWith("redirect:")) {
                vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
                forwardUrl = viewName.substring("redirect:".length(), viewName.length());
                RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible());
                return (View)this.getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
            } else if (viewName.startsWith("forward:")) {
                vrlogger.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName);
                forwardUrl = viewName.substring("forward:".length(), viewName.length());
                return new InternalResourceView(forwardUrl);
            } else if (this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
                vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
                return null;
            } else {
                vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a {} instance will be created for it", viewName, this.getViewClass().getSimpleName());
                return this.loadView(viewName, locale);
            }
        }
    }

 OK,看完源碼,我們繼續編寫代碼!

我們要接收前端傳過來的屬性,將它封裝成為對象!首先需要將前端頁面空間的name屬性編寫完畢!【操作】

編寫controller接收調試打印【操作】

    //員工添加功能
    //接收前端傳遞的參數,自動封裝成為對象[要求前端傳遞的參數名,和屬性名一致]
    @PostMapping("/emp")
    public String addEmp(Employee employee){
        System.out.println(employee);
        employeeDao.save(employee); //保存員工信息
        //回到員工列表頁面,可以使用redirect或者forward
        return "redirect:/emps";
    }

 

前端填寫數據,注意時間問題

點擊提交,后臺輸出正常!頁面跳轉及數據顯示正常!OK!

那我們將時間換一個格式提交

 

提交發現頁面出現了400錯誤!

生日我們提交的是一個日期 , 我們第一次使用的 / 正常提交成功了,后面使用 - 就錯誤了,所以這里面應該存在一個日期格式化的問題;

SpringMVC會將頁面提交的值轉換為指定的類型,默認日期是按照 / 的方式提交 ; 比如將2019/01/01 轉換為一個date對象。

那思考一個問題?我們能不能修改這個默認的格式呢?

我們去看webmvc的自動配置文件;找到一個日期格式化的方法,我們可以看一下

        @Bean
        public FormattingConversionService mvcConversionService() {
            WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
            this.addFormatters(conversionService);
            return conversionService;
        }

 

調用了 getDateFormat 方法;

    public String getDateFormat() {
        return this.dateFormat;
    }

 

這個在配置類中,所以我們可以自定義的去修改這個時間格式化問題,我們在我們的配置文件中修改一下;

spring.mvc.date-format=yyyy-MM-dd

 

這樣的話,我們現在就支持 - 的格式了,但是又不支持 /  了 , 2333吧

測試OK!

員工修改功能

我們要實現員工修改功能,需要實現兩步;

1. 點擊修改按鈕,去到編輯頁面,我們可以直接使用添加員工的頁面實現

2.顯示原數據,修改完畢后跳回列表頁面!

我們去實現一下:首先修改跳轉鏈接的位置;

<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.id}">編輯</a>

 

編寫對應的controller

    //to員工修改頁面
    @GetMapping("/emp/{id}")
    public String toUpdateEmp(@PathVariable("id") Integer id,Model model){
        //根據id查出來員工
        Employee employee = employeeDao.get(id);
        //將員工信息返回頁面
        model.addAttribute("emp",employee);
        //查出所有的部門,提供修改選擇
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments",departments);

        return "emp/update";
    }

 

 

我們需要在這里將add頁面復制一份,改為update頁面;需要修改頁面,將我們后臺查詢數據回顯

            <form th:action="@{/emp}" method="post">
                <div class="form-group">
                    <label>LastName</label>
                    <input name="lastName" type="text" class="form-control" th:value="${emp.lastName}">
                </div>
                <div class="form-group">
                    <label>Email</label>
                    <input name="email" type="email" class="form-control" th:value="${emp.email}">
                </div>
                <div class="form-group">
                    <label>Gender</label><br/>
                    <div class="form-check form-check-inline">
                        <input class="form-check-input" type="radio" name="gender" value="1"
                               th:checked="${emp.gender==1}">
                        <label class="form-check-label"></label>
                    </div>
                    <div class="form-check form-check-inline">
                        <input class="form-check-input" type="radio" name="gender" value="0"
                               th:checked="${emp.gender==0}">
                        <label class="form-check-label"></label>
                    </div>
                </div>
                <div class="form-group">
                    <label>department</label>
                    <!--提交的是部門的ID-->
                    <select class="form-control" name="department.id">
                        <option th:selected="${dept.id == emp.department.id}" th:each="dept:${departments}"
                                th:text="${dept.departmentName}" th:value="${dept.id}">1
                        </option>
                    </select>
                </div>
                <div class="form-group">
                    <label>Birth</label>
                    <input name="birth" type="text" class="form-control" th:value="${emp.birth}">
                </div>
                <button type="submit" class="btn btn-primary">修改</button>
            </form>

 

測試OK!

發現我們的日期顯示不完美,可以使用日期工具,進行日期的格式化!

<input name="birth" type="text" class="form-control" th:value="${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}">

 

數據回顯OK,我們繼續完成數據修改問題!

修改表單提交的地址:

<form th:action="@{/updateEmp}" method="post">

 

編寫對應的controller

    @PostMapping("/updateEmp")
    public String updateEmp(Employee employee){
        employeeDao.save(employee);
        //回到員工列表頁面
        return "redirect:/emps";
    }

 

發現頁面提交的沒有id;我們在前端加一個隱藏域,提交id;

 <input name="id" type="hidden" class="form-control" th:value="${emp.id}">

 

重啟,修改信息測試OK!

刪除員工

list頁面,編寫提交地址

<a class="btn btn-sm btn-danger" th:href="@{/delEmp/}+${emp.id}">刪除</a>

 

編寫Controller

    @GetMapping("/delEmp/{id}")
    public String delEmp(@PathVariable("id") Integer id){
        employeeDao.delete(id);
        return "redirect:/emps";
    }

 

測試OK!

定制錯誤頁面

我們只需要在模板目錄下添加一個error文件夾,文件夾中存放我們相應的錯誤頁面,比如404.html  或者 4xx.html 等等,SpringBoot就會幫我們自動使用了!

 注銷功能

<a class="nav-link" href="#" th:href="@{/user/loginOut}">Sign out</a>

 

對應的controller

    @GetMapping("/user/loginOut")
    public String loginOut(HttpSession session){
        session.invalidate();
        return "redirect:/index.html";
    }

學到這里,SpringBoot的基本開發就以及沒有問題了,我們后面去學習一下SpringBoot如何操作數據庫以及配置Mybatis;

 

posted on 2019-08-11 13:40 狂神說 閱讀(...) 評論(...) 編輯 收藏

統計