IoC容器的初始化包括BeanDefinition的Resource定位、载入和注册这三个基本的过程。

一、Resource定位。BeanDefinition的资源定位有resourceLoader通过统一的Resource接口来完成,这个Resource对各种形式的BeanDefinition的使用提供了统一接口。对于这些BeanDefinition的存在形式,相信不陌生,如:

FileSystemResource、ClassPathResource。这个过程类似于容器寻找数据的过程,就像用水桶装水要把水找到一样。

二、第二个关键的部分是BeanDefinition的载入,该载入过程把用户定义好的Bean表示成IoC容器内部的数据结构,而这个容器内部的数据结构就是BeanDefinition。简单说,BeanDefinition实际上是POJO对象在IoC容器中的抽象,这个BeanDefinition定义了一系列的数据来使得IoC容器能够方便地对POJO对象也就是Spring的Bean进行管理。即BeanDefinition就是Spring的领域对象。

三、第三个过程是向IoC容器注册这些BeanDefinition的过程。这个过程是通过调用BeanDefinitionRegistry接口的实现来完成的,这个注册过程把载入过程中解析得到的BeanDefinition向IoC容器进行注册。可以看到,在IoC容器内部,是通过使用一个HashMap来持有BeanDefinition数据的。

总结:

此处对于BeanPostProcessor接口的调用应该属于高级应用了,该思路常用来解决扩展或集成Spring框架,其核心的思路可以分为以下几步:

1、自定义实现类路径扫描类,决定哪些类应该被注入进Spring容器。

2、采用Java动态代理来动态实现对于声明接口类的注入。

3、实现BeanDefinitionRegistryPostProcessor,在Spring初始化初期将需要扫描导入Spring容器的类进行注入。

 4、通过代码动态创建

代码动态创建

我们通过getBean来获得对象,但这些对象都是事先定义好的,我们有时候要在程序中动态的加入对象.因为如果采用配置文件或者注解,我们要加入对象的话,还要重启服务,如果我们想要避免这一情况就得采用动态处理bean,包括:动态注入,动态删除。

本节大纲 :
(1)动态注入bean思路;
(2)动态注入实现代码;
(3)多次注入同一个bean的情况;
(4)动态删除;

接下来我们看下具体的内容:

(1)动态注入bean思路;

在具体进行代码实现的时候,我们要知道,Spring管理bean的对象是BeanFactory,具体的是DefaultListableBeanFactory,在这个类当中有一个注入bean的方法:registerBeanDefinition,在调用registerBeanDefinition方法时,需要BeanDefinition参数,那么这个参数怎么获取呢?Spring提供了BeanDefinitionBuilder可以构建一个BeanDefinition,那么我们的问题就是如何获取BeanFactory了,这个就很简单了,只要获取到ApplicationContext对象即可获取到BeanFacory了。

(2)动态注入实现代码;

综上所述,如果我们要编写一个简单里的例子的话,那么分以个几个步骤进行编码即可进行动态注入了:

<1>. 获取ApplicationContext;
<2>. 通过ApplicationContext获取到BeanFacotory;
<3>. 通过BeanDefinitionBuilder构建BeanDefiniton;
<4>. 调用beanFactory的registerBeanDefinition注入beanDefinition;
<5>. 使用ApplicationContext.getBean获取bean进行测试;

很明显我们需要先定义个类进行测试,比如TestService代码如下:

package com.dxz.test;

public class TestService {

    private String name;

    public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public void print() {
System.out.println("动态载入bean,name=" + name);
} }

注意:这里没有使用@Service和配置文件进行注入TestService。

那么下面我们的目标就是动态注入TestService了,根据以上的分析,我们进行编码,具体代码如下:

package com.dxz.test;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext; @SpringBootApplication
public class Application { public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args); // 获取BeanFactory
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) ctx
.getAutowireCapableBeanFactory(); // 创建bean信息.
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(TestService.class);
beanDefinitionBuilder.addPropertyValue("name", "张三"); // 动态注册bean.
defaultListableBeanFactory.registerBeanDefinition("testService", beanDefinitionBuilder.getBeanDefinition()); // 获取动态注册的bean.
TestService testService = ctx.getBean(TestService.class);
testService.print();
}
}

或者

