sesstion 如何创建

 Session对象的创建一般是源于这样的一条语句:
Session session = request.getSession(false);或者Session session = request.getSession();如果不在乎服务器压力可能多那么一点点的话。

在Tomcat的实现中,这个request是org.apache.catalina.connector.Request类的包装类org.apache.catalina.connector.RequestFacade的对象,它的两个#getSession()方法如下:

Java代码 复制代码  收藏代码
  1. public HttpSession getSession(boolean create) {   
  2.     if (request == null) {   
  3.         throw new IllegalStateException(   
  4.                         sm.getString("requestFacade.nullRequest"));   
  5.     }   
  6.   
  7.     if (SecurityUtil.isPackageProtectionEnabled()){   
  8.         return (HttpSession)AccessController.   
  9.             doPrivileged(new GetSessionPrivilegedAction(create));   
  10.     } else {   
  11.         return request.getSession(create);   
  12.     }   
  13. }  
    public HttpSession getSession(boolean create) {
        if (request == null) {
            throw new IllegalStateException(
                            sm.getString("requestFacade.nullRequest"));
        }

        if (SecurityUtil.isPackageProtectionEnabled()){
            return (HttpSession)AccessController.
                doPrivileged(new GetSessionPrivilegedAction(create));
        } else {
            return request.getSession(create);
        }
    }


Java代码 复制代码  收藏代码
  1. public HttpSession getSession() {   
  2.     if (request == null) {   
  3.         throw new IllegalStateException(   
  4.                         sm.getString("requestFacade.nullRequest"));   
  5.     }   
  6.   
  7.     return getSession(true);   
  8. }  
    public HttpSession getSession() {
        if (request == null) {
            throw new IllegalStateException(
                            sm.getString("requestFacade.nullRequest"));
        }

        return getSession(true);
    }


其实差不太多,最后都会进入org.apache.catalina.connector.Request的#getSession()方法。这个方法的源代码如下:

Java代码 复制代码  收藏代码
  1. public HttpSession getSession(boolean create) {   
  2.     Session session = doGetSession(create);   
  3.     if (session != null) {   
  4.         return session.getSession();   
  5.     } else {   
  6.         return null;   
  7.     }   
  8. }  
    public HttpSession getSession(boolean create) {
        Session session = doGetSession(create);
        if (session != null) {
            return session.getSession();
        } else {
            return null;
        }
    }


然后调用就到了#doGetSession()这个方法了。源代码如下

