Thymeleaf не подключает css файл

введите сюда описание изображения Попробовал много вариантов, в том числе и с официального сайта thymeleaf.
Я знаю, что эту тему уже обсуждали, но варианты, которые предлагали для решения мне не помогли

<link th:href="@{/styles/css/main.css}" rel="stylesheet" />

h2 {
font-family: sans-serif;
font-size: 1.5em;
text-transform: uppercase;
}

strong {
font-weight: 700;
background-color: yellow;
}

p {
font-family: sans-serif;
}

Ответы (4 шт):

Автор решения: Николай Гнап

Попробуйте так:

 <link rel="stylesheet" th:href="@{/styles/cssandjs.css/main.css}"/>

Если это не сработает измените расположение файлов css. Хранить css необходимо по такому пути.

src/main/resources/static/css

Дополнение Необходимо создать class

@Configuration
public class MvcConfig implements WebMvcConfigurer

И сделать метод

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/img/**")
                .addResourceLocations("file:/" + uploadPath + "/");
        registry.addResourceHandler("/email/**")
                .addResourceLocations("classpath:/static/**");
    }
→ Ссылка
Автор решения: Михаил Ребров

Нетерпеливым мотать в конец поста.
Всем кто тем, хочет разобраться читать все

Thymeleaf

Начнем с того, почему не работает: th:href="@{/styles/css/main.css}"
@{...} - это всего лишь компонент, который помогает формировать URL-выражения.
Например:
Из этого: <a th:href="@{/order/details(id=${orderId},type=${orderType})}">...</a>
Помогает делать это: <a href="/myapp/order/details?id=23&amp;type=online">...</a>

И что самое важное: он не призван предоставлять доступ к файлам, которые находятся в ресурсах.
Вся его область ответственности не выходит за рамки работы с шаблонами, а работа с ресурсами лежит за этими границами.

Другими словами фраза Thymeleaf не подключает css файл вынесенная в заголовок не имеет смысла, потому что Thymeleaf и не должен ничего подключать.

404 ошибка

Далее разберемся с тем, что происходит в браузере.
Браузер загружает страницу и видит там тег

<link rel="stylesheet" type="text/css" href="/styles/css/main.css"/>

после чего выполняет запрос GET http://localhost:8080/styles/css/main.css
получает ответ от веб-сервера о том, что по данному адресу ничего не найдено, выводит сообщение об ошибке в консоль и страница остается без стилей.

Здесь нам интересно разобраться в том, что это за веб-сервер такой и где он находится.

Отвечаю: по умолчанию в Spring Boot используется веб-сервер Apache Tomcat.
Фактически при запуске своего Spring Boot Application вы сначала запускаете свой веб-сервер, на котором Spring Boot автоматически деплоит ваше веб-приложение.

В этом легко убедиться посмотрев в вывод консоли при запуске Вашего приложения. логи Tomcatа

К слову это относительное новшество. До появления проекта Spring Boot не было встроенного веб-сервера и всем приходилось самостоятельно деплоить приложения на отдельно стоящие веб-сервера, коих работает на JVM огромное множество: Tomcat, Jetty, Netty, JBoss, WildFly, GlassFish, WebLogic и др. Добавить встроенный было решено для удобства и быстроты разработки, собственно это и было основной целью Spring Boot.

Возвращаясь к теме: абсолютно все HTTP запросы принимает веб-сервер, после чего в зависимости от своих настроек, он может передать их одному из развернутых на него приложений (в Вашем случае веб-сервер Apache Tomcat передает все запросы вашему приложению), после чего уже принятый запрос - это Ваша ответственность. Ваше приложение обрабатывает запрос, возвращает ответ веб-серверу, после чего данный HTTP Response отправляется от веб-сервера к клиенту(браузеру).

Такая вот цепочка.

Замечание:

В данном случае я немного упростил маршрут движения HTTP запроса и ответа.
На самом деле, помимо web-сервера есть еще понятие Application Server.
Если просто то Application Server это контейнер, который может запускать другие приложения, и который может принимать и отправлять запросы, при этом принятые запросы он перенаправляет запущенным в контейнере приложениям.
Простой веб-сервер этого не умеет. Например Apache HTTP Server(не путать с Tomcat) или nginx. Эти сервера могут отдавать статический контент расположенный на сервере(обычный файлы), выступать в качестве прокси серверов и балансировщиков. Но вот из коробки запускать другие приложения они не умеют (хотя имеют модули, с помощью которых можно этого достичь).
По сути любой Application Server является по определению и веб-сервером, потому что он может принимать http-запросы. Поэтому я не стал усложнять данную схему и называл Tomcat веб-сервером.
Но в производственной среде часто перед Application Server'ами ставят более высокопроизводительные веб-сервера(например тот же nginx), которые выступают балансировщиками, прокси, и нередко отвечают за раздачу статики. введите сюда описание изображения

Справка:

Обработка запроса в приложении

Итак, вот запрос пришел к нам в приложение: что дальше?

Одним из первых этот запрос обрабатывает маршрутизатор(Router).
Он берет запрос и сверяет его со всеми зарегистрированными в приложениями маршрутами. Если он находит совпадение, то он передает запрос в контроллер и методу указанными в маршруте. Если он не нашел совпадение, то приложение отправляет ответ 404. И как ты понимаешь, в твоем случае случилось именно это.

И я думаю у тебя назрел вопрос: А почему же он не нашел ни одного подходящего маршрута?
Ответ простой: Потому что его не было.

Посуди сам: Зачем нам по умолчанию вкладывать маршрут, который отдаёт статику из папки resources?

  • А что если наше приложение будет REST API, и ему вообще не нужно будет отдавать никаких стилей?
  • А что если в папке с ресурсами лежит файлы доступ к которым мы никак не хотим открывать?
  • А что если наши стили лежат не в папке с ресурсами а в какой-то произвольной папке на сервере...или вообще скажем: на другом хосте?

Получается, что включенная по умолчанию передача ресурсов приносила бы больше вреда чем пользы.

И тогда следующим логичным вопрос будет:
Откуда и как маршрутизатор получает список маршрутов.
на самом деле способов много, но наc сейчас больше всего интересует тот, который мы будем чаще всего использовать в Spring Boot приложении, а именно аннотации.
При запуске приложения определяются все классы помеченные аннотацией @Controller, и все методы, помеченные аннотациями:

  • @RequestMapping
  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @PatchMapping
  • @DeleteMapping

И добавляет в маршруты путь указанный в аннотации, контроллер и метод, к которым привязана аннотация и HTTP- метод:

  • для @GetMapping это GET
  • для @PostMapping это POST
  • для @PutMapping это PUT
  • для @PatchMapping это PATCH
  • для @DeleteMapping это DELETE
  • для @RequestMapping это ВСЕ ВЫШЕПЕРЕЧИСЛЕННЫЕ МЕТОДЫ

Соответственно, когда приходит запрос, маршрутизатор ищет совпадение пути и метода указанного в маршрути и при совпадении передает управление, указанному контроллеру в указанный метод.

Зная, все вышесказанное может возникнуть идея:
А почему бы не взять и самому не отдать этот самый CSS файл

Кастомный метод отдачи статики

Я начал именно с данного способа, потому что Вы должны понимать, что примерно происходит, когда Вы запрашиваете некий файл из папки ресурсов.

Что нам нужно сделать:

  • создать класс контроллера (пусть это будет ResourceController) и пометить его аннотацией @Controller
  • Создать метод (пусть это будет styles) и пометить его аннотацией @GetMapping в которую мы пропишем необходимый путь.
  • Получить содержимое файла из ресурсов
  • Привести содержимое файла к строке и отправить его в качестве ответа сервера.

Что нам важно помнить и учитывать:

  • Для того чтобы браузер смог понять что данные, которые мы ему возвращаем являются стилями, в ответе сервера необходимо установить заголовок: "Content-Type: text/css"
  • Для того чтобы вернуть тело запроса непосредственно из контроллера необходимо установить над методом аннотацию @ResponseBody, в таком случае контроллер не будет передавать управление другим компонентам
  • Для того чтобы вернуть HTTP Response с телом и с заголовками мы воспользуемся классом ResponseEntity, в тело которого мы передадим содержимое файла, а заголовки укажем с помощью класса HttpHeaders
  • Для получения содержимого файлов из папки ресурсов мы можем воспользоваться методом getResourceAsStream текущего 'ClassLoader''а.
package com.example.TestSpring;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

@Controller
public class ResourceController {

    @GetMapping("/styles/css/{code}.css")
    @ResponseBody
    public ResponseEntity<String> styles(@PathVariable("code") String code) throws IOException {
        // получаем содержимое файла из папки ресурсов в виде потока
        InputStream is = getClass().getClassLoader().getResourceAsStream("static/css/"+code+".css");
        // преобразуем поток в строку
        BufferedReader bf = new BufferedReader(new InputStreamReader(is));
        StringBuffer sb = new StringBuffer();
        String line = null;
        while((line = bf.readLine()) != null){
            sb.append(line+"\n");
        }
        
        // создаем объект, в котором будем хранить HTTP заголовки
        final HttpHeaders httpHeaders= new HttpHeaders();
        // добавляем заголовок, который хранит тип содержимого
        httpHeaders.add("Content-Type", "text/css; charset=utf-8");
        // возвращаем HTTP ответ, в который передаем тело ответа, заголовки и статус 200 Ok
        return new ResponseEntity<String>( sb.toString(), httpHeaders, HttpStatus.OK);
    }
}

Обратите внимание на то, что:

  • в маршруте я указал /styles/css/{code}.css
    {code} - это плейсхолдер, и он же является переменной пути (Path Variable)
    Это значит, что данный метод будет обрабатывать все запросы такого вида
    /styles/css/main.css, /styles/css/admin.css, /styles/css/application.css и пр

  • Для того чтобы получить значение данного плейсхолдера, я указал в параметрах метода параметр String code и установил перед ним аннотацию @PathVariable("code") для того чтобы Spring мог связать плейсхолдер {code} с данным параметром метода

  • заметьте, что путь в маршруте (/styles/css/{code}.css) и путь к файлу относительно папки ресурсов ("/static/css/" + code + ".css") отличаются.
    я старался максимально соответствовать вашему примеру и в нем была указана именно такая ссылка на css(/styles/css/main.css) и именно такой путь к нему в папке ресурсов (static/css/{code}.css)
    если это уже не соответствует действительности, то внесите соответствующие правки в ваш код

В результате всех проделанных действий и запуска приложения: введите сюда описание изображения

Как видите, сервер успешно вернул мне ответ с css файлом. В данном файле было одно правило, которое закрашивает фон в красный цвет и как видите, оно успешно применилось.

Данный метод я проиллюстрировал за тем чтобы Вы понимали как происходит отдача ресурсов, и для того чтобы Вы могли выходить из более сложных ситуаций:

  • когда файлы стилей находятся в какой-то папке на сервере за рамками Вашего проекта
  • когда Вам стили формируются через админку и хранятся в базе данных
  • когда Вы пользуетесь спригном в связке с другими фреймворками, которые переопределяют FrontController и т.д.

Также этот метод вы можете использовать и для того, чтобы отдавать другие ресурсы: изображения и пр.

После всего этого Вы можете мне сказать: "Здорово конечно...спасибо. А чо так сложно то? Нельзя как нибудь полегче?"
И я отвечу: "Можно!"

Отдача статики встроенными средствами SpringMVC

Несмотря на все те минусы, которые я описал в начале, отдача файлов из папки ресурсов по HTTP обладает и своими плюсами.
И главный плюс - это простота.
Если нам не нужно хранить стили на каком-то другом сервере или в CDN (сети доставки и распространения контента), если не нужно их хранить в базе данных или как-то предварительно подготавливать и компилировать их, если у нас достаточно простое приложение и нас устраивает, что стили находятся непосредственно рядом с приложением, то почему бы и нет!
Это просто и удобно, когда нам не нужно разрабатывать какое-то мега-сложное и монструозное приложение.

И да: просто отдавать все файлы подряд из папки ресурсов небезопасно...там могут быть и конфигурационные файлы, и настройки с паролями.

Но никто нам не мешает выделить какую-то отдельную область в которой будет храниться статика и настроить на отдачу только данную папку, при этом все остальные ресурсы останутся в безопасности.
Вот это уже вполне приемлемый вариант.
И как водится для этого уже есть встроенные инструменты:

Для того, чтобы настроить отдачу ресурсов нам нужно:

  • создать класс конфигурации.
  • сообщить спрингу что данный класс, будет использоваться для настройки, для этого над классом необходимо указать аннотацию @Configuration
  • сообщить спрингу что мы нам потребуется поддержка SpringMVC, для этого мы указываем аннотацию @EnableWebMvc
  • дальше нам нужно будет имплементировать интерфейс WebMvcConfigurer, собственно спринг для настройки просто будет поочереди вызывать методы, указанные в данном интерфейсе. По умолчанию они ничего не делают. Нам просто нужно переопределить необходимые нам методы.
  • и в конце нам нужно определить ОБРАБОТЧИКИ РЕСУРСОВ. По их названию понятно что они отвечают за обработку ресурсов. Для этого нам нужно будет переопределить метод addResourceHandlers и в нем настроить обработку ресурсов.

Приступим:

package com.example.TestSpring;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
@EnableWebMvc
public class MVCConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
                .addResourceHandler("/styles/css/**")
                .addResourceLocations("classpath:/static/css/");
    }
}

Что здесь происходит?:

  • мы получаем объект реестра обработчика ресурсов
  • далее мы обращаемся к этому реестру и говорим ему что нам нужно обрабатывать HTTP запросы соответствующие шаблону: /styles/css/**
  • также мы говорим, что все файлы, запрашиваемые по указанному пути стоит искать в этом месте classpath:/static/css/

На что стоит обратить внимание:

  • еще раз прошу обратить внимание, что у нас отличается URL и путь
    ссылке http://localhost:8080/styles/css/main.css будет соответствовать файл /static/css/main.css в папке ресурсов
  • мы указываем просим реестр искать папку относительно CLASSPATH
    в нашем случае папка ресурсов находится в CLASSPATH, поэтому нам допустимо так указывать.
  • И также стоит обратить внимание, что лучше не использовать данный метод, с контроллером из прошлого пункта, а если и есть такая необходимость, то нужно удостовериться в том, что Ваш контроллер и обработчик ресурсов из конфигурации не будут обрабатывать одинаковых урлов. В таком случае запрос будет обрабатываться по принципу: "Кто первый встал того и тапки"

Под конец добавлю, что ты можешь замапить и другие ресурсы:

package com.example.TestSpring;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
@EnableWebMvc
public class MVCConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/styles/css/**")
                .addResourceLocations("classpath:/static/css/");
        registry.addResourceHandler("/images/**")
                .addResourceLocations("classpath:/static/images/");
        registry.addResourceHandler("/js/**")
                .addResourceLocations("classpath:/static/js/");
    }
}

и ты получишь следующую маршрутизацию:

  • http://localhost:8080/styles/css/main.css -> /static/css/main.css
  • http://localhost:8080/images/test.jpg -> /static/images/test.jpg
  • http://localhost:8080/js/script.js -> /static/js/script.js
→ Ссылка
Автор решения: Irina Charewicz

Дтя тех, кто использует Spring Security (SecurityConfiguration), нужно добавить разрешение в методе configure(HttpSecurity http), например:

.antMatchers("/css/style.css","/js/bootstrap.js","/css/style.scss").permitAll()
→ Ссылка
Автор решения: Ramzan K

Попытался сделать как сказано в большом ответе сверху, не получилось. Пока точно не разобрался почему у меня работает именно так, но может кому-то поможет.

Конфигурационный класс:

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
    }
}

link в html

<link rel="stylesheet" href="/static/css/button.css">

Путь до самого файла css: src/main/resources/static/css/button.css

→ Ссылка