- 完善自研 MVC 框架
- 修复了
org.geektimes.web.mvc.FrontControllerServlet#initHandleMethods
方法路径重复拼接问题 - 通过反射修复了
org.geektimes.web.mvc.FrontControllerServlet#service
方法一个请求路径对应一个控制器类问题
- 完成 JNDI 获取数据库源
- 修复
src/main/webapp/META-INF/context.xml
存放路径问题
- 完成用户注册功能
- 把数据库操作相关方法抽出到
org.geektimes.projects.user.sql.JdbcTemplate
类
- 完善简易版的依赖注入框架,并通过该框架完成用户注册功能
- 根据第一周的
org.geektimes.projects.user.sql.JdbcTemplate
类进行相应的依赖注入,实现依赖注入链:UserController -> UserServiceImpl -> DatabaseUserRepository -> JdbcTemplate -> DBConnectionManager
- 实现用户注册数据校验(id > 0,6 < password 长度 < 32)
- 实现自定义 JMX MBean,通过 Jolokia 做 Servlet 代理
具体实现查看:org.geektimes.projects.user.web.listener.ComponentContextInitializerListener#contextInitialized
方法
自定义 JMX MBean 为:org.geektimes.projects.user.management.ComponentContextManager
- 扩展
org.eclipse.microprofile.config.spi.ConfigSource
中的实现
本地配置环境实现:org.geektimes.config.source.PropertiesConfigSource
系统 OS 环境变量实现: org.geektimes.config.source.SystemPropertiesConfigSource
- 扩展
org.eclipse.microprofile.config.spi.Converter
主要使用了 org.geektimes.utils.TypeTransFormUtils
进行转换,留下了复杂类型的扩展接口类:org.geektimes.config.converter.CustomizedConverter
- 本地配置文件:
META-INF/application.properties
- 完善
my-dependency-injection
模块,提供给user-web
模块使用
抽离 user-web
模块中的 ComponentContext
类到 my-dependency-injection
模块,添加 ServletContainerInitializer
实现类和 ComponentContextInitializer
监听器:
public class WebAppServletComponentInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext servletContext) throws ServletException {
// 增加 ComponentContextInitializer 进行 ComponentContext 初始化
servletContext.addListener(ComponentContextInitializer.class);
}
}
public class ComponentContextInitializer implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext servletContext = sce.getServletContext();
ComponentContext context = new ComponentContext();
context.init(servletContext);
}
}
- 完善
my-configuration
模块,使其能在my-web-mvc
模块中使用
MapBasedConfigSource
类构造器添加 lazy
懒加载判断,并把 source
私有常量改成保护权限的变量:
// private final Map<String, String> source;
protected Map<String, String> source;
FrontControllerServlet#init
方法中获取 servletContext
上下文,获取对应配置:
public class FrontControllerServlet extends HttpServlet {
@Override
public void init(ServletConfig servletConfig) {
ServletContext servletContext = servletConfig.getServletContext();
Object config = servletContext.getAttribute("config");
System.out.println("first out:" + ((Config)config).getConfigValue("application.name").getValue());
initHandleMethods();
}
}
- 修复本程序
org.geektimes.reactive.streams
包下的程序逻辑
- 主要是因为接受消息之后应该先处理再判断下一次是否超出设定的最大请求数即可
public class BusinessSubscriber<T> implements Subscriber<T> {
@Override
public void onNext(Object o) {
System.out.println("收到数据:" + o);
if (++count > 2) { // 当到达数据阈值时,取消 Publisher 给当前 Subscriber 发送数据
subscription.cancel();
return;
}
}
}
- 数据请求处理流程:
Publisher
->publisher.subscribe(Subscriber s)
->publisher.publish(T data);
- 需要注意的是,当队列正常处理完成时需要调用
org.reactivestreams.Subscriber#onComplete
方法,异常时需要调用org.reactivestreams.Subscriber#onError
方法
-
继续完善
my-rest-client POST
方法- 参考
org.geektimes.rest.client.HttpGetInvocation
类创建org.geektimes.rest.client.HttpPostInvocation
类,多加了Entity<?> entity
成员变量作为Post
请求的请求体对象
- 需要注意,
Post
请求需要设置请求头内容类型Content-type
org.geektimes.rest.client.DefaultInvocationBuilder#buildPost
方法返回值为新增的HttpPostInvocation
类的实例对象- 新增
org.geektimes.projects.user.web.controller.HelloWorldController#testPost
方法作为测试Post
请求的接口,请求路径为:127.0.0.1:8080/hello/test
,可以使用org.geektimes.rest.demo.RestClientDemo#main
方法直接测试
- 目前响应时需要用
javax.servlet.http.HttpServletResponse
设置响应数据,但是参考其他小伙伴的代码是不需要设置的,还没找到原因
- 参考
- 提供一套抽象 API 实现对象的序列化和反序列化
抽出一个序列化和反序列化抽象类 org.geektimes.cache.serialize.CacheSerializer
// 序列化方法
public final byte[] serialize(Serializable source) {
if (source == null) {
return new byte[0];
}
// 抽象方法 doSerialize 给子类实现
return doSerialize(source);
}
// 反序列化方法
public final <T> T deserialize(byte[] data) {
if (data == null || data.length == 0) {
return null;
}
// 抽象方法 doDeserialize 给子类实现
return doDeserialize(data);
}
默认实现类 org.geektimes.cache.serialize.DefaultCacheSerializer
public class DefaultCacheSerializer extends CacheSerializer {
@Override
public byte[] doSerialize(Serializable source) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);) {
objectOutputStream.writeObject(source);
return out.toByteArray();
} catch (IOException e) {
throw new CacheException(e.getMessage(), e);
}
}
@Override
public <T> T doDeserialize(byte[] data) {
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);) {
return (T) objectInputStream.readObject();
} catch (Exception e) {
throw new CacheException(e.getMessage(), e);
}
}
}
关于 Redis
的具体序列化和反序列化实现类 org.geektimes.cache.redis.DefaultRedisSerializer
public K decodeKey(ByteBuffer byteBuffer) {
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes, 0, bytes.length);
return serializer.deserialize(bytes);
}
public V decodeValue(ByteBuffer byteBuffer) {
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes, 0, bytes.length);
return serializer.deserialize(bytes);
}
public ByteBuffer encodeKey(K key) {
return ByteBuffer.wrap(serializer.serialize(key));
}
public ByteBuffer encodeValue(V value) {
return ByteBuffer.wrap(serializer.serialize(value));
}
- 通过 Lettuce 实现一套 Redis CacheManager 以及 Cache
具体参考:org.geektimes.cache.redis.LettuceCache
和 org.geektimes.cache.redis.LettuceCacheManager
,详细代码不列出
从本周开始使用 open-source
分支开发
- 使用
Spring Boot
来实现一个整合Gitee
或者Github
OAuth2 认证
本次作业相对比较简单,主要是 Spring Security
的一些配置,基本上就完成了,但是目的还是为了了解 OAuth 2 认证
本次主要是参考 Spring官网 OAuth2 教程 进行实现,下面是 OAuth 2 的认证流程:
Spring Security
的核心配置 GithubConfigurerAdapter
配置类:
@Configuration
public class GithubConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests(a -> a
// 匹配请求放行
.antMatchers("/", "/error", "/webjars/**").permitAll()
// 对其他请求均进行验证,但是没有对验证用户角色做权限管理
.anyRequest().authenticated()
//退出页面放行
).logout(l -> l.logoutSuccessUrl("/").permitAll()
// 配置跨域请求
).csrf(c -> c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// 异常拦截处理,均返回 403 状态码
).exceptionHandling(e -> e.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
// 设置 OAuth2.0 登录,采用默认的 OAuth2LoginConfigurer 配置
).oauth2Login();
}
}
因为是通过默认的 OAuth2LoginConfigurer
进行实现,配置文件中需要如下配置:
spring:
security:
oauth2:
client:
registration:
github:
# 客户端id
clientId: github-client-id
# 密钥
clientSecret: github-secret-key
随即访问在 GitHub 上配置的主页 http://localhost:8080
点击 click here
即可进行 OAuth 2 认证授权
- 如何解决多个
WebSecurityConfigurerAdapter
Bean 相同配置相互冲突的问题?
问题原因在于:WebSecurityConfigurerAdapter#getHttp
方法
protected final HttpSecurity getHttp() throws Exception {
if (this.http != null) {
return this.http;
}
...
// 此处通过 new 的方式构建 HttpSecurity 对象
this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects);
...
return this.http;
}
因为 WebSecurityConfigurerAdapter
在实例化的时候是 new 一个 HttpSecurity 对象,然后将其添加到 List<SecurityBuilder<? extends SecurityFilterChain>> securityFilterChainBuilders
集合中,在 WebSecurity
构建方法 performBuild
中遍历并创建 FilterChainProxy
对象:
@Override
protected Filter performBuild() throws Exception {
...
// 遍历 securityFilterChainBuilders 列表
// 里面的 SecurityBuilder 对象是WebSecurityConfigurerAdapter#init方法添加的
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}
// 创建 FilterChainProxy 对象
// 并将securityFilterChains设置为FilterChainProxy的filterChains属性
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
...
return result;
}
在真正的过滤器逻辑中,FilterChainProxy#doFilter
方法通过调用 doFilterInternal
方法间接调用 getFilters
方法获取实际的 SecurityFilterChain
过滤器
private List<Filter> getFilters(HttpServletRequest request) {
int count = 0;
for (SecurityFilterChain chain : this.filterChains) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, ++count,
this.filterChains.size()));
}
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
从上面代码来看,实际在具体匹配中只会返回第一个匹配的 SecurityFilterChain
的过滤器。到此,实际的 HttpSecurity
匹配逻辑完成,而作业中的问题是因为实际开发中,开发人员对 Spring Boot Security
使用不当导致配置覆盖问题。
具体多个 HttpSecurity
配置使用方式可以参考官方文档。
解决方法:所有的配置定义在一个配置类 com.yuancome.spring.security.oauth2.adapter.GlobalHttpSecurityConfig
中,根据配置定义顺序从上到下进行顺序限定
@Configuration
public class GlobalHttpSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public SecurityFilterChain filter1(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeRequests(a -> a
.antMatchers("/", "/error", "/webjars/**").permitAll()
.anyRequest().authenticated() // 对这些请求均进行验证,但是没有对验证用户角色做权限管理
).build();
}
@Bean
public SecurityFilterChain filter2(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeRequests(a -> a
.antMatchers("/", "/error", "/webjars/**").permitAll()
.anyRequest().authenticated() // 对这些请求均进行验证,但是没有对验证用户角色做权限管理
).csrf().disable().build();
}
}
- 如何清除某个
Spring Cache
所有的 Keys 关联的对象(如果Redis
中心化方案 -Redis + Sentinel
, 如果Redis
去中心化方案 -Redis Cluster
)
RedisCache
在每个缓存值 push
的时候保存到 Reids List
中,然后该 Redis Cache
存储的所有缓存的 key
,清除 RedisCache
的所有缓存只需要遍历 Redis List
,获取该 RedisCache
关联的所有 key
,然后依次删除即可。
RedisCache
构造函数初始化 key
的前缀字节数组和 Redis List
的 key
的名称。
public RedisCache(String name, Jedis jedis) {
Objects.requireNonNull(name, "The 'name' argument must not be null.");
Objects.requireNonNull(jedis, "The 'jedis' argument must not be null.");
this.name = name;
this.jedis = jedis;
prefixBytes = (this.name + ":").getBytes(StandardCharsets.UTF_8);
namespaceBytes = ("namespace:" + this.name).getBytes(StandardCharsets.UTF_8);
}
往 RedisCache
存数据时,先将 key
序列化,然后再加上前缀的字节数组 prefixBytes
,新的字节数组 actualKeyBytes
作为真正的Redis
的key
,并将 actualKeyBytes
存到 Redis List
中。
public void put(Object key, Object value) {
byte[] actualKeyBytes = getActualKeyBytes(key);
byte[] valueBytes = serialize(value);
jedis.set(actualKeyBytes, valueBytes);
jedis.lpush(namespaceBytes, actualKeyBytes);
}
private byte[] getActualKeyBytes(Object key) {
byte[] keyBytes = serialize(key);
return getMergeBytes(prefixBytes, keyBytes);
}
protected byte[] getMergeBytes(byte[] prefixBytes, byte[] sourceBytes) {
byte[] result = new byte[prefixBytes.length + sourceBytes.length];
System.arraycopy(prefixBytes, 0, result, 0, prefixBytes.length);
System.arraycopy(sourceBytes, 0, result, prefixBytes.length, sourceBytes.length);
return result;
}
RedisCache#clear
方法先根据 Redis List
的缓存的 key
列表依次执行删除操作,然后再删除 Redis List
自身即可。
public void clear() {
List<byte[]> list = jedis.lrange(namespaceBytes, 0L, jedis.llen(namespaceBytes));
if (list != null && !list.isEmpty()) {
list.forEach(jedis::del);
}
jedis.del(namespaceBytes);
}
- 如何将
RedisCacheManager
与@Cacheable
注解打通
将RedisCacheManager
与 @Cacheable
注解打通主要是通过 CacheManager
接口进行实现,可以看出我们的 RedisCacheManager
已经实现了 CacheManager
接口,只需要将其注入到 Spring
容器中即可:
@Configuration
public class RedisCacheConfig {
@Bean
public CacheManager redisCacheManager() {
String uri = "127.0.0.1";
return new RedisCacheManager(uri);
}
}
具体配置肯定不止这些,目前只是作为一个简单的示例进行演示。
- 完善
@org.geektimes.projects.user.mybatis.annotation.EnableMyBatis
实现,尽可能多地注入org.mybatis.spring.SqlSessionFactoryBean
中依赖的组件
根据 org.mybatis.spring.SqlSessionFactoryBean
类来看,主要有以下的组件:
private static final ResourcePatternResolver RESOURCE_PATTERN_RESOLVER = new PathMatchingResourcePatternResolver();
private static final MetadataReaderFactory METADATA_READER_FACTORY = new CachingMetadataReaderFactory();
private Resource configLocation;
private Configuration configuration;
private Resource[] mapperLocations;
private DataSource dataSource;
private TransactionFactory transactionFactory;
private Properties configurationProperties;
private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
private SqlSessionFactory sqlSessionFactory;
private String environment = SqlSessionFactoryBean.class.getSimpleName();
private boolean failFast;
private Interceptor[] plugins;
private TypeHandler<?>[] typeHandlers;
private String typeHandlersPackage;
private Class<? extends TypeHandler> defaultEnumTypeHandler;
private Class<?>[] typeAliases;
private String typeAliasesPackage;
private Class<?> typeAliasesSuperType;
private LanguageDriver[] scriptingLanguageDrivers;
private Class<? extends LanguageDriver> defaultScriptingLanguageDriver;
private DatabaseIdProvider databaseIdProvider;
private Class<? extends VFS> vfs;
private Cache cache;
private ObjectFactory objectFactory;
private ObjectWrapperFactory objectWrapperFactory;
使用时一般不会全部用到上面的所有组件参数,比较常用的就是 configLocation
,mapperLocations
,dataSource
等,由于时间关系就先添加简单的 typeHandlersPackage
,typeAliasesPackage
, 这两个分别指定了类型处理包
和 类型别名包
。
public @interface EnableMyBatis {
String typeHandlersPackage() default "";
String typeAliasesPackage() default "";
}
然后在 MyBatisBeanDefinitionRegistrar#registerBeanDefinitions
方法中补充:
beanDefinitionBuilder.addPropertyValue("typeHandlersPackage", attributes.get("typeHandlersPackage"));
beanDefinitionBuilder.addPropertyValue("typeAliasesPackage", attributes.get("typeAliasesPackage"));
此外还完善了 configurationProperties
:
// 获取配置文件路径
String configLocation = (String) attributes.get("configLocation");
beanDefinitionBuilder.addPropertyValue("configLocation", configLocation);
// 根据配置文件路径获取配置文件源
if (StringUtils.isNotEmpty(configLocation)) {
Properties properties = resolveConfigurationProperties(configLocation);
beanDefinitionBuilder.addPropertyValue("configurationProperties", properties);
}
至此,简单地完成了作业内容。
- 通过 Java 实现两种 (以及) 更多的一致性 Hash 算法 (可选) 实现服务节点动态更新
具体实现为spring-security-oauth2
的 src/main/java/com/yuancome/spring/security/loadbalance
包下 hash
子包为一致性哈希算法实现,random
子包为随机算法实现。
通用的代码逻辑:
Node
节点类:主要描述服务器节点的元数据信息
Cluster
接口:主要作为节点增减删除操作行为的抽象接口
AbstractCluster
抽象类:实现了 Cluster
接口,主要补充一些节点操作方法,暂时无抽象方法,仅作为钩子类用于后续扩展
- 将上次 MyBatis@Enable 模块驱动,封装成 SpringBoot Starter 方式
将上次的 MyBatis@Enable 模块驱动相关类抽取成 my-mybatis
模块,然后添加 EnableMyBatisExample
类以及 META-INF/spring.factories
和 META-INF/spring-autoconfigure-metadata.properties
配置文件,其中 META-INF/spring.factories
配置文件为 SpringBoot Starter 模块的自动配置文件,而 META-INF/spring-autoconfigure-metadata.properties
配置文件则是 EnableMyBatisExample
类的前置配置类,主要是注入一些条件 Bean。
到此时,封装已经基本完成,而 my-spring-boot-mybatis
则是使用my-mybatis
模块 SpringBoot Starter 方式的 demo。
具体代码不详细展开。
- 基于文件系统为 Spring Cloud 提供 PropertySourceLocator 实现
- 配置文件命名规则 (META-INF/config/default.properties 或者 META-INF/config/default.yaml)
具体实现代码为:spring-cloud-config-server
模块
- 通过 GraalVM 将一个简单 Spring Boot 工程构建为 Native Image
- 代码要自己手写 @Controller @RequestMapping("/helloworld")
- 相关插件可以参考 Spring Native Samples
- (可选) 理解 Hint 注解的使用
主要参考文档:
核心配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yuancome</groupId>
<artifactId>graalvm-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>graalvm-test</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<repackage.classifier/>
<spring-native.version>0.10.0</spring-native.version>
</properties>
<dependencies>
...
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>${spring-native.version}</version>
</dependency>
...
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>${repackage.classifier}</classifier>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
<!--Add the Spring AOT plugin-->
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>${spring-native.version}</version>
<executions>
<execution>
<id>test-generate</id>
<goals>
<goal>test-generate</goal>
</goals>
</execution>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native</id>
<properties>
<repackage.classifier>exec</repackage.classifier>
<native-buildtools.version>0.9.0</native-buildtools.version>
</properties>
<dependencies>
<!--Add the GraalVM Buildtools plugin-->
<dependency>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>junit-platform-native</artifactId>
<version>${native-buildtools.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!--Add the native-maven-plugin for build & package-->
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native-buildtools.version}</version>
<executions>
<execution>
<id>test-native</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
</execution>
<execution>
<id>build-native</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
核心主要是在 POM
文件的配置上,安装过程比较慢,需要代理。
运行方法:
// 编译打包文件
mvn -Pnative package
// 直接执行打包好的文件
graalvm-test/target/graalvm-test
根据运行比较,GraalVM
编译耗时在 4分钟
左右,但是可以提高启动速度约 5 - 6
倍。实际可以根据生产场景进行考虑使用。