Java代码 复制代码  收藏代码
  1. protected Session doGetSession(boolean create) {   
  2.     // 没有Context的话直接返回null   
  3.     if (context == null)   
  4.         return (null);   
  5.   
  6.     // 判断Session是否有效   
  7.     if ((session != null) && !session.isValid())   
  8.         session = null;   
  9.     if (session != null)   
  10.         return (session);   
  11.   
  12.     // 返回Manager对象,这里是StandardManager类的对象   
  13.     Manager manager = null;   
  14.     if (context != null)   
  15.         manager = context.getManager();   
  16.     if (manager == null)   
  17.         return (null); // Sessions are not supported   
  18.     // 判断是否有SessionID   
  19.     if (requestedSessionId != null) {   
  20.         try {   
  21.             // 在Manager中根据SessionID查找Session   
  22.             session = manager.findSession(requestedSessionId);   
  23.         } catch (IOException e) {   
  24.             session = null;   
  25.         }   
  26.         if ((session != null) && !session.isValid())   
  27.             session = null;   
  28.         if (session != null) {   
  29.             // 更新访问时间   
  30.             session.access();   
  31.             return (session);   
  32.         }   
  33.     }   
  34.   
  35.     // 创建新的Session   
  36.     if (!create)   
  37.         return (null);   
  38.     if ((context != null) && (response != null) && context.getCookies()   
  39.             && response.getResponse().isCommitted()) {   
  40.         throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));   
  41.     }   
  42.   
  43.     // 判断是否使用 "/" 作为Session Cookie的存储路径 并且 是否SessionID来自Cookie   
  44.     if (connector.getEmptySessionPath() && isRequestedSessionIdFromCookie()) {   
  45.         // 创建Session   
  46.         session = manager.createSession(getRequestedSessionId());   
  47.     } else {   
  48.         session = manager.createSession(null);   
  49.     }   
  50.   
  51.     // 创建一个新的Session Cookies   
  52.     if ((session != null) && (getContext() != null) && getContext().getCookies()) {   
  53.         Cookie cookie = new Cookie(Globals.SESSION_COOKIE_NAME, session.getIdInternal());   
  54.         // 配置Session Cookie   
  55.         configureSessionCookie(cookie);   
  56.         // 在响应中加入Session Cookie   
  57.         response.addCookieInternal(cookie);   
  58.     }   
  59.   
  60.     if (session != null) {   
  61.         // 更新访问时间   
  62.         session.access();   
  63.         return (session);   
  64.     } else {   
  65.         return (null);   
  66.     }   
  67.   
  68. }  
	protected Session doGetSession(boolean create) {
		// 没有Context的话直接返回null
		if (context == null)
			return (null);

		// 判断Session是否有效
		if ((session != null) && !session.isValid())
			session = null;
		if (session != null)
			return (session);

		// 返回Manager对象,这里是StandardManager类的对象
		Manager manager = null;
		if (context != null)
			manager = context.getManager();
		if (manager == null)
			return (null); // Sessions are not supported
		// 判断是否有SessionID
		if (requestedSessionId != null) {
			try {
				// 在Manager中根据SessionID查找Session
				session = manager.findSession(requestedSessionId);
			} catch (IOException e) {
				session = null;
			}
			if ((session != null) && !session.isValid())
				session = null;
			if (session != null) {
				// 更新访问时间
				session.access();
				return (session);
			}
		}

		// 创建新的Session
		if (!create)
			return (null);
		if ((context != null) && (response != null) && context.getCookies()
				&& response.getResponse().isCommitted()) {
			throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));
		}

		// 判断是否使用 "/" 作为Session Cookie的存储路径 并且 是否SessionID来自Cookie
		if (connector.getEmptySessionPath() && isRequestedSessionIdFromCookie()) {
			// 创建Session
			session = manager.createSession(getRequestedSessionId());
		} else {
			session = manager.createSession(null);
		}

		// 创建一个新的Session Cookies
		if ((session != null) && (getContext() != null) && getContext().getCookies()) {
			Cookie cookie = new Cookie(Globals.SESSION_COOKIE_NAME, session.getIdInternal());
			// 配置Session Cookie
			configureSessionCookie(cookie);
			// 在响应中加入Session Cookie
			response.addCookieInternal(cookie);
		}

		if (session != null) {
			// 更新访问时间
			session.access();
			return (session);
		} else {
			return (null);
		}

	}

这个方法说明了Session创建的大致过程,首先判断requestedSessionId是否存在,如果存在,那么根据这个ID去查找Session对象。如果requestedSessionId不存在或者没有取到Session,并且传递给#getSession(boolean)的参数为真,那么要创建一个新的Session,并且给客户端写回去一个Session Cookie。

首先,我感兴趣的是requestedSessionId的赋值,它到底是什么时候被赋值的呢?

还要向回看Tomcat的请求处理过程,请求曾到过这一步,org.apache.catalina.connector.CoyoteAdapter的#service()方法。里边有这样一句方法调用:postParseRequest(req, request, res, response)。就是这一步处理了SessionID的获取,这个方法调用了#parseSessionId()和parseSessionCookiesId()这两个方法,就是它对Session ID进行了提取,源代码分别如下:

