SpringBoot切面實現(xiàn)token權(quán)限校驗詳解
SpringBoot實現(xiàn)token權(quán)限
數(shù)據(jù)表
要實現(xiàn)權(quán)限校驗,首先數(shù)據(jù)表和實體類上需要有權(quán)限字段,我的表中permission和gender是通過外鍵約束permission表和gender表實現(xiàn)枚舉的,因為可拓展性更好
/* Navicat Premium Data Transfer Source Server : Yan Source Server Type : MySQL Source Server Version : 80027 Source Host : localhost:3306 Source Schema : coc Target Server Type : MySQL Target Server Version : 80027 File Encoding : 65001 Date: 07/05/2023 20:00:45 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主鍵', `account` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '賬號', `password` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密碼', `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '姓名', `gender` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '未知' COMMENT '性別', `telephone` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手機號', `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '郵箱', `signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '無簽名' COMMENT '簽名', `avatar_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'C:\\Users\\Yan\\Desktop\\user\\avatar\\avatar.jpg' COMMENT '頭像地址', `permission` int NULL DEFAULT 20 COMMENT '權(quán)限', `banned` bit(1) NULL DEFAULT b'0' COMMENT '是否被封禁', `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '無備注' COMMENT '備注', `create_time` datetime NOT NULL COMMENT '創(chuàng)建時間', PRIMARY KEY (`id`) USING BTREE, INDEX `f_gender`(`gender` ASC) USING BTREE, INDEX `f_permission`(`permission` ASC) USING BTREE, CONSTRAINT `f_gender` FOREIGN KEY (`gender`) REFERENCES `gender` (`gender_name`) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT `f_permission` FOREIGN KEY (`permission`) REFERENCES `permission` (`weight`) ON DELETE RESTRICT ON UPDATE CASCADE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC; SET FOREIGN_KEY_CHECKS = 1;
使用token
項目使用token校驗,那么可以通過請求發(fā)送過來的token取到redis緩存中token對應(yīng)的用戶id,因此需要一個存放token:id的hash表,由于我做的token驗證直接從表中取token校驗,同時也需要取對應(yīng)的id,所以token和id互為key和value
實體類
這是我用mybatis-plsu-generator根據(jù)數(shù)據(jù)表自動生成的
package com.greenjiao.coc.bean; 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 java.time.LocalDateTime; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import lombok.Data; /** * <p> * * </p> * * @author yan * @since 2023-05-07 */ @Data @TableName("user") public class User { /** * 主鍵 */ @TableId(value = "id", type = IdType.ASSIGN_ID) private String id; /** * 賬號 */ @TableField("account") private String account; /** * 密碼 */ @TableField("password") private String password; /** * 姓名 */ @TableField("name") private String name; /** * 性別 */ @TableField("gender") private String gender; /** * 手機號 */ @TableField("telephone") private String telephone; /** * 郵箱 */ @TableField("email") private String email; /** * 簽名 */ @TableField("signature") private String signature; /** * 頭像地址 */ @TableField("avatar_address") private String avatarAddress; /** * 權(quán)限 */ @TableField("permission") private Integer permission; /** * 是否被封禁 */ @TableField("banned") private Boolean banned; /** * 備注 */ @TableField("remark") private String remark; /** * 創(chuàng)建時間 */ @TableField("create_time") @JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime; }
權(quán)限枚舉類
定義不同權(quán)限身份
package com.greenjiao.coc.common.enumconstant; /** * 權(quán)限信息 */ public enum PERMISSION_TYPE { BANNED_USER(0, "封禁用戶"), BLOCKED_SPEECH_USER(10, "禁言用戶"), NORMAL_USER(20, "普通用戶"), PREMIUM_USER(30, "高級用戶"), NORMAL_ADMIN(100, "管理員"), TOP_ADMIN(999, "最高管理員"); private final Integer weight; private final String name; PERMISSION_TYPE(Integer weight, String name) { this.weight = weight; this.name = name; } public Integer getWeight() { return weight; } public String getName() { return name; } }
自定義注解
我們需要對一些控制層中特定的方法進行權(quán)限校驗,并且部分方法的權(quán)限要求可能是不同的,所以需要給方法添加自定義注解,注解中包含所需的權(quán)限
package com.greenjiao.coc.annotation; import com.greenjiao.coc.common.enumconstant.PERMISSION_TYPE; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 權(quán)限校驗注解 */ @Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Role { PERMISSION_TYPE permission() default PERMISSION_TYPE.TOP_ADMIN; }
控制層添加注解
我的用戶的控制層,除了登陸和注冊外,其他的方法都使用自定義注解Role設(shè)置對應(yīng)所需權(quán)限
package com.greenjiao.coc.controller; import com.greenjiao.coc.annotation.Role; import com.greenjiao.coc.bean.User; import com.greenjiao.coc.common.DataVO; import com.greenjiao.coc.common.enumconstant.PERMISSION_TYPE; import com.greenjiao.coc.service.impl.UserServiceImpl; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/coc/user") public class UserController { private final UserServiceImpl userService; public UserController(UserServiceImpl userService) { this.userService = userService; } @PostMapping("/register") public DataVO<User> register(@RequestBody User user) { return userService.register(user); } @PostMapping("/login") public DataVO<Map<String, Object>> login(@RequestBody User user) { return userService.login(user); } @Role(permission = PERMISSION_TYPE.NORMAL_USER) @PutMapping public DataVO<User> update(@RequestBody User user) { return userService.update(user); } @Role(permission = PERMISSION_TYPE.NORMAL_ADMIN) @DeleteMapping("/{id}") public DataVO<User> delete(@PathVariable String id) { return userService.delete(id); } @Role(permission = PERMISSION_TYPE.NORMAL_USER) @GetMapping public DataVO<User> selectAll(@RequestBody User user) { return userService.selectAll(user); } }
添加控制層切面
添加其中的權(quán)限校驗@Before注解指定切點
需要權(quán)限校驗的切點為controller包下所有類的所有方法,并且要擁有Role注解
package com.greenjiao.coc.aspect; import com.alibaba.fastjson2.JSON; import com.greenjiao.coc.annotation.Role; import com.greenjiao.coc.bean.User; import com.greenjiao.coc.common.DataVO; import com.greenjiao.coc.common.ServerConstants; import com.greenjiao.coc.exception.ExceptionEnum; import com.greenjiao.coc.exception.ExceptionTip; import com.greenjiao.coc.mapper.UserMapper; import com.greenjiao.coc.util.MyUtil; import com.greenjiao.coc.util.RedisUtil; import jakarta.servlet.http.HttpServletRequest; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; @Aspect @Component public class ControllerAspect { private final RedisUtil redisUtil; private final UserMapper userMapper; public ControllerAspect(RedisUtil redisUtil, UserMapper userMapper) { this.redisUtil = redisUtil; this.userMapper = userMapper; } // 指定切點為controller目錄中所有類的selectAll方法并且要求攜帶的參數(shù)是Map<String,Object> param @Pointcut(value = "execution(* com.greenjiao.coc.controller..selectAll(..)) && args(param)", argNames = "param") public void controllerPoint(Map<String, Object> param) { } @Pointcut(value = "execution(* com.greenjiao.coc.controller..*(..)) && @annotation(com.greenjiao.coc.annotation.Role)") public void controllerCheckRolePoint() { } /** * 將查詢的內(nèi)容統(tǒng)一轉(zhuǎn)為對應(yīng)實體類 * @param joinPoint * @param param * @return * @throws Throwable */ @Around(value = "controllerPoint(param) && args(..)", argNames = "joinPoint,param") public Object changeParam(ProceedingJoinPoint joinPoint, @RequestBody Map<String, Object> param) throws Throwable { Integer page = (Integer) param.get(ServerConstants.DEFAULT_PAGE_NAME); //獲得param中用于分頁的page和limit后將其移除,剩余在param中的鍵值對即為需要查詢的條件 param.remove(ServerConstants.DEFAULT_PAGE_NAME); Integer limit = (Integer) param.get(ServerConstants.DEFAULT_LIMIT_NAME); param.remove(ServerConstants.DEFAULT_LIMIT_NAME); String className = MyUtil.getClassName(joinPoint.getTarget().getClass().getName()); //工具類獲取全限定類名 Class<?> clazz = Class.forName(className); //反射機制創(chuàng)建類 Object obj = JSON.parseObject(JSON.toJSONString(param), clazz); //將Map中剩余的鍵值對轉(zhuǎn)為對應(yīng)類型的json對象 Map<String, Object> params = new HashMap<>(); //重新存放最后需要新返回的參數(shù),procceed方法的參數(shù)需要一個Object數(shù)組, params.put(ServerConstants.DEFAULT_BEAN_NAME, obj); //但是controller層中的selectAll方法又只有一個參數(shù), params.put(ServerConstants.DEFAULT_PAGE_NAME, page); //如果直接將鍵值對放到Object數(shù)組中將會報參數(shù)個數(shù)異常, params.put(ServerConstants.DEFAULT_LIMIT_NAME, limit); //所以這里將鍵值對放到Map中,再將Map放到Object數(shù)組中 return joinPoint.proceed(new Object[]{params}); //procceed方法的參數(shù)需要一個Object數(shù)組, } /** * 權(quán)限校驗 */ @Before("controllerCheckRolePoint()") public void checkRole(JoinPoint joinPoint){ // 獲取Role注解中的permission權(quán)重值 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Role role = method.getAnnotation(Role.class); Integer requiredWeight = role.permission().getWeight(); // 從redis表中查找token對應(yīng)的id,獲取對應(yīng)的用戶 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String token = request.getHeader("token"); String id = (String) redisUtil.hget(ServerConstants.REDIS_TOKEN_TABLE_NAME, token); User user = userMapper.selectById(id); // 校驗 if(user.getPermission() < requiredWeight){ throw new ExceptionTip(ExceptionEnum.LOW_PERMISSION); } } }
RedisUtil工具類
這是我權(quán)限校驗使用的RedisUtil工具類,由于無用的警告太多,看的煩我用@SuppressWarnings注解將所有警告忽略了
并且將其給Spring管理了,所以可以在其他地方自動注入
package com.greenjiao.coc.util; import jakarta.annotation.Resource; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Redis工具類 */ @SuppressWarnings("all") @Component public class RedisUtil { @Resource private RedisTemplate<String, Object> redisTemplate; /****************** common start ****************/ /** * 指定緩存失效時間 * * @param key 鍵 * @param time 時間(秒) * @return */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根據(jù)key 獲取過期時間 * * @param key 鍵 不能為null * @return 時間(秒) 返回0代表為永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判斷key是否存在 * * @param key 鍵 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 刪除緩存 * * @param key 可以傳一個值 或多個 */ @SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key)); } } } /****************** common end ****************/ /****************** String start ****************/ /** * 普通緩存獲取 * * @param key 鍵 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通緩存放入 * * @param key 鍵 * @param value 值 * @return true成功 false失敗 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通緩存放入并設(shè)置時間 * * @param key 鍵 * @param value 值 * @param time 時間(秒) time要大于0 如果time小于等于0 將設(shè)置無限期 * @return true成功 false 失敗 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 遞增 * * @param key 鍵 * @param delta 要增加幾(大于0) * @return */ public long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("遞增因子必須大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 遞減 * * @param key 鍵 * @param delta 要減少幾(小于0) * @return */ public long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("遞減因子必須大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } /****************** String end ****************/ /****************** Map start ****************/ /** * HashGet * * @param key 鍵 不能為null * @param item 項 不能為null * @return 值 */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 獲取hashKey對應(yīng)的所有鍵值 * * @param key 鍵 * @return 對應(yīng)的多個鍵值 */ public Map<Object, Object> hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * * @param key 鍵 * @param map 對應(yīng)多個鍵值 * @return true 成功 false 失敗 */ public boolean hmset(String key, Map<String, Object> map) { try { redisTemplate.opsForHash().putAll(key, map); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * HashSet 并設(shè)置時間 * * @param key 鍵 * @param map 對應(yīng)多個鍵值 * @param time 時間(秒) * @return true成功 false失敗 */ public boolean hmset(String key, Map<String, Object> map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一張hash表中放入數(shù)據(jù),如果不存在將創(chuàng)建 * * @param key 鍵 * @param item 項 * @param value 值 * @return true 成功 false失敗 */ public boolean hset(String key, String item, Object value) { try { redisTemplate.opsForHash().put(key, item, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一張hash表中放入數(shù)據(jù),如果不存在將創(chuàng)建 * * @param key 鍵 * @param item 項 * @param value 值 * @param time 時間(秒) 注意:如果已存在的hash表有時間,這里將會替換原有的時間 * @return true 成功 false失敗 */ public boolean hset(String key, String item, Object value, long time) { try { redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 刪除hash表中的值 * * @param key 鍵 不能為null * @param item 項 可以使多個 不能為null */ public void hdel(String key, Object... item) { redisTemplate.opsForHash().delete(key, item); } /** * 判斷hash表中是否有該項的值 * * @param key 鍵 不能為null * @param item 項 不能為null * @return true 存在 false不存在 */ public boolean hHasKey(String key, String item) { return redisTemplate.opsForHash().hasKey(key, item); } /** * hash遞增 如果不存在,就會創(chuàng)建一個 并把新增后的值返回 * * @param key 鍵 * @param item 項 * @param by 要增加幾(大于0) * @return */ public double hincr(String key, String item, long by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash遞減 * * @param key 鍵 * @param item 項 * @param by 要減少記(小于0) * @return */ public double hdecr(String key, String item, long by) { return redisTemplate.opsForHash().increment(key, item, -by); } /****************** Map end ****************/ /****************** Set start ****************/ /** * 根據(jù)key獲取Set中的所有值 * * @param key 鍵 * @return */ public Set<Object> sGet(String key) { try { return redisTemplate.opsForSet().members(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根據(jù)value從一個set中查詢,是否存在 * * @param key 鍵 * @param value 值 * @return true 存在 false不存在 */ public boolean sHasKey(String key, Object value) { try { return redisTemplate.opsForSet().isMember(key, value); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 將數(shù)據(jù)放入set緩存 * * @param key 鍵 * @param values 值 可以是多個 * @return 成功個數(shù) */ public long sSet(String key, Object... values) { try { return redisTemplate.opsForSet().add(key, values); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 將set數(shù)據(jù)放入緩存 * * @param key 鍵 * @param time 時間(秒) * @param values 值 可以是多個 * @return 成功個數(shù) */ public long sSetAndTime(String key, long time, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time > 0) expire(key, time); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 獲取set緩存的長度 * * @param key 鍵 * @return */ public long sGetSetSize(String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 移除值為value的 * * @param key 鍵 * @param values 值 可以是多個 * @return 移除的個數(shù) */ public long setRemove(String key, Object... values) { try { Long count = redisTemplate.opsForSet().remove(key, values); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /****************** Set end ****************/ /****************** List start ****************/ /** * 獲取list緩存的內(nèi)容 * * @param key 鍵 * @param start 開始 * @param end 結(jié)束 0 到 -1代表所有值 * @return */ public List<Object> lGet(String key, long start, long end) { try { return redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 獲取list緩存的長度 * * @param key 鍵 * @return */ public long lGetListSize(String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 通過索引 獲取list中的值 * * @param key 鍵 * @param index 索引 index>=0時, 0 表頭,1 第二個元素,依次類推;index<0時,-1,表尾,-2倒數(shù)第二個元素,依次類推 * @return */ public Object lGetIndex(String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 將list放入緩存 * * @param key 鍵 * @param value 值 * @return */ public boolean lSet(String key, Object value) { try { redisTemplate.opsForList().rightPush(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 將list放入緩存 * * @param key 鍵 * @param value 值 * @param time 時間(秒) * @return */ public boolean lSet(String key, Object value, long time) { try { redisTemplate.opsForList().rightPush(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 將list放入緩存 * * @param key 鍵 * @param value 值 * @return */ public boolean lSet(String key, List<Object> value) { try { redisTemplate.opsForList().rightPushAll(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 將list放入緩存 * * @param key 鍵 * @param value 值 * @param time 時間(秒) * @return */ public boolean lSet(String key, List<Object> value, long time) { try { redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根據(jù)索引修改list中的某條數(shù)據(jù) * * @param key 鍵 * @param index 索引 * @param value 值 * @return */ public boolean lUpdateIndex(String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除N個值為value * * @param key 鍵 * @param count 移除多少個 * @param value 值 * @return 移除的個數(shù) */ public long lRemove(String key, long count, Object value) { try { Long remove = redisTemplate.opsForList().remove(key, count, value); return remove; } catch (Exception e) { e.printStackTrace(); return 0; } } /****************** List end ****************/ }
測試
登陸
使用一個權(quán)限值為20的賬號登陸
執(zhí)行權(quán)限所需20以上的查詢用戶操作
獲取到數(shù)據(jù)
執(zhí)行權(quán)限所需100以上的刪除用戶操作
可見提示權(quán)限不足
到此這篇關(guān)于SpringBoot切面實現(xiàn)token權(quán)限校驗詳解的文章就介紹到這了,更多相關(guān)SpringBoot實現(xiàn)token權(quán)限內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java讀取Excel、docx、pdf和txt等文件萬能方法舉例
在Java開發(fā)中處理文件是常見需求,本文以實際代碼示例詳述如何使用ApachePOI庫及其他工具讀取和寫入Excel、Word、PDF等文件,介紹了ApachePOI、ApachePDFBox和EasyExcel等庫的使用方法,幫助開發(fā)者有效讀取不同格式文件,需要的朋友可以參考下2024-09-09Spring Boot 使用WebAsyncTask異步返回結(jié)果
這篇文章主要介紹了Spring Boot 使用WebAsyncTask異步返回結(jié)果的相關(guān)資料,需要的朋友可以參考下2018-02-02Spring Boot2.0實現(xiàn)靜態(tài)資源版本控制詳解
這篇文章主要給大家介紹了關(guān)于Spring Boot2.0實現(xiàn)靜態(tài)資源版本控制的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11詳解Spring MVC的異步模式(高性能的關(guān)鍵)
本篇文章主要介紹了詳解Spring MVC的異步模式(高性能的關(guān)鍵),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02