首页 文章 精选 留言 我的

精选列表

搜索[学习],共10000篇文章
优秀的个人博客,低调大师

源码学习之MyBatis的底层查询原理

导读 本文通过MyBatis一个低版本的bug(3.4.5之前的版本)入手,分析MyBatis的一次完整的查询流程,从配置文件的解析到一个查询的完整执行过程详细解读MyBatis的一次查询流程,通过本文可以详细了解MyBatis的一次查询过程。在平时的代码编写中,发现了MyBatis一个低版本的bug(3.4.5之前的版本),由于现在很多工程中的版本都是低于3.4.5的,因此在这里用一个简单的例子复现问题,并且从源码角度分析MyBatis一次查询的流程,让大家了解MyBatis的查询原理。 1 问题现象 1.1 场景问题复现 如下图所示,在示例Mapper中,下面提供了一个方法queryStudents,从student表中查询出符合查询条件的数据,入参可以为student_name或者student_name的集合,示例中参数只传入的是studentName的List集合 List<String> studentNames = new LinkedList<>(); studentNames.add("lct"); studentNames.add("lct2"); condition.setStudentNames(studentNames); <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap"> select * from student <where> <if test="studentNames != null and studentNames.size > 0 "> AND student_name IN <foreach collection="studentNames" item="studentName" open="(" separator="," close=")"> #{studentName, jdbcType=VARCHAR} </foreach> </if> <if test="studentName != null and studentName != '' "> AND student_name = #{studentName, jdbcType=VARCHAR} </if> </where> </select> 期望运行的结果是 select * from student WHERE student_name IN ( 'lct' , 'lct2' ) 但是实际上运行的结果是 ==> Preparing: select * from student WHERE student_name IN ( ? , ? ) AND student_name = ? ==> Parameters: lct(String), lct2(String), lct2(String) <== Columns: id, student_name, age <== Row: 2, lct2, 2 <== Total: 1 通过运行结果可以看到,没有给student_name单独赋值,但是经过MyBatis解析以后,单独给student_name赋值了一个值,可以推断出MyBatis在解析SQL并对变量赋值的时候是有问题的,初步猜测是foreach循环中的变量的值带到了foreach外边,导致SQL解析出现异常,下面通过源码进行分析验证 2 MyBatis查询原理 2.1 MyBatis架构 2.1.1 架构图 先简单来看看MyBatis整体上的架构模型,从整体上看MyBatis主要分为四大模块: 接口层:主要作用就是和数据库打交道 数据处理层:数据处理层可以说是MyBatis的核心,它要完成两个功能: 通过传入参数构建动态SQL语句; SQL语句的执行以及封装查询结果集成List<E> 框架支撑层:主要有事务管理、连接池管理、缓存机制和SQL语句的配置方式 引导层:引导层是配置和启动MyBatis 配置信息的方式。MyBatis 提供两种方式来引导MyBatis :基于XML配置文件的方式和基于Java API 的方式 2.1.2 MyBatis四大对象 贯穿MyBatis整个框架的有四大核心对象,ParameterHandler、ResultSetHandler、StatementHandler和Executor,四大对象贯穿了整个框架的执行过程,四大对象的主要作用为: ParameterHandler:设置预编译参数 ResultSetHandler:处理SQL的返回结果集 StatementHandler:处理sql语句预编译,设置参数等相关工作 Executor:MyBatis的执行器,用于执行增删改查操作 2.2 从源码解读MyBatis的一次查询过程 首先给出复现问题的代码以及相应的准备过程 2.2.1 数据准备 CREATE TABLE `student` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `student_name` varchar(255) NULL DEFAULT NULL, `age` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1; -- ---------------------------- -- Records of student -- ---------------------------- INSERT INTO `student` VALUES (1, 'lct', 1); INSERT INTO `student` VALUES (2, 'lct2', 2); 2.2.2 代码准备 1.mapper配置文件 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="mybatis.StudentDao"> <!-- 映射关系 --> <resultMap id="resultMap" type="mybatis.Student"> <id column="id" property="id" jdbcType="BIGINT" /> <result column="student_name" property="studentName" jdbcType="VARCHAR" /> <result column="age" property="age" jdbcType="INTEGER" /> </resultMap> <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap"> select * from student <where> <if test="studentNames != null and studentNames.size > 0 "> AND student_name IN <foreach collection="studentNames" item="studentName" open="(" separator="," close=")"> #{studentName, jdbcType=VARCHAR} </foreach> </if> <if test="studentName != null and studentName != '' "> AND student_name = #{studentName, jdbcType=VARCHAR} </if> </where> </select> </mapper> 2.示例代码 public static void main(String[] args) throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); //1.获取SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2.获取对象 SqlSession sqlSession = sqlSessionFactory.openSession(); //3.获取接口的代理类对象 StudentDao mapper = sqlSession.getMapper(StudentDao.class); StudentCondition condition = new StudentCondition(); List<String> studentNames = new LinkedList<>(); studentNames.add("lct"); studentNames.add("lct2"); condition.setStudentNames(studentNames); //执行方法 List<Student> students = mapper.queryStudents(condition); } 2.2.3 查询过程分析 1.SqlSessionFactory的构建 先看SqlSessionFactory的对象的创建过程 //1.获取SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 代码中首先通过调用SqlSessionFactoryBuilder中的build方法来获取对象,进入build方法 public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); } 调用自身的build方法 图1 build方法自身调用调试图例 在这个方法里会创建一个XMLConfigBuilder的对象,用来解析传入的MyBatis的配置文件,然后调用parse方法进行解析 图2 parse解析入参调试图例 在这个方法中,会从MyBatis的配置文件的根目录中获取xml的内容,其中parser这个对象是一个XPathParser的对象,这个是专门用来解析xml文件的,具体怎么从xml文件中获取到各个节点这里不再进行讲解。这里可以看到解析配置文件是从configuration这个节点开始的,在MyBatis的配置文件中这个节点也是根节点 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties> <property name="dialect" value="MYSQL" /> <!-- SQL方言 --> </properties> 然后将解析好的xml文件传入parseConfiguration方法中,在这个方法中会获取在配置文件中的各个节点的配置 图3 解析配置调试图例 以获取mappers节点的配置来看具体的解析过程 <mappers> <mapper resource="mappers/StudentMapper.xml"/> </mappers> 进入mapperElement方法 mapperElement(root.evalNode("mappers")); 图4 mapperElement方法调试图例 看到MyBatis还是通过创建一个XMLMapperBuilder对象来对mappers节点进行解析,在parse方法中 public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } 通过调用configurationElement方法来解析配置的每一个mapper文件 private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); } } 以解析mapper中的增删改查的标签来看看是如何解析一个mapper文件的 进入buildStatementFromContext方法 private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } } 可以看到MyBatis还是通过创建一个XMLStatementBuilder对象来对增删改查节点进行解析,通过调用这个对象的parseStatementNode方法,在这个方法里会获取到配置在这个标签下的所有配置信息,然后进行设置 图5 parseStatementNode方法调试图例 解析完成以后,通过方法addMappedStatement将所有的配置都添加到一个MappedStatement中去,然后再将mappedstatement添加到configuration中去 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); 可以看到一个mappedstatement中包含了一个增删改查标签的详细信息 图7 mappedstatement对象方法调试图例 而一个configuration就包含了所有的配置信息,其中mapperRegistertry和mappedStatements 图8 config对象方法调试图例 具体的流程 图9 SqlSessionFactory对象的构建过程 图9 SqlSessionFactory对象的构建过程 2.SqlSession的创建过程 SqlSessionFactory创建完成以后,接下来看看SqlSession的创建过程 SqlSession sqlSession = sqlSessionFactory.openSession(); 首先会调用DefaultSqlSessionFactory的openSessionFromDataSource方法 @Override public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); } 在这个方法中,首先会从configuration中获取DataSource等属性组成对象Environment,利用Environment内的属性构建一个事务对象TransactionFactory private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } 事务创建完成以后开始创建Executor对象,Executor对象的创建是根据 executorType创建的,默认是SIMPLE类型的,没有配置的情况下创建了SimpleExecutor,如果开启二级缓存的话,则会创建CachingExecutor public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; } 创建executor以后,会执行executor = (Executor) interceptorChain.pluginAll(executor)方法,这个方法对应的含义是使用每一个拦截器包装并返回executor,最后调用DefaultSqlSession方法创建SqlSession 图10 SqlSession对象的创建过程 3.Mapper的获取过程 有了SqlSessionFactory和SqlSession以后,就需要获取对应的Mapper,并执行mapper中的方法 StudentDao mapper = sqlSession.getMapper(StudentDao.class); 在第一步中知道所有的mapper都放在MapperRegistry这个对象中,因此通过调用 org.apache.ibatis.binding.MapperRegistry#getMapper方法来获取对应的mapper public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } 在MyBatis中,所有的mapper对应的都是一个代理类,获取到mapper对应的代理类以后执行newInstance方法,获取到对应的实例,这样就可以通过这个实例进行方法的调用 public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public Map<Method, MapperMethod> getMethodCache() { return methodCache; } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } } 获取mapper的流程为 图11 Mapper的获取过程 4.查询过程 获取到mapper以后,就可以调用具体的方法 //执行方法 List<Student> students = mapper.queryStudents(condition); 首先会调用 org.apache.ibatis.binding.MapperProxy#invoke的方法,在这个方法中,会调用org.apache.ibatis.binding.MapperMethod#execute public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; } 首先根据SQL的类型增删改查决定执行哪个方法,在此执行的是SELECT方法,在SELECT中根据方法的返回值类型决定执行哪个方法,可以看到在select中没有selectone单独方法,都是通过selectList方法,通过调用 org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)方法来获取到数据 @Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } 在selectList中,首先从configuration对象中获取MappedStatement,在statement中包含了Mapper的相关信息,然后调用 org.apache.ibatis.executor.CachingExecutor#query()方法 图12 query()方法调试图示 在这个方法中,首先对SQL进行解析根据入参和原始SQL,对SQL进行拼接 图13 SQL拼接过程代码图示 调用MapperedStatement里的getBoundSql最终解析出来的SQL为 图14 SQL拼接过程结果图示 接下来调用 org.apache.ibatis.parsing.GenericTokenParser#parse对解析出来的SQL进行解析 图15 SQL解析过程图示 最终解析的结果为 图16 SQL解析结果图示 最后会调用SimpleExecutor中的doQuery方法,在这个方法中,会获取StatementHandler,然后调用 org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize这个方法进行参数和SQL的处理,最后调用statement的execute方法获取到结果集,然后 利用resultHandler对结进行处理 图17 SQL处理结果图示 查询的主要流程为 图18 查询流程处理图示 5.查询流程总结 总结整个查询流程如下 图19 查询流程抽象 2.3 场景问题原因及解决方案 2.3.1 个人排查 这个问bug出现的地方在于绑定SQL参数的时候再源码中位置为 @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } 由于所写的SQL是一个动态绑定参数的SQL,因此最终会走到 org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql这个方法中去 public BoundSql getBoundSql(Object parameterObject) { BoundSql boundSql = sqlSource.getBoundSql(parameterObject); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings == null || parameterMappings.isEmpty()) { boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject); } // check for nested result maps in parameter mappings (issue #30) for (ParameterMapping pm : boundSql.getParameterMappings()) { String rmId = pm.getResultMapId(); if (rmId != null) { ResultMap rm = configuration.getResultMap(rmId); if (rm != null) { hasNestedResultMaps |= rm.hasNestedResultMaps(); } } } return boundSql; } 在这个方法中,会调用 rootSqlNode.apply(context)方法,由于这个标签是一个foreach标签,因此这个apply方法会调用到 org.apache.ibatis.scripting.xmltags.ForEachSqlNode#apply这个方法中去 @Override public boolean apply(DynamicContext context) { Map<String, Object> bindings = context.getBindings(); final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings); if (!iterable.iterator().hasNext()) { return true; } boolean first = true; applyOpen(context); int i = 0; for (Object o : iterable) { DynamicContext oldContext = context; if (first) { context = new PrefixedContext(context, ""); } else if (separator != null) { context = new PrefixedContext(context, separator); } else { context = new PrefixedContext(context, ""); } int uniqueNumber = context.getUniqueNumber(); // Issue #709 if (o instanceof Map.Entry) { @SuppressWarnings("unchecked") Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o; applyIndex(context, mapEntry.getKey(), uniqueNumber); applyItem(context, mapEntry.getValue(), uniqueNumber); } else { applyIndex(context, i, uniqueNumber); applyItem(context, o, uniqueNumber); } contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber)); if (first) { first = !((PrefixedContext) context).isPrefixApplied(); } context = oldContext; i++; } applyClose(context); return true; } 当调用appItm方法的时候将参数进行绑定,参数的变量问题都会存在bindings这个参数中区 private void applyItem(DynamicContext context, Object o, int i) { if (item != null) { context.bind(item, o); context.bind(itemizeItem(item, i), o); } } 进行绑定参数的时候,绑定完成foreach的方法的时候,可以看到bindings中不止绑定了foreach中的两个参数还额外有一个参数名字studentName->lct2,也就是说最后一个参数也是会出现在bindings这个参数中的, private void applyItem(DynamicContext context, Object o, int i) { if (item != null) { context.bind(item, o); context.bind(itemizeItem(item, i), o); } } 图20 参数绑定过程 最后判定 org.apache.ibatis.scripting.xmltags.IfSqlNode#apply @Override public boolean apply(DynamicContext context) { if (evaluator.evaluateBoolean(test, context.getBindings())) { contents.apply(context); return true; } return false; } 可以看到在调用evaluateBoolean方法的时候会把context.getBindings()就是前边提到的bindings参数传入进去,因为现在这个参数中有一个studentName,因此在使用Ognl表达式的时候,判定为这个if标签是有值的因此将这个标签进行了解析 图21 单个参数绑定过程 最终绑定的结果为 图22 全部参数绑定过程 因此这个地方绑定参数的地方是有问题的,至此找出了问题的所在。 2.3.2 官方解释 翻阅MyBatis官方文档进行求证,发现在3.4.5版本发行中bug fixes中有这样一句 图23 此问题官方修复github记录 图23 此问题官方修复github记录 修复了foreach版本中对于全局变量context的修改的bug issue地址为https://github.com/mybatis/mybatis-3/pull/966 修复方案为https://github.com/mybatis/mybatis-3/pull/966/commits/84513f915a9dcb97fc1d602e0c06e11a1eef4d6a 可以看到官方给出的修改方案,重新定义了一个对象,分别存储全局变量和局部变量,这样就会解决foreach会改变全局变量的问题。 图24 此问题官方修复代码示例 2.3.3 修复方案 升级MyBatis版本至3.4.5以上 如果保持版本不变的话,在foreach中定义的变量名不要和外部的一致 3 源码阅读过程总结 MyBatis源代码的目录是比较清晰的,基本上每个相同功能的模块都在一起,但是如果直接去阅读源码的话,可能还是有一定的难度,没法理解它的运行过程,本次通过一个简单的查询流程从头到尾跟下来,可以看到MyBatis的设计以及处理流程,例如其中用到的设计模式: 图25 MyBatis代码结构图 组合模式:如ChooseSqlNode,IfSqlNode等 模板方法模式:例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler Builder模式:例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder 工厂模式:例如SqlSessionFactory、ObjectFactory、MapperProxyFactory 代理模式:MyBatis实现的核心,比如MapperProxy、ConnectionLogger 4 文档参考 https://mybatis.org/mybatis-3/zh/index.htm

