Building a Supplier Management System with SpringBoot, Thymeleaf, MyBatis‑Plus, and Redis Caching
This article walks through the design, implementation, and optimization of a complete supplier management system built with SpringBoot, Thymeleaf, MyBatis‑Plus, MySQL, and Redis, covering project setup, configuration, core Java code, file upload handling, and caching strategies.
Difficulty Analysis
Although the author was initially shocked by the scope of a full‑featured management system, the overall workflow (design → documentation → coding → delivery) proved straightforward, allowing the core system to be built in about one and a half days, with an additional half day for optimization.
Project Review
Final Effect Demo:
Technical Stack
SpringBoot
Thymeleaf
MyBatis‑Plus
MySQL
PageHelper
Lombok
Redis (used later for page optimization)
Business Process Overview
The system includes login, user management with role assignment, news announcement management, product management (including categories and orders), and role‑based access control using Thymeleaf expressions, as well as basic file upload functionality.
Project Setup (Using a Template Engine)
1. Create Maven Project
Add the required dependencies and create the standard directory structure.
2. Write yaml configuration file
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/supplier?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: root
password: root
# thymeleaf configuration
thymeleaf:
cache: false
prefix: classpath:/templates/
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml3. Initial Project Setup
Configure common infrastructure such as exception handling, interceptors, filters, and constant classes.
Exception handling
@ControllerAdvice
public class ExceptionHandler {
private final org.slf4j.Logger logger = LoggerFactory.getLogger(this.getClass());
@org.springframework.web.bind.annotation.ExceptionHandler(Exception.class)
public ModelAndView exception(HttpServletRequest request, Exception e) throws Exception {
logger.error("Request URL:{},Exception:{}", request.getRequestURL(), e);
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
throw e;
}
ModelAndView mv = new ModelAndView();
mv.addObject("url", request.getRequestURL());
mv.addObject("exception", e);
mv.setViewName("error/error");
return mv;
}
}Interceptor
Used to protect resources that require login.
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getSession().getAttribute("user") == null) {
response.sendRedirect("/api");
return false;
}
return true;
}
}Register the interceptor:
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api", "/api/doLogin");
}
}4. Write Controller (FileController)
File upload page navigation:
// Navigate to file‑upload page
@RequestMapping("/file-upload")
public String userList() {
return "file-upload";
}File upload handling (single file):
@RequestMapping("/doAddForUser")
public String doAdd(User user, @RequestParam("file") MultipartFile files, HttpServletRequest request) throws IOException {
if (files != null && !files.isEmpty()) {
String name = UUID.randomUUID().toString().replace("-", "");
String ext = FilenameUtils.getExtension(files.getOriginalFilename());
String url = request.getSession().getServletContext().getRealPath("/upload/");
File dir = new File(url);
if (!dir.exists()) {
dir.mkdir();
}
files.transferTo(new File(url + "/" + name + "." + ext));
user.setAvatar(request.getContextPath() + "/upload/" + name + "." + ext);
}
// password encryption and other fields omitted for brevity
userService.save(user);
return "redirect:/api/users";
}Note: Multi‑file upload code is provided but not used in this project.
private void commons(Object obj, @RequestParam("file") CommonsMultipartFile[] files, HttpServletRequest request) throws IOException {
for (int i = 0; i < files.length; i++) {
if (files[i] != null && !files[i].isEmpty()) {
String name = UUID.randomUUID().toString().replace("-", "");
String ext = FilenameUtils.getExtension(files[i].getOriginalFilename());
String url = request.getSession().getServletContext().getRealPath("/upload/");
File dir = new File(url);
if (!dir.exists()) {
dir.mkdir();
}
files[i].transferTo(new File(url + "/" + name + "." + ext));
// set URL fields on obj according to index i
}
}
}5. Project Optimization (Redis Page Caching)
For non‑SPA projects, page‑level caching reduces database load during traffic spikes.
Import Redis dependencies
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2Redis yaml configuration
## Redis configuration
redis:
host: localhost
port: 6379
database: 0
connect-timeout: 10000ms
lettuce:
pool:
max-active: 8
max-wait: 10000ms
max-idle: 200
min-idle: 5Redis serialization bean
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate
redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate
redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}Cache the rendered HTML of the news list page:
@Autowired
private NewsService newsService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ThymeleafViewResolver viewResolver;
@RequestMapping(value = "/news", produces = "text/html;charset=utf-8")
@ResponseBody
public String roles(Model model, @RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest request, HttpServletResponse response) {
ValueOperations valueOps = redisTemplate.opsForValue();
String html = (String) valueOps.get("news-list");
if (!StringUtils.isEmpty(html)) {
return html;
}
PageHelper.startPage(pageNo, pageSize);
List
list = newsService.list();
PageInfo
pageInfo = new PageInfo<>(list);
model.addAttribute("news", list);
model.addAttribute("pageInfo", pageInfo);
WebContext context = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
html = viewResolver.getTemplateEngine().process("news-list", context);
if (!StringUtils.isEmpty(html)) {
valueOps.set("news-list", html, 60, TimeUnit.SECONDS);
}
return html;
}6. Remarks
Distinguish between @Controller (returns view pages) and @RestController (returns JSON). When caching whole pages, add @ResponseBody so the rendered HTML can be stored as a JSON string in Redis.
All source code has been synchronized to Gitee: https://gitee.com/gao-wumao/supplier . Feel free to clone the repository.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.