Java代码 复制代码  收藏代码
  1. protected void parseSessionId(org.apache.coyote.Request req, Request request) {   
  2.   
  3.     ByteChunk uriBC = req.requestURI().getByteChunk();   
  4.     // 判断URL中是不是有";jsessionid="这个字符串   
  5.     int semicolon = uriBC.indexOf(match, 0, match.length(), 0);   
  6.   
  7.     if (semicolon > 0) {   
  8.         // Parse session ID, and extract it from the decoded request URI   
  9.         // 在URL中提取Session ID   
  10.         int start = uriBC.getStart();   
  11.         int end = uriBC.getEnd();   
  12.   
  13.         int sessionIdStart = semicolon + match.length();   
  14.         int semicolon2 = uriBC.indexOf(';', sessionIdStart);   
  15.         if (semicolon2 >= 0) {   
  16.             request.setRequestedSessionId(new String(uriBC.getBuffer(), start + sessionIdStart,   
  17.                     semicolon2 - sessionIdStart));   
  18.             byte[] buf = uriBC.getBuffer();   
  19.             for (int i = 0; i < end - start - semicolon2; i++) {   
  20.                 buf[start + semicolon + i] = buf[start + i + semicolon2];   
  21.             }   
  22.             uriBC.setBytes(buf, start, end - start - semicolon2 + semicolon);   
  23.         } else {   
  24.             request.setRequestedSessionId(new String(uriBC.getBuffer(), start + sessionIdStart,   
  25.                     (end - start) - sessionIdStart));   
  26.             uriBC.setEnd(start + semicolon);   
  27.         }   
  28.         // 设定Session ID来自于URL   
  29.         request.setRequestedSessionURL(true);   
  30.   
  31.     } else {   
  32.         request.setRequestedSessionId(null);   
  33.         request.setRequestedSessionURL(false);   
  34.     }   
  35.   
  36. }  
	protected void parseSessionId(org.apache.coyote.Request req, Request request) {

		ByteChunk uriBC = req.requestURI().getByteChunk();
		// 判断URL中是不是有";jsessionid="这个字符串
		int semicolon = uriBC.indexOf(match, 0, match.length(), 0);

		if (semicolon > 0) {
			// Parse session ID, and extract it from the decoded request URI
			// 在URL中提取Session ID
			int start = uriBC.getStart();
			int end = uriBC.getEnd();

			int sessionIdStart = semicolon + match.length();
			int semicolon2 = uriBC.indexOf(';', sessionIdStart);
			if (semicolon2 >= 0) {
				request.setRequestedSessionId(new String(uriBC.getBuffer(), start + sessionIdStart,
						semicolon2 - sessionIdStart));
				byte[] buf = uriBC.getBuffer();
				for (int i = 0; i < end - start - semicolon2; i++) {
					buf[start + semicolon + i] = buf[start + i + semicolon2];
				}
				uriBC.setBytes(buf, start, end - start - semicolon2 + semicolon);
			} else {
				request.setRequestedSessionId(new String(uriBC.getBuffer(), start + sessionIdStart,
						(end - start) - sessionIdStart));
				uriBC.setEnd(start + semicolon);
			}
			// 设定Session ID来自于URL
			request.setRequestedSessionURL(true);

		} else {
			request.setRequestedSessionId(null);
			request.setRequestedSessionURL(false);
		}

	}


Java代码 复制代码  收藏代码
  1. protected void parseSessionCookiesId(org.apache.coyote.Request req, Request request) {   
  2.     Context context = (Context) request.getMappingData().context;   
  3.     if (context != null && !context.getCookies())   
  4.         return;   
  5.   
  6.     // 返回Cookie   
  7.     Cookies serverCookies = req.getCookies();   
  8.     int count = serverCookies.getCookieCount();   
  9.     if (count <= 0)   
  10.         return;   
  11.   
  12.     for (int i = 0; i < count; i++) {   
  13.         ServerCookie scookie = serverCookies.getCookie(i);   
  14.         // 判断是否有JSESSIONID这个名字的Cookie   
  15.         if (scookie.getName().equals(Globals.SESSION_COOKIE_NAME)) {   
  16.             // Override anything requested in the URL   
  17.             if (!request.isRequestedSessionIdFromCookie()) {   
  18.                 // 设定Session ID   
  19.                 convertMB(scookie.getValue());   
  20.                 request.setRequestedSessionId(scookie.getValue().toString());   
  21.                 // 如果之前在URL中读到了SessionID,那么会覆盖它   
  22.                 request.setRequestedSessionCookie(true);   
  23.                 request.setRequestedSessionURL(false);   
  24.                 if (log.isDebugEnabled())   
  25.                     log.debug(" Requested cookie session id is " + request.getRequestedSessionId());   
  26.             } else {   
  27.                 if (!request.isRequestedSessionIdValid()) {   
  28.                     convertMB(scookie.getValue());   
  29.                     request.setRequestedSessionId(scookie.getValue().toString());   
  30.                 }   
  31.             }   
  32.         }   
  33.     }   
  34.   
  35. }  
	protected void parseSessionCookiesId(org.apache.coyote.Request req, Request request) {
		Context context = (Context) request.getMappingData().context;
		if (context != null && !context.getCookies())
			return;

		// 返回Cookie
		Cookies serverCookies = req.getCookies();
		int count = serverCookies.getCookieCount();
		if (count <= 0)
			return;

		for (int i = 0; i < count; i++) {
			ServerCookie scookie = serverCookies.getCookie(i);
			// 判断是否有JSESSIONID这个名字的Cookie
			if (scookie.getName().equals(Globals.SESSION_COOKIE_NAME)) {
				// Override anything requested in the URL
				if (!request.isRequestedSessionIdFromCookie()) {
					// 设定Session ID
					convertMB(scookie.getValue());
					request.setRequestedSessionId(scookie.getValue().toString());
					// 如果之前在URL中读到了SessionID,那么会覆盖它
					request.setRequestedSessionCookie(true);
					request.setRequestedSessionURL(false);
					if (log.isDebugEnabled())
						log.debug(" Requested cookie session id is " + request.getRequestedSessionId());
				} else {
					if (!request.isRequestedSessionIdValid()) {
						convertMB(scookie.getValue());
						request.setRequestedSessionId(scookie.getValue().toString());
					}
				}
			}
		}

	}

