javascript
SpringAOP描述及实现_AspectJ详解_基于注解的AOP实现_SpringJdbcTemplate详解
AOP
AOP特點:
- 面向切面編程, 利用AOP對業務邏輯的各個部分進行抽取公共代碼, 降低耦合度, 提高代碼重用性, 同時提高開發效率.
- 采取橫向抽取, 取代傳統縱向繼承體系重復性代碼
- 解決事務管理, 性能監視, 安全檢查, 緩存, 日志等問題
- Spring AOP在運行期, 通過反向代理的方式解決類加載, 屬性注入
- AspectJ是基于Java的AOP框架, 在Spring使用AspectJ實現AOP
AOP實現機制:
底層采用代理機制實現AOP.
2 種代理機制: 1.采用JDK的的動態代理Proxy; 2.采用CGLIB字節碼增強
AOP專業術語:
Target: 目標類 ( 需要被代理的類 )
Joinpoint: 連接點 ( 可能需要使用的目標類方法 )
Advice: 增強代碼 ( 對連接點增強的代碼 )
PointCut: 切入點 ( 可能需要 Advice 增強的連接點 )
Weaving: 織入 ( 創建代理對象 proxy 執行切入點的方法 )
Aspect: 切面 ( Advice 與 PointCust的結合 )
JDK/CGLIB的AOP實現
下面通過JDK動態代理和CGLIB字節碼增強兩種方式實現AOP操作
當目標類沒有實現接口或者需要更好的性能的時候就需要考慮使用CGLIB實現動態Proxy
JDK動態代理:
1.目標類: Service層
2.切面類: 使用JDK動態代理對Service層代碼增強
3.工廠類: 獲得proxy對象
CGLIB字節碼增強動態代理:
原理: cglib動態生成一個代理類的子類, 子類重寫代理類的所有不是final的方法, 在子類中采用方法攔截技術攔截所有父類的方法調用, 順勢織入切面邏輯, 實現AOP, 它比JDK動態代理要快.
其操作流程與JDK動態代理一致.
下面只給出靜態代理工廠的代碼:
cglib的整個流程與JDK的動態代理都是一樣的, 就在底層處理接口和加載字節碼文件有區別
AOP聯盟通知類型
AOP聯盟定義Advice規范, 編寫Advice代碼需實現Advice接口.
Spring按照Advice在目標類中方法的連接點的位置, 分為5類
AOP聯盟的代理實現
- 使用Spring提供的ProxyFactoryBean模擬代理過程, 實現Spring的AOP:
使用環繞型通知進行演示(目標類與前面的一樣):
1.導入aop, aopalliance jar包 2.切面類(MyAspect)實現MethodInterceptor接口 3.實現MethodInterceptor中invoke方法, 手動織入橫向代碼 4.在applicationContext.xml中配置, 使用Spring提供的ProxyFactoryBean對目標類實現代理演示代碼:
切面類: public class MyAspect implements MethodInterceptor{@Overridepublic Object invoke(MethodInvocation mi) throws Throwable {System.out.println("前");//手動執行目標方法Object obj = mi.proceed();System.out.println("后");//返回目標方法執行的返回值return obj;} }配置applicationContext.xml:<!-- 獲得目標類對象 --><bean id="userService" class="com.f_aop.methodInterceptor.UserServiceImpl"></bean><!-- 創建切面類 --><bean id="myAspect" class="com.f_aop.methodInterceptor.MyAspect"></bean><!-- 創建代理類, 使用Spring配備的代理工廠 --><bean id="proxyService" class="org.springframework.aop.framework.ProxyFactoryBean"><!-- 指定接口 --><property name="interfaces" value="com.f_aop.methodInterceptor.UserService"></property><!-- 確定目標類對象 --><property name="target" ref="userService"></property> <!-- 確定Aspect, 由于interceptorNames的形參值是String[], 所以使用value, 而非ref --><property name="interceptorNames" value="myAspect"></property><property name="optimize" value="true"></property></bean>測試方法:@Testpublic void f1(){String XMLPATH="com/f_aop/methodInterceptor/applicationContext.xml";ApplicationContext applicationContext = new ClassPathXmlApplicationContext(XMLPATH);//使用proxyService, 而非userService//通過代理對象執行AdviceUserService userService = (UserService) applicationContext.getBean("proxyService");userService.addUser();userService.updateUser();userService.deleteUser();}運行結果: 前 add User 后 前 update User 后 前 delete User 后applicationContext.xml中創建代理類標簽詳解
ProxyFactoryBean: Spring的代理工廠,生成代理對象interfaces: 目標類實現的接口, 多個值使用<array><value>確定每個值單個值的時候直接使用valuetarget: 確定目標類interceptorNames: 確定切面類的名稱, 類型為String[], 使用value, 切記不使用refoptimize: 強制底層使用cglib當沒有設置optimize的值時:Spring自動判斷, 沒有接口使用cglib, 有接口使用jdk顯式設置optimize, 如果聲明optimize=true,無論是否有接口,都采用cglib上面這種代理實現, 是在applicationContext.xml配置文件中模擬代理工廠產生代理對象, 在測試函數中獲得是容器產生的代理對象proxyService.
- 利用AspectJ簡化Spring中ProxyFactoryBean的配置:
使用環繞型通知進行演示, 編寫流程:
下面使用代碼演示, 因為只需修改配置文件與測試類, 只給出配置文件代碼:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><bean id="userService" class="com.f_aop.aspectJ.UserServiceImpl"></bean><!-- 創建切面類 --><bean id="myAspect" class="com.f_aop.aspectJ.MyAspect"></bean><!-- 配置特殊切面 --><!-- proxy-target-class配置是否使用cglib --><aop:config proxy-target-class="true"><aop:pointcut id="myPointCut" expression="execution(* com.f_aop.aspectJ.*.*(..))"/><aop:advisor advice-ref="myAspect" pointcut-ref="myPointCut"/></aop:config> </beans><!--aop:config: 配置AOPproxy-target-class: 配置是否強行使用cglib, 效果與前面的optimize一樣pointcut: 配置切入點.expression: 配置切入點表達式,用于獲得目標類中需要增強的目標方法.advisor: 配置切入點與切面類, 指明哪些方法需要增強.advice-ref: 切入類對象引用.pointcut-ref: 切入點引用. -->相比于Spring提供的ProxyFactoryBean, AspectJ更加便捷.
AspectJ詳解
AspectJ是基于Java的AOP框架, 用于自定義AOP開發.
- 切入點表達式
用于描述目標類中的目標方法, 指定哪些方法可作為切入點.
下面說明切入點表達式寫法:
- AspectJ通知類型
與AOP聯盟一樣, AspectJ也定義了多種通知類型.
AspectJ總共6中通知類型:
1.before: 前置通知,用于校驗數據在目標方法之前執行, 若拋出異常, 組織目標方法運行. 2.afterReturning: 后置通知,常規數據處理目標方法執行后執行, 可獲得目標方法的返回值.目標方法出現異常, 方法不執行. 3.around: 環繞通知目標方法前后, 可阻止目標方法執行, 必須手動執行目標方法. 4.afterThrowing: 拋出異常通知目標方法出現異常后執行, 沒有出現異常就不執行. 5.after: 最終通知, 資源回收, 類似finally方法方法執行完, 無論方法中是否出現異常, 都將執行.環繞通知與其他通知之間的聯系: try{//前置: before//手動執行目標方法//后置: afterReturning } catch(){//捕獲異常: afterThrowing } finally{//最終通知: after }從上面看出, 完全可以使用環繞通知模擬前置通知, 后置通知, 環繞通知結合AfterThrowing, After實現AOP.
aop標簽對應的通知類型種類
使用AOP聯盟進行切面類編寫, 需要定義通知類型, 切面類必須實現特定接口(MethodInterceptor), 然后為目標方法添加增強代碼, 相比于AOP聯盟, AspectJ只要定義切面類, 增強代碼的使用完全交給配置文件, 避免代碼污染, 簡化操作.
使用AspectJ實現SpringAOP
基于xml配置通知類型的開發流程:
1.導入AOP聯盟, AspectJ, AOP依賴, Aspect規范 jar包.
2.編寫目標類: 接口與實現類.
3.編寫切面類: 編寫AspectJ的通知類型方法, 方法名任意, 無需實現什么接口.
4.配置xml: 配置通知類型.
5.測試.
下面給出演示代碼, 代碼中已經給出注釋加以說明(若有不懂請在評論區留言):
目標類 ( 接口與實現類 ): public interface UserService {void addUser();void updateUser();void deleteUser(); } public class UserServiceImpl implements UserService {public void addUser() {System.out.println("add User");}public void updateUser() {System.out.println("update User");}public void deleteUser() {System.out.println("delete User");} }切面類: package com.f_aop.aspectJFinal;import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint;public class MyAspect{// 測試前置通知與后置通知 // public void myBefore(JoinPoint jPoint){ // System.out.println("前置通知"+jPoint.getSignature().getName()); // } // // public void myAfterReturning(JoinPoint jPoint, Object ret){ // System.out.println("后置通知"+jPoint.getSignature().getName()+"--"+ret); // }public Object myAround(ProceedingJoinPoint joinPoint) throws Throwable{System.out.println("前置通知");//手動執行目標方法Object obj = joinPoint.proceed(); // 環繞通知與拋出異常通知的測試結果: // int i = 1/0; // 前置通知 // add User // 拋出異常通知/ by zero // 最終通知System.out.println("后置通知");return obj;}public void myAfterThrowing(JoinPoint jPoint, Throwable e){System.out.println("拋出異常通知"+e.getMessage());}public void myAfter(JoinPoint jPoint){System.out.println("最終通知");} }applicationContext.xml: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 創建目標類對象 --><bean id="userService" class="com.f_aop.aspectJFinal.UserServiceImpl"></bean><!-- 創建切面類 --><bean id="myAspect" class="com.f_aop.aspectJFinal.MyAspect"></bean><!-- 使用 config 配置AspectJ的AOP --><aop:config><!-- 聲明切入面 --><aop:aspect ref="myAspect"><!-- 配置目標方法的切入點 --><aop:pointcut id="myPointCut" expression="execution(* com.f_aop.aspectJFinal.UserServiceImpl.*(..))"/><!-- 配置通知類型的時候, method代表切入類方法, pointcut-ref代表目標類切入點.二者結合的意思就是目標類中哪些切入點需要切入方法進行增強.--><!-- 前置通知 <aop:before method="myBefore" pointcut-ref="myPointCut"/>后置通知, returning用于接收目標方法執行完后的返回值<aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut" returning="ret"/>--><!-- 拋出異常通知要配合環繞通知使用, 環繞通知拋出的異常使用拋出異常通知接收 --><aop:around method="myAround" pointcut-ref="myPointCut"/><!-- 拋出異常, throwing="e" 代表執行目標方法后,可能會拋出的異常通過 e 進行接收 --><aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e"/><!-- 最終通知 --><aop:after method="myAfter" pointcut-ref="myPointCut"/></aop:aspect></aop:config> </beans>測試方法:@Testpublic void f1(){String XMLPATH="com/f_aop/aspectJFinal/applicationContext.xml";ApplicationContext applicationContext = new ClassPathXmlApplicationContext(XMLPATH);UserService userService = (UserService) applicationContext.getBean("userService");//測試AOPuserService.addUser();userService.updateUser();userService.deleteUser();}基于注解的通知類型開發流程:
1.在剛開始配置注解的時候, 可以按照 xml 中bean, aop的配置信息來給類/屬性添加注解, 這樣不容易把邏輯搞混.
2.測試, 其實整個開發過程與 xml 配置沒什么區別, 都是一樣的, 只是形式上有區別.
在給各種類添加注解之間, 一定要牢記:
1.在 xml 配置文件中添加掃描, 掃描注解類:<context:component-scan base-package="com.demo.aspectJAnnotation"></context:component-scan> 2.確定AOP注解生效:<aop:aspectj-autoproxy></aop:aspectj-autoproxy>AspectJ中通知類型的注解種類:
1.@Aspect 聲明切面類, 不需要指定切面類名稱.等同于<aop:aspect ref="myAspect">, 一般與 @Component 結合使用, Component代表myAspect對象2.@Pointcut("execution(* com.f_aop.aspectJFinalAnnotation.UserServiceImpl.*(..))")聲明公共切入點, 通過"方法名"獲得切入點引用.等同于<aop:pointcut id="myPointCut" expression="execution(* com.f_aop.aspectJFinalAnnotation.UserServiceImpl.*(..))"/>2.@Before(value="execution(* com.demo..service.*.*(..))")前置通知, 直接添加在切面類方法前.等同于<aop:before method="myBefore" pointcut-ref="myPointCut"/>或者上面 @Before 也可寫做 @Before(value="myPointCut()") myPointCut是方法名此時要先在切面類中聲明公共切入點方法: @Pointcut("execution(* com.f_aop.aspectJFinalAnnotation.UserServiceImpl.*(..))")private void myPointCut(){}這樣寫的作用就是為了少寫代碼, 避免在多個切面類通知方法前都要加execution=(...).并且如果切入點表達式寫錯了, 也很難排查問題.(不懂請看下面的演示代碼)3.@AfterReturning(value="myPointCut()", returning="ret")后置通知, 直接添加在后置通知方法前.等同于<aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut" returning="ret"/>ret表示接收的返回值名稱, 含有與標簽中的ret一樣.4.@Around("myPointCut()")環繞通知, 添加在環繞方法前面.等同于<aop:around method="myAround" pointcut-ref="myPointCut"/>5.@AfterThrowing(value="myPointCut()", throwing="e")拋出異常通知, 添加在拋出異常通知方法前.等同于<aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e"/>6.@After("myPointCut()")最終通知, 添加在最終通知之前.等同于<aop:after method="myAfter" pointcut-ref="myPointCut"/>接下來給出演示代碼:
目標類: package com.f_aop.aspectJFinalAnnotation; import org.springframework.stereotype.Service;//生成UserService的bean: userService @Service("userService") public class UserServiceImpl implements UserService {public void addUser() {System.out.println("add User");}public void updateUser() {System.out.println("update User");}public void deleteUser() {System.out.println("delete User");} }xml 配置文件 applicationContext.xml: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:aop="http://www.springframework.org/schema/aop"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><!-- 掃描注解類 --><context:component-scan base-package="com.f_aop.aspectJFinalAnnotation"></context:component-scan><!-- 確定AOP注解生效 --><aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>切面類: package com.f_aop.aspectJFinalAnnotation;import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component;//獲得切面類Bean @Component //聲明切面類 @Aspect //由于二者都修飾同一個類, 所以不加idpublic class MyAspect{//直接設置切入點, 不使用自定義的公共切入點 // @Before("execution(* com.f_aop.aspectJFinalAnnotation.UserServiceImpl.*(..))") // public void myBefore(JoinPoint jPoint){ // System.out.println("前置通知"+jPoint.getSignature().getName()); // }// 設置切入點, 通過returning獲得返回值 // @AfterReturning(value="myPointCut()", returning="ret) // public void myAfterReturning(JoinPoint jPoint, Object ret){ // System.out.println("后置通知"+jPoint.getSignature().getName()+"--"+ret); // }@Pointcut("execution(* com.f_aop.aspectJFinalAnnotation.UserServiceImpl.*(..))")private void myPointCut(){//配置空方法,用于聲明公共切入點}@Around("myPointCut()")public Object myAround(ProceedingJoinPoint joinPoint) throws Throwable{System.out.println("前置通知");//手動執行目標方法Object obj = joinPoint.proceed();int i = 1/0; // 前置通知 // add User // 拋出異常通知/ by zero // 最終通知System.out.println("后置通知");return obj;}@AfterThrowing(value="myPointCut()", throwing="e")public void myAfterThrowing(JoinPoint jPoint, Throwable e){System.out.println("拋出異常通知"+e.getMessage());}@After("myPointCut()")public void myAfter(JoinPoint jPoint){System.out.println("最終通知");}}JdbcTemplate
主要是Spring提供操作Jdbc的工具類, 類似于DBUtils, 依賴于連接池DataSource.
開發流程:
1.創建數據庫表.
2.導入 C3P0/DBCP連接池, mysql驅動, Spring-jdbc, spring-tx, 等Spring其余核心jar包
3.創建JavaBean, 配置數據源
4.編寫DAO層
5.配置applicationContext.xml文件, 使用Spring管理DAO層對象
6.測試.
下面使用C3P0進行測試, DBCP與C3P0是一樣的, 只是在配置數據庫連接上有所不同.
數據庫:create table t_user(id int primary key,name varchar(10);password varchar(10) );User: public class User implements Serializable{private Integer id;private String name;private String password;//構造方法, get/set方法 }UserDao: public class UserDao{//使用Spring提供的JdbcTemplate, 在Spring配置文件中注入private JdbcTemplate jdbcTemplate;public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}//查詢所有的Userpublic List<User> findAll(){return jdbcTemplate.query("select * from t_user", ParameterizedBeanPropertyRowMapper.newInstance(User.class));}//演示查詢單個Userpublic void find(User user){String sql = "select * from t_user"; // String sql = "select * from t_user where id=?"; // Object findUserId = user.getId();//查詢條件信息就加Object參數 // List<Map<String,Object>> queryForList = jdbcTemplate.queryForList(sql, findUserId);//查詢所有直接不加參數List<Map<String, Object>> queryForList = jdbcTemplate.queryForList(sql);for (Map<String, Object> map : queryForList) {//一個map中儲存著一個學生的信息for (Map.Entry<String, Object> m : map.entrySet()) {System.out.println(m.getKey()+"--"+m.getValue());}}//匿名內部類, 實現RowMapper接口, 自定義查詢結果類型/*List<User> queryUsers = jdbcTemplate.query(sql, new RowMapper<User>(){@Overridepublic User mapRow(ResultSet resultSet, int i) throws SQLException {return new User(resultSet.getInt("id"), resultSet.getString("name"), resultSet.getString("password"));}});同理帶條件查詢, jdbcTemplate.query(sql, rowMapper, i)*/} // @Testpublic void insert(User user){String sql = "insert into t_user(id, name, password)values(?, ?, ?)";//Object數組中參數對應sql中"?"的順序Object[] obj = new Object[]{user.getId(), user.getName(), user.getPassword()};//受影響行數int row = jdbcTemplate.update(sql, obj);System.out.println("受影響行數"+row);}public void update(User user){String sql = "update t_user set name=? where id=?";Object[] obj = new Object[]{user.getName(), user.getId()};//受影響行數int row = jdbcTemplate.update(sql, obj);System.out.println("受影響行數"+row);}public void delete(User user){String sql = "delete from t_user where id=?";Object[] obj = new Object[]{user.getId()};//受影響行數int row = jdbcTemplate.update(sql, obj);System.out.println("受影響行數"+row);} }applicationContext.xml配置文件: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><!-- 創建數據源Bean --><bean id="comboPooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"><property name="driverClass" value="com.mysql.jdbc.Driver"></property><property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property><property name="user" value="root"></property><property name="password" value="12345"></property></bean><!-- 創建模板Bean, 向JdbcTemplate中注入數據源 --><bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"><property name="dataSource" ref="comboPooledDataSource"></property></bean><!-- 配置DAO, 向Dao注入JdbcTemplate Bean --><bean id="userDao" class="com.g_jdbc.c3p0.UserDao"><property name="jdbcTemplate" ref="jdbcTemplate"></property></bean></beans>測試方法: @Testpublic void f1(){String xml = "com/g_jdbc/c3p0/applicationContext.xml";ApplicationContext application = new ClassPathXmlApplicationContext(xml);UserDao bean = (UserDao) application.getBean("userDao");//查詢List<User> list = bean.findAll();for (User user : list) {System.out.println(user);}}這里給出DBCP的配置文件寫法, 用于區別C3P0:
二者只有創建數據源有所不同
使用JdbcTemplate還有這種簡化寫法:
Spring提供JdbcDaoSupport, 用于封裝JdbcTemplate, 使得DAO不用關心JdbcTemplate模板的處理.
1.讓Dao繼承JdbcDaoSupport, 創建模板的過程交給父類處理.
public class UserDao extends JdbcDaoSupport{//使用JdbcDaoSupport通過get方法獲得數據源,JdbcTemplate模板public List<User> findAll(){String sql = "select * from t_user";//使用getJdbcTemplate()獲取父類創建好的模板, 然后再執行查詢操作return this.getJdbcTemplate().query(sql, ParameterizedBeanPropertyRowMapper.newInstance(User.class));}}2.修改配置文件.
<bean id="comboPooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"><property name="driverClass" value="com.mysql.jdbc.Driver"></property><property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property><property name="user" value="root"></property><property name="password" value="12345"></property></bean><!-- 配置DAO使用JdbcDaoSupport, 底層已經封裝了JdbcTemplate, 只需配置DataSource即可--><bean id="userDao" class="com.g_jdbc.jdbcdaosupport.UserDao"><property name="dataSource" ref="comboPooledDataSource"></property></bean>3.測試:
//測試方法和C3P0, DBCP的測試方法是一樣的 @Testpublic void f1(){String xml = "com/g_jdbc/jdbcdaosupport/applicationContext.xml";ApplicationContext application = new ClassPathXmlApplicationContext(xml);UserDao bean = (UserDao) application.getBean("userDao");List<User> list = bean.findAll();for (User user : list) {System.out.println(user);}}總結
以上是生活随笔為你收集整理的SpringAOP描述及实现_AspectJ详解_基于注解的AOP实现_SpringJdbcTemplate详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: arial字体可以商用吗_【工作总结】莫
- 下一篇: string最大容量_string初步使