• 个人博客网站(四)


    简介:

    这节聊一聊项目的后端,之前也讲过,因为对前端不熟悉,所以在前端花了太多的时间,导致到后端开发的时候搞的人有点疲,所以很多东西从简了,很多细节东西没有考虑,只想着把基本功能做出来就好了。框架选择的是现在比较流行的Springboot+Mybatis+Tomcat+MySQLSpringboot是在Spring的基础上做了集成和配置简化,使用起来超级舒服。

    一、搭建Springboot框架

    1. 框架简介

    Springboot是现在非常火热的JavaEE框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程,为什么这么说呢,看一下他的几个特点:

    >>> 嵌入的Tomcat,无需部署WAR文件

    >>> 简化Maven配置

    >>> 自动配置Spring

    >>> 提供生产就绪型功能,如指标,健康检查和外部配置

    >>> XML也没有配置要求比较灵活

    然后我们选择Maven来管理我们的JAR包,这个没什么好说的,以后我们用一个pom文件就可以走天下了,不用在背着沉重的JAR包到处跑。

    2. Springboot项目初始化

    快速创建Springboot可以使用 https://start.spring.io/网页版生成项目然后导入Eclipse中或者使用IDEA创建,这里我们选择前者:首先我们访问 https://start.spring.io/ ,然后按照顺序选择填写:

    1. 项目管理工具,现在一般使用Maven来管理

    2. 项目开发语言

    3. 这是Springboot版本

    4. 项目唯一标识,可以用来确定下面的包名

    5. 项目名称

    6. 项目包名、打包方式、java版本

    7. 项目依赖:下面勾选对应的依赖,这里我只选了web可以视自己情况而定

    8. 生成项目

    这里直接生成yytf.zip包,我们解压后用eclipse使用导入maven项目方式导入项目(这里要注意刚导入后项目目录不是下图中这种结构,要等一会jar包下载完成后才会形成如下图结构),如下图目录就是导入后的目录结构,而pom文件里的参数也是我们之前在网页填写的对号入座。

    然后我们找到项目的启动文件,这里是BlogApplication.java,右键run as使用java application运行,控制台出现Started BlogApplication in 2.101 seconds (JVM running for 2.455)springboot启动成功。

    二、项目配置

    1. 配置pom.xml引入需要的JAR

    <dependencies>
        <!-- springboot启动依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
    
            <!-- 排除Springboot自带的日志工具 -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
                <!-- 使用外部TOMCAT方式一:排除Springboot自带的tomcat -->
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
            
        <!-- 使用外部TOMCAT方式二:添加provided -->
        <!-- provided:已提供依赖范围。使用此依赖范围的Maven依赖,对于编译和测试classpath有效,但在运行时候无效 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
            
        <!-- springboot整合log4j -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
            
        <!-- springboot整合mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
            
        <!-- springboot连接mysql驱动依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>
            
        <!-- springboot整合jwt的token验证 -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>2.2.0</version>
        </dependency>
    </dependencies>
        
    <!-- generate工具生成dao,mapper,xml -->
    <build>
        <finalName>blog</finalName>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <!-- 此配置不可缺,否则mybatis的Mapper.xml将会丢失 -->
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <!-- 指定资源的位置 -->
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
    </build>

    2. 配置外部tomcat部署项目

    2.1 maven依赖配置

    首先我们把打包方式改成war,在pom.xml<packaging>jar</packaging>修改成<packaging>war</packaging>,在上面的pom.xml中已经写清楚两种使用外部tomcat方式:第一种是去掉springboot内置的tomcat,第二中时在运行时使内部tomcat失效

    2.2 修改启动类

    Application.java中继承SpringBootServletInitializer并重写configure方法

    package com.yytf;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.builder.SpringApplicationBuilder;
    import org.springframework.boot.web.support.SpringBootServletInitializer;
    import org.springframework.scheduling.annotation.EnableScheduling;
    
    @MapperScan("com.yytf.dao")
    @SpringBootApplication
    @EnableScheduling
    public class Application extends SpringBootServletInitializer{
        
        @Override
        protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
            return application.sources(Application.class);
        }
        
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    
    }

    3.配置generatorConfig.xml自动生成vo,dao,mapper

    3.1pom.xml中修改build标签,代码上面已贴出

    3.2 首先安装eclipse插件:Mybatis Generator 1.3.5,Eclipse => Help => Eclipse Marketplace 搜索mybatis安装最新版本

    3.3 生成generatorConfig.xml文件

    New一个文件,然后选择mybatis generator configuration file,然后选择到generatorConfig.xml文件要保存的位置,例如这里保存到logsrcmain esources下面

    3.4 配置generatorConfig.xml文件,这里注意mysql驱动包要使用绝对路径,targetProject为生成文件位置,容易出错:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
    <generatorConfiguration>
    
        <!-- 必须使用绝对路径 -->
        <classPathEntry location="D:ProjectBlogRepositorymysqlmysql-connector-java5.1.6mysql-connector-java-5.1.6.jar"/>
        
        <context id="blogTable" targetRuntime="MyBatis3">
    
                <commentGenerator>
                    <!-- 是否去除自动生成的注释 true:是 : false:否 -->
                    <property name="suppressAllComments" value="true" />
                </commentGenerator>
    
                <!--数据库连接的信息:驱动类、连接地址、用户名、密码 -->
                <!-- Oracle连接方式 -->
                <!-- <jdbcConnection driverClass="oracle.jdbc.OracleDriver" connectionURL="jdbc:oracle:thin:@000.000.000.000:1521:orcl" 
                    userId="root" password="********"> </jdbcConnection> -->
                <!-- Mysql连接方式 -->
                <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                    connectionURL="jdbc:mysql://000.000.000.000:3306/blog?useUnicode=true&amp;characterEncoding=UTF-8" userId="root"
                    password="********" />
    
                <!-- 默认false,把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer,为 true时把JDBC DECIMAL 
                    和 NUMERIC 类型解析为java.math.BigDecimal -->
                <javaTypeResolver>
                    <property name="forceBigDecimals" value="false" />
                </javaTypeResolver>
    
                <!-- targetProject:生成PO类的位置 -->
                <javaModelGenerator targetPackage="com.yytf.vo"
                    targetProject="blog/src/main/java">
                    <!-- enableSubPackages:是否让schema作为包的后缀 -->
                    <property name="enableSubPackages" value="false" />
                    <!-- 从数据库返回的值被清理前后的空格 -->
                    <property name="trimStrings" value="true" />
                </javaModelGenerator>
    
                <!-- targetProject:mapper映射文件生成的位置 -->
                <sqlMapGenerator targetPackage="mapper"
                    targetProject="blog/src/main/resources">
                    <!-- enableSubPackages:是否让schema作为包的后缀 -->
                    <property name="enableSubPackages" value="false" />
                </sqlMapGenerator>
    
                <!-- targetPackage:mapper接口生成的位置 -->
                <javaClientGenerator type="XMLMAPPER"
                    targetPackage="com.yytf.dao" targetProject="blog/src/main/java">
                    <!-- enableSubPackages:是否让schema作为包的后缀 -->
                    <property name="enableSubPackages" value="false" />
                </javaClientGenerator>
    
                <!-- 指定数据库表 -->
                <!-- 文章表 -->
                <table tableName="article" enableCountByExample="false"
                    enableUpdateByExample="false" enableDeleteByExample="false"
                    enableSelectByExample="false" selectByExampleQueryId="false">
                </table>
    
                <!-- 分类表 -->
                <table tableName="category"
                    enableCountByExample="false" enableUpdateByExample="false"
                    enableDeleteByExample="false" enableSelectByExample="false"
                    selectByExampleQueryId="false">
                </table>
    
            </context>
    </generatorConfiguration>

    3.5 运行mybatis generator

    4.配置application.properties

    #springboot整合mybatis
    #实体类包
    mybatis.type-aliases-package=com.yytf.vo
    #mybatis配置文件和mapper文件位置
    mybatis.mapper-locations=classpath*:mapper/*.xml
    
    #mysql数据库连接信息
    spring.datasource.driverClassName = com.mysql.cj.jdbc.Driver
    spring.datasource.url = jdbc:mysql://000.000.000.000:3306/blogs?useUnicode=true&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8&amp
    spring.datasource.username = root
    spring.datasource.password = ********
    
    spring.datasource.max-idle=10
    spring.datasource.max-wait=10000
    spring.datasource.min-idle=5
    spring.datasource.initial-size=5
    
    
    #tomcat端口号
    server.port=8080
    server.session.timeout=10
    server.tomcat.uri-encoding=UTF-8
    server.context-path=/
    
    
    #log4j日志配置
    logging.config=classpath:log4j2.xml
    #log4j打印mybatis的sql语句
    #logging.level.com.yytf.dao=debug

    5.springboot整合mybatis

    5.1 引入依赖,pom.xml中引入:

    <!-- springboot整合mybatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.1</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.1</version>
    </dependency>
            
    <!-- springboot连接mysql驱动依赖 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.11</version>
    </dependency>

    5.2 application.properties里配置:

    #实体类包
    mybatis.type-aliases-package=com.yytf.vo
    #mybatis配置文件和mapper文件位置
    mybatis.mapper-locations=classpath*:mapper/*.xml
    #mysql数据库连接信息
    spring.datasource.driverClassName = com.mysql.cj.jdbc.Driver
    spring.datasource.url = jdbc:mysql://000.000.000.000:3306/blogs?useUnicode=true&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8&amp
    spring.datasource.username = root
    spring.datasource.password = ********

    5.3 在启动类Application.java中配置mapper扫描注解 @MapperScan("com.yytf.dao")

    6.springboot整合log4j

    6.1 pom.xml文件中添加log4jjar包依赖,首先排除内置的日志工具

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
    
            <!-- 排除Springboot自带的日志工具 -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    
        <!-- springboot整合log4j -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
    </dependencies>

    6.2 application.properties中配置指定log4j2.xml的位置

    #log4j日志配置
    logging.config=classpath:log4j2.xml
    #log4j打印mybatis的sql语句
    #logging.level.com.yytf.dao=debug

    6.3 配置log4j2.xml

    下面贴上代码:解释一下

    >>> LOG_HOME:这里的/var/log/blogs路径如果在linux就是对应的目录,如果在windows上则在当前盘符下新建一个var文件夹,例如tomcatD盘,则在D盘新建路径D:varloglogs

    >>> FILE_NAME:临时生成的日志文件名称,下面配置中第二天会被新生成的替代

    >>> Console标签:输出到控制台

    >>> RollingRandomAccessFile标签:周期性生成新的日志文件并存档

    >>> filePattern:存档的日志文件的命名规则,例如下面配置生成:D:varloglogs2018-09logs-2018-09-15-1.logi%代表1开始递增

    >>> TimeBasedTriggeringPolicy标签:生成新文件周期数,例如下面interval="1"是一天一个日志文件

    >>> SizeBasedTriggeringPolicy标签和i%是配合使用的,表示当日志文件大小超过20MB就生成新日志文件,新日志文件后缀i%就递:blogs-2018-09-15-2.log

    >>> 最后一个Logger name="com.yytf.dao"是在本地调试时把sql语句打印在控制台

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="WARN"> 
        <properties>
            <property name="LOG_HOME">/var/log/blogs</property>
            <property name="FILE_NAME">blogs</property>
            <property name="log.sql.level">info</property>
        </properties>
    
    
        <Appenders>  
            <Console name="Console" target="SYSTEM_OUT">  
                <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %l - %msg%n" />  
            </Console>
    
            <RollingRandomAccessFile name="RollingRandomAccessFile" fileName="${LOG_HOME}/${FILE_NAME}.log" filePattern="${LOG_HOME}/$${date:yyyy-MM}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.log">
                <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %l - %msg%n"/>
                <Policies>
                    <TimeBasedTriggeringPolicy interval="1"/>
                    <SizeBasedTriggeringPolicy size="10 MB"/>
                </Policies>
                <DefaultRolloverStrategy max="20"/>
            </RollingRandomAccessFile>
        </Appenders>  
    
        <Loggers>  
            <Root level="info">  
                <AppenderRef ref="Console" />
                <AppenderRef ref="RollingRandomAccessFile" />  
            </Root>
    
            <Logger name="com.yytf.dao" level="${log.sql.level}" additivity="false">
                 <AppenderRef ref="Console" />
            </Logger>
        </Loggers>  
    </Configuration>

    7.springboot使用内置定时任务

    7.1 启动类Application.java

    添加注解:@EnableScheduling

    7.2 定时任务类

    package com.yytf.util;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    @Component
    public class Timer {
        @Autowired
        EverydayTalkMapper everydayTalkMapper;
        
        @Scheduled(cron = "0 59 23 * * ?")
        public void scheduleUpdateTalkId() {
            System.out.println("======>" + System.currentTimeMillis());
        }
    }

    7.3 使用场景

    项目中每日一语需要每天变换,设置cron表达式为:cron = "0 59 23 * * ?"在每天0点前把项目中的存储每日一语的静态变量的信息更换。

    8.springboot使用JWTJava Web Token)做Token验证

    8.1 为什么要使用token验证

    >>> 因为我们的前后端是分离的,所有的请求都是跨域行为,所以sessionId一直是变化的,不能通过sessionId来获得用户信息

    >>> token的时候我们只用在第一次登录的时候查询一下数据库,然后在token的过期时间内就可以不用再与数据库用户表交互,减少IO操作。

    >>> 如果有多台服务器做负载均衡,因为token是无状态的,所以服务器之间可以共用token

    8.2 pom.xml中引入jwtjar包依赖

    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>2.2.0</version>
    </dependency>

    8.3 新建Token签发与解析验证类JavaWebToken.java注意payload里面不要存放敏感信息,例如不要存放密码等,因为该部分是对称加密,在客户端可以解密。SECRET是不能暴露出去的,相当于私钥,加密的方式由它决定。

    package com.yytf.util;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import com.auth0.jwt.JWTSigner;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.internal.com.fasterxml.jackson.databind.ObjectMapper;
    
    public class JavaWebToken {
     
            private static final String SECRET = "********";
            
            private static final String EXP = "exp";
            
            private static final String PAYLOAD = "payload";
         
            public static <T> String sign(T object, long maxAge) {
                try {
                    final JWTSigner signer = new JWTSigner(SECRET);
                    final Map<String, Object> claims = new HashMap<String, Object>();
                    ObjectMapper mapper = new ObjectMapper();
                    String jsonString = mapper.writeValueAsString(object);
                    claims.put(PAYLOAD, jsonString);
                    claims.put(EXP, System.currentTimeMillis() + maxAge);
                    return signer.sign(claims);
                } catch(Exception e) {
                    return null;
                }
            }
            
            public static<T> T unsign(String jwt, Class<T> classT) {
                final JWTVerifier verifier = new JWTVerifier(SECRET);
                try {
                    final Map<String,Object> claims= verifier.verify(jwt);
                    if (claims.containsKey(EXP) && claims.containsKey(PAYLOAD)) {
                        long exp = (Long)claims.get(EXP);
                        long currentTimeMillis = System.currentTimeMillis();
                        if (exp > currentTimeMillis) {
                            String json = (String)claims.get(PAYLOAD);
                            ObjectMapper objectMapper = new ObjectMapper();
                            return objectMapper.readValue(json, classT);
                        }
                    }
                    return null;
                } catch (Exception e) {
                    return null;
                }
            }
    }

    8.4 登录成功签发token

    loginController.java中:

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import com.yytf.util.JavaWebToken;
    import com.yytf.vo.UserVO;
    
    @Controller
    @RequestMapping(value="/login")
    public class LoginController {
        @RequestMapping(value="/login",method=RequestMethod.POST)
        @ResponseBody
        public ResponseUtil login(@RequestBody UserVO userVO) {
            
            UserVO resultUserVO = new UserVO();
            String userName = PropertiesUtil.getProperty("userName");
            String password = PropertiesUtil.getProperty("password");
            if(userVO.getUserName().equals(userName) && userVO.getPassword().equals(password)) {
                Map<String, UserVO> data = new HashMap<String, UserVO>();
                resultUserVO.setUserName(userName);
                String token = JavaWebToken.sign(resultUserVO, 1000L * 3600L * 3L);
                data.put("user", resultUserVO);
                return ResponseUtil.loginSuccess(data, token);
            }
            return ResponseUtil.fail("login fail");
        }
    }

    8.5 访问怎删改查接口验证是否登录

    前台传入token,然后验证token是否正确,如果不正确则返回前台登录界面

    public class CheckIsLogin {
    
        /**
         * 检查是否有操作权限
         * @param token
         * @return
         */
        public static boolean checkIsLogin(String token) {
            UserVO userVO = JavaWebToken.unsign(token, UserVO.class);
            String userName = PropertiesUtil.getProperty("userName");
            if(userVO != null && userVO.getUserName().equals(userName)) {
                return true;
            }
            return false;
        }
    }

    9.springboot使用CORS配置解决跨域问题

    package com.yytf.util;
    
    import org.springframework.boot.web.servlet.FilterRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    import org.springframework.web.filter.CorsFilter;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
    
    /**
     * 全局CORS配置解决跨域问题
    */
    @Configuration
    public class CORSConfiguration {
          /*方式一只支持一个域名配置*/
    //      @Bean
    //    public FilterRegistrationBean corsFilter() {
    //        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    //        CorsConfiguration config = new CorsConfiguration();
    //        config.setAllowCredentials(true);
    //        // 设置你要允许的网站域名,如果全允许则设为 *
    //        config.addAllowedOrigin("*");
    ////       config.addAllowedOrigin("http://test.com");
    //        // 如果要限制 HEADER 或 METHOD 请自行更改
    //        config.addAllowedHeader("*");
    //        config.addAllowedMethod("*");
    //        source.registerCorsConfiguration("/**", config);
    //        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
    //        // 这个顺序很重要哦,为避免麻烦请设置在最前
    //        bean.setOrder(0);
    //        return bean;
    //    }
        
        /*方式二支持多个域名*/
        @Bean
        public WebMvcConfigurer corsConfigurer() {
            return new WebMvcConfigurerAdapter() {
                @Override
                public void addCorsMappings(CorsRegistry registry) {
                    registry.addMapping("/**")
            .allowedOrigins("http://test.com","http://www.test.com")
                            .allowedHeaders("*")
                            .allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS")
                            .allowCredentials(true).maxAge(3600);
                }
            };
        }
    
    }

    三、Debug记录

    1.数据正常返回,前台却报404错误,因为后台方法缺少@ResponseBody注解

    2.mybatisforeach标签使用参数中间缺少逗号

    3.使用mybatis-generator生成dao,mapper时报错

    4.使用mybatis-generator生成dao,mapper时报错

    5.MySQL8以上(包含8)版本注意事项

    在前面的配置文件里代码已经贴出

    6.MapperService实例注入失败,不能被发现

    OJBK大概就这些了,剩下的都基本是苦力活就不说了,整个项目搞下来还是花了不少时间,超出了预期时间很多,不过也学到了不少东西。俗话说:纸上得来终觉浅,绝知此事要躬行,只用多写,多敲,多练我们才能进步的更快。我的自控力很差,玩起来就什么不管了,很难坚持一件事到底,所以我有时候想是不是要给自己每天规划一下要完成什么任务,要先坚持一个东西,比如写博客来锻炼自己的持久力,能做好一个,其它的慢慢就都能搞好了吧。渴望进步证明我还没有堕落到当一个咸鱼,哈哈,加油吧!

  • 相关阅读:
    go2基本类型
    go1
    android studio 使用
    ios34---GDC,dispatch_once
    ios33--线程通信
    ios33--线程安全
    ios32---线程的状态
    ios31--NSThread
    ios30---pthread, NSThread, GCD, NSOperation
    ios29--多线程
  • 原文地址:https://www.cnblogs.com/songzhen/p/my-blog-wesite-record-4.html
Copyright © 2020-2023  润新知