Tomcat就是通过上边的两个方法读到URL或者Cookie中存放的Session ID的。

了解了Session ID的获取,下面要看一下Session的查找过程,就是org.apache.catalina.session.StandardManager的#findSession()方法。这个方法是在它的基类中定义的,源代码如下:

Java代码 复制代码  收藏代码
  1. public Session findSession(String id) throws IOException {   
  2.     if (id == null)   
  3.         return (null);   
  4.     return (Session) sessions.get(id);   
  5. }  
    public Session findSession(String id) throws IOException {
        if (id == null)
            return (null);
        return (Session) sessions.get(id);
    }

代码很短,其中sessions是一个ConcurrentHashMap<String, Session>对象。那么这个sessions的对象是什么时候载入的Session呢?

启动的时候!可以看一下StandardManager#start()方法。最后调用了#load()方法,这个就是载入Session的方法了:

Java代码 复制代码  收藏代码
  1. public void load() throws ClassNotFoundException, IOException {   
  2.     if (SecurityUtil.isPackageProtectionEnabled()) {   
  3.         try {   
  4.             AccessController.doPrivileged(new PrivilegedDoLoad());   
  5.         } catch (PrivilegedActionException ex) {   
  6.             Exception exception = ex.getException();   
  7.             if (exception instanceof ClassNotFoundException) {   
  8.                 throw (ClassNotFoundException) exception;   
  9.             } else if (exception instanceof IOException) {   
  10.                 throw (IOException) exception;   
  11.             }   
  12.             if (log.isDebugEnabled())   
  13.                 log.debug("Unreported exception in load() " + exception);   
  14.         }   
  15.     } else {   
  16.         doLoad();   
  17.     }   
  18. }  
	public void load() throws ClassNotFoundException, IOException {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			try {
				AccessController.doPrivileged(new PrivilegedDoLoad());
			} catch (PrivilegedActionException ex) {
				Exception exception = ex.getException();
				if (exception instanceof ClassNotFoundException) {
					throw (ClassNotFoundException) exception;
				} else if (exception instanceof IOException) {
					throw (IOException) exception;
				}
				if (log.isDebugEnabled())
					log.debug("Unreported exception in load() " + exception);
			}
		} else {
			doLoad();
		}
	}

最后调用了#doLoad()方法来具体的载入Session,源代码如下:

Java代码 复制代码  收藏代码
  1. protected void doLoad() throws ClassNotFoundException, IOException {   
  2.     if (log.isDebugEnabled())   
  3.         log.debug("Start: Loading persisted sessions");   
  4.   
  5.     // 清空Map   
  6.     sessions.clear();   
  7.   
  8.     // 对应work/Catalina/localhost/%app name%/SESSIONS.ser文件   
  9.     File file = file();   
  10.     if (file == null)   
  11.         return;   
  12.     if (log.isDebugEnabled())   
  13.         log.debug(sm.getString("standardManager.loading", pathname));   
  14.     FileInputStream fis = null;   
  15.     ObjectInputStream ois = null;   
  16.     Loader loader = null;   
  17.     ClassLoader classLoader = null;   
  18.     try {   
  19.         // 载入Session缓存文件   
  20.         fis = new FileInputStream(file.getAbsolutePath());   
  21.         BufferedInputStream bis = new BufferedInputStream(fis);   
  22.         if (container != null)   
  23.             loader = container.getLoader();   
  24.         if (loader != null)   
  25.             classLoader = loader.getClassLoader();   
  26.         if (classLoader != null) {   
  27.             if (log.isDebugEnabled())   
  28.                 log.debug("Creating custom object input stream for class loader ");   
  29.             ois = new CustomObjectInputStream(bis, classLoader);   
  30.         } else {   
  31.             if (log.isDebugEnabled())   
  32.                 log.debug("Creating standard object input stream");   
  33.             ois = new ObjectInputStream(bis);   
  34.         }   
  35.     } catch (FileNotFoundException e) {   
  36.         if (log.isDebugEnabled())   
  37.             log.debug("No persisted data file found");   
  38.         return;   
  39.     } catch (IOException e) {   
  40.         log.error(sm.getString("standardManager.loading.ioe", e), e);   
  41.         if (ois != null) {   
  42.             try {   
  43.                 ois.close();   
  44.             } catch (IOException f) {   
  45.                 ;   
  46.             }   
  47.             ois = null;   
  48.         }   
  49.         throw e;   
  50.     }   
  51.   
  52.     synchronized (sessions) {   
  53.         try {   
  54.             // 读出Session个数   
  55.             Integer count = (Integer) ois.readObject();   
  56.             int n = count.intValue();   
  57.             if (log.isDebugEnabled())   
  58.                 log.debug("Loading " + n + " persisted sessions");   
  59.             //  读入Session   
  60.             for (int i = 0; i < n; i++) {   
  61.                 StandardSession session = getNewSession();   
  62.                 session.readObjectData(ois);   
  63.                 session.setManager(this);   
  64.                 sessions.put(session.getIdInternal(), session);   
  65.                 session.activate();   
  66.                 sessionCounter++;   
  67.             }   
  68.         } catch (ClassNotFoundException e) {   
  69.             log.error(sm.getString("standardManager.loading.cnfe", e), e);   
  70.             if (ois != null) {   
  71.                 try {   
  72.                     ois.close();   
  73.                 } catch (IOException f) {   
  74.                     ;   
  75.                 }   
  76.                 ois = null;   
  77.             }   
  78.             throw e;   
  79.         } catch (IOException e) {   
  80.             log.error(sm.getString("standardManager.loading.ioe", e), e);   
  81.             if (ois != null) {   
  82.                 try {   
  83.                     ois.close();   
  84.                 } catch (IOException f) {   
  85.                     ;   
  86.                 }   
  87.                 ois = null;   
  88.             }   
  89.             throw e;   
  90.         } finally {   
  91.             try {   
  92.                 if (ois != null)   
  93.                     ois.close();   
  94.             } catch (IOException f) {   
  95.             }   
  96.   
  97.             // 删除Session缓存文件   
  98.             if (file != null && file.exists())   
  99.                 file.delete();   
  100.         }   
  101.     }   
  102.   
  103.     if (log.isDebugEnabled())   
  104.         log.debug("Finish: Loading persisted sessions");   
  105. }  
	protected void doLoad() throws ClassNotFoundException, IOException {
		if (log.isDebugEnabled())
			log.debug("Start: Loading persisted sessions");

		// 清空Map
		sessions.clear();

		// 对应work/Catalina/localhost/%app name%/SESSIONS.ser文件
		File file = file();
		if (file == null)
			return;
		if (log.isDebugEnabled())
			log.debug(sm.getString("standardManager.loading", pathname));
		FileInputStream fis = null;
		ObjectInputStream ois = null;
		Loader loader = null;
		ClassLoader classLoader = null;
		try {
			// 载入Session缓存文件
			fis = new FileInputStream(file.getAbsolutePath());
			BufferedInputStream bis = new BufferedInputStream(fis);
			if (container != null)
				loader = container.getLoader();
			if (loader != null)
				classLoader = loader.getClassLoader();
			if (classLoader != null) {
				if (log.isDebugEnabled())
					log.debug("Creating custom object input stream for class loader ");
				ois = new CustomObjectInputStream(bis, classLoader);
			} else {
				if (log.isDebugEnabled())
					log.debug("Creating standard object input stream");
				ois = new ObjectInputStream(bis);
			}
		} catch (FileNotFoundException e) {
			if (log.isDebugEnabled())
				log.debug("No persisted data file found");
			return;
		} catch (IOException e) {
			log.error(sm.getString("standardManager.loading.ioe", e), e);
			if (ois != null) {
				try {
					ois.close();
				} catch (IOException f) {
					;
				}
				ois = null;
			}
			throw e;
		}

		synchronized (sessions) {
			try {
				// 读出Session个数
				Integer count = (Integer) ois.readObject();
				int n = count.intValue();
				if (log.isDebugEnabled())
					log.debug("Loading " + n + " persisted sessions");
				//  读入Session
				for (int i = 0; i < n; i++) {
					StandardSession session = getNewSession();
					session.readObjectData(ois);
					session.setManager(this);
					sessions.put(session.getIdInternal(), session);
					session.activate();
					sessionCounter++;
				}
			} catch (ClassNotFoundException e) {
				log.error(sm.getString("standardManager.loading.cnfe", e), e);
				if (ois != null) {
					try {
						ois.close();
					} catch (IOException f) {
						;
					}
					ois = null;
				}
				throw e;
			} catch (IOException e) {
				log.error(sm.getString("standardManager.loading.ioe", e), e);
				if (ois != null) {
					try {
						ois.close();
					} catch (IOException f) {
						;
					}
					ois = null;
				}
				throw e;
			} finally {
				try {
					if (ois != null)
						ois.close();
				} catch (IOException f) {
				}

				// 删除Session缓存文件
				if (file != null && file.exists())
					file.delete();
			}
		}

		if (log.isDebugEnabled())
			log.debug("Finish: Loading persisted sessions");
	}

