如题,springboot3.x + java17 + MP 整合最新jersey,各种请求类型(实战/详解) + 文件上传下载 + jersey资源注册 + 拦截器(JWT) + 跨域处理 + 全局异常 + Valid注解校验 等等 ,除非你必须整合security,否则或许吧,再加上redis,直接用吧
一、首先从请求资源说起!
1. jersey基础请求,定义资源,添加注解等等,,,
post,put请求遇到的坑,下面有标注
import com.xxx.config.api.AbstractResource;
import com.xxx.entity.UserEntity;
import com.xxx.service.UserService;
import com.xxx.util.jwt.PassToken;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.springframework.beans.factory.annotation.Autowired;
// 资源可访问路径, 注意这个前面的 “/” 别吃了
@Path("/user")
// 回参以json格式返回
@Produces(MediaType.APPLICATION_JSON)
public class UserResource extends AbstractResource {
@Autowired
private UserService userService;
@GET
@Path("/info")
// 获取个人信息,登录者,id通过header中的token获取
public Response mine() {
return this.ok(userService.mine());
}
@PUT
@Path("/edit")
// 这里增加了 @Valid 校验,看看实体对象中哪些属性不要的删掉
// 另外,使用@Valid 校验,由于这里是java17,注意不要导入了javax的包,否则无效
public Response edit(@Valid UserEntity entity) {
userService.edit(entity);
return this.successEdit();
}
@POST
@Path("/login")
// 自定义@PassToken 注解,不需要token也能访问
@PassToken(canPass = true)
// form传参,且请求类型必须为 application/x-www-form-urlencoded
public Response login(@FormParam("username") String username,
@FormParam("password") String password) {
return this.ok(userService.login(username, password));
}
@PUT
@Path("/logout")
// 登出,清除token
public Response logout() {
userService.logout();
return this.success();
}
@PUT
@Path("/register")
// 使用 BeanParam 注解,可以自动将请求参数绑定到实体类上,但是实体必须使用 @FormParam 注解修饰属性,否则会报错
// 而且,请求类型必须是 application/x-www-form-urlencoded
// 并且必须加上无参构造和所有有参构造......不太建议使用......
// 不加 @BeanParam 注解,请求类型必须是 application/json,简单,推荐
// public Response register(@Valid @BeanParam UserEntity entity) {
public Response register(@Valid UserEntity entity) {
userService.register(entity);
return this.success();
}
}
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// 针对如上的 @BeanParam ,实体为
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.FormParam;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* @TableName user_t
*/
@TableName(value = "user_t")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserEntity implements Serializable {
/**
* 用户id
*/
@FormParam("id")
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 用户名称
*/
@FormParam("name")
@NotNull(message = "用户名不能为空!")
private String name;
/**
* 密码
*/
@FormParam("password")
@NotNull(message = "密码不能为空!")
@TableField(select = false)
private String password;
/**
* 电话
*/
@FormParam("phone")
@NotNull(message = "电话号码不能为空!")
private String phone;
@Serial
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
2. 文件上传,本地文件下载/预览实现
import com.xxx.config.api.AbstractResource;
import com.xxx.config.exception.GenExceptCode;
import com.xxx.config.exception.ServiceException;
import com.xxx.util.FileUtil;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jdk.jfr.Description;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import java.io.File;
import java.io.InputStream;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
@Path("/file")
@Produces(MediaType.APPLICATION_JSON)
public class FileResource extends AbstractResource {
@POST
@Path("/upload")
@Description("上传文件")
@Consumes(MediaType.MULTIPART_FORM_DATA + ";charset=UTF-8")
public Response upload(@FormDataParam("file") InputStream inputStream,
@FormDataParam("file") FormDataContentDisposition disposition) {
// fileName utf8 处理上传文件名称
// 先decode转码,在用char转码
String fileName = disposition.getFileName();
// 兼容处理 RFC 6266规范 上传 文件编码格式
String fileNamePrefix = "UTF-8''";
boolean charFlag = fileName.contains(fileNamePrefix);
if (charFlag) {
fileName = URLDecoder.decode(fileName.replace(fileNamePrefix, ""), StandardCharsets.UTF_8);
} else {
fileName = new String(fileName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
}
// file full path
String filePath = "D:/files/" + fileName;
File tempFile = new File(filePath);
// 判断是否村子
if (tempFile.isFile() && tempFile.exists()) {
throw new ServiceException(GenExceptCode.Request_Param.name(), "上传失败,该文件已存在!");
}
// save file
File file = FileUtil.saveFile(inputStream, tempFile);
// 文件大小, 不要用 disposition 获取文件大小
// disposition.getSize(); // 这个获取不到文件大小,不知道为啥。。。
file.length();
return this.success();
}
@GET
@Path("/{id}/download")
@Description("下载文件")
// 浏览器请求:http://localhost:8080/{fileId}/download
public Response download(@PathParam("id") Integer id) {
// 通过id在数据库中获取到文件的具体路径,如"D:/files/xxx.docx"
String filePath = "";
File file = new File(filePath);
//如果文件不存在,提示404
if (!file.exists()) {
return this.err404();
}
// 直接将file对象给jersey处理就好,这里只是简单的封装了下
return this.successDownload(file);
}
@GET
@Path("/{id}/preview")
@Description("预览查看文件")
// 浏览器请求:http://localhost:8080/{fileId}/preview
public Response preview(@PathParam("id") Integer id) {
// 通过id在数据库中获取到文件的具体路径,如"D:/files/xxx.docx"
String filePath = "";
File file = new File(filePath);
//如果文件不存在,提示404
if (!file.exists()) {
return this.err404();
}
// 直接将file对象给jersey处理就好,这里只是简单的封装了下
return this.successPreview(file);
}
}
二、 跨域处理
(----------后续要在JerseyConfig中注册,这里先上代码----------)
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import java.io.IOException;
/**
* 跨域处理 此方法需要在 Jersey 中注册 资源 后才能使用
*/
public class CorsFilter implements ContainerResponseFilter {
public void filter(ContainerRequestContext request, ContainerResponseContext c) throws IOException {
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
//浏览器会先通过options请求来确认服务器是否可以正常访问,此时应放行
c.setStatus(HttpServletResponse.SC_OK);
}
c.getHeaders().add("Access-Control-Allow-Origin", "*");
c.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization");
c.getHeaders().add("Access-Control-Allow-Credentials", "true");
c.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
// CORS策略的缓存时间
c.getHeaders().add("Access-Control-Max-Age", "1209600");
}
}
三、去除APPLICATION_FORM_URLENCODED请求的警告信息,extends HiddenHttpMethodFilter,输出请求响应
(这个不需要注册jersey,直接使用配置)
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.core.MediaType;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jetty.http.HttpStatus;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 去除APPLICATION_FORM_URLENCODED请求的警告信息
*
* @version 1.0
* @since 1.0
*/
@Slf4j
@Configuration
public class HttpMethodFilter extends HiddenHttpMethodFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse r,
FilterChain fc) throws ServletException, IOException {
LocalDateTime startTime = LocalDateTime.now();
if ((RequestMethod.POST.name().equals(request.getMethod()) || RequestMethod.PUT.name().equals(request.getMethod()))
&& MediaType.APPLICATION_FORM_URLENCODED.equals(request.getContentType())) {
//Skip this filter and call the next filter in the chain.
fc.doFilter(request, r);
} else {
//Continue with processing this filter.
super.doFilterInternal(request, r, fc);
}
LocalDateTime endTime = LocalDateTime.now();
Duration duration = Duration.between(startTime, endTime);
String info = request.getMethod()
+ ">>>>>>"
+ request.getRequestURI()
+ "======>请求耗时: " + duration.toMillis() + " 毫秒,返回状态为:" + r.getStatus();
if ("favicon.ico".equalsIgnoreCase(request.getRequestURI())) {
return;
}
if (HttpStatus.OK_200 == r.getStatus()) log.info(info);
else log.error(info);
}
}
四、自定义RequestFilter implements ContainerRequestFilter
(----------后续要在JerseyConfig中注册,这里先上代码----------)
import com.xxx.config.api.AuditAware;
import com.xxx.config.api.ReturnResult;
import com.xxx.config.exception.GenExceptCode;
import com.xxx.util.jwt.JwtUtil;
import com.xxx.util.jwt.PassToken;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.http.HttpStatus;
import java.lang.reflect.Method;
/**
* 自定义拦截器实现
*/
public class RequestFilter implements ContainerRequestFilter {
@Context
private ResourceInfo resourceInfo;
@Override
public void filter(ContainerRequestContext requestContext) {
// 在这里进行拦截和处理操作
// 可以获取请求的信息,进行身份验证、参数校验等操作
// 如果需要终止请求并返回响应,可以使用requestContext.abortWith方法
// 获取自定义注解,当存在,则放行
Method method = resourceInfo.getResourceMethod();
if (method != null && method.isAnnotationPresent(PassToken.class)) {
PassToken aspect = method.getAnnotation(PassToken.class);
if (aspect != null && aspect.canPass()) {
return;
}
}
// 获取token
String headerToken = JwtUtil.getJwtFromHeader(requestContext);
String urlToken = JwtUtil.getJwtFromUrl(requestContext);
if (StringUtils.isEmpty(headerToken) && StringUtils.isEmpty(urlToken)) {
requestContext.abortWith(Response.ok(new ReturnResult(GenExceptCode.Operation_Denial.name(), "操作被拒!"))
.status(HttpStatus.UNAUTHORIZED_401).type(MediaType.APPLICATION_JSON_TYPE).build());
} else {
if (StringUtils.isEmpty(headerToken)) {
if (StringUtils.isEmpty(urlToken)) {
requestContext.abortWith(Response.ok(new ReturnResult(GenExceptCode.Operation_Denial.name(), "操作被拒!"))
.status(HttpStatus.UNAUTHORIZED_401).type(MediaType.APPLICATION_JSON_TYPE).build());
} else {
AuditAware.setUserId(JwtUtil.getUserIdFromToken(urlToken));
}
} else {
AuditAware.setUserId(JwtUtil.getUserIdFromToken(headerToken));
}
}
}
}
五、jersey配置类
这里必须注意,registerClasses的使用方法,不然你就一个一个资源添加吧
更多案例 ——》》 Springboot集成jersey打包jar找不到class处理
import com.xxx.config.exception.support.DefaultExceptionMapperSupport;
import com.xxx.config.exception.support.JsonMapperExceptionSupport;
import com.xxx.config.exception.support.UncaughtExceptionMapperSupport;
import com.xxx.config.exception.support.ValidationExceptionMapperSupport;
import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider;
import org.glassfish.jersey.logging.LoggingFeature;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.context.annotation.Configuration;
/**
* Jersey配置类
*/
@Configuration
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
// 开启日志
register(LoggingFeature.class);
// 注册文件上传
register(MultiPartFeature.class);
// 注册跨域处理
register(CorsFilter.class);
// 注册自定义拦截器实现
register(RequestFilter.class);
// 注册json序列化
register(JacksonJsonProvider.class);
// 注册异常类资源
register(DefaultExceptionMapperSupport.class);
register(JsonMapperExceptionSupport.class);
register(UncaughtExceptionMapperSupport.class);
register(ValidationExceptionMapperSupport.class);
// 注册包扫描 这个方法在开发使用没问题,但是打包jar后,找不到 class 文件
// packages("com.xxx.api");
// 定义扫描包含接口资源包
registerClasses(ClassUtil.findAllClasses("com.xxx.api"));
}
}
六、mabatis-plus简单配置
(看看就行)
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
// mapper java文件 路径扫描
@MapperScan("com.xxx.mapper")
// 开启事务管理
@EnableTransactionManagement
public class MybatisConfig {
/**
* 注册mybatis-plus分页插件
*/
@Bean
MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
七、自定义注解
使用见–userResource,实际逻辑见—RequestFilter
import java.lang.annotation.*;
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean canPass() default false;
}
八、全局异常,先上使用案例,具体详见代码
当然,你也可以用最优雅的断言方式,,,
...
...
public String login(String username, String password) {
if (username == null || password == null) {
throw new ServiceException(GenExceptCode.Request_Param.name(), "账户或密码不能为空!");
}
UserEntity userEntity = this.getOneByUsername(username, password);
if (userEntity == null) {
throw new ServiceException(GenExceptCode.Request_Param.name(), "账户或密码错误!");
}
AuditAware.setUserId(userEntity.getId());
// -1 永不过期
return JwtUtil.generateToken(userEntity.getId(), -1);
// TODO 生成token后保存到redis
}
...
...
九、效果
第一个有token,第二个没有token的效果
十、详见代码
配置文件,pom文件,以及其它源码
欢迎指正