@SpringBootApplication
public class Application { public static void main(String[] args) {
ClassPathResource res = new ClassPathResource("beans.xml"); // 获取BeanFactory
DefaultListableBeanFactory factory= new DefaultListableBeanFactory(); // 创建bean信息.
XmlBeanDefinitionReader beanDefinitionBuilder = new XmlBeanDefinitionReader(factory);
beanDefinitionBuilder.loadBeanDefinitions(res);
//...
}
}

执行代码我们会在控制台看到如下打印信息:

动态载入bean,name=张三

到这里,就证明我们的代码很成功了。

(3)多次注入同一个bean的情况;

多次注入同一个bean的,如果beanName不一样的话,那么会产生两个Bean;如果beanName一样的话,后面注入的会覆盖前面的。

第一种情况:beanName一样的代码:

beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(TestService.class);
beanDefinitionBuilder.addPropertyValue("name","李四");
defaultListableBeanFactory.registerBeanDefinition("testService", beanDefinitionBuilder.getBeanDefinition());

运行看控制台:

动态载入bean,name=李四

第二种情况:beanName不一样的代码:

beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(TestService.class);
beanDefinitionBuilder.addPropertyValue("name","李四");
defaultListableBeanFactory.registerBeanDefinition("testService1",beanDefinitionBuilder.getBeanDefinition());

此时如果没有更改别的代码直接运行的话,是会报如下错误的:

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [com.kfit.demo.service.TestService] is defined: expected single matching bean but found 2: testService1,testService

大体意思就是在getBean的时候,找到了两个bean,这时候就不知道要获取哪个了,所以在获取的时候,我们就要指定我们是要获取的testService还是testService1,只需要修改一句代码:

将代码:

TestService testService =ctx.getBean(TestService.class);
修改为:
TestService testService =ctx.getBean("testService");

(4)动态删除;

相对于动态注入,动态删除就很简单了,直接奉上代码:

//删除bean.
defaultListableBeanFactory.removeBeanDefinition("testService");

实现BeanDefinitionRegistryPostProcessor,在Spring初始化初期将需要扫描导入Spring容器的类进行注入

"对于Spring框架,现实公司使用的非常广泛,但是由于业务的复杂程度不同,了解到很多小伙伴们利用Spring开发仅仅是利用了Spring的IOC,即使是AOP也很少用,但是目前的Spring是一个大家族,形成了一个很大的生态,覆盖了我们平时开发的方方面面,抛开特殊的苛刻要求之外,Spring的生态其实已经很全面了,所以在此开个系列来研究下Spring提供给我们的一些平时不太却又很实用的内容。"

上一篇我们分析了BeanPostProcessor的基本使用,接下来我们分析下如何使用该类实现动态的接口注入,示例说明:在BeetlSQL框架中,在使用自动扫描注入时,我们通常只需要配置上要扫描的包路径,然后在该路径下声明对应的Dao接口类,这些接口类都默认继承BaseMapper接口类,然后我们在使用这些Dao类的时候,直接根据类型注入(@Autowired)即可使用,这个其实和Mybatis的那一套相似,也和Spring自身的Spring-data框架也类似。这个经常用于框架的开发,那么我就该部分的实现做相应的解释,这三个框架具体实现可能有差距,感兴趣的小伙伴自行去查看源码,我会以一个很简单的例子来讲解大概的实现逻辑。

问题描述:

继承Spring框架,实现声明某个自定义接口(UserMapper),改接口继承通用接口BaseMapper,(通用接口BaseMapper有默认的实现类),实现通过类型注入UserMapper类,然后通过Spring框架的上下文类(ApplicationContext实现类)的getBean()方法拿到UserMapper类来调用内部提供的方法。

1、声明BaseMapper接口类

package com.dxz.test;
public interface BaseMapper { public void add(String value); public void remove(String key);
} public class CustomBaseMapper implements BaseMapper {
private final Logger logger = Logger.getLogger(this.getClass().getName());
private List<String> dataList = new CopyOnWriteArrayList<>(); @Override
public void add(String value) {
logger.info("添加数据:" + value);
dataList.add(value);
} @Override
public void remove(String key) {
if (dataList.isEmpty())
throw new IllegalArgumentException("Can't remove because the list is Empty!");
}
}

接下来是继承Spring的核心代码

3、首先我们要先定义一个扫描某路径下的类,该类继承ClassPathBeanDefinitionScanner,自定义扫描类:DefaultClassPathScanner

