框架莫过于Spring了,那就以它为起点吧。
本文只为整理复习用,详细内容自行翻看以前文章。
有人说是Spring成就Java,其实也不是并无道理。
以XML注入bean的方式为入口,定位、加载、注册,最后将XML中bean标签解析成封装类BeanDefinitionHolder,将解析得到的封装类BeanDefinitionHold注册到IOC容器
//入口,通过ClassPathXmlApplicationContext的构造方法一直到此方法
public ClassPathXmlApplicationContext(
String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
throws BeansException {
super(parent);
//存储我们传入的配置文件路径
setConfigLocations(configLocations);
if (refresh) {
//整个IOC容器的入口
refresh();
}
}
//最终 beanName -> class类信息的封装BeanDefinition
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
将解析好的bean标签封装成BeanDefinition,以beanName作为key,存储在本地变量beanDefinitionMap中,但是到目前为止并没有实例化这些类,还不能直接使用。
AbstractApplicationContext##refresh()
下的finishBeanFactoryInitialization()#beanFactory.preInstantiateSingletons()
中通过遍历beanDefinitionNames
调用getBean()
实例化对象,让代码解耦统一走getBean()
更加的规范,完美解决循环依赖(程序初始化遍历一次,自己调用一次)在容器初始化bean之后,回调到后置通知方法,aop正好实现后置通知方法,经过判断如果满足当前的配置切点则生成代理类并返回到IOC容器中。
创建代理类时会把原始类、调用链路等信息通过构造方法保存下来,在创建动态代理时newInstance
时传入的回调handler
类也是this
,那么在调用该代理类时会到handler
类的invoke
方法
在invoke方法中通过递归以及增加下标的方法使调用链路执行反射调用,如果不能正常匹配到对应的Advice类,则在到最后一个时直接反射调用原始类并返回
如果能正常匹配到,例如上面的AspectJAfterAdvice则是先反射调用原始类,然后再调用后置切面方法,整个流程结束
在依赖注入之后,在AOP之前实现,在spring-mvc包下找到RequestMappingHandlerMapping这个类,通过Springboot的自动装配机制初始化该类,该类实现了InitializingBean
接口,实现了afterPropertiesSet方法,以此为入口。
接下来开始初始化mapping的入口,判断只有加了@Controller || @RequestMapping
这两个注解的才符合条件,接下来获取方法上面的注解内容组成请求路径。最终存储位置:
//保存注册信息的MAP
//this.registry.put(mapping,
//new MappingRegistration<>(mapping, handlerMethod, directPaths, name, corsConfig != null));
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
//this.pathLookup.add(path, mapping); 路径对应mapping
private final MultiValueMap<String, T> pathLookup = new LinkedMultiValueMap<>();
以上为Springboot中自动装配流程。
在MVC中直接开始进入运行阶段,通过Servlet的实现类DispatcherServlet
类里找init方法(在父类GenericServlet#init()
),然后创建web容器会同步配置和刷新容器(IOC的入口),在完成Spring的IOC,DI之后,才会走初始化Mapping。
也就是说MVC是从DispatcherServlet#Init开始的,而Springboot是从自动装配先初始化IOC容器开始的。
在init初始化容器之后,才会进入初始化MVC的组件,例如:handlerMapping、参数适配、视图预处理等等,在handlerMapping中取出对应类型的bean并存储在本地变量List
。
开始调用阶段,Servlet的入口肯定是自己实现的doGet/doPost方法,最终在FrameworkServlet
找到,通过里面的getHandler() -> getHandlerInternal() -> initLookupPath
方法根据请求路径取出对应的handlerMethod beanName和Method
,最后在通过registry
循序获取mapping的注册信息,再通过注册信息里面的handler 调用bean工厂中的getBean获取到实例对象。
拿到对应路径的实例化对象后,通过invoke调用到原始类中的方法,回去返回值后通过ModelAndView写入到Response中返回到前端页面。
扩展接口 | 作用 |
---|---|
BeanFactoryPostProcessor | 处理bean之前对beanFactory进行预先处理 |
BeanDefinitionRegistryPostProcessor | 自定义添加bean |
BeanPostProcessor | 在初始化Bean前后的回调 |
ApplicationContextAware | 获得上下文 |
InitializingBean | bean创建完成,所有属性注入完成后执行 |
DisposableBean | 在bean销毁前执行 |
ApplicationListener | 事件监听 |
如果一个Bean实现了Aware接口,则能在bean中获取相应的Spring资源。
Aware接口 | set属性 | 作用 |
---|---|---|
BeanNameAware | setBeanName | 获得当前类在容器中的beanName |
BeanClassLoaderAware | setBeanClassLoader | 获得当前的类加载器 |
BeanFactoryAware | setBeanFactory | 获得bean工厂以获取其他的bean |
EnvironmentAware | setEnvironment | 获得当前运行的环境相关 |
EmbeddedValueResolverAware | setEmbeddedValueResolver | EL解析表达式 |
ResourceLoaderAware | setResourceLoader | 获得加载bean的资源加载器 |
ApplicatioinEventPublisherAware | setApplicatioinEventPublisher | 获得时间发布者 |
MessageSourceAware | setMessageSource | 获取国际化信息 |
ApplicationContextAware | setApplicationContext | 获取上下文 |
ServletContextAware | setServletContext | Servlet的上下文 |
Class.newInstance()
获取当前实现类的beanName,准备如下代码,在初始化当前类时,会将beanName回调到该方法来
@Component
public class TestBeanNameAware implements BeanNameAware {
@Override
public void setBeanName(String name) {
System.out.println("#获得TestBeanNameAware实现类的beanName:"+name);
}
}
@Component
public class TestBeanClassLoaderAware implements BeanClassLoaderAware {
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
System.out.println("#获得当前加载类的classLoader:"+classLoader);
}
}
可以通过该bean工厂获取指定名称的对象
@Component
public class TestBeanFactoryAware implements BeanFactoryAware {
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("#获得管理bean的容器:"+beanFactory.getClass().getName());
//
Object nameAware = beanFactory.getBean("testBeanNameAware");
System.out.println(nameAware);
}
}
aop就是典型的例子,在初始化bean前后回调,可以在回调中返回继承类或者代理类。
@Component
public class TestInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// System.out.println("初始化之前:"+bean.getClass().getName()+","+beanName);
if(beanName.equals("com.example.demo.action.TestAction")){
System.out.println("初始化之后:"+bean.getClass().getName()+","+beanName);
return new Test1Action(); //设置成实现类或者继承类 动态修改bean的初始化
}
return null;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// System.out.println("初始化之后:"+bean.getClass().getName()+","+beanName);
if(beanName.equals("com.example.demo.action.TestAction")){
System.out.println("初始化之后:"+bean.getClass().getName()+","+beanName);
}
return null;
}
}
例如MVC的HandlerMapping回调
@Component
public class TestInitializingBean implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("#初始化完毕当前类回调,后置回调之前");
}
}
实例:Mybatis
@Component
public class TestBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
/**
* 实现自定义bean并注册到BeanDefinitionRegistry
* @param registry
* @throws BeansException
*/
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
//TestAction 无任何加载的标识 可以用这种方法向容器注册 mybatis里面用到
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(TestAction.class);
registry.registerBeanDefinition(TestAction.class.getName(),builder.getBeanDefinition());
System.out.println("注册自定义bean:"+builder.getBeanDefinition().getBeanClass());
}
/**
* 主要是用来自定义修改持有的bean里面的属性值
* @param beanFactory
* @throws BeansException
*/
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
for (String definitionName : beanFactory.getBeanDefinitionNames()) {
if ("com.example.demo.action.TestAction".equals(definitionName)) {
System.out.println("###beanName:" + definitionName);
BeanDefinition definition = beanFactory.getBeanDefinition(definitionName);
//获取bean的定义的值 动态修改值 需要有setter方法
MutablePropertyValues pv = definition.getPropertyValues();
pv.addPropertyValue("name", "王二麻子");
System.out.println("###TestAction#name重新赋值");
break;
}
}
}
}
实现了FactoryBean的类,容器初始化该类的时候会调用当前getObject()方法
//也可自己注册 初始该注册类时也会调用到该方法
BeanDefinitionBuilder builder1 = BeanDefinitionBuilder.genericBeanDefinition(TestFatoryBean.class);
registry.registerBeanDefinition("aaabbbccc",builder1.getBeanDefinition());
@Component
public class TestFatoryBean implements FactoryBean {
/**
* 初始化这个类的时候 TestFatoryBean的定义就已经被替换成了 TestFactoryBean1 这个类
* 在其他类注解TestFactoryBean1 就可以直接用
* @return 返回的对象实例
* @throws Exception
*/
@Override
public Object getObject() throws Exception {
System.out.println("#初始化TestFatoryBean.getObject()");
return new TestFactoryBean1();
}
/**
* @return 返回的对象类型
*/
@Override
public Class<?> getObjectType() {
System.out.println("#初始化TestFatoryBean.getObjectType()");
return TestFactoryBean1.class;
}
}
@Component
public class TestAbstractApplicationContext extends AbstractApplicationContext {
@Override
protected void refreshBeanFactory() throws BeansException, IllegalStateException {
System.out.println("#刷新容器事件");
}
@Override
protected void closeBeanFactory() {
System.out.println("#关闭容器事件");
}
/**
* 返回已有容器 || 自定义的容器
* @return
* @throws IllegalStateException
*/
@Override
public ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException {
return null;
}
}
public class TestDeferredImportSelector implements ImportSelector {
//直接实现 传入类的包名路径
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
System.out.println("#延时引入需要加载的importingClassMetadata");
return new String[]{TestImportAction.class.getName()};
}
}
第二种用法,可按分组实现:
public class TestDeferredImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return null;
}
/**
* 如果实现了分组 优先按分组来 就是去分组的实现类
* @return 返回实现分组的类 (实现DeferredImportSelector.Group)
*/
@Override
public Class<? extends Group> getImportGroup() {
return Test123.class;
}
private static class Test123 implements DeferredImportSelector.Group{
AnnotationMetadata annotationMetadata;
@Override
public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
System.out.println("#延时引入需要加载的process");
this.annotationMetadata = metadata;
//这里可以根据原数据做分组 让每个分组加载不同的类
}
@Override
public Iterable<Entry> selectImports() {
System.out.println("#延时引入需要加载的selectImports");
String[] strings = {TestImportAction.class.getName()};
return Arrays.asList(strings).stream().
map((className) -> new Entry(this.annotationMetadata,className)).collect(Collectors.toList());
}
}
}
spring.factories
中可以为org.springframework.context.ApplicationListener
的值,最终保存到本地变量List> listeners
中EventPublishingRunListener
EventPublishingRunListener
类中获取到构造方法保存的监听,然后进行回调ConfigFileApplicationListener
类,然后通过加载配置中PropertySourceLoader
类名的值得到PropertiesPropertySourceLoader,YamlPropertySourceLoader
两个配置解析类自动装配是根据Spring通过classloader获取指定key的值为需要自动装配的类,这里的key值为EnableAutoConfiguration
类的全路径
//实现代码
//AutoConfigurationImportSelector类
//通过classLoader获取指定key需要自动装配的类
List<String> configurations =
//getSpringFactoriesLoaderFactoryClass return EnableAutoConfiguration.class;
//这里的key也就是自动装配EnableAutoConfiguration类路径
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());
也就是说需要自动转配的类,在META-INF/spring.factories
文件添加key为EnableAutoConfiguration全路径
,值为需要自动装配的类路径即可。
resources
目录下准备META-INF/spring.factories
,写入下面内容org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.start.test2.service.HelloService,\
com.start.test2.service.Hello1Service
然后将build打包,保证打包能够成功!
//采用注解的方式使用star-test-2的类
@Autowired
HelloService helloService;
@Autowired
Hello1Service hello1Service;
调用方法能够调用成功,这样就可以实现一个公用的star了。
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。
介绍一下作为orm框架使用的升级之路吧
server:
port: 12233
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.0.100:3306/demo_test?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
username: root
password: 123456
#mybatis xml资源文件扫描
mybatis:
mapperLocations: classpath:mapper/**.xml
TestMapper接口
//注解莫忘
@Mapper
public interface TestMapper {
void insert(Map<String, Object> map);
int update(Map<String, Object> map);
List<Map<String, Object>> select(Long id);
void delete(Long id);
}
TestMapper.xml文件
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.TestMapper">
<insert id="insert" parameterType="Map" useGeneratedKeys="true" keyProperty="id">
insert into user_info(`name`,`age`,`crad`) values (#{name},#{age},#{crad})
insert>
//其他的省略了 后面详细的有贴
mapper>
调用直接和注解service一样使用
@Resource
TestMapper testMapper;
//testMapper.insert(map);
1.利用自动装配初始一个类MybatisAutoConfiguration,在静态类AutoConfiguredMapperScannerRegistrar实现了spring的回调,作为扫描mapper接口的入口MapperScannerConfigurer
2.找到的mapper接口注册到容器,将beanClass替换成了MapperFactoryBean类,也就是初始化Mapper接口时实际上在初始化MapperFactoryBean类
1.通过MybatisAutoConfiguration类中@Bean注解创建sqlsessionFactoryBean时扫描mapper文件,将Mapper文件中扫描出来的sql等信息保存到了Configuration#mappedStatements Map里面,将命名空间也就是接口信息保存到了Configuration#MapperRegistry类knownMappers Map中,key为接口全路径,value同时用MapperProxyFactory包装key
2.在MapperFactoryBean中实现了FactoryBean接口,实现了getObject方法,该方法的意思是实例化对象时由自己做主,这里返回的就是上面的MapperProxyFactory的包装类,调用类为MapperProxy。
3.在MybatisAutoConfiguration类中@Bean创建SqlSessionTemplate时,通过构造方法生成了一个代理的sqlSessionProxy,调用类为SqlSessionInterceptor
1.当容器初始化Mapper接口类时实际上得到的是MapperFactoryBean类对象,但该类实现了getObject方法,返回了最终的实例化对象,是一个代理类MapperProxy,也就是mapper接口实际上注入的是MapperProxy代理类
2.也就是会执行到MapperProxy#invoke类,然后在使用sqlSession时实际上是到代理类SqlSessionInterceptor,在该类完成了sqlSession的新建和复用以及事务提交和回滚
3.接下来才到真正的执行器executor,这里可能存在多层代理,插件的执行入口也在这里实现
4.最后在prepareStatement完成数据库连接getConnection,并执行sql,最终由handleResultSets处理结果集并返回
表中的字段是字符串,但是Java数据类型是List
//定义处理的泛型为List,这里就处理入参和出参之间的转换了
@Slf4j
@Component
public class MyTypeHandler implements TypeHandler<List<String>> {
@Override
public void setParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
log.info("set ========= " + parameter);
//这里采用偷懒的写法了
ps.setString(i,parameter.toString().replaceAll("\\[|\\]",""));
}
@Override
public List<String> getResult(ResultSet rs, String columnName) throws SQLException {
log.info("getResult == String");
String string = rs.getString(columnName);
//出参处理
List<String> list = Arrays.asList(string.split(","));
return list;
}
@Override
public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
log.info("getResult == columnIndex");
return null;
}
@Override
public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {
log.info("getResult == columnIndex");
return null;
}
}
需要注意的点
xml中入参写法别忘记,#{bookNames,typeHandler=com.example.demo.plugin.MyTypeHandler}
xml中出参映射resultMap不能忘,
在很多业务中,我们可能需要去拦截执行的sql,达到不修改原有的代码业务去处理一些东西。例如:分页操作,数据权限,sql执行次数和时长等待,这时就可以用到这个Interceptor拦截器了。
回顾一下核心对象
Configuration 初始化基础配置,一些重要的类型对象,例如:插件,映射器,factory工厂,typeHandler对象等等。该类贯穿全局
SqlSessionFactory SqlSession工厂
SqlSession 顶层工作API,和数据库交互,完成CRUD功能
Executor 执行器,是mybatis调度的核心,负责SQL语句的生成和查询缓存的维护
StatementHandler 封装了JDBC Statement操作,负责对应操作,例如:设置参数,结果集转出
ParameterHandler 参数转换处理
MapperedStatement 对执行sql节点的封装
SqlSource 动态生成sql语句,并封装到BoundSql中
BoundSql 动态生成的sql和参数的封装
ResultSetHandler 结果集处理,将JDBC类型转换成Java数据类型
TypeHandler 类型转换器,可自定义实现
Mybatis支持对Executor、StatementHandler、ParmeterHandler和ResultSetHandler接口进行拦截,也就是会对这几个接口进行代理。例如实现查询拦截器
@Slf4j
@Component
@Intercepts(value = {@Signature(type = Executor.class, method = "query",
//arg对应的参数数组
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class MyPagePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
log.info("进入插件 ================");
Object target = invocation.getTarget();
log.info("原始类来源:{}", target.getClass().getName());
Object[] args = invocation.getArgs();
MappedStatement st = (MappedStatement) args[0];
BoundSql sql = st.getBoundSql(args[1]);
log.info("执行SQL:" + sql.getSql());
Object arg = args[1];
log.info("传入参数:{}", arg);
return invocation.proceed();
}
}
那么以后查询统计就可以用插件实现了,而不是用切面来了!
用过就知道,最便捷最简单的物理分页插件了。
Java查询代码
@GetMapping("/data/select")
public Object select() {
//配置页码参数
PageHelper.startPage(1,1);
Map<String, Object> map = new HashMap<>();
//查询
List<Map<String, Object>> mapList = testMapper.select(map);
PageHelper.clearPage();
return mapList;
}
sql语句
select * from user_info
日志打印
==> Preparing: SELECT count(0) FROM user_info
<== Columns: count(0)
<== Row: 4
<== Total: 4
==> Preparing: select * from user_info LIMIT ?
==> Parameters: 1(Integer)
上述日志可以看到,先会执行一条count的sql,然后才会执行真正手写的sql,并且还带了limit查询。
这里需要注意常犯的错,PageHelper.startPage(0, 10);
/**
* 计算起止行号
*/
private void calculateStartAndEndRow() {
//例如 1,10 -> (1-1)*10 =0
this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;
//例如 1,10 -> 0+ 10 * 1=10
//但是如果是0 0+10*0= 0 最终执行的sql就是limit 0,0
this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);
}
这块谨记,PageHelper分页插件的页码是从1开始的!!!
回顾一下mybatis的调用流程,从我们自己手写的xxxMapper接口开始
MapperProxy#invoke ->
内部类PlainMethodInvoker#invoke ->
MapperMethod#execute ->
SqlSessionTemplate#selectList->
内部代理类SqlSessionInterceptor#invoke(创建新的sqlseesion,事务的自动提交在这里控制,执行器插件织入均在这里初始化) ->
DefaultSqlSession#selectList ->
BaseExecutor#query(此处executor可被插件代理,甚至多层) ->
到对应的执行器例如SimpleExecutor#doQuery(预处理参数prepareStatement->
PreparedStatementHandler#parameterize中的parameterHandler可被多层代理) ->
SimpleStatementHandler#query(随执行器而定,可被多层代理) ->
DefaultResultSetHandler#handleResultSets(ResultSetHandler可被代理) ->
返回list
可被代理的 Executor,ParameterHandler,StatementHandler,ResultSetHandler均可被插件代理,本篇文章重点分析的是Executor
在pagehelper-spring-boot-autoconfigure-1.4.0.jar/META-INF/spring.factories
找到如下内容
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration
在PageHelperAutoConfiguration
这个类初始化interceptor插件并保存下来。
在Plugin#getSignatureMap
源码解析中有如下插件
//对应这个注解内容
@Intercepts(value = {@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
最终对boundSql的拦截处理,生成一个新的BoundSql即countBoundSql,执行count查询,如果查询结果为0,那就根据配置是否决定继续查询数据list。
这里记录一下值得注意的几个点:
page==0 || pageSize==0
只会执行count查询,分页查询不会查询。pageSizeZero=true
那就继续查询。基于springboot的自动装配机制,先扫描启动类目录下的class类,再是自动装配类EnableAutoConfiguration
配置下的类初始化,手写开发的查询插件先初始化PageInterceptor分页插件,在InterceptorChain#pluginAll
最后封装的是PageInterceptor分页插件,然后分页最终执行的返回并不是invocation.proceed()
,也就是手写的查询插件会失效掉。
Netty是一个基于异步、事件驱动的网络应用程序框架,用于快速开发高性能、高可靠性的网络IO程序,是目前最流行的NIO框架。Netty在互联网领域,大数据分布式计算,游戏行业,通信IM行业获得了广泛的应用。
Dubbo、RocketMQ、Tomcat等内部都采用了Netty。
public static void main(String[] args) {
NioEventLoopGroup b1 = new NioEventLoopGroup();
NioEventLoopGroup b2 = new NioEventLoopGroup();
ServerBootstrap bs = new ServerBootstrap()
.group(b1, b2)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
//使用自带的String编解码器
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
//服务端业务处理的handler
pipeline.addLast(new MyChatServerHandler());
}
});
ChannelFuture f = bs.bind(19900).addListener(future -> {
if (future.isSuccess()) {
System.out.println("已启动netty服务:" + 19900);
}
});
try {
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
b1.shutdownGracefully();
b2.shutdownGracefully();
}
}
public class MyChatServerHandler extends SimpleChannelInboundHandler<String> {
//保存长链接的channel通道
static Map<ChannelId, Channel> channelMap = new ConcurrentHashMap<>();
/**
* 协议 name#msg
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("服务端收到消息:" + msg);
String[] split = msg.split("#");
if (split.length == 1) {
//输了昵称才算新加入的
if (!channelMap.containsKey(ctx.channel().id())) {
channelMap.put(ctx.channel().id(), ctx.channel());
}
}
//消息群发
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(new Date());
for (Map.Entry<ChannelId, Channel> entry : channelMap.entrySet()) {
if (split.length == 1) {
entry.getValue().writeAndFlush(dateStr + ":欢迎" + msg + "加入群聊!");
} else {
entry.getValue().writeAndFlush(dateStr + ":" + msg.replace("#", " : "));
}
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("新的客户端连接" + ctx.channel());
}
}
public static void main(String[] args) {
//客户端只需要一个事件分组
NioEventLoopGroup group = new NioEventLoopGroup();
//构建的类也不同
Bootstrap bs = new Bootstrap()
.group(group)
//选择的channel也不同
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
//handler处理链是一致的
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new MyChatClientHandler());
}
});
//连接到服务端的指定端口
ChannelFuture future = bs.connect("127.0.0.1", 19900);
try {
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
group.shutdownGracefully();
}
}
public class MyChatClientHandler extends SimpleChannelInboundHandler<String> {
//保存第一次连接成功输入的昵称
static String name;
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
//打印消息
System.out.println(msg);
}
/**
* 协议 name#msg
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与服务端连接成功:" + ctx.channel() + ",请输入你的昵称:");
//启动一个发送消息线程 只要键盘输入过就发送到服务端
new Thread(() -> {
Scanner sc = new Scanner(System.in);
while (true) {
String next = sc.nextLine();
if (name == null) {
//第一次发送则初始化昵称 协议仅供理解,硬是要输昵称带#的忽略
name = next;
ctx.writeAndFlush(name);
} else {
//发送规定协议的报文
ctx.writeAndFlush(name + "#" + next);
}
}
}).start();
}
}
接下来就可以启动一个服务端,多个客户端来任意聊天了。
上述内容仅供了解使用,正式开发下肯定是要考虑多台机器的,可能channel不在同一台机,这个时候就要使用到分布式中间件了。
RPC一般指远程过程调用。 RPC是远程过程调用(Remote Procedure Call)的缩写形式。
首先看下服务的演变过程:
接口请求也在慢慢演变:
总体而言就是随着服务的增多,也伴随着服务之间的调用频繁和繁琐,这就有了PRC这代名词。
PRC普通应用在分布式架构中,先看下分布式服务派系
RPC的核心职能,以dubbo图解为例
这个机制现在用的很广泛了,例如cloud中的注册中心和配置中心。
大概了解一下理论后,接下来我们用代码来实操,以便更深入的认识PRC。
客户端
1.通过bean的初始化回调判断是否需要注入动态代理
2.在动态代理回调类中使用Netty调用远程服务,并发送约定协议的消息
3.使用回调机制返回服务端响应,并返回原始类
服务端
1.在bean的回调判断是否为发布的服务,是的话保存在公共map中,初始化时启动Rpc服务
2.调用服务解析消息后,通过请求的service获取指定的service,通过反射调用,并将结果返回
关于Rpc服务地址
正常的RPC服务,会先从注册中心获取这个服务发布的地址,也就是我们配置中的地址实际上是注册中心的地址
建立连接后,应该会保持心跳,第二次调用不再重新建立连接
关于阻塞异步回调
实际上还有熔断机制,应该处理掉一直等待的回调
基于SpringBoot的全家桶,只了解过部分源码,基本上大同小异核心思想是一样的,没有文章记录了,使用可以看看以前的文章。
上一篇:随手记录第八话 – Java基础整合篇
下一篇:随手记录第十话 – xxx
非学无以广才,非志无以成学。