优秀的个人博客,低调大师

带你学习MindSpore中算子使用方法

摘要:本文分享下MindSpore中算子的使用和遇到问题时的解决方法。 本文分享自华为云社区《【MindSpore易点通】算子使用问题与解决方法》,作者:chengxiaoli。 简介 算子的调用是构建模型的基础,准确的找到能满足需求的算子并能正确的使用,可以有事半功倍的效果。本次就分享下MindSpore中算子的使用和遇到问题时的解决方法给大家。 图中是简单的归纳了算子使用的这几个步骤,本文的内容主要是根据图中的步骤再进行解析说明。 分析使用需求 首先了解下什么是算子,通俗的说对一个函数进行某一项操作都可以认为是一个算子,最基础的就是加减乘除这些操作。所以算子并不难理解,只是很多算子它包含的计算会更加的复杂。 一般情况下在自定义网络模型时会用到各种类型的算子,MindSpore中分为Primitive算子和nn算子,Primitive算子是开放给用户的最低阶算子接口,一个Primitive算子对应一个原语,它封装了底层的Ascend、GPU、AICPU、CPU等多种算子的具体实现,为用户提供基础算子能力。 还可以继续分为计算算子和框架算子。计算算子主要负责具体的计算,而框架算子主要用于构图,自动微分等功能,都可以从mindspore.ops模块导入使用。同时mindspore.nn模块是对mindspore.ops模块的封装。所以在构建网络模型时建议使用mindspore.nn模块,优点是使用方便,当然如果想要探索更多算子的信息,建议使用mindspore.ops模块。 需求大致可以分成两种,自定义网络模型时的算子需求和从第三方框架迁移模型时对标算子的需求。 这里给大家提供一个对比第三方框架算子使用的样例给大家参考。 样例:nn.Embedding 层与pytorch相比缺少了Padding操作,有其余的算子可以实现吗? 分析:在PyTorch中padding_idx的作用是将embedding矩阵中padding_idx位置的词向量置为0,并且反向传播时不会更新padding_idx位置的词向量。 方法:在mindspore中,可以手动将embedding的padding_idx位置对应的权重初始化为0;并且在训练时通过mask的操作,过滤掉padding_idx位置对应的Loss。MindSpore中与Mask相关的算子有:nn.Dropout、RandomChoicWithMask、NMSWithMask。 类似样例中的情况,就需要我们对计算过程分析清楚,当没有完全匹配的算子时也需要考虑将需求拆分为多步进行。当然我们不可能对所有算子的特性都了解,所以当遇到疑惑是请访问这里:宇宙尽头 官网查找接口 在清楚需求之后,就需要找到能够实现需求的算子了,查找这一步相对简单,官网中有算子的目录和详细的使用介绍页面,能迅速定位到相同类型算子的位置,查看算子详情页查看是否有合适的算子,在将算子加入到复杂的脚本前,建议先只单独运行算子样例试一试。 自检:当然即使找到了符合需求的算子,并且单独运行时也没有问题,也不能够避免在加入到自己的项目代码中完全不报错。在算子使用中通常的报错原因是使用的MindSpore版本和参考的教程不一致、算子不支持目前的硬件环境以及传入的参数不符合算子要求,所以为了避免不必要的报错,下面给大家提供几个查看接口的正确姿势: 根据自己安装的MindSpore版本查找对应版本的接口; 第三方模型迁移时建议先通过API映射查找; 查看算子是否支持自己的硬件平台以及内存大小; 重点:到算子详情页查看参数和参数的类型。 BUG:报错不要慌,论坛来帮忙:如果以上几点方法还是不能够避免报错,并且报错的信息也不能够帮助定位并解决问题,那么请到论坛发求助帖子,或许您是遇到BUG啦。 这里找出两个算子的特性和用法给大家参考: 样例1:如何让算子输出之一为tuple类型或list类型? 方法:框架的算子输出只支持Tensor类型,不支持tuple或list。 样例2:mindspore中可以直接打印卷积核的参数吗? 方法1:是可以直接打印的 net = nn.Conv2d(***) print(net.weight) print(net.weight.asnumpy) 方法2:也可以使用for循环遍历 for *** net.get_parameters() 算子的自定义 MindSpore也在一直更新和完善中,也会有暂不支持的算子,遇到这种情况欢迎大家提需求到论坛或者Gitee中,官网也有完整的自定义算子教程,欢迎大家进行算子的开发,感谢大家的贡献。 自定义算子教程: 自定义算子(CPU):https://mindspore.cn/tutorials/experts/zh-CN/master/operation/op_cpu.html# 自定义算子(GPU):https://mindspore.cn/tutorials/experts/zh-CN/master/operation/op_gpu.html# 自定义算子(Ascend):https://mindspore.cn/tutorials/experts/zh-CN/master/operation/op_ascend.html 样例:怎样自定义让一个Tensor包含的值作为另一个Tensor的下标? 方法:既然是自定义算子,第一个tensor可以改为int类型或者listint类型啊。下面的sizes是listInt,exclude_outside是int attr.list=sizes,exclude_outside attr_sizes.type=listInt attr_sizes.value=all attr_sizes.paramType=required attr_exclude_outside.type=int attr_exclude_outside.value=all attr_exclude_outside.paramType=optional attr_exclude_outside.defaultValue=0 干货分享:除了以上的教程和样例,也有优秀的论坛成员分享的自定义算子的详细过程和体会: 自定义算子(CPU)windows版本: https://bbs.huaweicloud.com/forum/thread-175270-1-1.html 深夜:在?我用本地环境pytest带你玩自定义算子: https://bbs.huaweicloud.com/forum/thread-177908-1-1.html 通过上图指导,在遇到使用报错或者自定义算子报错时,也可以到论坛或者Gitee中展示遇到的问题。 总结 以上就是分析使用需求、选择算子、解决报错的过程,欢迎大家多尝试,因为有论坛在给您保驾护航,重要发现:宇宙的尽头是论坛。 点击关注,第一时间了解华为云新鲜技术~

优秀的个人博客,低调大师

HarmonyOS官方模板学习 之 Grid Ability(Java)

