FastDFS分布式文件系统之防盗链权限控制
问题所在FastDFS采用的是tracker和storage的模式。tracker负责负载均衡storage负责文件存储、文件同步、提供文件访问接口（也就是说用户要从storage直接获取文件）其实，storage的http端口是开着的，外界只要知道端口和文件路径信息就可以直接访问到文件。这种操作，对于文件而言是不安全的。解决方式在storage上配置访问规则，打开防盗链功能，首先要确保不能直接访问到文件。其次，限制访问文件时必须带有一个本地应用服务器生成的token，这个token需要有文件id、自己设置的密钥、时间戳三个部分组成。但是只有本地应用服务器保存密钥是不可行的，需要把密钥同时保存到storage上，用于token的解密验证工作。配置storage进入etc/fdfs，编辑http.conf文件编辑后的内容Javaresources下新建fdfs_cilent.conf文件#默认值为30s connect_timeout = 10 #默认值为30s network_timeout = 30 charset = UTF-8 #tracker的http端口 http.tracker_http_port = 8888 #服务器 tracker_server = 192.168.1.106:22122 #开启防盗链，防盗链密钥 http.anti_steal_token = true http.secret_key= 密钥主要的工具类文件/** * 上传下载等文件操作工具类 */ public class FastDFSClient { private static org.slf4j.Logger logger = LoggerFactory.getLogger(FastDFSClient.class); /*** * 初始化加载FastDFS的TrackerServer配置 */ static { try { String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath(); ClientGlobal.init(filePath); } catch (Exception e) { logger.error("FastDFS Client Init Fail!",e); } } public static void main(String[] args) throws UnsupportedEncodingException, NoSuchAlgorithmException, MyException { String fileName = "M00/00/00/wKgBa2U4dPSAO7fXAAC5PC_5iKw01.docx"; String groupName = "group1"; String sourceUrl = getSourceUrl(groupName,fileName); System.out.println(sourceUrl); } public static FastDFSFile getFastDFSFile(MultipartFile file,String author) { InputStream inputStream = null; //获取文件名称（带有后缀） String allFileName = file.getOriginalFilename(); //获取文件扩展名 String ext = ""; byte[] content = null; if (allFileName != null && !"".equals(allFileName)) { ext = (allFileName.substring(allFileName.lastIndexOf("."))).replaceAll("\\.",""); } //文件名称（不带后缀） String fileName = allFileName.replaceAll("." + ext, ""); try { inputStream = file.getInputStream(); //获取文件所有的字节内容 content = inputStream.readAllBytes(); //一定要关闭输入流，不然会报错 inputStream.close(); } catch (IOException e) { throw new CustomException("文件上传错误", ResultCode.UPLOAD_ERROR); } //创建文件实体类对象 FastDFSFile fastDFSFile = new FastDFSFile(); fastDFSFile.setContent(content); fastDFSFile.setSize(content.length); fastDFSFile.setName(fileName); fastDFSFile.setExt(ext); fastDFSFile.setAuthor(author); return fastDFSFile; } /*** * 文件上传 * @param file * @return */ public static String[] upload(FastDFSFile file) { //获取文件基础信息,有多少长度就要是多少，不然会报空指针 NameValuePair[] meta_list = new NameValuePair[4]; meta_list[0] = new NameValuePair("author", file.getAuthor()); meta_list[1] = new NameValuePair("name",file.getName()); meta_list[2] = new NameValuePair("ext",file.getExt()); meta_list[3] = new NameValuePair("size", file.getSize()+""); //接收返回数据 String[] uploadResults = null; StorageClient storageClient=null; try { //创建StorageClient客户端对象 storageClient = getTrackerClient(); /*** * 文件上传 * 1)文件字节数组 * 2)文件扩展名 * 3)文件作者 */ uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list); } catch (Exception e) { logger.error("Exception when uploadind the file:" + file.getName(), e); } if (uploadResults == null && storageClient!=null) { logger.error("upload file fail, error code:" + storageClient.getErrorCode()); } //获取组名 String groupName = uploadResults[0]; //获取文件存储路径 String remoteFileName = uploadResults[1]; return uploadResults; } /*** * 获取文件信息 * @param groupName:组名 * @param remoteFileName：文件存储完整名 * @return */ public static FileInfo getFile(String groupName, String remoteFileName) { try { StorageClient storageClient = getTrackerClient(); return storageClient.get_file_info(groupName, remoteFileName); } catch (Exception e) { logger.error("Exception: Get File from Fast DFS failed", e); } return null; } /*** * 文件下载 * @param groupName * @param remoteFileName * @return */ public static InputStream downFile(String groupName, String remoteFileName) { try { //创建StorageClient StorageClient storageClient = getTrackerClient(); //下载文件 byte[] fileByte = storageClient.download_file(groupName, remoteFileName); InputStream ins = new ByteArrayInputStream(fileByte); return ins; } catch (Exception e) { logger.error("Exception: Get File from Fast DFS failed", e); } return null; } /*** * 文件删除 * @param groupName * @param remoteFileName * @throws Exception */ public static void deleteFile(String groupName, String remoteFileName) throws Exception { //创建StorageClient StorageClient storageClient = getTrackerClient(); //删除文件 int i = storageClient.delete_file(groupName, remoteFileName); } /*** * 获取Storage客户端 * @return * @throws IOException */ private static StorageClient getTrackerClient() throws IOException { TrackerServer trackerServer = getTrackerServer(); StorageClient storageClient = new StorageClient(trackerServer, null); return storageClient; } /*** * 获取Tracker * @return * @throws IOException */ private static TrackerServer getTrackerServer() throws IOException { TrackerClient trackerClient = new TrackerClient(); TrackerServer trackerServer = trackerClient.getConnection(); return trackerServer; } /** * 生成防盗链token * @param groupName 文件组名称，group1 * @param fileName 文件路径，不带group：M00/00/00/wKg4C1tFmTWAFPKBAADdeFFxlXA240.png * @return * @throws UnsupportedEncodingException * @throws NoSuchAlgorithmException * @throws MyException */ public static String getSourceUrl(String groupName,String fileName) throws UnsupportedEncodingException, NoSuchAlgorithmException, MyException { //时间戳 int lts = (int)(System.currentTimeMillis() / 1000); System.out.println(ClientGlobal.getG_secret_key()); System.out.println("ip:"+FastDFSClient.getFile(groupName, fileName).getSourceIpAddr()); //ip自动获取 String ip = FastDFSClient.getFile(groupName, fileName).getSourceIpAddr(); String token = ProtoCommon.getToken(fileName, lts, ClientGlobal.getG_secret_key()); //初始化secret_key //生成防盗链的文件访问url return "http://" +ip+ ":8888/" +groupName+"/" + fileName + "?token=" + token + "&ts=" + lts; } /*** * 获取Storage组 * @param groupName * @return * @throws IOException */ public static StorageServer[] getStoreStorages(String groupName) throws IOException { //创建TrackerClient TrackerClient trackerClient = new TrackerClient(); //获取TrackerServer TrackerServer trackerServer = trackerClient.getConnection(); //获取Storage组 return trackerClient.getStoreStorages(trackerServer, groupName); } /*** * 获取Storage信息,IP和端口 * @param groupName * @param remoteFileName * @return * @throws IOException */ public static ServerInfo[] getFetchStorages(String groupName,String remoteFileName) throws IOException { TrackerClient trackerClient = new TrackerClient(); TrackerServer trackerServer = trackerClient.getConnection(); return trackerClient.getFetchStorages(trackerServer, groupName, remoteFileName); } /*** * 获取Tracker服务地址 * @return * @throws IOException */ public static String getTrackerUrl() throws IOException { return "http://"+getTrackerServer().getInetSocketAddress().getHostString()+":"+ ClientGlobal.getG_tracker_http_port()+"/"; } } 附属的文件类/** * 文件实体类 */ @Data @AllArgsConstructor @NoArgsConstructor public class FastDFSFile { //文件名字 private String name; //文件内容 private byte[] content; //文件扩展名 private String ext; //文件MD5摘要值 private String md5; //文件创建作者 private String author; //文件大小 private Integer size; public FastDFSFile(String name, byte[] content, String ext) { super(); this.name = name; this.content = content; this.ext = ext; } } 参考资料https://www.jianshu.com/p/bd077cdc2c82https://www.cnblogs.com/fwfjava/p/17674975.html
基于过滤拦截技术实现的身份认证方案
基于过滤拦截技术实现的身份认证方案系统基本流程用户发送请求过滤器过滤掉，访问需要保持登录状态的资源但是又没有携带token的请求拦截器拦截掉，访问需要具备相应权限的资源但是又没有对应权限的请求进入业务逻辑代码层级configMyFilterConfig：自定义过滤器的基础配置，对所有资源都进行过滤MyInterceptorConfig：自定义拦截器的基础配置，设置所需拦截的放行的URLcontrollerUserController：接口层entityUser：用户的实体对象。目前为了简化操作，不必新建权限表，所以在用户的实体对象里存放了list<String>集合，即用户的属性里就带着用户的所有权限信息filter：MyFilter：自定义过滤器，这块主要是判断用户请求有没有携带token，如果没有携带token，而且还访问的是需要携带token(即保持登录)的接口，那么就过滤掉用户的这次请求。但是，有的资源是属于开放资源，即不需要用户保持登录状态，因此，在这个模块，做了白名单列表，过滤器不对白名单列表进行过滤。interceptorMyInterceptor：自定义拦截器，这块主要是拦截需要权限的接口，获取用户的token，并且解析出用户权限信息，拿用户权限信息和接口所需权限进行比对，失败则拒绝，成功则让请求进行接口层。serviceUserService：接口所对应的具体业务逻辑，这块目前主要是有判断用户名密码，进而生成token，返回前端。utilJWTUtil：JWT工具类，可以用于生成JWT，解密JWT
MockMvc详解MockMvc是什么简单来说，对于web应用来讲，你需要对你的各个模块进行测试，但是通过启动项目使用postman来测试的话比较麻烦（程序启动速度慢，测试验证不方便，依赖网络环境），那么为了使我们的单元测试更加简单、高效，我们引入了MockMvcMockMvc实现了对于Http请求的模拟，能够直接使用网络的形式，转换到contoller的调用，可以使得测试速度快、不依赖网络环境，而且还提供了专门的一套验证的工具。测试的逻辑MockMvcBuilder构造MockMvc的构造器MockMvc调用perform，执行一个RequestBuilder请求，调用controller的业务处理逻辑perform返回ResultActions,返回操作的结果，通过ResultActions，提供了统一的验证方式使用StatusResultMatchers对请求结果进行验证使用ContentResultMatchers对请求的内容进行验证如何获取mockMvc实例两个获取mockMvc实例的方式：StandaloneMockMvcBuilderDefaultMockMvcBuilder具体实现方式：MockMvcBuilders.webAppContextSetup(WebApplicationContext context)指定WebApplicationContext，将会从该上下文获取相应的控制器并得到相应的MockMvcMockMvcBuilders.standaloneSetup(Object... controllers)通过参数指定一组控制器，这样就不需要从上下文获取了比如this.mockMvc = MockMvcBuilders.standaloneSetup(this.controller).build();## 构造请求——MockMvcRequestBuilders构造请求示例：MockMvcRequestBuilders.get("/user")MockMvcRequestBuilders主要APIMockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables)：根据uri模板和uri变量值得到一个GET请求方式的MockHttpServletRequestBuilder；如get(/user/{id}, 1L)； MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables)：同get类似，但是是POST方法； MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables)：同get类似，但是是PUT方法； MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables) ：同get类似，但是是DELETE方法； MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables)：同get类似，但是是OPTIONS方法； MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables)： 提供自己的Http请求方法及uri模板和uri变量，如上API都是委托给这个API； MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object... urlVariables)：提供文件上传方式的请求，得到MockMultipartHttpServletRequestBuilder； RequestBuilder asyncDispatch(final MvcResult mvcResult)：创建一个从启动异步处理的请求的MvcResult进行异步分派的RequestBuilder； 指定请求参数——MockHttpServletRequestBuilder当然，我们不仅可以设置它的请求方式，同样也可以设置它的请求参数MockHttpServletRequestBuilder主要APIMockHttpServletRequestBuilder header(String name, Object... values)/MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders)：添加头信息； MockHttpServletRequestBuilder contentType(MediaType mediaType)：指定请求的contentType头信息； MockHttpServletRequestBuilder accept(MediaType... mediaTypes)/MockHttpServletRequestBuilder accept(String... mediaTypes)：指定请求的Accept头信息； MockHttpServletRequestBuilder content(byte[] content)/MockHttpServletRequestBuilder content(String content)：指定请求Body体内容； MockHttpServletRequestBuilder cookie(Cookie... cookies)：指定请求的Cookie； MockHttpServletRequestBuilder locale(Locale locale)：指定请求的Locale； MockHttpServletRequestBuilder characterEncoding(String encoding)：指定请求字符编码； MockHttpServletRequestBuilder requestAttr(String name, Object value) ：设置请求属性数据； MockHttpServletRequestBuilder sessionAttr(String name, Object value)/MockHttpServletRequestBuilder sessionAttrs(Map<string, object=""> sessionAttributes)：设置请求session属性数据； MockHttpServletRequestBuilder flashAttr(String name, Object value)/MockHttpServletRequestBuilder flashAttrs(Map<string, object=""> flashAttributes)：指定请求的flash信息，比如重定向后的属性信息； MockHttpServletRequestBuilder session(MockHttpSession session) ：指定请求的Session； MockHttpServletRequestBuilder principal(Principal principal) ：指定请求的Principal； MockHttpServletRequestBuilder contextPath(String contextPath) ：指定请求的上下文路径，必须以“/”开头，且不能以“/”结尾； MockHttpServletRequestBuilder pathInfo(String pathInfo) ：请求的路径信息，必须以“/”开头； MockHttpServletRequestBuilder secure(boolean secure)：请求是否使用安全通道； MockHttpServletRequestBuilder with(RequestPostProcessor postProcessor)：请求的后处理器，用于自定义一些请求处理的扩展点；ResultActions在请求体全部构建完成之后，我们就要针对响应的内容做断言，来验证我们测试的有效性通过调用MockMvc.perform()之后会返回一个ResultActions对象，通过ResultActions我们可以做以下三件事：ResultActions andExpect(ResultMatcher matcher) ：添加验证断言来判断执行请求后的结果是否是预期的；ResultActions andDo(ResultHandler handler) ：添加结果处理器，用于对验证成功后执行的动作，如输出下请求/结果信息用于调试；MvcResult andReturn() ：返回验证成功后的MvcResult；用于自定义验证/下一步的异步处理；
代码层级梳理代码层级annotation层ExcelVoAttribut主要用于写@ExcelVoAttribute注解，可以更灵活的配置Excel数据表导出的一些基本配置LogDirection主要用于写@LogDirection注解，注解里包含一个字段（接口描述），通过接口添加这个注解，填写上接口的功能，当用户在访问接口时候，可以利用aop技术，把用户的操作记到日志里aspect层LogDrectionAspect主要定义了aop切点的位置和增强的方法/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Aspect @Component @Slf4j public class LogDirectionAspect { @Pointcut("@annotation(logDirection)") public void doLogDirection(LogDirection logDirection){ } @Around(value = "doLogDirection(logDirection)",argNames = "pjp,logDirection") public Object doBefore(ProceedingJoinPoint pjp,LogDirection logDirection) throws Throwable{ if (!"".equals(logDirection.direction())) { log.info(logDirection.direction()+"接口被调用"); return pjp.proceed(); } return "接口信息不规范"; } }@Pointcut定义切点，此处定义切点为使用@LogDirection的方法@Around定义增强的方法，通过around的参数ProceedingJoinPoint可以很好的控制程序能不能继续执行下一步，在权限判定发挥着比较重要的作用拓展：利用下面的这种方法，需要在xml文件中配置bean实例，当然也可以在方法中通过手动扫包的方法获得bean实例进行操作/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Aspect public class MyAdvice { /** * 配置切点 */ @Pointcut("execution(* com.soft1851.spring.web.dao..*.insert*(..))") public void pointCut() { } /** * 配置前置增强 */ @Before("MyAdvice.pointCut()") public void beforeMethod() { System.out.println("等待数据插入"); } /** * 配置后置增强 */ @AfterReturning("execution(* com.soft1851.spring.web.dao..*.*(..))") public void afterMethod(){ System.out.println("关闭数据库"); } }xml配置实例 <bean name="forumDao" class="com.soft1851.spring.web.dao.impl.ForumDaoImpl"/> <bean name="myAdvice" class="com.soft1851.spring.web.interceptor.MyAdvice"/> <aop:aspectj-autoproxy/>common层ResponseResult和ResultCode这两个类可以将响应体统一进行封装，将接口的响应体规范化，使前端获取数据也更加友好贴下代码，防止以后找不到ResponseResult类package com.soft1851.music.admin.common; import lombok.Data; import java.io.Serializable; /** * @author Johnny * @Date: 2020/4/21 19:52 * @Description: */ @Data public class ResponseResult implements Serializable { private static final long serialVersionUID = -3948389268046368059L; private Integer code; private String msg; private Object data; private ResponseResult() { } public ResponseResult(Integer code, String msg) { this.code = code; this.msg = msg; } public static ResponseResult success() { ResponseResult result = new ResponseResult(); result.setResultCode(ResultCode.SUCCESS); return result; } public static ResponseResult success(Object data) { ResponseResult result = new ResponseResult(); result.setResultCode(ResultCode.SUCCESS); result.setData(data); return result; } public static ResponseResult failure(ResultCode resultCode) { ResponseResult result = new ResponseResult(); result.setResultCode(resultCode); return result; } public static ResponseResult failure(ResultCode resultCode, Object data) { ResponseResult result = new ResponseResult(); result.setResultCode(resultCode); result.setData(data); return result; } public void setResultCode(ResultCode code) { this.code = code.code(); this.msg = code.message(); } }ResultCode类（枚举）package com.soft1851.music.admin.common; /** * @author Johnny * @Date: 2020/4/21 19:52 * @Description: */ public enum ResultCode { /* 成功状态码 */ SUCCESS(1, "成功"), /* 通用错误：10001-19999 */ PARAM_IS_INVALID(10001, "参数无效"), PARAM_IS_BLANK(10002, "参数为空"), PARAM_TYPE_BIND_ERROR(10003, "参数类型错误"), PARAM_NOT_COMPLETE(10004, "参数缺失"), HTTP_METHOD_ERROR(10005, "请求方法错误"), HTTP_METHOD_NOT_ALLOWED(10006, "请求方法不允许"), HTTP_NOT_FOUND(10007, "请求地址错误"), BOUND_STATEMENT_NOT_FOUND(10008, "Mybatis未绑定"), CONNECTION_ERROR(10009, "网络连接错误"), ARITHMETIC_ERROR(100010, "计算错误"), /* 用户错误：20001-29999*/ USER_NOT_SIGN_IN(20001, "请先登录"), USER_PASSWORD_ERROR(20002, "密码错误"), USER_ACCOUNT_ERROR(20003, "账号错误"), USER_VERIFY_CODE_ERROR(20004, "验证码错误"), USER_CODE_TIMEOUT(20005, "验证码失效"), USER_ACCOUNT_FORBIDDEN(20006, "账号已被禁用"), USER_SIGN_UP_FAIL(20007, "用户注册失败"), USER_SIGN_IN_FAIL(20008, "用户登录失败"), USER_NOT_FOUND(20009, "用户不存在"), USER_NO_AUTH(20019, "用户权限不足"), USER_TOKEN_EXPIRES(200010, "Token已过期"), /* 业务错误：30001-39999 */ SMS_ERROR(30001, "短信业务出现问题"), UPLOAD_ERROR(30002, "上传文件业务出现问题"), CAPTCHA_ERROR(30003, "验证码业务出现问题"), /* 数据错误：40001-49999 */ RESULT_CODE_DATA_NONE(50001, "数据未找到"), DATA_IS_WRONG(50002, "数据有误"), DATA_ALREADY_EXISTED(50003, "数据已存在"), DATABASE_ERROR(50004, "数据库操作异常"), /* 服务器或系统错误：50001-599999 */ SERVER_ERROR(50000, "服务器错误，请稍后重试"), SYSTEM_ERROR(40001, "系统错误，请稍后重试"), /* 接口错误：60001-69999 */ INTERFACE_INNER_INVOKE_ERROR(60001, "内部系统接口调用异常"), INTERFACE_OUTER_INVOKE_ERROR(60002, "外部系统接口调用异常"), INTERFACE_FORBID_VISIT(60003, "该接口禁止访问"), INTERFACE_ADDRESS_INVALID(60004, "接口地址无效"), INTERFACE_REQUEST_TIMEOUT(60005, "接口请求超时"), INTERFACE_EXCEED_LOAD(60006, "接口负载过高"), /* 权限错误：70001-79999 */ PERMISSION_NO_ACCESS(70001,"无访问权限"); private Integer code; private String message; ResultCode(Integer code, String message) { this.code = code; this.message = message; } public Integer code() { return this.code; } public String message() { return this.message; } public static String getMessage(String name) { for (ResultCode item : ResultCode.values()) { if (item.name().equals(name)) { return item.message; } } return name; } public static Integer getCode(String name) { for (ResultCode item : ResultCode.values()) { if (item.name().equals(name)) { return item.code; } } return null; } @Override public String toString() { return this.name(); } }config层CaptchaConfig主要是为kaptcha谷歌验证码的一些基本信息进行配置，比如字体，验证码位数等等附上一些kaptha的基本配置信息Constant描述默认值kaptcha.border图片边框，合法值：yes , noyeskaptcha.border.color边框颜色，合法值： r,g,b (and optional alpha) 或者 white,black,blue.blackkaptcha.border.thickness边框厚度，合法值：>01kaptcha.image.width图片宽200kaptcha.image.height图片高50kaptcha.producer.impl图片实现类com.google.code.kaptcha.impl.DefaultKaptchakaptcha.textproducer.impl文本实现类com.google.code.kaptcha.text.impl.DefaultTextCreatorkaptcha.textproducer.char.string文本集合，验证码值从此集合中获取abcde2345678gfynmnpwxkaptcha.textproducer.char.length验证码长度5kaptcha.textproducer.font.names字体Arial, Courierkaptcha.textproducer.font.size字体大小40px.kaptcha.textproducer.font.color字体颜色，合法值： r,g,b 或者 white,black,blue.blackkaptcha.textproducer.char.space文字间隔2kaptcha.noise.impl干扰实现类com.google.code.kaptcha.impl.DefaultNoisekaptcha.noise.color干扰 颜色，合法值： r,g,b 或者 white,black,blue.blackkaptcha.obscurificator.impl图片样式： 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpycom.google.code.kaptcha.impl.WaterRipplekaptcha.background.impl背景实现类com.google.code.kaptcha.impl.DefaultBackgroundkaptcha.background.clear.from背景颜色渐变，开始颜色light greykaptcha.background.clear.to背景颜色渐变， 结束颜色whitekaptcha.word.impl文字渲染器com.google.code.kaptcha.text.impl.DefaultWordRendererkaptcha.session.keysession keyKAPTCHA_SESSION_KEYkaptcha.session.datesession dateKAPTCHA_SESSION_DATECorsConfig主要是为跨域做一些基本的配置，学习阶段后端必备代码展示，防止以后忘记/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Configuration public class CorsConfig { @Bean public FilterRegistrationBean<CorsFilter> corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); //放行所有跨域的客户端domain config.addAllowedOrigin("*"); //允许的请求方法列表 String[] requestMethods = {"GET", "POST", "PUT", "DELETE", "OPTIONS"}; List<String> allowedRequestMethods = Arrays.asList(requestMethods); config.setAllowedMethods(allowedRequestMethods); //允许的客户端请求头列表 String[] requestHeaders = {"x-requested-with", "Content-Type", "Authorization"}; List<String> allowedHeaders = Arrays.asList(requestHeaders); config.setAllowedHeaders(allowedHeaders); //允许的响应头列表 String[] responseHeaders = {"Access-Control-Expose-Headers", "Authorization"}; List<String> allowedExposedHeaders = Arrays.asList(responseHeaders); config.setExposedHeaders(allowedExposedHeaders); source.registerCorsConfiguration("/**", config); FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source)); // 这个顺序很重要,设置在最前 bean.setOrder(0); return bean; } }MybatisPlusConfig主要是为mybatisplus做一些基本配置（如果要有分页查询数据，则这个配置文件必须要有）/** * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @EnableTransactionManagement @Configuration @MapperScan("com.baomidou.cloud.service.*.mapper*") public class MybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); //设置请求的页面大于最大页操作，true调回到首页，false继续请求，默认为false paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true)); return paginationInterceptor; } }MySqlGenerator代码生成器，功能强大无比，根据数据表，一键生成各种层级，各种实体/** * @ClassName MySqlGenerator * @Description 代码生成器 * @Author Johnny * @Date 2020/4/16 * @Version 1.0 */ @Slf4j public class MySqlGenerator { public static void main(String[] args) { //全局策略配置 GlobalConfig config = new GlobalConfig(); //获得当前项目根路径d:/dev/SpringBoot/spring-boot-learning String projectPath = System.getProperty("user.dir"); config.setActiveRecord(true) //作者注释 .setAuthor("Johnny") //代码生成输出路径 .setOutputDir(projectPath + "/src/main/java") //覆盖已有文件，默认false .setFileOverride(true) //是否打开输出目录窗口。默认true .setOpen(false) //开启swagger2模式 //.setSwagger2(true) //开启ActiveRecord模式 .setActiveRecord(true) //mapper添加restMap .setBaseResultMap(true) //mapper添加Base_Column_List .setBaseColumnList(true) //时间类型对应策略，默认time_pack //.setDateType(DateType.TIME_PACK) //相关包中的接口和类名后缀 .setMapperName("%sMapper") .setServiceName("%sService") .setServiceImplName("%sServiceImpl"); //数据库表配置，通过该配置，可指定需要生成哪些表或者排除哪些表 StrategyConfig strategyConfig = new StrategyConfig(); //是否大写命名 strategyConfig.setCapitalMode(true) //是否跳过视图 .setSkipView(true) //数据库表映射到实体的命名策略为驼峰式 .setNaming(NamingStrategy.underline_to_camel) //生成表，可以写多个,如果不加参数，默认为所有表 .setInclude() .setEntityBuilderModel(true) .setEntityLombokModel(true) .setRestControllerStyle(true) .setEntityTableFieldAnnotationEnable(true); //包名配置 PackageConfig packageConfig = new PackageConfig(); //父包名 packageConfig.setParent("com.soft1851.music.admin") .setMapper("mapper") .setService("service") .setController("controller") .setXml("xml") .setEntity("entity"); //数据源配置 DataSourceConfig dataSourceConfig = new DataSourceConfig(); dataSourceConfig.setDbType(DbType.MYSQL) .setDriverName("com.mysql.jdbc.Driver") .setUrl("jdbc:mysql://rm-m5ee476bu350735gjeo.mysql.rds.aliyuncs.com:3306/cloud_music?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true") .setUsername("root") .setPassword("XuNiit_#"); AutoGenerator autoGenerator = new AutoGenerator(); autoGenerator.setGlobalConfig(config) .setStrategy(strategyConfig) .setDataSource(dataSourceConfig) .setTemplateEngine(new FreemarkerTemplateEngine()) .setPackageInfo(packageConfig); autoGenerator.execute(); log.info("=============代码生成成功================"); } }RedisConfig主要对RedisConfig缓存数据库进行一些基本配置/** * @author Johnny * @Date: 2020/4/21 20:45 * @Description: */ @Configuration @EnableCaching public class RedisConfig { @Bean CacheManager cacheManager(RedisConnectionFactory connectionFactory) { //初始化RedisCacheWrite RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig(); //设置默认过期时间为1天 defaultCacheConfig.entryTtl(Duration.ofDays(1)); //初始化RedisCacheManger return new RedisCacheManager(redisCacheWriter, defaultCacheConfig); } @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object,Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值（默认使用JDK的序列化方式） Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); //使用stringRedisSerializer来序列化和反序列化redis的value值 template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; } @Bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) { StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(); stringRedisTemplate.setConnectionFactory(factory); return stringRedisTemplate; } }WebConfig这个类中主要配置了拦截器，可以对部分页面进行统一拦截，当然这个只是统一的拦截，具体的拦截所要进行的操作，还在下文/** * @author Johnny * @Date: 2020/4/21 20:58 * @Description: */ @Configuration public class WebConfig implements WebMvcConfigurer { @Resource private LoginInterceptor loginInterceptor; @Resource private JwtInterceptor jwtInterceptor; /** * 添加拦截器配置 */ @Override public void addInterceptors(InterceptorRegistry registry) { //拦截路径可自行配置多个 可用 ，分隔开 registry.addInterceptor(loginInterceptor).addPathPatterns("/sysAdmin/login").excludePathPatterns("/**").excludePathPatterns("/static/**"); // registry.addInterceptor(jwtInterceptor).addPathPatterns("/**").excludePathPatterns("/sysAdmin/login", "/captcha","/songList/page").excludePathPatterns("/static/**"); } }controller层controller层暂时带过，后面有时间的话把controller层常用注解汇总整理下dto层目前还没有将entity、vo、dto统一放到domain里，dto里主要是用户请求接口时需要传的参数，vo主要时后端返回前端展示的参数，而entity就是我们和数据库表所吻合的类。举例：我们数据库中的用户表的字段肯定不只时账号和密码，会有一些其他字段，比如加密盐、用户基本信息等等，但是用户请求登录接口时，只需要传账号和密码就可以了，此时此刻，为了防止资源的浪费，我们就定义一个dto来接收前端传来的参数。当用户进入主界面时，查看个人信息，后端并不需要将用户id、加密盐等私密信息展示到前端，此时，同样为了节省资源，我们定义vo视图对象，将前端需要的参数放进去，再传入前端。entity层entity层主要是将数据库中的表一对一的写好，此处是直接用代码生成器生成exceptionCustomException定义全局异常处理，使用时直接throws CustomException就好/** * @author Johnny * @Date: 2020/4/22 10:48 * @Description: */ public class CustomException extends RuntimeException { protected ResultCode resultCode; public CustomException(String msg, ResultCode resultCode) { super(msg); this.resultCode = resultCode; } public ResultCode getResultCode() { return resultCode; } }GlobalException也是全局异常处理，但和上面略有区别，非手动throw，spring在遇到异常时，会自动来这个类里查找，如果已经有配置过的，就会按照配置要求输出内容/** * @author Johnny * @Date: 2020/4/30 18:45 * @Description: */ @ControllerAdvice public class GlobalException { @ExceptionHandler(ConstraintViolationException.class) ResponseResult handleConstraintViolationException(ConstraintViolationException e) { return ResponseResult.failure(ResultCode.DATA_IS_WRONG); } }JwtException同CustomException类格式一样，但这个侧重于处理Jwt模块的异常filterChannelFilter/** * @author Johnny * @Date: 2020/4/21 20:19 * @Description: */ @WebFilter(urlPatterns = "/*",filterName = "channelFilter") public class ChannelFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { ServletRequest requestWrapper = null; if (servletRequest instanceof HttpServletRequest) { requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest); } if (requestWrapper == null) { filterChain.doFilter(servletRequest,servletResponse); }else{ filterChain.doFilter(requestWrapper, servletResponse); } } @Override public void destroy() { } }LoginFilter这个类主要针对用户登录进行过滤/** * @author Johnny * @Date: 2020/4/26 10:46 * @Description: */ @Slf4j @WebFilter(urlPatterns = "/login",filterName = "LoginFilter") public class LoginFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { ServletRequest requestWrapper = null; if (servletRequest instanceof HttpServletRequest) { String url = ((HttpServletRequest) servletRequest).getRequestURI(); //判断接口是否为导入接口 if ("/resource/guide".equals(url)) { Part file = ((HttpServletRequest) servletRequest).getPart("file"); log.info("文件名：" + file); } requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest); } if (requestWrapper == null) { filterChain.doFilter(servletRequest, servletResponse); }else{ filterChain.doFilter(requestWrapper, servletResponse); } } @Override public void destroy() { } }handle层GlobalExceptionHandle定义全局异常/** * @author Johnny * @Date: 2020/4/22 10:40 * @Description: */ @RestControllerAdvice(annotations = {RestController.class, Controller.class}) @Slf4j public class GlobalExceptionHandle { //自定义异常 /** * JwtException * @param exception * @return */ @ExceptionHandler(value = {JwtException.class}) @ResponseBody public ResponseResult sendError(JwtException exception) { log.error(exception.getMessage()); return ResponseResult.failure(exception.getResultCode()); } /** * CustomException * @param exception * @return */ @ExceptionHandler(value = {CustomException.class}) @ResponseBody public ResponseResult sendError(CustomException exception) { log.error(exception.getMessage()); return ResponseResult.failure(exception.getResultCode()); } //系统级异常 /** * InvalidClassException * @param exception * @return */ @ExceptionHandler(value = {InvalidClassException.class}) @ResponseBody public ResponseResult sendError(InvalidClaimException exception) { log.error(exception.getMessage()); //返回token已经过期 return ResponseResult.failure(ResultCode.USER_TOKEN_EXPIRES); } /** * NullPointerException * @param exception * @return */ @ExceptionHandler(value = {NullPointerException.class}) @ResponseBody public ResponseResult sendError(NullPointerException exception) { log.error(exception.getMessage()); //空指针提示数据未找到 return ResponseResult.failure(ResultCode.RESULT_CODE_DATA_NONE); } /** * IOException * @param exception * @return */ @ExceptionHandler(value = {IOException.class}) @ResponseBody public ResponseResult sendError(IOException exception) { log.error(exception.getMessage()); //io异常提示验证码出现问题 return ResponseResult.failure(ResultCode.CAPTCHA_ERROR); }GlobalResponseHandle处理全局的响应头（当接口返回类型非ResponseResult类型时，可以自动转换）/** * @author Johnny * @Date: 2020/4/22 10:55 * @Description: */ @ControllerAdvice public class GlobalResponseHandle implements ResponseBodyAdvice { /** * 处理响应的具体方法 * @param returnType * @param converterType * @return */ @Override public boolean supports(MethodParameter returnType, Class converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof ResponseResult) { return body; }else{ return ResponseResult.success(body); } } }interceptorJwtInterceptor/** * @author Johnny * @Date: 2020/4/23 19:41 * @Description: */ @Slf4j @Component public class JwtInterceptor implements HandlerInterceptor { @Resource SysRoleService roleService; @Resource private RedisService redisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); log.info("token———————————"+token); //认证 if (token == null) { log.info("用户未登录"); return false; }else{ //登录之后 //从请求头当中取出用户id String adminId = request.getHeader("id"); log.info("用户id" + adminId); //在redis中检查是否存在adminId为key的数据 if (!redisService.existsKey(adminId)) { log.info("redis中不存在此键"); }else{ String secrect = redisService.getValue(adminId, String.class); //从token中解析出roles字符串 String roles = JwtTokenUtil.getRoles(token,secrect); //反序列化成List List<SysRole> sysRoles = JSONArray.parseArray(roles, SysRole.class); //从request中取得客户端传输的roleId String roleId = request.getParameter("roleId"); log.info("roleId:"+roleId); boolean flag = roleService.checkRole(sysRoles,Integer.parseInt(roleId)); if(flag){ return true; }else{ log.info("用户不具备此角色"); } } return false; } } }LoginInterceptor/** * @author Johnny * @Date: 2020/4/21 21:00 * @Description: */ @Slf4j @Component public class LoginInterceptor implements HandlerInterceptor { @Resource private RedisService redisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handle) { //将request包装成HttpServletWrapper类型 RequestWrapper requestWrapper = new RequestWrapper(request); //取得请求的json对象 String body = requestWrapper.getBody(); log.info(body); //从redis中取得指定用户名的验证码 JSONObject jsonObject = JSONObject.parseObject(body); String name = jsonObject.getString("name"); System.out.println(name); String password = jsonObject.getString("password"); System.out.println(password); String verifyCode = jsonObject.getString("verifyCode"); System.out.println(verifyCode); LoginDto loginDto = LoginDto.builder().name(name).password(password).verifyCode(verifyCode).build(); return true; // if (redisService.existsKey(loginDto.getName())) { // //获取redis中的验证码 // String correctCode = redisService.getValue(name, String.class); // //忽略大小写，成功则放行到controller调用登录接口 // if (correctCode.equalsIgnoreCase(verifyCode)) { // return true; // }else{ // log.info("验证码错误"); // return false; // } // }else{ // log.info("验证码失效"); // return false; // } } }mapper层这个目前不做过多赘述，我会尽快整理下mybatisplus的常用语法发出来util层ExcelConsumer/** * @author Johnny * @Date: 2020/4/28 21:53 * @Description: */ @Slf4j public class ExcelConsumer<T> implements Runnable { /** * 一张工作表可以容纳的最大行数 */ private static Integer SHEET_SIZE = 100000; /** * 导出的Vo数据类型 */ private Class<T> clazz; /** * 工作簿 */ private SXSSFWorkbook wb; /** * 工作表名称 */ private String sheetName; /** * 数据缓冲区对象 */ private ExportDataAdapter<T> exportDataAdapter; /** * 线程同步 */ private CountDownLatch latch; /** * 构造方法 * * @param clazz */ public ExcelConsumer(Class<T> clazz, ExportDataAdapter<T> exportDataAdapter, SXSSFWorkbook wb, CountDownLatch latch, String sheetName) { if (clazz == null || wb == null || exportDataAdapter == null || latch == null) { log.error("ExcelConsumer::初始化对象参数不能为空"); return; } this.clazz = clazz; this.exportDataAdapter = exportDataAdapter; this.wb = wb; this.latch = latch; this.sheetName = sheetName == null ? "UnNamedSheet" : sheetName; } @Override public void run() { //初始化excel导出工具类 ExcelUtil<T> excelUtil = new ExcelUtil<>(this.clazz); Sheet sheet = null; int sheetNo = 0; int rowNum = 1; T vo; //生产者还在生产数据 while (latch.getCount() > 1) { //生成sheetName if (rowNum == 1) { sheetNo++; sheet = excelUtil.createSheet(wb, sheetName.concat(Integer.toString(sheetNo))); excelUtil.setColumnTitle(sheet); } //获取数据 vo = exportDataAdapter.getData(); //往excel添加一行数据 excelUtil.addRowData(vo, wb, sheet, rowNum); rowNum++; //准备生成下一个sheetName if (rowNum == SHEET_SIZE + 1) { rowNum = 1; } } //生产者不再生产数据，取剩余数据，并将数据写入excel Integer reminderDataSize = exportDataAdapter.getDataSize(); T reminderData; if (reminderDataSize > 0) { for (int i = 0; i < reminderDataSize; i++) { reminderData = exportDataAdapter.getData(); if (rowNum == 1) { sheetNo++; sheet = excelUtil.createSheet(wb, sheetName.concat(Integer.toString(sheetNo))); excelUtil.setColumnTitle(sheet); } excelUtil.addRowData(reminderData, wb, sheet, rowNum); rowNum++; if (rowNum == SHEET_SIZE + 1) { rowNum = 1; } } } log.info("数据导出完成"); latch.countDown(); } }ExcelUtil/** * @author Johnny * @Date: 2020/4/28 21:47 * @Description: */ public class ExcelUtil<T> { Class<T> clazz; /** * 表头字段列表 */ private List<Field> fields; /** * 数字单元格对象 */ private CellStyle decimalCellStyle = null; /** * 日期时间单元格对象 */ private CellStyle dateTimeCellStyle = null; /** * 构造方法 * * @param clazz */ public ExcelUtil(Class<T> clazz) { this.clazz = clazz; } /** * 添加一条数据 * * @param vo:需要导出的vo对象 * @param wb：工作簿 * @param sheet：工作表 * @param rowNum：当前行号 */ public void addRowData(T vo, SXSSFWorkbook wb, Sheet sheet, int rowNum) { //创建一行 Row row = sheet.createRow(rowNum); Field field; Cell cell; ExcelVoAttribute attr; int fieldSize = fields.size(); // 遍历入参对象的所有属性 for (int j = 0; j < fieldSize; j++) { // 通过反射获得需要导出的入参对象的所有属性 field = fields.get(j); // 设置实体类私有属性可访问 field.setAccessible(true); // 获取所有添加了注解的属性 attr = field.getAnnotation(ExcelVoAttribute.class); // 给每个属性创建一个单元格 cell = row.createCell(j); try { this.setCellValue(attr, field.get(vo), wb, cell); } catch (IllegalAccessException e) { e.printStackTrace(); } } } /** * 根据注解类判断字段类型设置excel单元格数据格式方法 * * @param attr * @param valueObject * @param workbook * @param cell */ private void setCellValue(ExcelVoAttribute attr, Object valueObject, SXSSFWorkbook workbook, Cell cell) { String returnValue; if (attr.isNumber()) { cell.setCellStyle(getCellStyle(attr, workbook)); returnValue = valueObject == null || "".equals(valueObject) ? "0" : valueObject.toString(); BigDecimal num = new BigDecimal(returnValue); cell.setCellValue(num.doubleValue()); } else if (attr.isDateTime()) { cell.setCellStyle(getCellStyle(attr, workbook)); DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); returnValue = df.format((TemporalAccessor) valueObject); cell.setCellValue(returnValue); } else { returnValue = valueObject == null ? "" : valueObject.toString(); cell.setCellValue(returnValue); } } /** * 根据注解类判断字段类型，返回excel单元格数据格式方法 * * @param attr * @param workbook * @return CellStyle */ private CellStyle getCellStyle(ExcelVoAttribute attr, SXSSFWorkbook workbook) { if (attr.isNumber()) { if (decimalCellStyle == null) { decimalCellStyle = workbook.createCellStyle(); //此处设置数字单元格格式 DataFormat df = workbook.createDataFormat(); //千分位,保留1位小数 decimalCellStyle.setDataFormat(df.getFormat("#,##0.0")); } return decimalCellStyle; } if (attr.isDateTime()) { if (dateTimeCellStyle == null) { dateTimeCellStyle = workbook.createCellStyle(); //此处设置日期时间单元格格式 DataFormat df = workbook.createDataFormat(); dateTimeCellStyle.setDataFormat(df.getFormat("yyyy-MM-dd HH:mm:ss")); } return dateTimeCellStyle; } return null; } /** * 创建工作页Sheet * * @param wb * @param sheetName * @return Sheet */ public Sheet createSheet(SXSSFWorkbook wb, String sheetName) { return wb.createSheet(sheetName); } /** * 设置excel列头及格式 * * @param sheet */ public void setColumnTitle(Sheet sheet) { if (fields == null) { this.fields = this.getSortFields(); } Row row; Cell cell; ExcelVoAttribute attr; Field field; int fieldSize = fields.size(); row = sheet.createRow(0); for (int i = 0; i < fieldSize; i++) { field = fields.get(i); attr = field.getAnnotation(ExcelVoAttribute.class); cell = CellUtil.createCell(row, i, attr.name()); // 设置列宽，根据相应的字段名的长度等比 sheet.setColumnWidth(i, attr.name().getBytes().length * 400); } } /** * 获取输出对象字段列表，并根据注解进行字段排序 * * @return */ private List<Field> getSortFields() { List<Field> fields = Arrays.stream(clazz.getDeclaredFields()).filter(x -> x.isAnnotationPresent(ExcelVoAttribute.class)).collect(Collectors.toList()); List<Field> sortList = new ArrayList<>(fields); //排序 for (Field field : fields) { ExcelVoAttribute excelVoAttribute = field.getAnnotation(ExcelVoAttribute.class); int sortNo = excelVoAttribute.column(); sortList.set(sortNo, field); } return sortList; } }ExcelUtils/** * @author Johnny * @Date: 2020/4/25 22:56 * @Description: */ @Slf4j public class ExcelUtils { /** * 导出歌曲 * @param response * @param list * @param map * @param title */ public static void exportExcel(HttpServletResponse response, List list, Map<String, String> map, String title) { //通过工具类创建writer ExcelWriter writer = ExcelUtil.getWriter(true); //自定义标题别名 Set<Map.Entry<String, String>> entries = map.entrySet(); //迭代器遍历数据 Iterator<Map.Entry<String, String>> iterator = entries.iterator(); while (iterator.hasNext()) { Map.Entry<String, String> next = iterator.next(); //自定义表头 writer.addHeaderAlias(next.getKey(), next.getValue()); } //合并单元格后的标题行，使用默认标题样式 writer.merge(map.size() - 1, title); //一次性写出内容，使用默认样式，强制输出标题 writer.write(list, true); //out为outputStream，需要写出到的目标流 try { writer.flush(response.getOutputStream(), true); } catch (IOException e) { log.info("歌单导出异常"); e.printStackTrace(); } writer.close(); } /** * 导入歌曲 * @param file * @return */ public static List<Song> importExcel(File file) { List<Song> songs = new ArrayList<>(); InputStream inputStream = null; try { inputStream = new FileInputStream(file); } catch (FileNotFoundException e) { e.printStackTrace(); } //通过getReader方法确定读取那个sheet中的数据 ExcelReader reader = ExcelUtil.getReader(inputStream, "sheet1"); //回去总行数 List<List<Object>> read = reader.read(1, reader.getRowCount()); for (List<Object> objects : read) { //对每行数据取出构建一个song对象 Song song = Song.builder() .songName(objects.get(0).toString()) .songId(UUID.randomUUID().toString().replace("-", "")) .sortId("0") .singer(objects.get(1).toString()) .duration(objects.get(2).toString()) .thumbnail(objects.get(3).toString()) .url(objects.get(4).toString()) .lyric(objects.get(5).toString()) .commentCount(0) .playCount(0) .deleteFlag("0") .updateTime(LocalDateTime.now()) .createTime(LocalDateTime.now()) .build(); songs.add(song); } return songs; } }ExportDataAdapter/** * @author Johnny * @Date: 2020/4/28 21:53 * @Description: */ public class ExportDataAdapter<T>{ /** * 默认队列大小 */ private static Integer DEFAULT_SIZE = 1000; private BlockingQueue<T> resourceQueue = null; public ExportDataAdapter() { this.resourceQueue = new LinkedBlockingQueue<T>(DEFAULT_SIZE); } /** * 添加数据 * * @param data */ public void addData(T data) { try { resourceQueue.put(data); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 获取剩余数据数量 * * @return */ public Integer getDataSize() { return resourceQueue.size(); } /** * 从队列中获取数据 * * @return */ public T getData() { try { return resourceQueue.take(); } catch (InterruptedException e) { e.printStackTrace(); } return null; } }FileUtil/** * @author Johnny * @Date: 2020/4/26 10:41 * @Description: */ public class FileUtil { public static File fileConversion(MultipartFile file) { int n; File file1 = new File(file.getOriginalFilename()); try { InputStream in = file.getInputStream(); OutputStream os = new FileOutputStream(file1); byte[] bytes = new byte[4096]; while ((n=in.read(bytes,0,4096))!=-1){ os.write(bytes,0,n); File file2 = new File(file1.toURI()); file2.delete(); } } catch (IOException e) { e.printStackTrace(); } return file1; } }JwtTokenUtil/** * @Author Johnny * @Date 2020/4/15 * @Version 1.0 */ @Slf4j public class JwtTokenUtil { /** * 加密 * * @param userId * @param expiresAt * @return String */ public static String getToken(final String userId, String userRole,final String secrect,Date expiresAt) { String token = null; try { token = JWT.create() .withIssuer("auth0") .withClaim("userId", userId) .withClaim("userRole", userRole) .withExpiresAt(expiresAt) // 使用了HMAC256加密算法, mySecret是用来加密数字签名的密钥 .sign(Algorithm.HMAC256(secrect)); } catch (UnsupportedEncodingException e) { log.error("不支持的编码格式"); } return token; } /** * 解密 * @param token * @return DecodedJWT */ public static DecodedJWT deToken(final String token,final String secrect) { DecodedJWT jwt; JWTVerifier verifier = null; try { verifier = JWT.require(Algorithm.HMAC256(secrect)) .withIssuer("auth0") .build(); } catch (UnsupportedEncodingException e) { log.error("不支持的编码格式"); } assert verifier != null; jwt = verifier.verify(token); return jwt; } /** * 获取userId * * @param token * @return String */ public static String getUserId(String token,String secrect) { return deToken(token,secrect).getClaim("userId").asString(); } /** * 获取role * * @param token * @return String */ public static String getRoles(String token,String secrect) { return deToken(token,secrect).getClaim("userRole").asString(); } /** * 验证是否过期 * * @param token * @return boolean */ public static boolean isExpiration(String token,String secrect) { return deToken(token,secrect).getExpiresAt().before(new Date()); } public static void main(String[] args) { // String token = getToken("2000100193", new Date(System.currentTimeMillis() + 10L * 1000L)); // System.out.println(token); // while (true) { // boolean flag = isExpiration(token); // System.out.println(flag); // if (flag) { // System.out.println("token已失效"); // break; // } // try { // Thread.sleep(1000); // } catch (InterruptedException e) { // e.printStackTrace(); // } // } // String token = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJhdXRoMCIsImV4cCI6MTU4NzY3MTQ3OCwidXNlcklkIjoiREUzNUQ3Q0MwNUFGOTZBMjFEN0FERkM4NjUxRTY2MTQifQ.uT2W3QcE3744WNN3inEKT8lUVJs6xAC7TodDCaWkcyM"; // System.out.println(deToken(token).getClaim("userId").asString()); } }Md5Util/** * @author Johnny */ public class Md5Util { /** * @param pwd 需要加密的字符串 * @param isUpper 字母大小写(false为默认小写，true为大写) * @param bit 加密的位数（16,32,64） * @return String */ public static String getMd5(String pwd, boolean isUpper, Integer bit) { String md5 = ""; try { // 创建加密对象 MessageDigest md = MessageDigest.getInstance("md5"); if (bit == 64) { Base64.Encoder encoder = Base64.getEncoder(); md5 = encoder.encodeToString(md.digest(pwd.getBytes(StandardCharsets.UTF_8))); } else { // 计算MD5函数 md.update(pwd.getBytes()); byte b[] = md.digest(); int i; StringBuilder sb = new StringBuilder(); for (byte value : b) { i = value; if (i < 0) { i += 256; } if (i < 16) { sb.append("0"); } sb.append(Integer.toHexString(i)); } md5 = sb.toString(); if (bit == 16) { //截取32位md5为16位 md5 = md5.substring(8, 24).toString(); if (isUpper) { md5 = md5.toUpperCase(); } return md5; } } //转换成大写 if (isUpper) { md5 = md5.toUpperCase(); } } catch (Exception e) { e.printStackTrace(); System.out.println("md5加密抛出异常！"); } return md5; } public static void main(String[] args) { String a = "123456"; String md5a = getMd5(a, true, 32); System.out.println(md5a); System.out.println(md5a.length()); }ThreadPool/** * @author Johnny * @Date: 2020/4/28 21:55 * @Description: */ public class ThreadPool { /** * 异步线程 */ private final static ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 100, 10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(2000), r -> new Thread(r, "excelExportThread"), new ThreadPoolExecutor.AbortPolicy()); public static ThreadPoolExecutor getExecutor() { return executor; } }TreeBuilder/** * @author Johnny * @Date: 2020/4/23 18:46 * @Description: */ public class TreeBuilder { /** * 两层循环实现建树 * * @param treeNodes 传入的树节点列表 * @return */ public static List<TreeNode> buildTreeByLoop(List<TreeNode> treeNodes) { List<TreeNode> trees = new ArrayList<>(); for (TreeNode treeNode : treeNodes) { if (treeNode.getParentId() == 0) { trees.add(treeNode); } for (TreeNode it : treeNodes) { if (it.getParentId().equals(treeNode.getId())) { if (treeNode.getSubMenus() == null) { treeNode.setSubMenus(new ArrayList<>()); } treeNode.getSubMenus().add(it); } } } return trees; } /** * 使用递归方法建树 * * @param treeNodes * @return */ public static List<TreeNode> buildTreeByRecursive(List<TreeNode> treeNodes) { List<TreeNode> trees = new ArrayList<>(); for (TreeNode treeNode : treeNodes) { if (treeNode.getParentId() == 0) { trees.add(findChildren(treeNode, treeNodes)); } } return trees; } /** * 递归查找子节点 * * @param treeNodes * @return */ public static TreeNode findChildren(TreeNode treeNode, List<TreeNode> treeNodes) { for (TreeNode it : treeNodes) { if (treeNode.getId().equals(it.getParentId())) { if (treeNode.getSubMenus() == null) { treeNode.setSubMenus(new ArrayList<>()); } treeNode.getSubMenus().add(findChildren(it, treeNodes)); } } return treeNode; } }TreeNode/** * @author Johnny * @Date: 2020/4/23 18:46 * @Description: */ @Data @NoArgsConstructor @AllArgsConstructor @Builder public class TreeNode { private Integer id; private Integer parentId; private Integer type; private String title; private String icon; private String path; private Integer sort; private List<TreeNode> subMenus; public TreeNode(Integer id, Integer parentId, Integer type,String title, String icon, String path, Integer sort) { this.id = id; this.parentId = parentId; this.type = type; this.title = title; this.icon = icon; this.path = path; this.sort = sort; } }
Aop和MockMvc学习Java中的四个元注解@Document@Documented注解标记的元素，Javadoc工具会将此注解标记元素的注解信息包含在javadoc中.默认，注解信息不会包含在Javadoc中@Inherited说明子类可以继承父类中的该注解、示例package org.springmorning.demo.javabase.annotation.meta; import java.lang.annotation.Documented; import java.lang.annotation.Inherited; @Documented @Inherited public @interface Book { String name(); String publishedDate(); String author(); }package org.springmorning.demo.javabase.annotation.meta; @Book( name = "Spring in Action", author = "Johnny", publishedDate = "2020-10-1" ) public class DocumentAnnotation {}@Retention注解的保留位置@Retention(RetentionPolicy.SOURCE) //注解仅存在于源码中，在class字节码文件中不包含@Retention(RetentionPolicy.CLASS) // 默认的保留策略，注解会在class字节码文件中存在，但运行时无法获得，@Retention(RetentionPolicy.RUNTIME) // 注解会在class字节码文件中存在，在运行时可以通过反射获取到@Target注解的作用目标@Target(ElementType.TYPE) //接口、类、枚举、注解@Target(ElementType.FIELD) //字段、枚举的常量@Target(ElementType.METHOD) //方法@Target(ElementType.PARAMETER) //方法参数@Target(ElementType.CONSTRUCTOR) //构造函数@Target(ElementType.LOCAL_VARIABLE)//局部变量@Target(ElementType.ANNOTATION_TYPE)//注解@Target(ElementType.PACKAGE) ///包mock示例controller层接口示例@RequestMapping(value = "home") public String home(){ RequestAttributes at = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes sra = (ServletRequestAttributes) at; HttpServletRequest request = sra.getRequest(); Cookie[] cookies = request.getCookies(); String name = ""; String device = request.getHeader("platform"); for (Cookie cookie : cookies) { name = cookie.getValue(); } if(name==null || device==null){ return "I don't know who you are"; } return "Welcome to "+name+" from "+device; }测试方法package com.soft1851.boot.aop.controller; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import javax.servlet.http.Cookie; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @WebAppConfiguration class HelloControllerTest { //用于模拟嗲用Controller的接口发送请求 private MockMvc mockMvc; //预加载内容，用来初始化对HelloController的模拟 @BeforeEach public void setUp() throws Exception { mockMvc = MockMvcBuilders.standaloneSetup(new HelloController()).build(); } @Test public void hello() throws Exception{ Cookie cookie = new Cookie("username","tom"); mockMvc.perform(MockMvcRequestBuilders.get("/api/home") .header("User-Agent","Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Mobile Safari/537.36") .header("platform","DesktopWeb") .contentType("application/x-protobuf") .accept("*/*") .cookie(cookie)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string("Welcome to tom from DesktopWeb")) .andDo(MockMvcResultHandlers.print()); } }测试流程准备测试环境通过MockMvc执行请求添加验证断言添加结果处理器得到MvcResult进行自定义断言/进行下一步的异步请求卸载测试环境MockMvcRequestBuilders常用apiMockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables)：根据uri模板和uri变量值得到一个GET请求方式的MockHttpServletRequestBuilder；如get(/user/{id}, 1L)； MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables)：同get类似，但是是POST方法； MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables)：同get类似，但是是PUT方法； MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables) ：同get类似，但是是DELETE方法； MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables)：同get类似，但是是OPTIONS方法； MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables)： 提供自己的Http请求方法及uri模板和uri变量，如上API都是委托给这个API； MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object... urlVariables)：提供文件上传方式的请求，得到MockMultipartHttpServletRequestBuilder； RequestBuilder asyncDispatch(final MvcResult mvcResult)：创建一个从启动异步处理的请求的MvcResult进行异步分派的RequestBuilder；MockHttpServletRequestBuilder常用apiMockHttpServletRequestBuilder header(String name, Object... values)/MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders)：添加头信息； MockHttpServletRequestBuilder contentType(MediaType mediaType)：指定请求的contentType头信息； MockHttpServletRequestBuilder accept(MediaType... mediaTypes)/MockHttpServletRequestBuilder accept(String... mediaTypes)：指定请求的Accept头信息； MockHttpServletRequestBuilder content(byte[] content)/MockHttpServletRequestBuilder content(String content)：指定请求Body体内容； MockHttpServletRequestBuilder param(String name,String... values)：请求传入参数 MockHttpServletRequestBuilder cookie(Cookie... cookies)：指定请求的Cookie； MockHttpServletRequestBuilder locale(Locale locale)：指定请求的Locale； MockHttpServletRequestBuilder characterEncoding(String encoding)：指定请求字符编码； MockHttpServletRequestBuilder requestAttr(String name, Object value) ：设置请求属性数据； MockHttpServletRequestBuilder sessionAttr(String name, Object value)/MockHttpServletRequestBuilder sessionAttrs(Map<string, object=""> sessionAttributes)：设置请求session属性数据； MockHttpServletRequestBuilder flashAttr(String name, Object value)/MockHttpServletRequestBuilder flashAttrs(Map<string, object=""> flashAttributes)：指定请求的flash信息，比如重定向后的属性信息； MockHttpServletRequestBuilder session(MockHttpSession session) ：指定请求的Session； MockHttpServletRequestBuilder principal(Principal principal) ：指定请求的Principal； MockHttpServletRequestBuilder contextPath(String contextPath) ：指定请求的上下文路径，必须以“/”开头，且不能以“/”结尾； MockHttpServletRequestBuilder pathInfo(String pathInfo) ：请求的路径信息，必须以“/”开头； MockHttpServletRequestBuilder secure(boolean secure)：请求是否使用安全通道； MockHttpServletRequestBuilder with(RequestPostProcessor postProcessor)：请求的后处理器，用于自定义一些请求处理的扩展点；MockMultipartHttpServletRequestBuilder常用apiMockMultipartHttpServletRequestBuilder继承自MockHttpServletRequestBuilderMockMultipartHttpServletRequestBuilder file(String name, byte[] content)/MockMultipartHttpServletRequestBuilder file(MockMultipartFile file)：指定要上传的文件；ResultActions常用api调用MockMvc.perform(RequestBuilder requestBuilder)后将得到ResultActions，通过ResultActions完成如下三件事ResultActions andExpect(ResultMatcher matcher) ：添加验证断言来判断执行请求后的结果是否是预期的； ResultActions andDo(ResultHandler handler) ：添加结果处理器，用于对验证成功后执行的动作，如输出下请求/结果信息用于调试； MvcResult andReturn() ：返回验证成功后的MvcResult；用于自定义验证/下一步的异步处理；ResultMatcher/MockMvcResultMatchers常用apiResultMatcher用来匹配执行完请求后的结果验证，其就一个match(MvcResult result)断言方法，如果匹配失败将抛出相应的异常HandlerResultMatchers handler()：请求的Handler验证器，比如验证处理器类型/方法名；此处的Handler其实就是处理请求的控制器； RequestResultMatchers request()：得到RequestResultMatchers验证器； ModelResultMatchers model()：得到模型验证器； ViewResultMatchers view()：得到视图验证器； FlashAttributeResultMatchers flash()：得到Flash属性验证； StatusResultMatchers status()：得到响应状态验证器； HeaderResultMatchers header()：得到响应Header验证器； CookieResultMatchers cookie()：得到响应Cookie验证器； ContentResultMatchers content()：得到响应内容验证器； JsonPathResultMatchers jsonPath(String expression, Object ... args)/ResultMatcher jsonPath(String expression, Matcher matcher)：得到Json表达式验证器； XpathResultMatchers xpath(String expression, Object... args)/XpathResultMatchers xpath(String expression, Map<string, string=""> namespaces, Object... args)：得到Xpath表达式验证器； ResultMatcher forwardedUrl(final String expectedUrl)：验证处理完请求后转发的url（绝对匹配）； ResultMatcher forwardedUrlPattern(final String urlPattern)：验证处理完请求后转发的url（Ant风格模式匹配，@since spring4）； ResultMatcher redirectedUrl(final String expectedUrl)：验证处理完请求后重定向的url（绝对匹配）； ResultMatcher redirectedUrlPattern(final String expectedUrl)：验证处理完请求后重定向的url（Ant风格模式匹配，@since spring4）