package com.dxz.test;

import java.io.IOException;
import java.util.Arrays;
import java.util.Set; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter; public class DefaultClassPathScanner extends ClassPathBeanDefinitionScanner { private final String DEFAULT_MAPPER_SUFFIX = "Mapper"; public DefaultClassPathScanner(BeanDefinitionRegistry registry) {
super(registry, false);
} private String mapperManagerFactoryBean; /**
* 扫描包下的类-完成自定义的Bean定义类
*
* @param basePackages
* @return
*/
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
// 如果指定的基础包路径中不存在任何类对象,则提示
if (beanDefinitions.isEmpty()) {
logger.warn("系统没有在 '" + Arrays.toString(basePackages) + "' 包中找到任何Mapper,请检查配置");
} else {
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
} /**
* 注册过滤器-保证正确的类被扫描注入
*/
protected void registerFilters() {
addIncludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
throws IOException {
String className = metadataReader.getClassMetadata().getClassName();
// TODO 这里设置包含条件-此处是个扩展点,可以根据自定义的类后缀过滤出需要的类
return className.endsWith(DEFAULT_MAPPER_SUFFIX);
}
});
addExcludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
throws IOException {
String className = metadataReader.getClassMetadata().getClassName();
return className.endsWith("package-info");
}
});
} /**
* 重写父类的判断是否能够实例化的组件-该方法是在确认是否真的是isCandidateComponent 原方法解释:
* 确定给定的bean定义是否有资格成为候选人。 默认实现检查类是否不是接口,也不依赖于封闭类。 以在子类中重写。
*
* @param beanDefinition
* @return
*/
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
// 原方法这里是判断是否为顶级类和是否是依赖类(即接口会被排除掉-由于我们需要将接口加进来,所以需要覆盖该方法)
return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
} /**
* 扩展方法-对扫描到的含有BeetlSqlFactoryBean的Bean描述信息进行遍历
*
* @param beanDefinitions
*/
void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
String mapperClassName = definition.getBeanClassName();
// 必须在这里加入泛型限定,要不然在spring下会有循环引用的问题
definition.getConstructorArgumentValues().addGenericArgumentValue(mapperClassName);
// 依赖注入
definition.getPropertyValues().add("mapperInterface", mapperClassName);
// 根据工厂的名称创建出默认的BaseMapper实现
definition.getPropertyValues().add("mapperManagerFactoryBean",
new RuntimeBeanReference(this.mapperManagerFactoryBean));
definition.setBeanClass(BaseMapperFactoryBean.class);
// 设置Mapper按照接口组装
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
logger.info("已开启自动按照类型注入 '" + holder.getBeanName() + "'.");
}
} public void setMapperManagerFactoryBean(String mapperManagerFactoryBean) {
this.mapperManagerFactoryBean = mapperManagerFactoryBean;
}
}

4、核心的接口实现类:BaseMapperFactoryBean

package com.dxz.test;

import java.lang.reflect.Proxy;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener; public class BaseMapperFactoryBean<T>
implements FactoryBean<T>, InitializingBean, ApplicationListener<ApplicationEvent>, ApplicationContextAware {
/**
* 要注入的接口类定义
*/
private Class<T> mapperInterface; /**
* Spring上下文
*/
private ApplicationContext applicationContext; // 也因该走工厂方法注入得来 private BaseMapper mapperManagerFactoryBean; public BaseMapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
} @Override
public T getObject() throws Exception {
// 采用动态代理生成接口实现类,核心实现
return (T) Proxy.newProxyInstance(applicationContext.getClassLoader(), new Class[] { mapperInterface },
new MapperJavaProxy(mapperManagerFactoryBean, mapperInterface));
} @Override
public Class<?> getObjectType() {
return this.mapperInterface;
} @Override
public boolean isSingleton() {
return true;
} @Override
public void afterPropertiesSet() throws Exception {
// TODO 判断属性的注入是否正确-如mapperInterface判空
if (null == mapperInterface)
throw new IllegalArgumentException("Mapper Interface Can't Be Null!!");
} /**
* Handle an application event.
*
* @param event
* the event to respond to
*/
@Override
public void onApplicationEvent(ApplicationEvent event) {
// TODO 可依据事件进行扩展
} @Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
} public void setMapperInterface(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
} public void setMapperManagerFactoryBean(BaseMapper mapperManagerFactoryBean) {
this.mapperManagerFactoryBean = mapperManagerFactoryBean;
}
}

