Skip to content

Commit

Permalink
Merge pull request #26 from vnobo/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
vnobo authored Oct 31, 2024
2 parents 11e15af + 1ef7bbc commit e12cb7f
Show file tree
Hide file tree
Showing 119 changed files with 379 additions and 19,007 deletions.
45 changes: 43 additions & 2 deletions boot/platform/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
### Overview
## Project Structure

The project is structured in a typical Spring Boot fashion, with the main application entry point in the
`com.plate.boot` package. It includes various sub-packages for different concerns such as security, relational data
handling, and common utilities.

## Features

The application includes the following key features:

- **User Management**: Handles user-related operations and authentication.
- **Security**: Implements security configurations and OAuth2 support.
- **Logging**: Manages application logs efficiently with pagination and cleanup.
- **Menus**: Manages menu items and their associated permissions.
- **Tenant Support**: Provides multi-tenancy capabilities.
- **Caching**: Utilizes caching mechanisms to improve performance.

## Dependencies

The project relies on several dependencies, including:

- Spring Boot 3.x.x
- Spring Data R2DBC
- Spring Security
- Redis
- PostgreSQL

## Getting Started

To run the application, you need to have Java 17 and Maven installed. Then, you can start the application by running the
following command from the root directory of the project:

This will start the Spring Boot application, and it should be accessible at `http://localhost:8080`.

## API Documentation

The API documentation is available using Swagger UI. Once the application is running, you can access the Swagger UI at:

## Contributing

Contributions are welcome! If you find any issues or want to add new features, feel free to open an issue or submit a
pull request.


<p>This project is a comprehensive application framework built on top of Spring Boot, Angular, and other modern technologies. It includes features for user management, security, logging, and more. The application is designed to be reactive and supports RSocket and RESTful communication patterns.<p>
56 changes: 56 additions & 0 deletions boot/platform/src/main/java/com/plate/boot/BootApplication.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.plate.boot;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Component;

/**
* @author <a href="https://github.com/vnobo">Alex Bob</a>
Expand All @@ -11,4 +15,56 @@ public class BootApplication {
public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}

@Component
static class AppRunner implements ApplicationRunner {

private final DatabaseClient databaseClient;

AppRunner(DatabaseClient databaseClient) {
this.databaseClient = databaseClient;
}

@Override
public void run(ApplicationArguments args) {
String sql = """
do $$
declare
v_code text;
v_phone text;
batch_size int := 1000; -- 每批提交的记录数
commit_count int := 0; -- 记录已提交的批次数
begin
for i in 1..100000
loop
-- 生成用户代码
if i < 1000 then
v_code := 'U' || lpad(i::text, 4, '0');
else
v_code := 'U' || (i + 3)::text;
end if;
-- 生成手机号码
v_phone := '1708911826' || lpad(i::text, 2, '0');
-- 插入用户数据
insert into se_users(code, username, password, name, phone, email, bio, creator, updater)
values (v_code, 'user' || i,
'{pbkdf2}7d8a68bc5d507bd19bc153ff10bcdef66f5a5f3d0c1ab2438630e50b5c65894bccc2c7e4404c5afa',
'普通用户' || i, v_phone, 'user' || i || '@qq.com', null, 'U1000', 'U1000');
-- 每插入 batch_size 条记录后提交事务
commit_count := commit_count + 1;
if commit_count % batch_size = 0 then
commit;
end if;
end loop;
-- 提交剩余的记录
commit;
end $$;
""";
databaseClient.sql(() -> sql).fetch().rowsUpdated().subscribe();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.plate.boot.commons.base;

import com.plate.boot.commons.utils.BeanUtils;
import com.plate.boot.commons.utils.ContextUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.r2dbc.convert.R2dbcConverter;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
Expand Down Expand Up @@ -72,7 +71,7 @@ public abstract class AbstractDatabase extends AbstractService {
*/
protected <T> Flux<T> queryWithCache(Object key, Query query, Class<T> entityClass) {
Flux<T> source = this.entityTemplate.select(query, entityClass);
source = source.flatMapSequential(ContextUtils::serializeUserAuditor);
source = source.flatMapSequential(BeanUtils::serializeUserAuditor);
return queryWithCache(key, source).cache();
}