@[toc](目录) # Grid Ability(Java) ## 介绍 使用Java语言开发,用于Phone设备的Feature Ability模板,使用XML布局,显示内容为两部分网格表,网格每行显示4个项目,网格内元素可进行拖拽排序。 ## 搭建环境 安装DevEco Studio,详情请参考[DevEco Studio下载](https://developer.harmonyos.com/cn/develop/deveco-studio)。 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境: 如果可以直接访问Internet,只需进行[下载HarmonyOS SDK](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/environment_config-0000001052902427)操作。 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考[配置开发环境](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/environment_config-0000001052902427)。 ## 代码结构解读 注意:'#'代表注释 后台功能 ```json gridabilityjava │ MainAbility.java │ MyApplication.java │ ├─component │ DragLayout.java #自定义的拖拽功能组件 │ GridView.java #自定义的Grid视图组件,extends TableLayout │ ├─model │ GridItemInfo.java #Grid item 模型 │ ├─provider │ GridAdapter.java #给Grid提供实例化好的item 组件列表;提供了计算单个item的宽度的方法 │ ├─slice │ MainAbilitySlice.java #主能力页,负责实例化自定义的DragLayout拖拽组件 │ └─utils AppUtils.java #工具类,提供了从element资源中中获取value;获取屏幕的坐标的方法 ``` 这是几个java类之间的关系 ![image.png](https://harmonyos.oss-cn-beijing.aliyuncs.com/images/202106/580eaf59927ec18dd90122d1ceee31f6a4f3ff.png?x-oss-process=image/resize,w_658,h_771) 页面资源 ```json resources ├─base │ ├─element │ │ color.json │ │ float.json │ │ integer.json │ │ string.json │ │ │ ├─graphic │ │ background_bottom_button.xml #页面底部按钮形状 │ │ background_bottom_layout.xml #页面底部布局形状 │ │ background_item_active_button.xml #grid item 激活形状 │ │ background_item_button.xml #grid item 默认形状 │ │ background_table_layout_down.xml #下面的 grid 形状 │ │ background_table_layout_up.xml #上面的 grid 形状 │ │ │ ├─layout │ │ ability_main.xml #主显示页面 │ │ app_bar_layout.xml #app工具栏布局页面 │ │ grid_item.xml #单个grid item布局页面 │ │ │ ├─media │ │ 5G.png │ │ back.png │ │ back_white.png ``` ## 页面布局 ### ability_main.xml #主显示页 此页面由DirectionalLayout、StackLayout、DependentLayout 布局构成,整体布局是上下布局。 上面时app工具栏,使用了StackLayout布局,通过includ标签引入到主页面。 下面是支持拖拽的GridView,由DependentLayout 和DirectionalLayout布局组成,使用的组件有ScrollView、GridView、Text、Button、Image。 ![image.png](https://harmonyos.oss-cn-beijing.aliyuncs.com/images/202106/995bf5c59199f2ed3d79375f5593bd8e67a9a8.png?x-oss-process=image/resize,w_696,h_639) ### app_bar_layout.xml #app工具栏布局页面 ![image.png](https://harmonyos.oss-cn-beijing.aliyuncs.com/images/202106/03992ad991e2af350920520652a8c66bc1cdc5.png?x-oss-process=image/resize,w_265,h_149) ### grid_item.xml #单个grid item布局页面 ![image.png](https://harmonyos.oss-cn-beijing.aliyuncs.com/images/202106/2800b6b146e1585dde0483cfac61a140a44c94.png?x-oss-process=image/resize,w_264,h_125) ## 后台逻辑 ### 1.初始化上面的GridView 先构建item模拟数据列表,将构建好的数据传递给GridAdapter 初始化item组件列表,通过GridView.setAdapter方法给每个item组件绑定长按事件,并设置GridView的TAG属性(TAG就是指上面的GridView还是下面的GridView)。 ```java /** * 初始化上面的Grid item */ private void initUpListItem() { //构建item模拟数据列表 List upperItemList = new ArrayList<>(); for (int i = 0; i < UP_ITEM_COUNT; i++) { int iconId = icons[i]; String text = texts[i]; upperItemList.add(new GridItemInfo(text, iconId, UP_GRID_TAG)); } GridView gridView = (GridView) slice.findComponentById(ResourceTable.Id_grid_view_up); //将构建好的数据传递给GridAdapter 初始化item组件列表 GridAdapter adapter = new GridAdapter(slice.getContext(), upperItemList); //通过GridView.setAdapter方法给每个item组件绑定长按事件 gridView.setAdapter(adapter, longClickListener); //设置GridView的TAG属性 gridView.setTag(UP_GRID_TAG); } ``` ### 2.初始化下面的GridView 逻辑同上 ```java /** * 初始化下面的Grid item */ private void initDownListItem() { String itemText = AppUtils.getStringResource(slice.getContext(), ResourceTable.String_grid_item_text); List lowerItemList = new ArrayList<>(); for (int i = 0; i < DOWN_ITEM_COUNT; i++) { //随意取的图标 int iconId = icons[i + 5]; String text = texts[i + 5]; lowerItemList.add(new GridItemInfo(text, iconId, DOWN_GRID_TAG)); } if (slice.findComponentById(ResourceTable.Id_grid_view_down) instanceof GridView) { GridView gridView = (GridView) slice.findComponentById(ResourceTable.Id_grid_view_down); GridAdapter adapter = new GridAdapter(slice.getContext(), lowerItemList); gridView.setAdapter(adapter, longClickListener); gridView.setTag(DOWN_GRID_TAG); } } ``` ### 3.初始化底部的按钮 这个地方做了一个屏幕适配,就是根据屏幕的宽度、边距来设置按钮的宽度, 同时添加了按钮的监听事件,点击按钮 关闭当前Ability。 ```java /** * Calculating button width based on screen width. * The actual width is the screen width minus the margin of the buttons. * 设置底部 2个按钮的宽度 */ private void initBottomItem() { int screenWidth = AppUtils.getScreenInfo(slice.getContext()).getPointXToInt(); //计算按钮宽度 int buttonWidth = (screenWidth - AttrHelper.vp2px(80, slice.getContext())) / 2; Component leftButton = slice.findComponentById(ResourceTable.Id_bottom_left_button); leftButton.setWidth(buttonWidth); //关闭Ability leftButton.setClickedListener(component -> slice.terminateAbility()); Component rightButton = slice.findComponentById(ResourceTable.Id_bottom_right_button); rightButton.setWidth(buttonWidth); //关闭Ability rightButton.setClickedListener(component -> slice.terminateAbility()); } ``` ### 4.初始化app工具栏 这个没做什么,似乎是想根据本地化信息,设置返回箭头的方向,因为有的语言是从右往左看的。 ```java /** * 检查指定 Locale 的文本布局是否从右到左。 * 设置返回箭头的方向 */ private void initAppBar() { if (TextTool.isLayoutRightToLeft(Locale.getDefault())) { Image appBackImg = (Image) slice.findComponentById(ResourceTable.Id_left_arrow); appBackImg.setRotation(180); } } ``` ### 5.初始化监听事件 包括返回按钮的返回事件、ScrollView的touch事件。 touch事件包含大量的细节操作,如拖拽时有一个阴影效果,滚动条的处理,拖拽交换结束的处理,过渡效果,上下grid 有效区域的计算,拖拽完成将拖拽的组件添加到对应grid的操作等,参照着拿来用吧。 ```java /** * 初始化监听事件,包括返回按钮返回事件、ScrollView的touch事件 */ private void initEventListener() { //‘返回按钮’的监听事件 if (slice.findComponentById(ResourceTable.Id_left_arrow) instanceof Image) { Image backIcon = (Image) slice.findComponentById(ResourceTable.Id_left_arrow); // backIcon.setClickedListener(component -> slice.terminateAbility()); } //ScrollView的 Touch事件监听,拿来用就可以了 scrollView.setTouchEventListener( (component, touchEvent) -> { //按下屏幕的位置 MmiPoint downScreenPoint = touchEvent.getPointerScreenPosition(touchEvent.getIndex()); switch (touchEvent.getAction()) { //表示第一根手指触摸屏幕。这表示交互的开始 case TouchEvent.PRIMARY_POINT_DOWN: currentDragX = (int) downScreenPoint.getX(); currentDragY = (int) downScreenPoint.getY(); //获取指针索引相对于偏移位置的 x 和 y 坐标。 MmiPoint downPoint = touchEvent.getPointerPosition(touchEvent.getIndex()); scrollViewTop = (int) downScreenPoint.getY() - (int) downPoint.getY(); scrollViewLeft = (int) downScreenPoint.getX() - (int) downPoint.getX(); return true; //表示最后一个手指从屏幕上抬起。这表示交互结束 case TouchEvent.PRIMARY_POINT_UP: //恢复下面grid的描述 changeTableLayoutDownDesc(ResourceTable.String_down_grid_layout_desc_text); case TouchEvent.CANCEL: if (isViewOnDrag) { selectedView.setScale(1.0f, 1.0f); selectedView.setAlpha(1.0f); selectedView.setVisibility(Component.VISIBLE); isViewOnDrag = false; isScroll = false; return true; } break; //表示手指在屏幕上移动 case TouchEvent.POINT_MOVE: if (!isViewOnDrag) { break; } int pointX = (int) downScreenPoint.getX(); int pointY = (int) downScreenPoint.getY(); this.exchangeItem(pointX, pointY); if (UP_GRID_TAG.equals(selectedView.getTag())) { this.swapItems(pointX, pointY); } this.handleScroll(pointY); return true; } return false; } ); } ``` ## 归纳总结 ### 1.自定义组件在构造函数中传递slice 这样的目的是便于获取页面的其它组件。 ```java Component itemLayout=LayoutScatter.getInstance(slice.getContext()) .parse(ResourceTable.Layout_grid_item, null, false); ``` 需要注意的是slice指代的是页面,但是自定义组件往往是有自己的布局文件的,一般不在slice中,所以不要通过slice获取自定义组件的子组件,获取不到,不过可以通过LayoutScatter获取 ```java //错误的方式 Component gridItem= slice.findComponentById(ResourceTable.Layout_grid_item); //正确的方式 Component gridItem = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_grid_item, null, false); ``` ### 2.单位转换vp2px java组件对象宽高、边距的单位默认时px, 从element中获取的值需要进行单位转换,可以使用AttrHelper.vp2px 将vp转换为px。 ```java if (gridItem.findComponentById(ResourceTable.Id_grid_item_text) instanceof Text) { Text textItem = (Text) gridItem.findComponentById(ResourceTable.Id_grid_item_text); textItem.setText(item.getItemText()); textItem.setTextSize(AttrHelper.fp2px(10, context)); } ``` ### 3.子组件的获取 获取一个组件对象后,可以使用该组件对象的findComponentById方法继续获取内部的子组件 ```java Component gridItem = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_grid_item, null, false); Image imageItem = (Image) gridItem.findComponentById(ResourceTable.Id_grid_item_image); ``` ### 4.TableLayout的使用 TableLayout继承自ComponentContainer,提供用于在带有表格的组件中排列组件的布局。 TableLayout 提供了用于对齐和排列组件的接口,以在带有表格的组件中显示组件。 排列方式、行列数、元件位置均可配置。 例如 removeAllComponents();可以用来清除 ComponentContainer 管理的所有组件,addComponent 用来将组件添加到ComponentContainer 容器中。示例中GridView就是继承自TableLayout。 ```java /** * The setAdapter * * @param adapter adapter * @param longClickedListener longClickedListener */ void setAdapter(GridAdapter adapter, LongClickedListener longClickedListener) { //清除 ComponentContainer 管理的所有组件 removeAllComponents(); //遍历item组件列表 for (int i = 0; i < adapter.getComponentList().size(); i++) { //为组件中的长按事件注册一个监听器(组件被点击并按住) adapter.getComponentList().get(i).setLongClickedListener(longClickedListener); //将组件添加到容器中 addComponent(adapter.getComponentList().get(i)); } } ``` ## 效果展示 示例代码模拟了一下手机控制中心,编辑快捷开关的效果 |原效果|模拟效果| |-|-| |![image.png](https://harmonyos.oss-cn-beijing.aliyuncs.com/images/202106/c362eba72a004a3896d752987de60df614e988.png?x-oss-process=image/resize,w_331,h_692)|![动画2.gif](https://harmonyos.oss-cn-beijing.aliyuncs.com/images/202106/18c5aea55f50cb0521b243a0278f415bffe0c2.gif?x-oss-process=image/resize,w_331,h_692)| ## 完整代码 文章相关附件可以点击下面的原文链接前往下载 原文链接:https://harmonyos.51cto.com/posts/6257#bkwz