5、定义默认的BaseMapper的FactoryBean-MapperManagerFactoryBean

package com.dxz.test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method; public class MapperJavaProxy implements InvocationHandler { private BaseMapper baseMapper; private Class<?> interfaceClass; public MapperJavaProxy(BaseMapper baseMapper, Class<?> interfaceClass) {
this.baseMapper = baseMapper;
this.interfaceClass = interfaceClass;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException("mapperInterface is not interface.");
} if (baseMapper == null) {
baseMapper = new CustomBaseMapper();
}
return method.invoke(baseMapper, args);
}
}

7、调用时的核心配置类:DefaultClassRegistryBeanFactory

package com.dxz.test;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext; public class DefaultClassRegistryBeanFactory
implements ApplicationContextAware, BeanDefinitionRegistryPostProcessor, BeanNameAware { private String scanPackage; private String beanName; private String mapperManagerFactoryBean; private ApplicationContext applicationContext; public String getScanPackage() {
return scanPackage;
} public void setScanPackage(String scanPackage) {
this.scanPackage = scanPackage;
} public String getMapperManagerFactoryBean() {
return mapperManagerFactoryBean;
} public void setMapperManagerFactoryBean(String mapperManagerFactoryBean) {
this.mapperManagerFactoryBean = mapperManagerFactoryBean;
} @Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
if (StringUtils.isEmpty(this.scanPackage)) {
throw new IllegalArgumentException("scanPackage can't be null");
}
String basePackage2 = this.applicationContext.getEnvironment().resolvePlaceholders(this.scanPackage);
String[] packages = StringUtils.tokenizeToStringArray(basePackage2,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
DefaultClassPathScanner defaultClassPathScanner = new DefaultClassPathScanner(beanDefinitionRegistry);
defaultClassPathScanner.setMapperManagerFactoryBean(mapperManagerFactoryBean);
defaultClassPathScanner.registerFilters(); defaultClassPathScanner.doScan(packages);
} @Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory)
throws BeansException { } @Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
} @Override
public void setBeanName(String name) {
this.beanName = name;
}
}

8、调用测试

8.1、假设你在包目录:colin.spring.basic.advanced.inject.dao下声明自定义的类UserMapper

public interface UserMapper extends BaseMapper {
}

8.2、声明配置类:ClassRegistryBeanScannerConfig

package com.dxz.test;

@Configuration
public class ClassRegistryBeanScannerConfig { @Bean(name = "mapperManagerFactoryBean")
public MapperManagerFactoryBean configMapperManagerFactoryBean() {
MapperManagerFactoryBean mapperManagerFactoryBean = new MapperManagerFactoryBean();
return mapperManagerFactoryBean;
} @Bean
public DefaultClassRegistryBeanFactory configDefaultClassRegistryBeanFactory() {
DefaultClassRegistryBeanFactory defaultClassRegistryBeanFactory = new DefaultClassRegistryBeanFactory();
defaultClassRegistryBeanFactory.setScanPackage("colin.spring.basic.advanced.inject.dao");
defaultClassRegistryBeanFactory.setMapperManagerFactoryBean("mapperManagerFactoryBean");
return defaultClassRegistryBeanFactory;
}
}

8.3、测试调用

package com.dxz.test;

public class Snippet {
public static void main(String[] args) {
AnnotationConfigApplicationContext acApplicationCOntext = new AnnotationConfigApplicationContext("colin.spring.basic.advanced.inject");
UserMapper userMapper = acApplicationCOntext.getBean(UserMapper.class);
userMapper.add("lalaldsf");
acApplicationCOntext.stop();
}
}