Expand All @@ -96,7 +95,7 @@ protected <T> Flux<T> queryWithCache(Object key, String sql,
Flux<T> source = executeSpec
.map((row, rowMetadata) -> this.r2dbcConverter.read(entityClass, row, rowMetadata))
.all();
source = source.flatMapSequential(ContextUtils::serializeUserAuditor);
source = source.flatMapSequential(BeanUtils::serializeUserAuditor);
return queryWithCache(key, source).cache();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@
import com.google.common.collect.Maps;
import com.plate.boot.commons.exception.JsonException;
import com.plate.boot.commons.exception.JsonPointerException;
import com.plate.boot.commons.exception.RestServerException;
import com.plate.boot.security.core.UserAuditor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.Cache;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.unit.DataSize;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.beans.PropertyDescriptor;
import java.util.Map;
import java.util.StringJoiner;
import java.util.stream.Collectors;
Expand All @@ -35,37 +37,13 @@
@Component
public final class BeanUtils implements InitializingBean {

/**
* A private constant field representing an instance of ByteArrayOutputStream.
* This is used for efficient byte array output operations, typically in scenarios
* where frequent writes to a byte stream are necessary before ultimately
* converting the data into a byte array. The ByteArrayOutputStream is mutable
* and can be reused after calling reset() method.
*/
private final static ByteArrayOutputStream BYTE_ARRAY_OUTPUT_STREAM;
/**
* A private constant field representing an object output stream.
* This stream is used for serializing objects and writing them to a byte stream.
* It is declared as final, indicating that once initialized, it should not be reassigned.
*/
private final static ObjectOutputStream OBJECT_OUTPUT_STREAM;

/**
* Represents the maximum size of data that can be held in memory.
* This threshold is utilized to determine when data should be processed differently,
* such as being written to disk, to avoid exceeding memory constraints.
*/
public static DataSize MAX_IN_MEMORY_SIZE;

static {
try {
BYTE_ARRAY_OUTPUT_STREAM = new ByteArrayOutputStream();
OBJECT_OUTPUT_STREAM = new ObjectOutputStream(BYTE_ARRAY_OUTPUT_STREAM);
} catch (IOException e) {
throw RestServerException.withMsg("Init static ObjectOutputStream error.", e);
}
}

/**
* Converts a JSON node at a specified JSON Path into an object of the provided class type.
* <p>
Expand Down Expand Up @@ -126,6 +104,12 @@ public static <T> byte[] objectToBytes(T object) {
public static String cacheKey(Object... objects) {
StringJoiner keyJoiner = new StringJoiner("&");
for (var obj : objects) {
if (obj instanceof Pageable pageable) {
keyJoiner.add(pageable.getPageNumber() + "_" + pageable.getPageSize());
for (var sort : pageable.getSort()) {
keyJoiner.add(sort.getProperty() + "_" + sort.getDirection().name());
}
}
var objMap = BeanUtils.beanToMap(obj, true);
var setStr = objMap.entrySet().stream().map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.toSet());
Expand Down Expand Up @@ -167,11 +151,9 @@ public static DataSize getBeanSize(Object obj) {
return DataSize.ofBytes(0);
}
try {
BYTE_ARRAY_OUTPUT_STREAM.reset();
OBJECT_OUTPUT_STREAM.writeObject(obj);
OBJECT_OUTPUT_STREAM.flush();
return DataSize.ofBytes(BYTE_ARRAY_OUTPUT_STREAM.size());
} catch (IOException e) {
int size = objectToBytes(obj).length;
return DataSize.ofBytes(size);
} catch (Exception e) {
log.error("Bean Size IO exception! msg: {}", e.getLocalizedMessage());
return DataSize.ofBytes(0);
}
Expand Down Expand Up @@ -276,6 +258,53 @@ public static <T> Map<String, Object> beanToMap(T bean, final boolean isToUnderl
return objectMapper.convertValue(bean, type);
}

/**
* Serializes the {@link UserAuditor} objects contained within the properties of the given input object.
* This method inspects the object's properties to identify those of type {@link UserAuditor} and applies
* a serialization process to them. It is designed to work with objects that may contain auditor fields
* which need to be processed before further operations, such as saving to a database or sending over a network.
*
* @param <T> The generic type of the object whose properties are to be serialized.
* @param object The object whose properties are to be inspected and potentially serialized.
* @return A {@link Mono} wrapping the original object after all {@link UserAuditor} properties have been processed.
* The Mono completes when all processing is finished.
*/
public static <T> Mono<T> serializeUserAuditor(T object) {
PropertyDescriptor[] propertyDescriptors = org.springframework.beans.BeanUtils.getPropertyDescriptors(object.getClass());
var propertyFlux = Flux.fromArray(propertyDescriptors)
.filter(propertyDescriptor -> propertyDescriptor.getPropertyType() == UserAuditor.class)
.flatMap(propertyDescriptor -> serializeUserAuditorProperty(object, propertyDescriptor));
return propertyFlux.then(Mono.just(object));
}

/**
* Handles the property descriptor for an object, particularly focusing on serializing
* a {@link UserAuditor} within the given object by fetching additional user details
* and updating the object accordingly.
*
* @param <T> The type of the object containing the property to be handled.
* @param object The object whose property described by {@code propertyDescriptor} is to be processed.
* @param propertyDescriptor The descriptor of the property within {@code object} that refers to a {@link UserAuditor}.
* @return A {@link Mono} emitting a message indicating success or failure:
* - If the user auditor is empty, emits a warning message and completes.
* - If successful in serializing the user auditor, emits a success message and completes.
* - Emits an error signal if there are issues invoking methods on the property descriptor.
*/
private static <T> Mono<String> serializeUserAuditorProperty(T object, PropertyDescriptor propertyDescriptor) {
UserAuditor userAuditor = (UserAuditor) ReflectionUtils.invokeMethod(propertyDescriptor.getReadMethod(), object);
if (ObjectUtils.isEmpty(userAuditor)) {
String msg = "User auditor is empty, No serializable." + propertyDescriptor.getName();
log.warn(msg);
return Mono.just(msg);
}
return ContextUtils.USERS_SERVICE.loadByCode(userAuditor.code()).cache().flatMap(user -> {
ReflectionUtils.invokeMethod(propertyDescriptor.getWriteMethod(), object, UserAuditor.withUser(user));
log.debug("{} serialize user auditor property: {}",
object.getClass().getSimpleName(), propertyDescriptor.getName());
return Mono.just("User auditor serializable success. " + propertyDescriptor.getName());
});
}

/**
* Sets the maximum size of data that can be stored in memory before being written to disk.
* This method allows configuration of the maximum in-memory size limit, which is particularly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,15 @@
import com.github.f4b6a3.ulid.UlidCreator;
import com.plate.boot.commons.exception.RestServerException;
import com.plate.boot.security.SecurityDetails;
import com.plate.boot.security.core.UserAuditor;
import com.plate.boot.security.core.user.UsersService;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
Expand Down Expand Up @@ -88,6 +82,7 @@ public final class ContextUtils implements InitializingBean {
throw RestServerException.withMsg("SHA-256 algorithm not found", e);
}
}

/**
* A shared static instance of Jackson's {@link ObjectMapper}, which is utilized for
* serializing and deserializing Java objects to and from JSON format. This singleton pattern
Expand Down Expand Up @@ -171,63 +166,6 @@ public static Mono<SecurityDetails> securityDetails() {
.cast(SecurityDetails.class);
}

/**
* Serializes the {@link UserAuditor} objects contained within the properties of the given input object.
* This method inspects the object's properties to identify those of type {@link UserAuditor} and applies
* a serialization process to them. It is designed to work with objects that may contain auditor fields
* which need to be processed before further operations, such as saving to a database or sending over a network.
*
* @param <T> The generic type of the object whose properties are to be serialized.
* @param object The object whose properties are to be inspected and potentially serialized.
* @return A {@link Mono} wrapping the original object after all {@link UserAuditor} properties have been processed.
* The Mono completes when all processing is finished.
*/
public static <T> Mono<T> serializeUserAuditor(T object) {
PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(object.getClass());
var propertyFlux = Flux.fromArray(propertyDescriptors)
.filter(propertyDescriptor -> propertyDescriptor.getPropertyType() == UserAuditor.class)
.flatMap(propertyDescriptor -> serializeUserAuditorProperty(object, propertyDescriptor));
return propertyFlux.then(Mono.just(object));
}

/**
* Handles the property descriptor for an object, particularly focusing on serializing
* a {@link UserAuditor} within the given object by fetching additional user details
* and updating the object accordingly.
*
* @param <T> The type of the object containing the property to be handled.
* @param object The object whose property described by {@code propertyDescriptor} is to be processed.
* @param propertyDescriptor The descriptor of the property within {@code object} that refers to a {@link UserAuditor}.
* @return A {@link Mono} emitting a message indicating success or failure:
* - If the user auditor is empty, emits a warning message and completes.
* - If successful in serializing the user auditor, emits a success message and completes.
* - Emits an error signal if there are issues invoking methods on the property descriptor.
*/
private static <T> Mono<String> serializeUserAuditorProperty(T object, PropertyDescriptor propertyDescriptor) {
try {
UserAuditor userAuditor = (UserAuditor) propertyDescriptor.getReadMethod().invoke(object);
if (ObjectUtils.isEmpty(userAuditor)) {
String msg = "User auditor is empty, No serializable." + propertyDescriptor.getName();
log.warn(msg);
return Mono.just(msg);
}
return USERS_SERVICE.loadByCode(userAuditor.code()).cache().flatMap(user -> {
try {
propertyDescriptor.getWriteMethod().invoke(object, UserAuditor.withUser(user));
String msg = "User auditor serializable success. " + propertyDescriptor.getName();
log.debug(msg);
return Mono.just(msg);
} catch (IllegalAccessException | InvocationTargetException e) {
return Mono.error(RestServerException.withMsg(
"User auditor serialization getWriteMethod invoke error!", e));
}
});
} catch (IllegalAccessException | InvocationTargetException e) {
return Mono.error(RestServerException.withMsg(
"User auditor serialization getReadMethod invoke error!", e));
}
}

/**
* Generates the next unique identifier using ULID (Universally Unique Lexicographically Sortable Identifier).
* <p>
Expand Down
Loading

0 comments on commit e12cb7f

Please sign in to comment.