优秀的个人博客,低调大师

HarmonyOS学习路之开发篇——Data Ability

Data Ability基本概念 使用Data模板的Ability(以下简称“Data”)有助于应用管理其自身和其他应用存储数据的访问,并提供与其他应用共享数据的方法。Data既可用于同设备不同应用的数据共享,也支持跨设备不同应用的数据共享。 数据的存放形式多样,可以是数据库,也可以是磁盘上的文件。Data对外提供对数据的增、删、改、查,以及打开文件等接口,这些接口的具体实现由开发者提供。 URI介绍 Data的提供方和使用方都通过URI(Uniform Resource Identifier)来标识一个具体的数据,例如数据库中的某个表或磁盘上的某个文件。HarmonyOS的URI仍基于URI通用标准,格式如下: scheme:协议方案名,固定为“dataability”,代表Data Ability所使用的协议类型 authority:设备ID。如果为跨设备场景,则为目标设备的ID;如果为本地设备场景,则不需要填写。 path:资源的路径信息,代表特定资源的位置信息。 query:查询参数。 f ragment:可以用于指示要访问的子资源。 URI示例: 跨设备场景:dataability://device_id/com.domainname.dataability.persondata/person/10 本地设备:dataability:///com.domainname.dataability.persondata/person/10 创建Data 使用Data模板的Ability形式仍然是Ability,因此,开发者需要为应用添加一个或多个Ability的子类,来提供程序与其他应用之间的接口。Data为结构化数据和文件提供了不同API接口供用户使用,因此,开发者需要首先确定好使用何种类型的数据。本章节主要讲述了创建Data的基本步骤和需要使用的接口。 Data提供方可以自定义数据的增、删、改、查,以及文件打开等功能,并对外提供这些接口。 确定数据存储方式 确定数据的存储方式,Data支持以下两种数据形式: 文件数据:如文本、图片、音乐等。 结构化数据:如数据库等。 实现UserDataAbility UserDataAbility用于接收其他应用发送的请求,提供外部程序访问的入口,从而实现应用间的数据访问。 实现UserDataAbility,需要在“Project”窗口当前工程的主目录(“entry > src > main > java > com.xxx.xxx”)选择“File > New > Ability > Empty Data Ability”,设置“Data Name”后完成UserDataAbility的创建。 Data提供了文件存储和数据库存储两组接口供用户使用。 文件存储 开发者需要在Data中重写FileDescriptor openFile​(Uri uri, String mode)方法来操作文件:uri为客户端传入的请求目标路径;mode为开发者对文件的操作选项,可选方式包含“r”(读), “w”(写), “rw”(读写)等。 ohos.rpc.MessageParcel类提供了一个静态方法,用于获取MessageParcel实例。开发者可通过获取到的MessageParcel实例,使用dupFileDescriptor()函数复制待操作文件流的文件描述符,并将其返回,供远端应用访问文件。 示例:根据传入的uri打开对应的文件 private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0xD00201, "Data_Log"); @Override public FileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { // 创建messageParcel MessageParcel messageParcel = MessageParcel.obtain(); File file = new File(uri.getDecodedPathList().get(0)); //get(0)是获取URI完整字段中查询参数字段。 if (mode == null || !"rw".equals(mode)) { file.setReadOnly(); } FileInputStream fileIs = new FileInputStream(file); FileDescriptor fd = null; try { fd = fileIs.getFD(); } catch (IOException e) { HiLog.info(LABEL_LOG, "failed to getFD"); } // 绑定文件描述符 return messageParcel.dupFileDescriptor(fd); } 数据库存储 1、初始化数据库连接。 系统会在应用启动时调用onStart()方法创建Data实例。在此方法中,开发者应该创建数据库连接,并获取连接对象,以便后续和数据库进行操作。为了避免影响应用启动速度,开发者应当尽可能将非必要的耗时任务推迟到使用时执行,而不是在此方法中执行所有初始化。 示例:初始化的时候连接数据库 private static final String DATABASE_NAME = "UserDataAbility.db"; private static final String DATABASE_NAME_ALIAS = "UserDataAbility"; private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0xD00201, "Data_Log"); private OrmContext ormContext = null; @Override public void onStart(Intent intent) { super.onStart(intent); DatabaseHelper manager = new DatabaseHelper(this); ormContext = manager.getOrmContext(DATABASE_NAME_ALIAS, DATABASE_NAME, BookStore.class); } 2、编写数据库操作方法。 Ability定义了6个方法供用户处理对数据库表数据的增删改查。这6个方法在Ability中已默认实现,开发者可按需重写。 |方法|描述| |:--|--| | ResultSet query​(Uri uri, String[] columns, DataAbilityPredicates predicates) | 查询数据库 | |int insert​(Uri uri, ValuesBucket value) | 向数据库中插入单条数据 | |int batchInsert​(Uri uri, ValuesBucket[] values) | 向数据库中插入多条数据 | |int delete​(Uri uri, DataAbilityPredicates predicates) | 删除一条或多条数据 | |int update​(Uri uri, ValuesBucket value, DataAbilityPredicates predicates)| 更新数据库 | |DataAbilityResult[] executeBatch​(ArrayList<DataAbilityOperation> operations)| 批量操作数据库 | 3、批量操作数据库 这些方法的使用说明如下: query() 该方法接收三个参数,分别是查询的目标路径,查询的列名,以及查询条件,查询条件由类DataAbilityPredicates构建。根据传入的列名和查询条件查询用户表的代码示例如下: public ResultSet query(Uri uri, String[] columns, DataAbilityPredicates predicates) { if (ormContext == null) { HiLog.error(LABEL_LOG, "failed to query, ormContext is null"); return null; } // 查询数据库 OrmPredicates ormPredicates = DataAbilityUtils.createOrmPredicates(predicates,User.class); ResultSet resultSet = ormContext.query(ormPredicates, columns); if (resultSet == null) { HiLog.info(LABEL_LOG, "resultSet is null"); } // 返回结果 return resultSet; } insert() 该方法接收两个参数,分别是插入的目标路径和插入的数据值。其中,插入的数据由ValuesBucket封装,服务端可以从该参数中解析出对应的属性,然后插入到数据库中。此方法返回一个int类型的值用于标识结果。接收到传过来的用户信息并把它保存到数据库中的代码示例如下: public int insert(Uri uri, ValuesBucket value) { // 参数校验 if (ormContext == null) { HiLog.error(LABEL_LOG, "failed to insert, ormContext is null"); return -1; } // 构造插入数据 User user = new User(); user.setUserId(value.getInteger("userId")); user.setFirstName(value.getString("firstName")); user.setLastName(value.getString("lastName")); user.setAge(value.getInteger("age")); user.setBalance(value.getDouble("balance")); // 插入数据库 boolean isSuccessful = ormContext.insert(user); if (!isSuccessful) { HiLog.error(LABEL_LOG, "failed to insert"); return -1; } isSuccessful = ormContext.flush(); if (!isSuccessful) { HiLog.error(LABEL_LOG, "failed to insert flush"); return -1; } DataAbilityHelper.creator(this, uri).notifyChange(uri); int id = Math.toIntExact(user.getRowId()); return id; } batchInsert() 该方法为批量插入方法,接收一个ValuesBucket数组用于单次插入一组对象。它的作用是提高插入多条重复数据的效率。该方法系统已实现,开发者可以直接调用。 delete() 该方法用来执行删除操作。删除条件由类DataAbilityPredicates构建,服务端在接收到该参数之后可以从中解析出要删除的数据,然后到数据库中执行。根据传入的条件删除用户表数据的代码示例如下: public int delete(Uri uri, DataAbilityPredicates predicates) { if (ormContext == null) { HiLog.error(LABEL_LOG, "failed to delete, ormContext is null"); return -1; } OrmPredicates ormPredicates = DataAbilityUtils.createOrmPredicates(predicates,User.class); int value = ormContext.delete(ormPredicates); DataAbilityHelper.creator(this, uri).notifyChange(uri); return value; } update() 此方法用来执行更新操作。用户可以在ValuesBucket参数中指定要更新的数据,在DataAbilityPredicates中构建更新的条件等。更新用户表的数据的代码示例如下: public int update(Uri uri, ValuesBucket value, DataAbilityPredicates predicates) { if (ormContext == null) { HiLog.error(LABEL_LOG, "failed to update, ormContext is null"); return -1; } OrmPredicates ormPredicates = DataAbilityUtils.createOrmPredicates(predicates,User.class); int index = ormContext.update(ormPredicates, value); HiLog.info(LABEL_LOG, "UserDataAbility update value:" + index); DataAbilityHelper.creator(this, uri).notifyChange(uri); return index; } executeBatch() 此方法用来批量执行操作。DataAbilityOperation中提供了设置操作类型、数据和操作条件的方法,用户可自行设置自己要执行的数据库操作。该方法系统已实现,开发者可以直接调用。 说明 上述代码示例中,初始化了数据库类BookStore.class,并通过实体类User.class对该数据库的表User进行增删改查操作。 关于对象关系映射数据库的具体逻辑,以及示例中BookStore.class与User.class的逻辑关系,可参考“对象关系映射数据库开发指导”。 注册UserDataAbility 和Service类似,开发者必须在配置文件中注册Data。 配置文件中该字段在创建Data Ability时会自动创建,name与创建的Data Ability一致。 需要关注以下属性: type: 类型设置为data uri: 对外提供的访问路径,全局唯一 permissions: 访问该data ability时需要申请的访问权限 说明 如果权限非系统权限,需要在配置文件中进行自定义。请参考权限开发指导中关于“自定义权限”的相关说明。 { "name": ".UserDataAbility", "type": "data", "visible": true, "uri": "dataability://com.example.myapplication5.DataAbilityTest", "permissions": [ "com.example.myapplication5.DataAbility.DATA" ] } 访问Data 开发者可以通过DataAbilityHelper类来访问当前应用或其他应用提供的共享数据。DataAbilityHelper作为客户端,与提供方的Data进行通信。Data接收到请求后,执行相应的处理,并返回结果。DataAbilityHelper提供了一系列与Data Ability对应的方法。 下面介绍DataAbilityHelper具体的使用步骤。 声明使用权限 如果待访问的Data声明了访问需要权限,则访问此Data需要在配置文件中声明需要此权限。声明请参考权限申请字段说明。 "reqPermissions": [ { "name": "com.example.myapplication5.DataAbility.DATA" }, // 访问文件还需要添加访问存储读写权限 { "name": "ohos.permission.READ_USER_STORAGE" }, { "name": "ohos.permission.WRITE_USER_STORAGE" } ] 创建DataAbilityHelper DataAbilityHelper为开发者提供了creator()方法来创建DataAbilityHelper实例。该方法为静态方法,有多个重载。最常见的方法是通过传入一个context对象来创建DataAbilityHelper对象。 获取helper对象示例: DataAbilityHelper helper = DataAbilityHelper.creator(this); 访问Data Ability DataAbilityHelper为开发者提供了一系列的接口来访问不同类型的数据(文件、数据库等)。 访问文件 DataAbilityHelper为开发者提供了FileDescriptor openFile​(Uri uri, String mode)方法来操作文件。此方法需要传入两个参数,其中uri用来确定目标资源路径,mode用来指定打开文件的方式,可选方式包含“r”(读), “w”(写), “rw”(读写),“wt”(覆盖写),“wa”(追加写),“rwt”(覆盖写且可读)。 该方法返回一个目标文件的FD(文件描述符),把文件描述符封装成流,开发者就可以对文件流进行自定义处理。 访问文件示例: // 读取文件描述符 FileDescriptor fd = helper.openFile(uri, "r"); FileInputStream fis = new FileInputStream(fd); // 使用文件描述符封装成的文件流,进行文件操作 访问数据库 DataAbilityHelper为开发者提供了增、删、改、查以及批量处理等方法来操作数据库。 说明 对数据库的操作方法,详见数据管理中各数据库类型的开发指南。 方法 描述 ResultSet query​(Uri uri, String[] columns, DataAbilityPredicates predicates) 查询数据库 int insert​(Uri uri, ValuesBucket value) 向数据库中插入单条数据 int batchInsert​(Uri uri, ValuesBucket[] values) 向数据库中插入多条数据 int delete​(Uri uri, DataAbilityPredicates predicates) 删除一条或多条数据 int update​(Uri uri, ValuesBucket value, DataAbilityPredicates predicates) 更新数据库 DataAbilityResult[] executeBatch​(ArrayList<DataAbilityOperation> operations) 批量操作数据库 这些方法的使用说明如下: query() 查询方法,其中uri为目标资源路径,columns为想要查询的字段。开发者的查询条件可以通过DataAbilityPredicates来构建。查询用户表中id在101-103之间的用户,并把结果打印出来,代码示例如下: DataAbilityHelper helper = DataAbilityHelper.creator(this); // 构造查询条件 DataAbilityPredicates predicates = new DataAbilityPredicates(); predicates.between("userId", 101, 103); // 进行查询 ResultSet resultSet = helper.query(uri, columns, predicates); // 处理结果 resultSet.goToFirstRow(); do { // 在此处理ResultSet中的记录; } while(resultSet.goToNextRow()); insert() 新增方法,其中uri为目标资源路径,ValuesBucket为要新增的对象。插入一条用户信息的代码示例如下: DataAbilityHelper helper = DataAbilityHelper.creator(this); // 构造插入数据 ValuesBucket valuesBucket = new ValuesBucket(); valuesBucket.putString("name", "Tom"); valuesBucket.putInteger("age", 12); helper.insert(uri, valuesBucket); batchInsert() 批量插入方法,和insert()类似。批量插入用户信息的代码示例如下: DataAbilityHelper helper = DataAbilityHelper.creator(this); // 构造插入数据 ValuesBucket[] values = new ValuesBucket[2]; values[0] = new ValuesBucket(); values[0].putString("name", "Tom"); values[0].putInteger("age", 12); values[1] = new ValuesBucket(); values[1].putString("name", "Tom1"); values[1].putInteger("age", 16); helper.batchInsert(uri, values); delete() 删除方法,其中删除条件可以通过DataAbilityPredicates来构建。删除用户表中id在101-103之间的用户,代码示例如下: DataAbilityHelper helper = DataAbilityHelper.creator(this); // 构造删除条件 DataAbilityPredicates predicates = new DataAbilityPredicates(); predicates.between("userId", 101, 103); helper.delete(uri, predicates); update() 更新方法,更新数据由ValuesBucket传入,更新条件由DataAbilityPredicates来构建。更新id为102的用户,代码示例如下: DataAbilityHelper helper = DataAbilityHelper.creator(this); // 构造更新条件 DataAbilityPredicates predicates = new DataAbilityPredicates(); predicates.equalTo("userId", 102); // 构造更新数据 ValuesBucket valuesBucket = new ValuesBucket(); valuesBucket.putString("name", "Tom"); valuesBucket.putInteger("age", 12); helper.update(uri, valuesBucket, predicates); executeBatch() 此方法用来执行批量操作。DataAbilityOperation中提供了设置操作类型、数据和操作条件的方法,开发者可自行设置自己要执行的数据库操作。插入多条数据的代码示例如下: DataAbilityHelper helper = DataAbilityHelper.creator(abilityObj, insertUri); // 构造批量操作 ValuesBucket value1 = initSingleValue(); DataAbilityOperation opt1 = DataAbilityOperation.newInsertBuilder(insertUri).withValuesBucket(value1).build(); ValuesBucket value2 = initSingleValue2(); DataAbilityOperation opt2 = DataAbilityOperation.newInsertBuilder(insertUri).withValuesBucket(value2).build(); ArrayList<DataAbilityOperation> operations = new ArrayList<DataAbilityOperation>(); operations.add(opt1); operations.add(opt2); DataAbilityResult[] result = helper.executeBatch(insertUri, operations); 相关实例 针对Data Ability开发,有以下示例工程可供参考: DataAbility 本示例演示了如何使用Data Ability对数据库进行增、删、改、查,以及读取文本文件。 针对Data Ability开发,有以下Codelabs可供参考: 关系型数据库 基于Data Ability的关系型数据库和数据管理能力,实现数据库相关应用服务的快速开发。