CXF之三 Tomcat中发布Web Service的更多相关文章

  1. CXF实战之在Tomcat中公布Web Service(二)

    服务接口及实现类请參考WebService框架CXF实战(一) 创建Maven Web项目,在pom.xml中加入CXF和Spring Web的引用,因为CXFServlet须要Spring Web的 ...

  2. tomcat发布web service教程

    这几天一直在准备找工作,自学了关于web service的一些基本的内容,也遇到了不少问题.现在就把我自己学到的知识和大家分享一下,由于是初学,所以有什么错误的地方请大家帮忙指正,感激不尽~~!! 1 ...

  3. 如何在Eclipse或者Myeclipse中使用tomcat(配置tomcat,发布web项目)?(图文详解)(很实用)

    前期博客 Eclipse里的Java EE视图在哪里?MyEclipse里的Java EE视图在哪里?MyEclipse里的MyEclipse Java Enterprise视图在哪里?(图文详解) ...

  4. Eclipse+tomcat+axis2进行web service部署

    用Eclipse+axis2+tomcat进行web service部署 2016-12-07 目录  1 安装JDK  1.1 下载JDK  1.2 安装和配置JDK  1.3 验证2 安装Ecli ...

  5. 用JAX-WS在Tomcat中发布WebService

    JDK中已经内置了Webservice发布,不过要用Tomcat等Web服务器发布WebService,还需要用第三方Webservice框架.Axis2和CXF是目前最流行的Webservice框架 ...

  6. Apache CXF实战之四 构建RESTful Web Service

    Apache CXF实战之一 Hello World Web Service Apache CXF实战之二 集成Sping与Web容器 Apache CXF实战之三 传输Java对象 这篇文章介绍一下 ...

  7. 新手Axis2 发布Web Service之路

    由于公司的需求,需要写几个银行接口写模拟器(Mock Server),此次接口需要发布成一个WEB Service. 一开始,我以为只要负责写接口的业务层就行了,具体的框架或是环境搭建可以不用管.在与 ...

  8. 在Tomcat中部署Web项目的操作方法,maven项目在Tomcat里登录首页报404

     maven项目在Tomcat里登录首页报404, 解决:编辑conf/server.xml进行配置<Host>里的<Context>标签里的path. <Context ...

  9. 使用Apache CXF和Spring集成创建Web Service(zz)

    使用Apache CXF和Spring集成创建Web Service 您的评价:       还行  收藏该经验       1.创建HelloWorld 接口类 查看源码 打印? 1 package ...

随机推荐

  1. spring +hibernate 启动优化【转】

    最近在负责一个大项目,项目组成员包括项目经理大概10个人左右.项目技术用struts+spring+hibernate实现.项目的规模相对来说是比较大的,总共有10大模块,每个大模块又分为有十几个.甚 ...

  2. linux mysql为root用户初始化密码和改变root密码

    初始化密码: 由于安装MySQL完后,MySQL会自动提供一个不带密码的root用户,为了安全起见给root设置密码: #mysqladmin -u root password 123 (123为密码 ...

  3. Linux内核中的中断

    http://blog.csdn.net/weiqing1981127/article/details/8298585 中断处理程序是被内核调用来响应中断的,它运行在中断上下文,中断处理程序是上半部, ...

  4. spring aop环绕通知

    [Spring实战]—— 9 AOP环绕通知   假如有这么一个场景,需要统计某个方法执行的时间,如何做呢? 典型的会想到在方法执行前记录时间,方法执行后再次记录,得出运行的时间. 如果采用Sprin ...

  5. 情人节红攻瑰--Delphi版本

    在oschina上看到了用c写的红玫瑰, 以前只见过用js写的, 就随手用delphi翻译了c的代码, 效果还不错哈.... 原c作者jokeym贴子 http://www.oschina.net/c ...

  6. Java IO2:字节流

    输入输出流: • 输入/输出时, 数据在通信通道中流动. 所谓“数据流(stream)”指的是所有数据通信通道之中,数据的起点和终点. 信息的通道就是一个数据流.只要是数据从一个地方“流” 到另外一个 ...

  7. 102. Binary Tree Level Order Traversal

    题目: Given a binary tree, return the level order traversal of its nodes' values. (ie, from left to ri ...

  8. POJ3083——Children of the Candy Corn(DFS+BFS)

    Children of the Candy Corn DescriptionThe cornfield maze is a popular Halloween treat. Visitors are ...

  9. 转:JavaScript中函数与对象的关系

    来自:http://www.nowamagic.net/javascript/js_RelationOfFunctionAndObject.php 在ajax兴起以前,很多人写JavaScript可以 ...

  10. 给TextView中的text上下左右添加一张图片

    1,在xml中配置使用,android:drawableLeft 等 其它的有:android:drawableTop,android:drawableBottom,android:drawableR ...