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&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 автоматически деплоит ваше веб-приложение.
В этом легко убедиться посмотрев в вывод консоли при запуске Вашего приложения.

К слову это относительное новшество. До появления проекта 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), которые выступают балансировщиками, прокси, и нередко отвечают за раздачу статики.
Справка:
- Ссылка на Tomcat на сайте Apache Founadtion
- Ссылка на Tomcat в Википедии
- "Сервер приложений" в Википедии
- "Веб сервер" в Википедии
Обработка запроса в приложении
Итак, вот запрос пришел к нам в приложение: что дальше?
Одним из первых этот запрос обрабатывает маршрутизатор(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.csshttp://localhost:8080/images/test.jpg->/static/images/test.jpghttp://localhost:8080/js/script.js->/static/js/script.js
Дтя тех, кто использует Spring Security (SecurityConfiguration), нужно добавить разрешение в методе configure(HttpSecurity http), например:
.antMatchers("/css/style.css","/js/bootstrap.js","/css/style.scss").permitAll()
Попытался сделать как сказано в большом ответе сверху, не получилось. Пока точно не разобрался почему у меня работает именно так, но может кому-то поможет.
Конфигурационный класс:
@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