优秀的个人博客,低调大师

HarmonyOS学习路之开发篇——Service Ability

Service Ability Service Ability基本概念 基于Service模板的Ability(以下简称“Service”)主要用于后台运行任务(如执行音乐播放、文件下载等),但不提供用户交互界面。Service可由其他应用或Ability启动,即使用户切换到其他应用,Service仍将在后台继续运行。 Service是单实例的。在一个设备上,相同的Service只会存在一个实例。如果多个Ability共用这个实例,只有当与Service绑定的所有Ability都退出后,Service才能够退出。由于Service是在主线程里执行的,因此,如果在Service里面的操作时间过长,开发者必须在Service里创建新的线程来处理,防止造成主线程阻塞,应用程序无响应。 创建Service 介绍如何创建一个Service。 1、创建Ability的子类,实现Service相关的生命周期方法。Service也是一种Ability,Ability为Service提供了以下生命周期方法,通过重写这些方法,来添加其他Ability请求与Service Ability交互时的处理方法。 onStart() 该方法在创建Service的时候调用,用于Service的初始化。在Service的整个生命周期只会调用一次,调用时传入的Intent应为空。 onCommand() 在Service创建完成之后调用,该方法在客户端每次启动该Service时都会调用,用户可以在该方法中做一些调用统计、初始化类的操作。 onConnect​() 在Ability和Service连接时调用,该方法返回IRemoteObject对象,用户可以在该回调函数中生成对应Service的IPC通信通道,以便Ability与Service交互。Ability可以多次连接同一个Service,系统会缓存该Service的IPC通信对象,只有第一个客户端连接Service时,系统才会调用Service的onConnect方法来生成IRemoteObject对象,而后系统会将同一个RemoteObject对象传递至其他连接同一个Service的所有客户端,而无需再次调用onConnect方法。 onDisconnect​() 在Ability与绑定的Service断开连接时调用。 onStop() 在Service销毁时调用。Service应通过实现此方法来清理任何资源,如关闭线程、注册的侦听器等。 创建Service的代码示例如下: public class ServiceAbility extends Ability { @Override public void onStart(Intent intent) { super.onStart(intent); } @Override public void onCommand(Intent intent, boolean restart, int startId) { super.onCommand(intent, restart, startId); } @Override public IRemoteObject onConnect(Intent intent) { return super.onConnect(intent); } @Override public void onDisconnect(Intent intent) { super.onDisconnect(intent); } @Override public void onStop() { super.onStop(); } } 2、注册Service。 Service也需要在应用配置文件中进行注册,注册类型type需要设置为service。 { "module": { "abilities": [ { "name": ".ServiceAbility", "type": "service", "visible": true ... } ] ... } ... } 启动Service 介绍通过startAbility()启动Service以及对应的停止方法。 启动Service Ability为开发者提供了startAbility()方法来启动另外一个Ability。因为Service也是Ability的一种,开发者同样可以通过将Intent传递给该方法来启动Service。不仅支持启动本地Service,还支持启动远程Service。 开发者可以通过构造包含DeviceId、BundleName与AbilityName的Operation对象来设置目标Service信息。这三个参数的含义如下: DeviceId:表示设备ID。如果是本地设备,则可以直接留空;如果是远程设备,可以通过ohos.distributedschedule.interwork.DeviceManager提供的getDeviceList获取设备列表,详见《API参考》。 BundleName:表示包名称。 AbilityName:表示待启动的Ability名称。 启动本地设备Service的代码示例如下: Intent intent = new Intent(); Operation operation = new Intent.OperationBuilder() .withDeviceId("") .withBundleName("com.domainname.hiworld.himusic") .withAbilityName("com.domainname.hiworld.himusic.ServiceAbility") .build(); intent.setOperation(operation); startAbility(intent); 启动远程设备Service的代码示例如下: Intent intent = new Intent(); Operation operation = new Intent.OperationBuilder() .withDeviceId("deviceId") .withBundleName("com.domainname.hiworld.himusic") .withAbilityName("com.domainname.hiworld.himusic.ServiceAbility") .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) // 设置支持分布式调度系统多设备启动的标识 .build(); intent.setOperation(operation); startAbility(intent); 执行上述代码后,Ability将通过startAbility() 方法来启动Service。 如果Service尚未运行,则系统会先调用onStart()来初始化Service,再回调Service的onCommand()方法来启动Service。 如果Service正在运行,则系统会直接回调Service的onCommand()方法来启动Service。 停止Service Service一旦创建就会一直保持在后台运行,除非必须回收内存资源,否则系统不会停止或销毁Service。开发者可以在Service中通过terminateAbility()停止本Service或在其他Ability调用stopAbility()来停止Service。 停止Service同样支持停止本地设备Service和停止远程设备Service,使用方法与启动Service一样。一旦调用停止Service的方法,系统便会尽快销毁Service。 连接Service 如果Service需要与Page Ability或其他应用的Service Ability进行交互,则须创建用于连接的Connection。Service支持其他Ability通过connectAbility()方法与其进行连接。 在使用connectAbility()处理回调时,需要传入目标Service的Intent与IAbilityConnection的实例。IAbilityConnection提供了两个方法供开发者实现:onAbilityConnectDone()是用来处理连接Service成功的回调,onAbilityDisconnectDone()是用来处理Service异常死亡的回调。 创建连接Service回调实例的代码示例如下: // 创建连接Service回调实例 private IAbilityConnection connection = new IAbilityConnection() { // 连接到Service的回调 @Override public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int resultCode) { // Client侧需要定义与Service侧相同的IRemoteObject实现类。开发者获取服务端传过来IRemoteObject对象,并从中解析出服务端传过来的信息。 } // Service异常死亡的回调 @Override public void onAbilityDisconnectDone(ElementName elementName, int resultCode) { } }; 连接Service的代码示例如下: // 连接Service Intent intent = new Intent(); Operation operation = new Intent.OperationBuilder() .withDeviceId("deviceId") .withBundleName("com.domainname.hiworld.himusic") .withAbilityName("com.domainname.hiworld.himusic.ServiceAbility") .build(); intent.setOperation(operation); connectAbility(intent, connection); 同时,Service侧也需要在onConnect()时返回IRemoteObject,从而定义与Service进行通信的接口。onConnect()需要返回一个IRemoteObject对象,HarmonyOS提供了IRemoteObject的默认实现,用户可以通过继承LocalRemoteObject来创建自定义的实现类。Service侧把自身的实例返回给调用侧的代码示例如下: // 创建自定义IRemoteObject实现类 private class MyRemoteObject extends LocalRemoteObject { MyRemoteObject(){ } } // 把IRemoteObject返回给客户端 @Override protected IRemoteObject onConnect(Intent intent) { return new MyRemoteObject(); } Service Ability生命周期 与Page类似,Service也拥有生命周期,如图1所示。根据调用方法的不同,其生命周期有以下两种路径: 启动Service 该Service在其他Ability调用startAbility()时创建,然后保持运行。其他Ability通过调用stopAbility()来停止Service,Service停止后,系统会将其销毁。 连接Service 该Service在其他Ability调用connectAbility()时创建,客户端可通过调用disconnectAbility​()断开连接。多个客户端可以绑定到相同Service,而且当所有绑定全部取消后,系统即会销毁该Service。 图1 Service生命周期 前台Service 一般情况下,Service都是在后台运行的,后台Service的优先级都是比较低的,当资源不足时,系统有可能回收正在运行的后台Service。 在一些场景下(如播放音乐),用户希望应用能够一直保持运行,此时就需要使用前台Service。前台Service会始终保持正在运行的图标在系统状态栏显示。 使用前台Service并不复杂,开发者只需在Service创建的方法里,调用keepBackgroundRunning()将Service与通知绑定。调用keepBackgroundRunning()方法前需要在配置文件中声明ohos.permission.KEEP_BACKGROUND_RUNNING权限,同时还需要在配置文件中添加对应的backgroundModes参数。在onStop()方法中调用cancelBackgroundRunning​()方法可停止前台Service。 使用前台Service的onStart()代码示例如下: // 创建通知,其中1005为notificationId NotificationRequest request = new NotificationRequest(1005); NotificationRequest.NotificationNormalContent content = new NotificationRequest.NotificationNormalContent(); content.setTitle("title").setText("text"); NotificationRequest.NotificationContent notificationContent = new NotificationRequest.NotificationContent(content); request.setContent(notificationContent); // 绑定通知,1005为创建通知时传入的notificationId keepBackgroundRunning(1005, request); 在配置文件中,“module > abilities”字段下对当前Service做如下配置: { "name": ".ServiceAbility", "type": "service", "visible": true, "backgroundModes": ["dataTransfer", "location"] }

