由于我们在做springboot项目时,用的前后端分离
,处理原则就变成了"前端处理托管静态资源,后端仅提供API接口",避免后端直接承载静态资源存储和分发
前后端分离的静态资源分工
-
前端:负责静态资源的构建、存储和分发
-
后端:仅提供JSON格式API接口,不处理前端静态资源的渲染和转发
-
提高静态资源访问速度
-
静态资源映射是在
WebMvcConfigurer这个接口里的addResourceHandlers方法
java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 本地静态资源目录(如 D:/uploads/ 下的图片)
String localResourcePath = "file:D:/uploads/";
// 前端访问路径:http://localhost:8081/static/**
registry.addResourceHandler("/static/**")
.addResourceLocations(localResourcePath)
// 缓存禁用(开发环境避免浏览器缓存)
.setCachePeriod(0);
}
- 前端访问示例:
http://localhost:8081/static/2024/05/12/xxx.jpg(对应本地D:/uploads/2024/05/12/xxx.jpg)。
用一个添加用户的案例,用户有头像上传功能
Controller
java
@PostMapping("/insert")
public Result addUser(
@Validated @RequestPart("userDTO")UserDTO userDTO,
@RequestPart(value = "avatar",required = false)MultipartFile avatar){
//头像上传,是要用MultipartFile类型的,,用@RequestPart方式
try {
Result result = new Result();
userDTO.setAvatar(avatar);
User user = userService.addUserByUser(userDTO);
UserVo userVo = new UserVo();
BeanUtils.copyProperties(user,userVo);
String msg = String.format("用户【%s】添加成功,初始密码为手机号后六位:%s",
user.getAccount(), user.getTel().substring(user.getTel().length() - 6));
result.setCode(200);
result.setMsg(msg);
result.setData(userVo);
return result;
}catch (IllegalArgumentException e){
return Result.error(e.getMessage());
} catch (IOException e) {
return Result.error("头像上传失败");
}catch (RuntimeException e){
e.printStackTrace();
return Result.error("添加用户失败");
}catch (Exception e){
log.error("用户添加未知异常",e);
return Result.error("系统异常,联系管理员");
}
}
Impl实现类
java@Override public User addUserByUser(UserDTO userDTO) throws IOException { boolean exists = this.lambdaQuery().eq(User::getAccount, userDTO.getAccount()) .exists(); if(exists){ throw new IllegalArgumentException("用户名已存在"); } //上传头像,参数传进来之后通过fileUploadUtil工具类来处理上传文件, String avatar = fileUploadUtil.uploadAvatar(userDTO.getAvatar()); //处理密码 String tel = userDTO.getTel(); if(tel.length()<6){ throw new IllegalArgumentException("手机号长度不足6位无法生成初始密码"); } String originPwd = tel.substring(tel.length()-6); //转换DTD为实体类 User user = new User(); BeanUtils.copyProperties(userDTO,user); // 设置密码 user.setPwd(originPwd); user.setAvatar(avatar); user.setRegisterTime(LocalDateTime.now()); user.setDelflag("0"); boolean save = save(user); if(save){ System.out.println("用户["+user.getAccount()+"]初始密码:"+originPwd); }else{ throw new RuntimeException("添加异常"); } //保存用户角色关联关系 Integer roleId = userDTO.getRoleId(); if(roleId!=null){ Role role = roleDao.selectById(roleId); if(role==null || "1".equals(role.getDelflag())){ throw new IllegalArgumentException("选择的角儿不存在或已删除"); } UserRole userRole = new UserRole(); userRole.setUserId(user.getUserId()); userRole.setRoleId(roleId); userRoleDao.insert(userRole); }else { throw new IllegalArgumentException("请选择用户角色"); } return user; }
上传文件工具类
传文件之前可以在yml配置文件里设置路径
bash
file:
upload:
base-path: D:/images/campus/ # 本地存储路径
access-path: /images/partners # 前端访问的URL前缀
default-avatar: /api/avatar/default.jpg # 手动放在base-path里的默认头像
java
@Component
public class FileUploadUtil {
@Value("${file.upload.base-path}")
private String basePath;
@Value("${file.upload.access-path}")
private String accessPath;
@Value("${file.upload.default-avatar}")
private String defaultAvatar;
public String uploadAvatar(MultipartFile avatarFile) throws IOException {
if(avatarFile ==null || avatarFile.isEmpty()){
return defaultAvatar;
}
//校验文件类型
String originalFilename= avatarFile.getOriginalFilename();
if(originalFilename ==null){
throw new IllegalArgumentException("上传文件无后缀名,不支持该格式");
}
String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
if(!".jpg,.jpeg,.png,.gif".contains(suffix)){
throw new IllegalArgumentException("仅支持jpg、jpeg、png、gif格式的头像");
}
//生成唯一文件名
String fileName = UUID.randomUUID().toString() + suffix;
File storageDir = new File(basePath);
if(!storageDir.exists()){
storageDir.mkdirs();
}
//保存文件
File destFile = new File(basePath+fileName);
avatarFile.transferTo(destFile);
//生成访问路径
return accessPath+fileName;
}
WebConfig
java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 虚拟路径 /images/partners/** 映射到本地路径 D:\images\campus
registry.addResourceHandler("/images/partners/**")
.addResourceLocations("file:D:/images/campus");
}
如果有security
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth->
auth
.requestMatchers("/user/login").permitAll()
//给静态资源通过
.requestMatchers("/images/partners/**").permitAll()
.anyRequest().permitAll()
)
.cors(cors->cors.configurationSource(corsConfigurationSource()))
.csrf(csrf->csrf.disable());
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
记得加 前后端的跨域
我是在security里面定义了
java
// 定义CORS具体规则
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// 允许的前端域名(生产环境需指定具体域名,避免*)
configuration.setAllowedOrigins(Collections.singletonList("*"));
// 允许的请求方法(GET、POST等)
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
// 允许的请求头(包括自定义头,如JWT的Authorization)
configuration.setAllowedHeaders(Collections.singletonList("*"));
// 允许前端获取的响应头
configuration.setExposedHeaders(Arrays.asList("Authorization"));
// 是否允许携带Cookie(跨域请求默认不携带,如需开启需前后端配合)
configuration.setAllowCredentials(false);
// 预检请求的缓存时间(避免频繁预检)
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 对所有接口生效
source.registerCorsConfiguration("/**", configuration);
return source;
}
}