mybatis源码学习(一):Mapper的绑定
在mybatis中,我们可以像下面这样通过声明对应的接口来绑定XML中的mapper,这样可以让我们尽早的发现XML的错误。
定义XML:
- <?xml version="1.0" encoding="UTF-8" ?>
- <!--
- Copyright 2009-2016 the original author or authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
- <!DOCTYPE mapper
- PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
- "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="org.apache.ibatis.binding.BoundBlogMapper">
- <resultMap id="blog" type="Blog">
- <id property="id" column="id"/>
- <result property="title" column="title"/>
- </resultMap>
- <select id="selectBlogWithPostsUsingSubSelect" parameterType="int" resultMap="blog">
- select * from Blog where id = #{id}
- </select>
- </mapper>
定义mapper接口:
- public interface BoundBlogMapper {
- Blog selectBlog(int id);
- }
在代码中使用:
- public class MapperTest {
- public static void main(String[] args){
- //创建datasource,具体过程封装在工厂类中,不在详述
- DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
- TransactionFactory transactionFactory = new JdbcTransactionFactory();
- Environment environment = new Environment("Production", transactionFactory, dataSource);
- Configuration configuration = new Configuration(environment);
- configuration.setLazyLoadingEnabled(true);
- configuration.setUseActualParamName(false); // to test legacy style reference (#{0} #{1})
- configuration.getTypeAliasRegistry().registerAlias(Blog.class);
- configuration.addMapper(BoundBlogMapper.class);
- SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
- SqlSession session = sqlSessionFactory.openSession();
- BoundBlogMapper mapper = session.getMapper(BoundBlogMapper.class);
- Blog b = mapper.selectBlogWithPostsUsingSubSelect(1);
- session.close();
- }
- }
那么有两个问题需要了解:
一,mapper接口并未定义实现类,为什么mybatis可以获取到对应的对象?
二,mapper是如何执行对应的SQL的?
来看看session.getMapper()到底做了什么。它是从configuration中获取到对应的mapper对象,而configuration又是从mapperRegistry中获取,因此我们直接看mapperregistry中的方法:
- 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);
- }
- }
knownMapper是一个Map对象,保存了Mapper类型和对应的MapperProxyFactory。
而在我们往configuration添加mapper时,实际上就是将对应的mapper和其MapperProxyFactory添加到了knownMapper中:
- public <T> void addMapper(Class<T> type) {
- if (type.isInterface()) {
- if (hasMapper(type)) {
- throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
- }
- boolean loadCompleted = false;
- try {
- knownMappers.put(type, new MapperProxyFactory<T>(type));
- // It's important that the type is added before the parser is run
- // otherwise the binding may automatically be attempted by the
- // mapper parser. If the type is already known, it won't try.
- MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
- parser.parse();
- loadCompleted = true;
- } finally {
- if (!loadCompleted) {
- knownMappers.remove(type);
- }
- }
- }
- }
回到getMapper中,我们可以看到真正获取mapper实例是交给代理工厂的newInstance方法处理的,来看下MapperProxyFactory类:
- 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) {
- //我们得到的mapper对象是由JDK动态代理创建
- 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对象是由JDK动态代理所创建的,而mapperInterface就是我们需要代理的接口,这就回答了先前的问题一。
继续研究问题二,上面的代码可以看到实际的代理对象是MapperProxy。
- public class MapperProxy<T> implements InvocationHandler, Serializable {
- private static final long serialVersionUID = -6424540398559729838L;
- private final SqlSession sqlSession;
- private final Class<T> mapperInterface;
- private final Map<Method, MapperMethod> methodCache;
- public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
- this.sqlSession = sqlSession;
- this.mapperInterface = mapperInterface;
- this.methodCache = methodCache;
- }
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- try {
- //如果是Object中声明的方法,那么直接invoke
- if (Object.class.equals(method.getDeclaringClass())) {
- return method.invoke(this, args);
- } else if (isDefaultMethod(method)) {
- return invokeDefaultMethod(proxy, method, args);
- }
- } catch (Throwable t) {
- throw ExceptionUtil.unwrapThrowable(t);
- }
- //对应mapper中的方法,委托给mapperMethod执行
- final MapperMethod mapperMethod = cachedMapperMethod(method);
- return mapperMethod.execute(sqlSession, args);
- }
- private MapperMethod cachedMapperMethod(Method method) {
- MapperMethod mapperMethod = methodCache.get(method);
- if (mapperMethod == null) {
- mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
- methodCache.put(method, mapperMethod);
- }
- return mapperMethod;
- }
- @UsesJava7
- private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
- throws Throwable {
- final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
- .getDeclaredConstructor(Class.class, int.class);
- if (!constructor.isAccessible()) {
- constructor.setAccessible(true);
- }
- final Class<?> declaringClass = method.getDeclaringClass();
- return constructor
- .newInstance(declaringClass,
- MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
- | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
- .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
- }
- /**
- * Backport of java.lang.reflect.Method#isDefault()
- */
- private boolean isDefaultMethod(Method method) {
- return (method.getModifiers()
- & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC
- && method.getDeclaringClass().isInterface();
- }
- }
可以看到mapper方法中对应的方法其实是委托给MapperMethod执行的。
- public class MapperMethod {
- private final SqlCommand command;
- private final MethodSignature method;
- public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
- this.command = new SqlCommand(config, mapperInterface, method);
- this.method = new MethodSignature(config, mapperInterface, method);
- }
- 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);
- if (method.returnsOptional() &&
- (result == null || !method.getReturnType().equals(result.getClass()))) {
- result = OptionalUtil.ofNullable(result);
- }
- }
- 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;
- }
- private Object rowCountResult(int rowCount) {
- final Object result;
- if (method.returnsVoid()) {
- result = null;
- } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
- result = rowCount;
- } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
- result = (long)rowCount;
- } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
- result = rowCount > 0;
- } else {
- throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
- }
- return result;
- }
- private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
- MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
- if (!StatementType.CALLABLE.equals(ms.getStatementType())
- && void.class.equals(ms.getResultMaps().get(0).getType())) {
- throw new BindingException("method " + command.getName()
- + " needs either a @ResultMap annotation, a @ResultType annotation,"
- + " or a resultType attribute in XML so a ResultHandler can be used as a parameter.");
- }
- Object param = method.convertArgsToSqlCommandParam(args);
- if (method.hasRowBounds()) {
- RowBounds rowBounds = method.extractRowBounds(args);
- sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args));
- } else {
- sqlSession.select(command.getName(), param, method.extractResultHandler(args));
- }
- }
- private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
- List<E> result;
- Object param = method.convertArgsToSqlCommandParam(args);
- if (method.hasRowBounds()) {
- RowBounds rowBounds = method.extractRowBounds(args);
- result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
- } else {
- result = sqlSession.<E>selectList(command.getName(), param);
- }
- // issue #510 Collections & arrays support
- if (!method.getReturnType().isAssignableFrom(result.getClass())) {
- if (method.getReturnType().isArray()) {
- return convertToArray(result);
- } else {
- return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
- }
- }
- return result;
- }
- private <T> Cursor<T> executeForCursor(SqlSession sqlSession, Object[] args) {
- Cursor<T> result;
- Object param = method.convertArgsToSqlCommandParam(args);
- if (method.hasRowBounds()) {
- RowBounds rowBounds = method.extractRowBounds(args);
- result = sqlSession.<T>selectCursor(command.getName(), param, rowBounds);
- } else {
- result = sqlSession.<T>selectCursor(command.getName(), param);
- }
- return result;
- }
- private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) {
- Object collection = config.getObjectFactory().create(method.getReturnType());
- MetaObject metaObject = config.newMetaObject(collection);
- metaObject.addAll(list);
- return collection;
- }
- @SuppressWarnings("unchecked")
- private <E> Object convertToArray(List<E> list) {
- Class<?> arrayComponentType = method.getReturnType().getComponentType();
- Object array = Array.newInstance(arrayComponentType, list.size());
- if (arrayComponentType.isPrimitive()) {
- for (int i = 0; i < list.size(); i++) {
- Array.set(array, i, list.get(i));
- }
- return array;
- } else {
- return list.toArray((E[])array);
- }
- }
- private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) {
- Map<K, V> result;
- Object param = method.convertArgsToSqlCommandParam(args);
- if (method.hasRowBounds()) {
- RowBounds rowBounds = method.extractRowBounds(args);
- result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey(), rowBounds);
- } else {
- result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey());
- }
- return result;
- }
- public static class ParamMap<V> extends HashMap<String, V> {
- private static final long serialVersionUID = -2212268410512043556L;
- @Override
- public V get(Object key) {
- if (!super.containsKey(key)) {
- throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());
- }
- return super.get(key);
- }
- }
- public static class SqlCommand {
- private final String name;
- private final SqlCommandType type;
- public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
- final String methodName = method.getName();
- final Class<?> declaringClass = method.getDeclaringClass();
- MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
- configuration);
- if (ms == null) {
- if(method.getAnnotation(Flush.class) != null){
- name = null;
- type = SqlCommandType.FLUSH;
- } else {
- throw new BindingException("Invalid bound statement (not found): "
- + mapperInterface.getName() + "." + methodName);
- }
- } else {
- name = ms.getId();
- type = ms.getSqlCommandType();
- if (type == SqlCommandType.UNKNOWN) {
- throw new BindingException("Unknown execution method for: " + name);
- }
- }
- }
- public String getName() {
- return name;
- }
- public SqlCommandType getType() {
- return type;
- }
- private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
- Class<?> declaringClass, Configuration configuration) {
- String statementId = mapperInterface.getName() + "." + methodName;
- if (configuration.hasStatement(statementId)) {
- return configuration.getMappedStatement(statementId);
- } else if (mapperInterface.equals(declaringClass)) {
- return null;
- }
- for (Class<?> superInterface : mapperInterface.getInterfaces()) {
- if (declaringClass.isAssignableFrom(superInterface)) {
- MappedStatement ms = resolveMappedStatement(superInterface, methodName,
- declaringClass, configuration);
- if (ms != null) {
- return ms;
- }
- }
- }
- return null;
- }
- }
- public static class MethodSignature {
- private final boolean returnsMany;
- private final boolean returnsMap;
- private final boolean returnsVoid;
- private final boolean returnsCursor;
- private final boolean returnsOptional;
- private final Class<?> returnType;
- private final String mapKey;
- private final Integer resultHandlerIndex;
- private final Integer rowBoundsIndex;
- private final ParamNameResolver paramNameResolver;
- public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
- Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
- if (resolvedReturnType instanceof Class<?>) {
- this.returnType = (Class<?>) resolvedReturnType;
- } else if (resolvedReturnType instanceof ParameterizedType) {
- this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
- } else {
- this.returnType = method.getReturnType();
- }
- this.returnsVoid = void.class.equals(this.returnType);
- this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
- this.returnsCursor = Cursor.class.equals(this.returnType);
- this.returnsOptional = Jdk.optionalExists && Optional.class.equals(this.returnType);
- this.mapKey = getMapKey(method);
- this.returnsMap = this.mapKey != null;
- this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
- this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
- this.paramNameResolver = new ParamNameResolver(configuration, method);
- }
- public Object convertArgsToSqlCommandParam(Object[] args) {
- return paramNameResolver.getNamedParams(args);
- }
- public boolean hasRowBounds() {
- return rowBoundsIndex != null;
- }
- public RowBounds extractRowBounds(Object[] args) {
- return hasRowBounds() ? (RowBounds) args[rowBoundsIndex] : null;
- }
- public boolean hasResultHandler() {
- return resultHandlerIndex != null;
- }
- public ResultHandler extractResultHandler(Object[] args) {
- return hasResultHandler() ? (ResultHandler) args[resultHandlerIndex] : null;
- }
- public String getMapKey() {
- return mapKey;
- }
- public Class<?> getReturnType() {
- return returnType;
- }
- public boolean returnsMany() {
- return returnsMany;
- }
- public boolean returnsMap() {
- return returnsMap;
- }
- public boolean returnsVoid() {
- return returnsVoid;
- }
- public boolean returnsCursor() {
- return returnsCursor;
- }
- /**
- * return whether return type is {@code java.util.Optional}
- * @return return {@code true}, if return type is {@code java.util.Optional}
- * @since 3.5.0
- */
- public boolean returnsOptional() {
- return returnsOptional;
- }
- private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
- Integer index = null;
- final Class<?>[] argTypes = method.getParameterTypes();
- for (int i = 0; i < argTypes.length; i++) {
- if (paramType.isAssignableFrom(argTypes[i])) {
- if (index == null) {
- index = i;
- } else {
- throw new BindingException(method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters");
- }
- }
- }
- return index;
- }
- private String getMapKey(Method method) {
- String mapKey = null;
- if (Map.class.isAssignableFrom(method.getReturnType())) {
- final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
- if (mapKeyAnnotation != null) {
- mapKey = mapKeyAnnotation.value();
- }
- }
- return mapKey;
- }
- }
- }
我们可以看到MapperMethod会根据方法解析对应的XML,最后交给sqlsession去处理。
至于sqlsession如何处理,及解析的过程在下一篇中继续介绍。
mybatis源码学习(一):Mapper的绑定的更多相关文章
- Spring mybatis源码篇章-sql mapper配置文件绑定mapper class类
前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-MybatisDAO文件解析(二) 背景知识 MappedStatement是mybatis操作sql ...
- Spring mybatis源码学习指引目录
前言: 分析了很多方面的mybatis的源码以及与spring结合的源码,但是难免出现错综的现象,为了使源码陶冶更为有序化.清晰化,特作此随笔归纳下分析过的内容.博主也为mybatis官方提供过pul ...
- mybatis源码学习(一) 原生mybatis源码学习
最近这一周,主要在学习mybatis相关的源码,所以记录一下吧,算是一点学习心得 个人觉得,mybatis的源码,大致可以分为两部分,一是原生的mybatis,二是和spring整合之后的mybati ...
- Mybatis源码解析(三) —— Mapper代理类的生成
Mybatis源码解析(三) -- Mapper代理类的生成 在本系列第一篇文章已经讲述过在Mybatis-Spring项目中,是通过 MapperFactoryBean 的 getObject( ...
- mybatis源码学习:一级缓存和二级缓存分析
目录 零.一级缓存和二级缓存的流程 一级缓存总结 二级缓存总结 一.缓存接口Cache及其实现类 二.cache标签解析源码 三.CacheKey缓存项的key 四.二级缓存TransactionCa ...
- mybatis源码学习:基于动态代理实现查询全过程
前文传送门: mybatis源码学习:从SqlSessionFactory到代理对象的生成 mybatis源码学习:一级缓存和二级缓存分析 下面这条语句,将会调用代理对象的方法,并执行查询过程,我们一 ...
- mybatis源码学习:插件定义+执行流程责任链
目录 一.自定义插件流程 二.测试插件 三.源码分析 1.inteceptor在Configuration中的注册 2.基于责任链的设计模式 3.基于动态代理的plugin 4.拦截方法的interc ...
- Mybatis源码学习第六天(核心流程分析)之Executor分析
今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ...
- Mybatis源码学习之整体架构(一)
简述 关于ORM的定义,我们引用了一下百度百科给出的定义,总体来说ORM就是提供给开发人员API,方便操作关系型数据库的,封装了对数据库操作的过程,同时提供对象与数据之间的映射功能,解放了开发人员对访 ...
- mybatis源码学习(三)-一级缓存二级缓存
本文主要是个人学习mybatis缓存的学习笔记,主要有以下几个知识点 1.一级缓存配置信息 2.一级缓存源码学习笔记 3.二级缓存配置信息 4.二级缓存源码 5.一级缓存.二级缓存总结 1.一级缓存配 ...
随机推荐
- PTA数据结构与算法题目集(中文) 7-9
PTA数据结构与算法题目集(中文) 7-9 7-9 旅游规划 (25 分) 有了一张自驾旅游路线图,你会知道城市间的高速公路长度.以及该公路要收取的过路费.现在需要你写一个程序,帮助前来咨询的游 ...
- javascript入门 之 ztree (十 checkbox选中事件)
<!DOCTYPE html> <HTML> <HEAD> <TITLE> ZTREE DEMO - beforeCheck / onCheck< ...
- MySQL中的事务和MVCC
本篇博客参考掘金小册--MySQL 是怎样运行的:从根儿上理解 MySQL 以及极客时间--MySQL实战45讲. 虽然我们不是DBA,可能对数据库没那么了解,但是对于数据库中的索引.事务.锁,我们还 ...
- matplotlib TransformedBbox 和 LockableBbox
TransformedBbox 和 LockableBbox 都是BboxBase的子类.TransformedBbox支持使用变换来初始化bbox, LockableBbox可实现锁定bbox的边不 ...
- split(" {1,}") 含义
将字符串按照括号内的内容分割成字符数组 这里括号内是正则表达式,X{m,n}代表X至少重复m次,至多重复n次 这里x为空格,至少重复1次,就是将字符串以一个或多个空格分割 如"1 2 ab ...
- PHP安全(文件包含、变量覆盖、代码执行)
文件包含漏洞 本地文件包含 截断技巧: ../../etc/passwd%00(\x00 \0) 利用操作系统对目录最大长度的限制,可以不需要0字节而达到截断的目的.目录字符串,在windows下25 ...
- Gatling脚本编写技巧篇(二)
脚本示例: import io.gatling.core.Predef._ import io.gatling.http.Predef._ import scala.concurrent.durati ...
- Python生成一维码
参考页面 https://pypi.org/project/python-barcode/ 利用python-barcode的库 一.安装python-barcode库 #安装前提条件库 pip in ...
- 虚拟机体验NAS私人云全揭秘:序言——虚拟机体验NAS私人云缘由
"世界在新冠肺炎疫情后将永远改变",对于2020春天在全球蔓延的新冠肺炎疫情,美国前国务卿基辛格做了这样的评价.确实,也改变了我们.春节期间,本着少添乱的原则,响应国家号召,自我隔 ...
- 设计模式-原型模式(Prototype)【重点:浅复制与深复制】
讲故事 最近重温了一下星爷的<唐伯虎点秋香>,依然让我捧腹不已,幻想着要是我也能有一名秋香如此的侍女,夫复何求呀,带着这个美好的幻想沉沉睡去... 突然想到,我是一名程序猿呀,想要什么对象 ...