优秀的个人博客,低调大师

Go语言学习12-类型转换

类型转换 1. 概念 类型转换是把一个类型的值转换成为另一个类型的值。把这个值原来的类型称为源类型,而这个值被转换后的类型称为目标类型。 如果 T 是值 x 的目标类型,那么相应的类型转换表达式如下: T(x) // x可以是一个表达式,不过这个表达式的结果值只能有一个 如果代表目标类型的字面量始于操作符 * 或 <- ,后者它是没有结果声明列表的函数类型,那么往往需要用圆括号括起来,以避免歧义的产生。例如: *string(v) // 等同于 *(string(v)),先将变量v代表的值转换为string类型的值,然后再获取指向它的指针类型值。 (*string)(v) // 把变量v的值转换为指针类型*string的值。 <-chan int(v) // 等同于 <-(chan int(v)),先将变量v代表的值转换为chan int类型的值,然后再从此通道类型值中接收一个int类型的值。 (<-chan int)(v) // 把变量v的值转换为通道类型<-chan int的值。 func()(v) // Go语言理解为任何无参数声明但有一个结果声明的匿名函数。 (func())(v) // 把变量v的值转换为函数类型func()的值。 func() int(v) // 等同于(func() int)(v),把v的值转换成一个有结果声明的函数的类型。 对于常量 x,如果它能够被转换为类型 T 的值,那么它们符合如下情况: x 可以被类型T的值代表。例如,iota 可以表示一个大于等于零的整数常量。他可以把 uint 类型的值代表。类型表达式 uint(iota) 是合法的,它的结果值会是一个 uint 类型的常量。 x 是一个浮点数常量,T 是一个浮点数类型,并且 x 在(根据IEEE-754标准中描述的向偶数舍入规则)被舍入之后可以被类型 T 的值代表。例如: float32(0.49999998) // 求值结果是一个float32类型的常量0.5 如果 x 是一个整数常量,并且 T 是一个 string 类型,那么将会遵循一套规则来决定类型转换的结果。它同样适合于非常量的值。这将在后面的 与 string类型相关的转换 处讲解。 对于非常量 x,它能够被转换为类型 T 的值,那么它们符合如下情况:值 x 可以被赋值给类型 T 的变量。例如: type Computer interface { CpuType() string } type Laptop struct { cpuType string } func (self Laptop) CpuType() string { return self.cpuType } 类型转换表达式: // 合法,求值结果会是一个Computer类型的值。因为类型Laptop是接口类型Computer的一个实现类型 Computer(Laptop{cpuType: "Intel Core i5"}) 值 x 的类型和类型 T 的潜在类型是相等的。例如: type MyString string // 类型转换表达式 MyString("Huazie") // 合法,类型MyString的潜在类型就是string类型。 值 x 的类型和类型 T 都是未命名的指针类型,并且它们的基本类型(指向那个值的类型)的潜在类型是相等的。例如: var str1 string // 类型转换表达式 (*string)(&str1) // 合法,求值结果是一个*MyString类型的值。 值 x 的类型和类型 T 都是整数类型或都是浮点数类型。例如: var i32 uint32 var f32 folat32 // 类型转换表达式 int64(i32) // 合法。 float64(f32) // 合法。 值 x 的类型和类型 T 都是复数类型。例如: var comp64 complex64 // 类型转换表达式 complex128(comp64) // 合法。 值 x 是一个整数类型值或是一个元素类型为 byte 或 rune 的切片类型值,且 T 是一个 string 类型。例如: string([]byte{'a'}) // 合法,求值结果是string类型值"a"。 值 x 是一个 string 类型值,且T是一个元素类型为 byte 或 rune 的切片类型。 []rune("Huazie") // 合法 2. 数值类型之间的转换 可以通过常量声明或者数据类型转换把一个int类型的变量,数值常量1024是无类型的,例如: var number int = 1024 或把数值常量 1024 赋给一个 int 类型的变量: int(1024) 对于非常量的数值类型值,规则如下: 当把一个整数类型值从需要较少二进制位表示的整数类型转换到需要较多二进制位表示的整数类型(比如从 int8 类型转换到 int16 类型)的时候 : 如果这个整数类型值是有符号的,那么该符号位上的(最左边的)那个二进制值将作为扩展项填充在转换过程中新增的那些二进制位上,否则将会把 0 作为扩展项进行填充。这种扩展方式是针对整数类型值的补码而言的。例如:int16 类型值 -32767 的十六进制表示是 0xffff 。它的补码是 0x8001 。此补码最左边的二级制位上的二级制值是 1。如果要把这个 int16 类型值转换为 int32 类型值,就需要用最左边的这个值 1 填充在高位一侧新增的那16个二进制位上。类型转换之后的补码是 0xffff8001 。在这个补码之上再求其补码以得其原码,即 0x80007fff 。此原码表示的就是十进制数 -32767 ,类型转换前的那个数值相等。 当把一个整数类型值从需要较多二级制位表示的整数类型转换到需要较少二级制位表示的整数类型的时候,需要把多余的若干个较高位置的二进制值裁掉,而只保留与目标类型所需二进制位数相当的若干个较低位置的二进制值。例如,int16 类型值 -32767,如果要把它转换为一个 int8 类型值,就需要对其补码 0x8001 截取较低 8 为的二进制值,得到 0x01。由于此值的最左边的二进制位上是 0,所以它本身就是类型转换总会得到一个有效的数值。但对于整数常量来说,这样的类型转换就会造成一个编译错误。例如,类型转换表达式 int8(-32767) 会使编译器报错,因为整数常量 -32767 超出了 int8 类型所能表示的数值范围。 当把一个浮点数类型值向整数类型值进行转换的时候,该浮点数类型值的小数部分将被抹去。例如,如果有一个 float32 类型的变量 f32 且其值为 -32767.345 ,那么类型表达式 int32(f32) 的求值结果为 -32767 。如果浮点数类型值在被抹去小数位之后超出了目标整数类型的表示范围,那么该值还会被截短。例如,在类型表达式 int8(f32) 被求值的过程中会首先 float32 类型值 -32767.345 的小数部分却去掉,然后再将其中较高的 24 位的二进制值截掉,最终得到结果 1 。 当把一个整数或浮点数转换为一个浮点数类型的值或者把一个复数转换为一个复数类型的值的时候,该值将会被依据目标类型的精度进行舍入操作。例如,在 float32 类型的变量 x 中存储的值可能会超出 IEEE-754 标准中规定的 32 位(二进制值代表的)浮点数的精度。但是,类型表达式 float32(x) 的求值结果一定会是 x 的值向32位浮点数的精度转化之后的值。算术表达式 x + 0.1 的结果值可能会超出32位浮点数的精度,但是类型转换表达式 float32(x + 0.1) 的求值结果却不会这样。 在非常量的浮点数类型值或复数类型值的类型转换中,当目标类型的精度不能够满足被转换的值的需要的时候,虽然转换会成功,但其结果将是不确定的,这依赖于不同平台的Go语言的具体实现。 3. 与string类型相关的转换 当把一个有符号整数值或无符号整数值向字符串类型转换的时候,将会产生出一个字符串类型值。被转换的整数值应该是一个有效的 Unicode 代码点的代表。在作为结果的字符串类型值中的就是那个 Unicode 代码点对应的字符。在底层,这个字符串类型值是由该 Unicode 代码点的 UTF-8 编码值表示的。如果被转换的整数值不能代表一个有效的 Unicode 代码点,那么转换结果将会是“\ufffd”,即 Unicode 字符“�”。例如: string(0x4e2d) // 求值结果为“中”,其UTF-8编码为\xe4\xb8\xad string('国') // 求值结果值为“国”,其UTF-8编码为\xe5\x9b\xbd string(-1) // 求值结果为“�”,整数值-1不能代表一个有效的Unicode代码点。 如果有一个目标类型是 string 类型的别名类型是 MyString,那么可以将它视同为 string 类型。例如: MyString(0x4e2d) // 等同于string(0x4e2d) 当把一个元素类型为 byte 的切片类型值向字符串类型转换时,将会产生出一个字符串类型值。这个字符串类型值实际上就是由被转换的切片类型值中的每个字节类型值依次组合而成的。如果切片类型值为 nil,那么类型转换的结果将会是“”。例如: string([]byte{'g', '\x6f', '\x6c', '\x61', 'n', 'g'})//求值结果是"golang" 由于使用 "\x" 为前导并后跟两位十六进制数可以表示宽度为一个字节的值,因此一个字节类型的值也就可以由这种方法表示。如果源类型是一个 [ ]byte 类型的别名类型,那么可以将它视同为 [ ]byte 类型。 当把一个元素类型为 rune 的切片类型值向字符串类型转换时,将会产生出一个字符串类型值。这个字符串类型值实际上就是依次串联每个 rune 类型值后的结果。如果切片类型值为 nil,那么类型转换的结果将会是“”。例如: string([]rune{ 0x4e2D, 0x56fd })//求值结果是"中国" 如果源类型是一个 [ ]rune 类型的别名类型,那么我们可以将它视同为 [ ]rune 类型。 当把一个字符串类型值向 [ ]byte 类型转换时,其结果将会是把该字符串类型值按字节拆分后的结果。对于“”来说,转换后的结果一定是 [ ]byte 类型的空值 nil 。例如: []byte("hello")//结果是[]byte{104, 101, 108, 108, 111} 在这个 [ ]byte 类型值中的每个元素都是对应字符的ASCII编码值的十进制表示形式。如果目标类型是一个 [ ]byte 类型的别名类型,那么可以将它视同为 [ ]byte 类型。 当把一个字符串类型值向 [ ]rune 类型转换时,其结果将会是把该字符串类型值按字符拆分后的结果。对于 "" 来说,转换后的结果一定是 [ ]rune 类型的空值 nil 。 []rune("中国") // 结果是[]byte{20013, 22269} 在这个 [ ]rune 类型值中的每个元素都是对应字符的 Unicode 代码点的十进制表示形式。如果目标类型是一个 [ ]rune 类型的别名类型,那么可以将它视同为 [ ]rune 类型。 UTF-8 这种编码方式会把一个字符编码为一个或多个字节。对于同一个字符串类型值来说,与它对应的字节序列和字符序列中的元素并不一定是一 一对应的。字节序列中的单个字节并不一定能代表一个完整的字符。例如,以字符串类型值“中国”为例: // 字节序列的前三个元素代表了字符'中'的UTF-8编码值,而后三个元素则代表了字符'国'的UTF-8编码值。 []byte{228, 184, 173, 229, 155, 189} // 这个字符序列中的第一个元素代表了字符'中'的Unicode代码点,而第二个元素则代表了字符'国'的Unicode代码点。 []byte{20013, 22269} 对于每一个ASCII编码可表示的字符来说,它的 Unicode 代码点和 UTF-8 编码值与其 ASCII 编码值都分别是一致的,且它们都可以由一个字节类型值代表。对于一个包含了 ASCII 编码可表示的字符的字符串类型值来说,与它对应的字节序列和字符序列中的元素值必定也是一一对应的。 byte 类型值和 rune 类型值都属于整数值的一种。所有整数值都可以由十进制字面量、八进制字面量和十六进制字面量来代表。可以把任意一种方式表示的 rune 字面量赋给任何整数类型的变量,只要该 rune 字面量对应的 Unicode 代码点不超出那个整数类型的表示范围。例如: var nation int16 = '国' // '国' == 0x56fd == 22269 []byte{ 'g', '\x6f', '0x6c', '\u0061', '\156', '\U00000067' } // 求值结果是"golang" 4. 别名类型值之间的转换 类型是 MyString 是 string 类型的别名类型。如果一个整数值分别转换为这两个类型的值,将会得到相同的结果。把一个字符串字面量赋给 MyString 类型的变量: var ms MyString = "中国" 在 MyString 类型的值之上应用切片操作: ms[1] 在某个数据类型和它的别名类型之间以及同一个数据类型的多个别名类型之间的类型转换是合法的。并且,在这种类型转换的过程中并不会创造出新的值,而仅仅是变换了一下那个已存在的值的所属类型。 结语 本篇主要介绍了Go语言数据使用中类型转换相关的内容,下一篇我们将会介绍Go语言的一些内建函数的使用,敬请期待!!! 最后附上知名的Go语言开源框架: etcd: 一个高可用的键值存储系统。它可被用于建立共享配置系统和服务发现系统。它的灵感来自于Apache ZooKeeper。我们可以在https://github.com/coreos/etcd上找到它的源码。