大致知道了Session的读取过程,后面就是Session没找到时创建Session的过程了。具体就是org.apache.catalina.session.StandardManager的#createSession()方法:

Java代码 复制代码  收藏代码
  1. public Session createSession(String sessionId) {   
  2.     if ((maxActiveSessions >= 0) && (sessions.size() >= maxActiveSessions)) {   
  3.         rejectedSessions++;   
  4.         throw new IllegalStateException(sm.getString("standardManager.createSession.ise"));   
  5.     }   
  6.     return (super.createSession(sessionId));   
  7. }  
	public Session createSession(String sessionId) {
		if ((maxActiveSessions >= 0) && (sessions.size() >= maxActiveSessions)) {
			rejectedSessions++;
			throw new IllegalStateException(sm.getString("standardManager.createSession.ise"));
		}
		return (super.createSession(sessionId));
	}

最后调用到了它的基类的#createSession()方法了。

Java代码 复制代码  收藏代码
  1. public Session createSession(String sessionId) {   
  2.     // 创建一个新的Session   
  3.     Session session = createEmptySession();   
  4.   
  5.     // 初始化Session的属性   
  6.     session.setNew(true);   
  7.     session.setValid(true);   
  8.     session.setCreationTime(System.currentTimeMillis());   
  9.     session.setMaxInactiveInterval(this.maxInactiveInterval);   
  10.     // 如果Session ID为null,那么就生成一个   
  11.     if (sessionId == null) {   
  12.         sessionId = generateSessionId();   
  13.     }   
  14.     session.setId(sessionId);   
  15.     sessionCounter++;   
  16.   
  17.     return (session);   
  18.   
  19. }  
	public Session createSession(String sessionId) {
		// 创建一个新的Session
		Session session = createEmptySession();

		// 初始化Session的属性
		session.setNew(true);
		session.setValid(true);
		session.setCreationTime(System.currentTimeMillis());
		session.setMaxInactiveInterval(this.maxInactiveInterval);
		// 如果Session ID为null,那么就生成一个
		if (sessionId == null) {
			sessionId = generateSessionId();
		}
		session.setId(sessionId);
		sessionCounter++;

		return (session);

	}

通过上述过程,一个新的Session就创建出来了。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值