springboot利用AOP完成日志統(tǒng)計(jì)的詳細(xì)步驟
步驟寫(xiě)的很詳細(xì),可以直接復(fù)制拿來(lái)用的,其中用到了過(guò)濾器、自定義注解以及AOP切面,來(lái)完成日志記錄統(tǒng)計(jì),感興趣的收藏起來(lái),以后遇到了可以直接用。
可能步驟會(huì)比較多,但是整體跟著思路下來(lái),應(yīng)該沒(méi)什么大問(wèn)題的。
項(xiàng)目用到了過(guò)濾器,可能有的人會(huì)不理解,之所以用過(guò)濾器是因?yàn)橄胍谌罩居涗沺ost請(qǐng)求的json數(shù)據(jù)。
請(qǐng)求的時(shí)候,是通過(guò)request的body來(lái)傳輸?shù)?。在AOP后置方法中獲取request里面的body,是取不到,直接為空。
原因很簡(jiǎn)單:因?yàn)槭橇?。想想看,java中的流也是只能讀一次,因?yàn)槲沂窃贏OP后置方法獲取的,控制器實(shí)際上已經(jīng)讀過(guò)了一次,后置方法再讀自然為空了。所以用過(guò)濾器來(lái)進(jìn)行解決了這個(gè)問(wèn)題。
1、創(chuàng)建日志表
這里我用的是mysql,假如您用的別的數(shù)據(jù)庫(kù),可以自行根據(jù)數(shù)據(jù)庫(kù)類(lèi)型進(jìn)行修改。
CREATE TABLE `log` ( `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主鍵', `create_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '創(chuàng)建人', `create_time` datetime NULL DEFAULT NULL COMMENT '創(chuàng)建時(shí)間', `update_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最近更新時(shí)間', `update_time` datetime NULL DEFAULT NULL COMMENT '最近更新人', `update_count` int(11) NULL DEFAULT NULL COMMENT '更新次數(shù)', `delete_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '刪除標(biāo)志', `delete_time` datetime NULL DEFAULT NULL COMMENT '刪除日期', `delete_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '刪除人', `cost_time` int(11) NULL DEFAULT NULL COMMENT '花費(fèi)時(shí)間', `ip` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'ip', `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '日志描述', `request_param` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '請(qǐng)求參數(shù)', `request_json` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '請(qǐng)求json數(shù)據(jù)', `request_type` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '請(qǐng)求類(lèi)型', `request_url` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '請(qǐng)求路徑', `username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '請(qǐng)求用戶(hù)', `operation_type` int(3) NULL DEFAULT NULL COMMENT '操作類(lèi)型', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
2、創(chuàng)建實(shí)體類(lèi)
我的項(xiàng)目運(yùn)用到了mybatisplus、swagger、lombok,你們可以根據(jù)自己項(xiàng)目框架寫(xiě)對(duì)應(yīng)的實(shí)體類(lèi)。BaseModel 是我們封裝了一個(gè)基礎(chǔ)實(shí)體類(lèi),專(zhuān)門(mén)存放關(guān)于操作人的信息,然后實(shí)體類(lèi)直接繼承。
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.annotations.ApiModelProperty; import cn.org.xaas.mybatis.model.BaseModel; import lombok.Data; import lombok.ToString; @TableName(value = "log") @Data @ToString(callSuper = true) public class Log extends BaseModel { @ApiModelProperty(value = "花費(fèi)時(shí)間") @TableField(value = "cost_time") private Integer costTime; @ApiModelProperty(value = "ip") @TableField(value = "ip") private String ip; @ApiModelProperty(value = "日志描述") @TableField(value = "description") private String description; @ApiModelProperty(value = "請(qǐng)求參數(shù)") @TableField(value = "request_param") private String requestParam; @ApiModelProperty(value = "請(qǐng)求json數(shù)據(jù)") @TableField(value = "request_json") private String requestJson; @ApiModelProperty(value = "請(qǐng)求類(lèi)型") @TableField(value = "request_type") private String requestType; @ApiModelProperty(value = "請(qǐng)求路徑") @TableField(value = "request_url") private String requestUrl; @ApiModelProperty(value = "請(qǐng)求用戶(hù)") @TableField(value = "username") private String username; @ApiModelProperty(value = "操作類(lèi)型") @TableField(value = "operation_type") private Integer operationType; }
3、創(chuàng)建枚舉類(lèi)
用來(lái)記錄日志操作類(lèi)型
public enum OperationType { /** * 操作類(lèi)型 */ UNKNOWN("unknown"), DELETE("delete"), SELECT("select"), UPDATE("update"), INSERT("insert"); OperationType(String s) { this.value = s; } private String value; public String getValue() { return value; } public void setValue(String value) { this.value = value; } }
4、創(chuàng)建自定義注解
import java.lang.annotation.*; @Target({ElementType.PARAMETER, ElementType.METHOD})//作用于參數(shù)或方法上 @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SystemLog { /** * 日志名稱(chēng) * * @return */ String description() default ""; /** * 操作類(lèi)型 * * @return */ OperationType type() default OperationType.UNKNOWN; }
5、獲取ip的util
import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHostException; @Slf4j @Component public class IpInfoUtil { /** * 獲取客戶(hù)端IP地址 * * @param request 請(qǐng)求 * @return */ public String getIpAddr(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); if ("127.0.0.1".equals(ip)) { //根據(jù)網(wǎng)卡取本機(jī)配置的IP InetAddress inet = null; try { inet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { e.printStackTrace(); } ip = inet.getHostAddress(); } } // 對(duì)于通過(guò)多個(gè)代理的情況,第一個(gè)IP為客戶(hù)端真實(shí)IP,多個(gè)IP按照','分割 if (ip != null && ip.length() > 15) { if (ip.indexOf(",") > 0) { ip = ip.substring(0, ip.indexOf(",")); } } if ("0:0:0:0:0:0:0:1".equals(ip)) { ip = "127.0.0.1"; } return ip; } }
6、線(xiàn)程池util
利用線(xiàn)程異步記錄日志。所以直接用了一個(gè)util維護(hù)線(xiàn)程池。
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolUtil { /** * 線(xiàn)程緩沖隊(duì)列 */ private static BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(100); /** * 核心線(xiàn)程數(shù),會(huì)一直存活,即使沒(méi)有任務(wù),線(xiàn)程池也會(huì)維護(hù)線(xiàn)程的最少數(shù)量 */ private static final int SIZE_CORE_POOL = 5; /** * 線(xiàn)程池維護(hù)線(xiàn)程的最大數(shù)量 */ private static final int SIZE_MAX_POOL = 10; /** * 線(xiàn)程池維護(hù)線(xiàn)程所允許的空閑時(shí)間 */ private static final long ALIVE_TIME = 2000; private static ThreadPoolExecutor pool = new ThreadPoolExecutor(SIZE_CORE_POOL, SIZE_MAX_POOL, ALIVE_TIME, TimeUnit.MILLISECONDS, bqueue, new ThreadPoolExecutor.CallerRunsPolicy()); static { pool.prestartAllCoreThreads(); } public static ThreadPoolExecutor getPool() { return pool; } public static void main(String[] args) { System.out.println(pool.getPoolSize()); } }
7、HttpServletRequest實(shí)現(xiàn)類(lèi)
這個(gè)就是重寫(xiě)的一個(gè)HttpServletRequest類(lèi)。
import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; public class BodyReaderRequestWrapper extends HttpServletRequestWrapper { private final String body; /** * @param request */ public BodyReaderRequestWrapper(HttpServletRequest request) { super(request); StringBuilder sb = new StringBuilder(); InputStream ins = null; BufferedReader isr = null; try { ins = request.getInputStream(); if (ins != null) { isr = new BufferedReader(new InputStreamReader(ins)); char[] charBuffer = new char[128]; int readCount = 0; while ((readCount = isr.read(charBuffer)) != -1) { sb.append(charBuffer, 0, readCount); } } else { sb.append(""); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (isr != null) { isr.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (ins != null) { ins.close(); } } catch (IOException e) { e.printStackTrace(); } } body = sb.toString(); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream byteArrayIns = new ByteArrayInputStream(body.getBytes()); ServletInputStream servletIns = new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } @Override public int read() throws IOException { return byteArrayIns.read(); } }; return servletIns; } }
8、添加過(guò)濾器
這個(gè)過(guò)濾器我添加了一個(gè)路徑,就是代表需要json日志的接口,可以在list當(dāng)中添加路徑,不需要取request當(dāng)中json數(shù)據(jù)的可以不配置。
import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; public class BodyReaderRequestFilter implements Filter { private static final Pattern SHOULD_NOT_FILTER_URL_PATTERN; static { List<String> urlList = new ArrayList<>(); // 想要通過(guò)aop記錄request當(dāng)中body數(shù)據(jù)的,就需要進(jìn)行配置路徑 urlList.add("(socket/.*)"); urlList.add("(test/test1)"); urlList.add("(test/test2)"); StringBuilder sb = new StringBuilder(); for (String url : urlList) { sb.append(url); sb.append("|"); } sb.setLength(sb.length() - 1); SHOULD_NOT_FILTER_URL_PATTERN = Pattern.compile(sb.toString()); } @Override public void init(FilterConfig filterConfig) { } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 獲取訪(fǎng)問(wèn)的url String servletPath = request.getServletPath(); if (SHOULD_NOT_FILTER_URL_PATTERN.matcher(servletPath).find()) { BodyReaderRequestWrapper requestWrapper = new BodyReaderRequestWrapper(request); if (requestWrapper == null) { filterChain.doFilter(request, response); } else { filterChain.doFilter(requestWrapper, response); } }else { filterChain.doFilter(request, response); } } @Override public void destroy() { } }
想要讓過(guò)濾器生效需要注入到容器當(dāng)中。
import cn.org.bjca.szyx.xaas.equipment.filter.BodyReaderRequestFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyServerConfig { @Bean public FilterRegistrationBean myFilter(){ FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(new BodyReaderRequestFilter()); return registrationBean; } }
9、添加AOP核心類(lèi)
對(duì)于切面,我們可以通過(guò)指定包名,進(jìn)行日志統(tǒng)計(jì),也可以選擇根據(jù)自定義的注解在方法上添加,然后進(jìn)行統(tǒng)計(jì),根據(jù)自己的實(shí)際情況,在切點(diǎn)進(jìn)行配置即可。
LogDao我是沒(méi)有提供的,每個(gè)項(xiàng)目框架不一樣,自行根據(jù)情況進(jìn)行編寫(xiě),就是保存數(shù)據(jù)庫(kù)就可以了。
import cn.hutool.core.util.IdUtil; import cn.hutool.json.JSONUtil; import cn.org.xaas.core.util.HeaderSecurityUtils; import cn.org.xaas.equipment.annotation.SystemLog; import cn.org.xaas.equipment.dao.LogDao; import cn.org.xaas.equipment.model.base.Log; import cn.org.xaas.equipment.utils.IpInfoUtil; import cn.org.xaas.equipment.utils.ThreadPoolUtil; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.NamedThreadLocal; import org.springframework.stereotype.Component; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.Method; import java.util.Date; import java.util.HashMap; import java.util.Map; @Aspect @Component @Slf4j public class SystemLogAspect { private static final ThreadLocal<Date> beginTimeThreadLocal = new NamedThreadLocal<Date>("ThreadLocal beginTime"); @Autowired private LogDao logDao; @Autowired private IpInfoUtil ipInfoUtil; @Autowired(required = false) private HttpServletRequest request; /** * Controller層切點(diǎn),注解方式 */ //@Pointcut("execution(* *..controller..*Controller*.*(..))") @Pointcut("@annotation(cn.org.xaas.equipment.annotation.SystemLog)") public void controllerAspect() { } /** * 前置通知 (在方法執(zhí)行之前返回)用于攔截Controller層記錄用戶(hù)的操作的開(kāi)始時(shí)間 * * @param joinPoint 切點(diǎn) * @throws InterruptedException */ @Before("controllerAspect()") public void doBefore(JoinPoint joinPoint) throws InterruptedException { //線(xiàn)程綁定變量(該數(shù)據(jù)只有當(dāng)前請(qǐng)求的線(xiàn)程可見(jiàn)) Date beginTime = new Date(); beginTimeThreadLocal.set(beginTime); } /** * 后置通知(在方法執(zhí)行之后并返回?cái)?shù)據(jù)) 用于攔截Controller層無(wú)異常的操作 * * @param joinPoint 切點(diǎn) */ @AfterReturning("controllerAspect()") public void after(JoinPoint joinPoint) { try { // 獲取操作人,每個(gè)系統(tǒng)不一樣,一般存儲(chǔ)與session,此處就不展示了 String username = HeaderSecurityUtils.getUserName(); // 讀取json數(shù)據(jù) String openApiRequestData = getJSON(request); Map<String, String[]> requestParams = request.getParameterMap(); Log log = new Log(); if (openApiRequestData != null) { log.setRequestJson(JSONUtil.toJsonStr(openApiRequestData)); } log.setId(IdUtil.simpleUUID()); log.setUsername(username); //日志標(biāo)題 String description = getControllerMethodInfo(joinPoint).get("description").toString(); log.setDescription(description); //日志類(lèi)型 log.setOperationType((int) getControllerMethodInfo(joinPoint).get("type")); //日志請(qǐng)求url log.setRequestUrl(request.getRequestURI()); //請(qǐng)求方式 log.setRequestType(request.getMethod()); //請(qǐng)求參數(shù) log.setRequestParam(JSONUtil.toJsonStr(requestParams)); //其他屬性 log.setIp(ipInfoUtil.getIpAddr(request)); log.setCreateBy(username); log.setUpdateBy(username); log.setCreateTime(new Date()); log.setUpdateTime(new Date()); log.setDeleteFlag("0"); //請(qǐng)求開(kāi)始時(shí)間 long beginTime = beginTimeThreadLocal.get().getTime(); long endTime = System.currentTimeMillis(); //請(qǐng)求耗時(shí) Long logElapsedTime = endTime - beginTime; log.setCostTime(logElapsedTime.intValue()); //持久化(存儲(chǔ)到數(shù)據(jù)或者ES,可以考慮用線(xiàn)程池) ThreadPoolUtil.getPool().execute(new SaveSystemLogThread(log, logDao)); } catch (Exception e) { log.error("AOP后置通知異常", e); } } /** * 獲取request的body * * @param request * @return */ public String getJSON(HttpServletRequest request) { ServletInputStream inputStream = null; InputStreamReader inputStreamReader = null; BufferedReader streamReader = null; StringBuilder responseStrBuilder = new StringBuilder(); try { inputStream = request.getInputStream(); inputStreamReader = new InputStreamReader(inputStream, "UTF-8"); streamReader = new BufferedReader(inputStreamReader); String inputStr; while ((inputStr = streamReader.readLine()) != null) { responseStrBuilder.append(inputStr); } } catch (IOException ioException) { ioException.printStackTrace(); } finally { try { if (inputStream != null) { inputStream.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (inputStreamReader != null) { inputStreamReader.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (streamReader != null) { streamReader.close(); } } catch (IOException e) { e.printStackTrace(); } } return responseStrBuilder.toString(); } /** * 保存日志至數(shù)據(jù)庫(kù) */ private static class SaveSystemLogThread implements Runnable { private Log log; private LogDao logDao; public SaveSystemLogThread(Log esLog, LogDao logDao) { this.log = esLog; this.logDao = logDao; } @Override public void run() { logDao.insert(log); } } /** * 獲取注解中對(duì)方法的描述信息 用于Controller層注解 * * @param joinPoint 切點(diǎn) * @return 方法描述 * @throws Exception */ public static Map<String, Object> getControllerMethodInfo(JoinPoint joinPoint) throws Exception { Map<String, Object> map = new HashMap<String, Object>(16); //獲取目標(biāo)類(lèi)名 String targetName = joinPoint.getTarget().getClass().getName(); //獲取方法名 String methodName = joinPoint.getSignature().getName(); //獲取相關(guān)參數(shù) Object[] arguments = joinPoint.getArgs(); //生成類(lèi)對(duì)象 Class targetClass = Class.forName(targetName); //獲取該類(lèi)中的方法 Method[] methods = targetClass.getMethods(); String description = ""; Integer type = null; for (Method method : methods) { if (!method.getName().equals(methodName)) { continue; } Class[] clazzs = method.getParameterTypes(); if (clazzs.length != arguments.length) { //比較方法中參數(shù)個(gè)數(shù)與從切點(diǎn)中獲取的參數(shù)個(gè)數(shù)是否相同,原因是方法可以重載哦 continue; } description = method.getAnnotation(SystemLog.class).description(); type = method.getAnnotation(SystemLog.class).type().ordinal(); map.put("description", description); map.put("type", type); } return map; } }
10、接口測(cè)試
import cn.org.xaas.equipment.annotation.SystemLog; import cn.org.xaas.equipment.constant.OperationType; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/test") public class TestController { @PostMapping("/test1") @SystemLog(description = "根據(jù)id查詢(xún)某某數(shù)據(jù)",type = OperationType.SELECT) public void test1(@RequestParam("id")String id){ System.out.println(id); } @PostMapping("/test2") @SystemLog(description = "根據(jù)id查詢(xún)某某數(shù)據(jù),傳json",type = OperationType.SELECT) public void test2(@RequestBody String id){ System.out.println(id); } }
調(diào)用第一個(gè)測(cè)試接口:
調(diào)用第二個(gè)測(cè)試接口:
到此這篇關(guān)于springboot利用AOP完成日志統(tǒng)計(jì)的文章就介紹到這了,更多相關(guān)springboot日志統(tǒng)計(jì)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot中利用AOP和攔截器實(shí)現(xiàn)自定義注解
- SpringBoot使用AOP實(shí)現(xiàn)統(tǒng)計(jì)全局接口訪(fǎng)問(wèn)次數(shù)詳解
- SpringBoot通過(guò)AOP與注解實(shí)現(xiàn)入?yún)⑿r?yàn)詳情
- SpringBoot使用AOP統(tǒng)一日志管理的方法詳解
- Springboot+AOP實(shí)現(xiàn)時(shí)間參數(shù)格式轉(zhuǎn)換
- springboot使用AOP+反射實(shí)現(xiàn)Excel數(shù)據(jù)的讀取
- springboot利用aop實(shí)現(xiàn)接口異步(進(jìn)度條)的全過(guò)程
- SpringBoot中通過(guò)AOP整合日志文件的實(shí)現(xiàn)
- Spring?BOOT?AOP基礎(chǔ)應(yīng)用教程
相關(guān)文章
Spring中Websocket身份驗(yàn)證和授權(quán)的實(shí)現(xiàn)
在Web應(yīng)用開(kāi)發(fā)中,安全一直是非常重要的一個(gè)方面,本文主要介紹了Spring中Websocket身份驗(yàn)證和授權(quán)的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08JavaWeb頁(yè)面中防止點(diǎn)擊Backspace網(wǎng)頁(yè)后退情況
當(dāng)鍵盤(pán)敲下后退鍵(Backspace)后怎么防止網(wǎng)頁(yè)后退情況呢?今天小編通過(guò)本文給大家詳細(xì)介紹下,感興趣的朋友一起看看吧2016-11-11簡(jiǎn)單理解java泛型的本質(zhì)(非類(lèi)型擦除)
泛型在java中有很重要的地位,在面向?qū)ο缶幊碳案鞣N設(shè)計(jì)模式中有非常廣泛的應(yīng)用。泛型是參數(shù)化類(lèi)型的應(yīng)用,操作的數(shù)據(jù)類(lèi)型不限定于特定類(lèi)型,可以根據(jù)實(shí)際需要設(shè)置不同的數(shù)據(jù)類(lèi)型,以實(shí)現(xiàn)代碼復(fù)用。下面小編來(lái)簡(jiǎn)單講一講泛型2019-05-05Netty網(wǎng)絡(luò)編程實(shí)戰(zhàn)之搭建Netty服務(wù)器
Netty是JBOSS開(kāi)源的一款NIO網(wǎng)絡(luò)編程框架,可用于快速開(kāi)發(fā)網(wǎng)絡(luò)的應(yīng)用。Netty是一個(gè)異步的、基于事件驅(qū)動(dòng)的網(wǎng)絡(luò)應(yīng)用框架,用于快速開(kāi)發(fā)高性能的服務(wù)端和客戶(hù)端。本文將詳細(xì)說(shuō)說(shuō)如何搭建Netty服務(wù)器,需要的可以參考一下2022-10-10解讀httpclient的validateAfterInactivity連接池狀態(tài)檢測(cè)
這篇文章主要為大家介紹了httpclient的validateAfterInactivity連接池狀態(tài)檢測(cè)解讀*,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11java實(shí)現(xiàn)服務(wù)器文件打包zip并下載的示例(邊打包邊下載)
這篇文章主要介紹了java實(shí)現(xiàn)服務(wù)器文件打包zip并下載的示例,使用該方法,可以即時(shí)打包文件,一邊打包一邊傳輸,不使用任何的緩存,讓用戶(hù)零等待,需要的朋友可以參考下2014-04-04