优秀的个人博客,低调大师

Go语言学习7-函数类型

3.Go语言数据类型 上篇我们了解了Go语言的字典类型,本篇主要了解函数和方法。主要如下: 3.5 函数和方法 在Go语言中,函数类型是一等类型,可以把函数当做一个值来传递和使用。函数类型的值(简称为函数值)既可以作为其他函数的参数,也可以作为其他函数的结果(之一)。 3.5.1 类型表示法 函数类型指代了所有可以接受若干参数并能够返回若干结果的函数。 声明一个函数类型总会以关键字 func 作为开始,紧跟在关键字 func 之后的应该是这个函数的签名,包括了参数声明列表(在左边)和结果声明列表(在右边),两者用空格分隔。参数声明列表必须由圆括号括起来,多个参数声明之间需用逗号分隔。 参数声明是参数名称在前,参数类型在后,中间以空格分隔。如果有一个参数列表,除了一个名称为 name、类型为 string 的参数之外,还包括一个名称为 age 、类型为 int 的参数。参数列表如下: (name string, age int) 注意:在同一个参数声明列表中的所有参数名称都必须是唯一的。 如果相邻的两个参数属于同一数据类型,那么我们只需要写一次参数类型。在上面的参数类型中添加一个名称为 level 、类型为 int 的参数: (name string, age, level int) 这个就相当于: (name string, age int, level int) 当然,这里甚至可以省略所有参数的名称。但是强烈不推荐这种做法,它的可读性很差。 现在向参数声明中添加一个名称为 informations 、类型为 …string 的可变长参数: (name string, age int, level int, informations ...string) 注意:可变参数必须是参数列表中的最后一个。 函数类型声明的结果声明列表中一般包含若干个结果声明。结果声明列表的编写规则与参数声明基本一致。不过有两点区别: 只存在可变长参数的声明而不存在可变长结果的声明; 如果结果声明列表中只有一个结果声明且这个结果声明中并不包含结果的名称,那么就可以忽略它的圆括号。 如下, bool 就是这个函数类型的唯一结果的类型声明。该结果声明独自组成了该函数类型的结果声明列表。 func (name string, age int, level int, informations ...string) bool 如果我们需要命名这个结果为 done,可以如下编写: func (name string, age int, level int, informations ...string) (done bool) 注意:这时的结果声明列表必须被圆括号括起来了。命名的结果其名称可以作为附属于该函数类型声明的文档的一部分,方便其他阅读的人员了解其含义。 一个函数类型可以有一个结果声明的列表,这是因为Go语言的函数类型可以有多个结果,这是Go语言的先进特性之一。如下函数类型声明: func (name string, age int, level int, informations ...string) (effected uint, err error) 为函数声明多个结果可以让每个结果的职责更加单一,这既易于理解又方便使用。如上可以利用这一特性将错误值作为结果(之一)返回给调用它的代码,而不是包错误抛出来,然后再不得不在调用它的地方编写若干代码来抓住这个错误。(有关Go语言的错误处理机制后续博文会详细讨论) 函数类型的多个结果声明可以从不同的角度来体现函数的内部操作的结果。例如: func (name string, age int, level int, informations ...string) (done bool, id uint, synchronized bool) 假设上面声明的函数类型专门用于保存某项数据,它的3个结果的作用如下: done : 用于表示数据是否被成功保存。 id : 数据被保存后的ID,此ID可以被用来检索数据。 synchronized : 用于表示数据是否被同步到相关系统中。 这样该函数的调用法会更加清晰明了地获知具体的操作结果,处理这些操作的结果的代码也会更加简单和扁平化。 3.5.2 值表示法 函数类型的零值是nil。未初始化的函数类型的变量的值就是nil。函数类型的值分为两类:命名函数值和匿名函数值。在Go语言中,很多时候通常称命名函数值为命名函数,称匿名函数值为匿名函数,但是它们都是值的一种。 命名函数 命名函数的声明一般由关键字func、函数名称、函数签名(由参数声明列表和结果声明列表组成)和函数体组成。如果在函数的签名中包含了结果声明列表,那么在该函数的函数体中的任何可到达的流程分支的最后一条语句都必须是终止语句。终止语句有很多种,比如以关键字return或goto开始的语句、仅包含针对内建函数panic(用于产生一个运行时恐慌)的调用表达式的语句。 定义了一个用于取模运算的 Module 函数: func Module(x, y int) int { return x % y } 注意:在关键字 return 右边的结果必须在数量上与该函数的结果声明列表中的内容完全一致,且在对应位置的结果的类型上存在可赋予的关系,否则将不能通过编译。 为 Module 函数的结果命名,例如: func Module(x, y int) (result int){ return x % y } 为函数的结果命名会使它们能过以常规变量的形式存在,就像函数的参数那样。当结果被命名,它们在函数被调用时就会被初始化为对应的数据类型的零值。如果这样的函数的函数体中有一条不带任何参数的 return 语句,那么在执行到这条 return 语句的时候,作为结果的变量的当前值就会被返回给函数调用方。例如: func Module(x, y int) (result int){ result = x % y return } 如上面 Module 函数被调用时,变量 result 被初始化为 int 类型的零值 0。当该函数的函数体中的第一条语句被执行时,变量 result 被赋予了表达式 x % y 的结果值。当该函数体中的无参数的 return 语句被执行时,result 的当前值就会作为结果被返回给函数调用方。 知识点: Go语言命名函数的声明还可以省略掉函数体。这意味着,该函数会由外部程序(如汇编语言程序)实现,而不会由Go语言程序实现。 匿名函数 匿名函数由函数字面量表示。函数字面量也是表达式的一种。在声明的内容上,匿名函数与命名函数的区别也只是少了一个函数名称。如下匿名函数: func (x, y int) (result int){ result = x % y return } 函数字面量也可以看做是对某个函数类型的即时实现,它比函数类型声明多了一个函数体。一个函数字面量可以被赋给一个变量,也可以被直接调用。 3.5.3 属性和基本操作 函数作为Go语言的数据类型之一,可以把函数作为一个变量的类型。例如声明一个变量: var recorder func (name string, age int, level int)(done bool) 声明过后,所有符合这个函数类型的实现都可以被赋给变量 recorder,如下: recorder = func (name string, age int, level int) (done bool) { //省略若干实现语句 return } 注意:被赋给变量 recorder 的函数字面量必须与 recorder 的类型拥有相同的函数签名。 可以在一个函数类型的变量上直接应用函数表达式来调用它,例如: done := recorder("Huazie", 23, 1) 注意:被赋值的变量在数量上必须与函数的结果声明列表中的内容完全一致,且对应位置的变量和结果的类型上存在可赋予的关系。同样适用于对命名函数进行调用并赋值的情况。 在函数字面量被编写出来的时候直接调用它,例如: recorder = func (name string, age int, level int) (done bool) { //省略若干实现语句 return }(“Huazie”, 23, 1) 如上所示函数既然可以作为变量的值,那么也就可以像其他值一样在函数之间传递(即作为其他函数的参数或其他函数的结果)。 现在举出一个例子,现在要声明一个可以对一段文本进行加密的函数,同时,要求可以根据不同的应用场景实时地、频繁地对加密算法进行变更。如上,我们应该声明一个能够生成加密函数的函数,然后在程序运行期间,根据不同的要求使用这个函数来生成需要的加密函数。此外,所有用于封装加密算法的函数都应该是同一个函数类型的,这有利于加密算法的无缝替换。 首先声明一个如下的函数类型: type Encipher func(plaintext string) []byte 如上Encipher是函数类型 func(plaintext string) []byte 的别名,这个函数接收一个 string 类型的参数,并且返回一个元素类型为 byte 的切片类型的结果,这分别代表了一类比较通用的加密算法的输入数据和输出数据。 有了这个用于封装加密算法的函数类型之后,如下声明可以生成加密函数的函数: func GenEncryptionFunc(encrypt Encipher) func(string) (ciphertext string) { return func(plaintext string) string { return fmt.Sprintf("%x", encrypt(plaintext)) } } 如上看着比较复杂的函数 GenEncryptionFunc 的签名中包括了一个参数声明和一个结果声明。其中,参数声明中的参数类型就是之前定义的用于封装加密算法的函数类型,结果声明表示了一个函数类型的结果。而这个函数类型正是 GenEncryptionFunc 函数所生成的加密函数的类型,它接收一个 string 类型的明文作为参考,并返回一个 string 类型的密文作为结果。 在 GenEncryptionFunc 函数的函数体内直接返回了复合加密函数类型的匿名函数。这个匿名函数的函数体内这一条语句首先调用了名称为 encrypt 的函数,对匿名函数的参数的明文加密;然后,它使用了标准库代码包 fmt 中的 Sprintf 函数,把 encrypt 函数的调用结果转换为字符串。该字符串的内容实际上是用十六进制数表示的加密结果,而这个加密结果实际上是 []byte 类型的。 每一次调用 GenEncryptionFunc 函数时,传递给他的那个加密算法函数都会一直被对应的加密函数引用这。只要生成的加密函数还可以被访问,其中的加密算法函数就会一直存在,而不会被Go语言的垃圾回收器回收。理解GenEncryptionFunc函数所涉及到的一些概念: 知识点: 闭包这个词源自于通过“捕获”自由变量的绑定对函数文本执行的“闭合”动作。 只有当函数类型是一等类型并且其值可以作为其他函数的参数或结果的时候,才能够编写出实现闭包的代码。函数类型是Go语言支持函数式编程范式的重要体现,也就是我们编写函数式风格代码的主要手段。函数还可以附属于任何自定义的数据类型,或者与接口类型和结构体类型相结合作为针对某个或某些数据类型的操作方法。 3.5.4 方法 方法就是附属于某个自定义的数据类型的函数。一个方法就是一个与某个接受者关联的函数。方法的声明中包含了关键字func、接收者声明、方法名称、参数声明列表、结果声明列表和方法体。其中的接收者声明、参数声明列表和结果声明列表统称为方法签名,而方法体可以在某些情况下被忽略。例如: type MyIntSlice []int func (self MyIntSlice) Max() (result int) { //省略若干实现语句 return } 如上,我们首先自定义了一个数据类型MyIntSlice,可以看做 []int 的别名类型。同时,这里还声明了一个方法。在这个名称为 Max 的方法中,接收者声明为(self MyIntSlice)。右边的标识符表示该方法所属的数据类型,即 MyIntSlice ; 左边的接收者标识符则代表了 MyIntSlice 类型的值在方法 Max 中的名称。 方法声明中的接收者声明有关的几条编写规则: 接收者声明中的类型必须是某个自定义的数据类型,或者是一个与某个自定义数据类型对应的指针类型。但不论接收者的类型是哪一种,接收者的基本类型都会是那个自定义数据类型。接收者的基本类型既不能是一个指针类型,也不能是一个接口类型。例如, 方法声明: func (self *MyIntSlice) Min() (result int)//接收者的类是*MyIntSlice,而其基本类型是MyIntSlice. 接收者声明中的类型必须由非限定标识符代表。方法所属的数据类型的声明必须与该方法声明处在同一个代码包内。 接收者标识符不能是空标识符“_”, 并且必须在其所在的方法签名中是唯一的。 如果接收者的值(由接收者标识符代表)未在当前方法的方法体内被引用,那么我们就可以将这个接收者标识符从当前方法的接收者声明中删除掉。注意,这条不建议这么做,原因和函数声明中的参数声明类似,会使代码的可读性变差。 在Go语言中,常常把接收者类型是某个自定义数据类型的方法叫做该数据类型的值方法,而把接收者类型是某个自定义数据类型对应的指针类型的方法叫作该数据类型的指针方法。 对于一个接收者的基本类型来说,它所包含的方法的名称之间不能有重复。如果这个接收者的基本类型是一个结构体类型,还需要保证它包含的字段和方法的名称之间不能出现重复。 定义一个方法: func (self *MyIntSlice) Min() (result int) 该方法的类型: func Min() (self *MyIntSlice, result int) 注意:形如上述方法的类型表示的函数的值只能算是一个函数,而不能叫作方法。这样的函数并没有与任何自定义数据类型相关联。 在接收者的基本类型确定的情况下,如何在值方法和指针方法做出选择: 在某个自定义数据类型的值上,只能够调用与这个数据类型相关联的值方法,而在指向这个值的指针值上,却能够调用与其数据类型关联的值方法和指针方法。虽然自定义数据类型的方法集合中不包含与它关联的指针类型,但是我们仍能够通过这个类型的值调用它的指针方法,这里需要使用取地址符&。 在指针方法中一定能够改变接收者的值。而在值方法中,对接收者的值的改变对于该方法之外一般是无效的。以接收者标识符代表的接收者的值实际上也是当前方法所属的数据类型的当前值的一个复制品。对于值方法来说,由于这个接收者的值就是一个当前值的复制品,所以对它的改变并不会影响到当前值。而对于指针方法来说,这个接收者的值则是一个当前值的指针的复制品。依据这个指针对当前值修改,就等于直接对该值进行了改变。不过有个例外,当接收者的类型如果是引用类型的别名类型,那么在该类型值的值方法中对该值的改变也是对外有效的。 本篇就聊到这里,下篇继续未完的Go语言数据类型… 最后附上知名的Go语言开源框架: Docker: 一个软件部署解决方案,也是一个轻量级的应用容器框架。使用 Docker,我们可以轻松地打包、发布和运行任何应用。现在,Docker 已经成为了名副其实的 Go 语言杀手级应用框架。其官网:http://www.docker.com。非官方的中文网站 : http://www.docker.org.cn

优秀的个人博客,低调大师

Golang学习笔记--unsafe.Pointer和uintptr

如果你看go的源码,尤其是runtime的部分的源码,你一定经常会发现unsafe.Pointer和uintptr这两个函数,例如下面就是runtime里面的map源码实现里面的一个函数: func (b *bmap) overflow(t *maptype) *bmap { return *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize)) } 那么这两个方法有什么用呢?下面我们来重点介绍一下。 Go中的指针及与指针对指针的操作主要有以下三种: 一普通的指针类型,例如var intptr *T,定义一个T类型指针变量。 二内置类型uintptr,本质是一个无符号的整型,它的长度是跟平台相关的,它的长度可以用来保存一个指针地址。 三是unsafe包提供的Pointer,表示可以指向任意类型的指针。 1.普通的指针类型 count := 1 Counter(&count) fmt.Println(count) func Counter(count *int) { *count++ } 普通指针可以通过引用来修改变量的值,这个跟C语言指针有点像。 2.uintptr类型 uintptr用来进行指针计算,因为它是整型,所以很容易计算出下一个指针所指向的位置。uintptr在builtin包中定义,定义如下: // uintptr is an integer type that is large enough to hold the bit pattern of any pointer. // uintptr是一个能足够容纳指针位数大小的整数类型 type uintptr uintptr 虽然uintpr保存了一个指针地址,但它只是一个值,不引用任何对象。因此使用的时候要注意以下情况: 1.如果uintptr地址相关联对象移动,则其值也不会更新。例如goroutine的堆栈信息发生变化 2.uintptr地址关联的对象可以被垃圾回收。GC不认为uintptr是活引用,因此unitptr地址指向的对象可以被垃圾收集。 一个uintptr可以被转换成unsafe.Pointer,同时unsafe.Pointer也可以被转换为uintptr。可以使用使用uintptr + offset计算出地址,然后使用unsafe.Pointer进行转换,格式如下:p = unsafe.Pointer(uintptr(p) + offset) n := 10 b := make([]int, n) for i:= 0;i< n;i++ { b[i] = i } fmt.Println(b) // [0 1 2 3 4 5 6 7 8 9] // 取slice的最后的一个元素 end := unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + 9 * unsafe.Sizeof(b[0])) // 等价于unsafe.Pointer(&b[9]) fmt.Println(*(*int)(end)) // 9 3.unsafe.Pointer unsafe.Pointer是特别定义的一种指针类型,它可以包含任意类型变量的地址。Pointer在unsafe包中定义,定义如下: package unsafe // ArbitraryType is here for the purposes of documentation only and is not actually // part of the unsafe package. It represents the type of an arbitrary Go expression. // ArbitraryType在这里不是unsafe包的实际的一部分,仅仅是为了文档记录 type ArbitraryType int type Pointer *ArbitraryType func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr 官方文档unsafe.Pointer的使用有以下说明: Pointer represents a pointer to an arbitrary type. There are four special operations available for type Pointer that are not available for other types: // Pointer代表了一个任意类型的指针。Pointer类型有四种特殊的操作是其他类型不能使用的: - A pointer value of any type can be converted to a Pointer. // 任意类型的指针可以被转换为Pointer - A Pointer can be converted to a pointer value of any type. // Pointer可以被转换为任务类型的值的指针 - A uintptr can be converted to a Pointer. // uintptr可以被转换为Pointer - A Pointer can be converted to a uintptr. // Pointer可以被转换为uintptr Pointer therefore allows a program to defeat the type system and read and write arbitrary memory. It should be used with extreme care. // 因此Pointer允许程序不按类型系统的要求来读写任意的内存,应该非常小心地使用它。 所以unsafe.Pointer做的主要是用来进行桥接,用于不同类型的指针进行互相转换。 在任何情况下,结果都必须继续指向原分配的对象。 4.unsafe.Pointer,uintptr与普通指针的互相转换 unsafe.Pointer和普通指针的相互转换 var f float64 = 1.0 fmt.Println(Float64bits(f)) // 4607182418800017408 func Float64bits(f float64) uint64 { return *((*uint64)(unsafe.Pointer(&f))) } 借助unsafe.Pointer指针,实现float64转换为uint64类型。当然,我们不可以直接通过*p来获取unsafe.Pointer指针指向的真实变量的值,因为我们并不知道变量的具体类型。另外一个重要的要注意的是,在进行普通类型转换的时候,要注意转换的前后的类型要有相同的内存布局,下面两个结构也能完成转换,就因为他们有相同的内存布局 type s1 struct { id int name string } type s2 struct { field1 *[5]byte filed2 int } b := s1{name:"123"} var j s2 j = *(*s2)(unsafe.Pointer(&b)) fmt.Println(j) unsafe.Pointer和uintrptr的互相转换及配合 uintptr类型的主要是用来与unsafe.Pointer配合使用来访问和操作unsafe的内存。unsafe.Pointer不能执行算术操作。要想对指针进行算术运算必须这样来做: 1.将unsafe.Pointer转换为uintptr 2.对uintptr执行算术运算 3.将uintptr转换回unsafe.Pointer,然后访问uintptr地址指向的对象 需要小心的是,上面的步骤对于垃圾收集器来说应该是原子的,否则可能会导致问题。 例如,在第1步之后,引用的对象可能被收集。如果在步骤3之后发生这种情况,指针将是一个无效的Go指针,并可能导致程序崩溃 unsafe.Pointer和uintrptr的转换 package main import ( "fmt" "unsafe" ) type Person struct { age int name string } func main() { p := &Person{age: 30, name: "Bob"} //获取到struct s中b字段的地址 p := unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Offsetof(p.name)) //将其转换为一个string的指针,并且打印该指针的对应的值 fmt.Println(*(*string)(p)) }

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册