天天看點

java開源項目jeecg結構與代碼全解析

一.搭建 1.前端

npm install

npm run serve

2.後端

老生常談的配置,修改mysql與redis即可。

二.業務功能介紹

功能上jeecgboot主要提供了系列的代碼生成器、模闆頁面、報表頁面。

1.報表功能

主要提供報表的相關操作。提供了積木報表插件,可以自定義資料報表、圖形報表。并将報表挂載到菜單上。

2.線上開發

也就是代碼生成器,可以可視化的在頁面上建立資料庫表,并通過資料庫表生成前背景代碼。減少業務代碼開發的時間。

3.系統管理

使用者管理、角色管理、機構管理、消息管理等基礎子產品。

4.系統監控

主要負責各種日志、監控的統一處理。

5.頁面元件樣式

常見案例、詳情頁、結果頁、異常頁、清單頁、表單頁主要提供了樣式頁面與控件頁面示例。在開發過程中如果需要模闆直接複制代碼即可。詳情請

三.背景架構介紹 1.概括

其中報表和代碼生成器沒有提供源碼,如果有興趣可以自行檢視jar包源碼。

2.架構核心包jeecg-boot-base

jeecg-boot-base包括了下文的幾個部分。

1.接口包jeecg-boot-base-api 1.對外接口jeecg-system-cloud-api

使用feign+hystrix實作了服務間調用加熔斷,單機環境并沒有使用。

2.服務内接口jeecg-system-local-api

該包提供了下文使用的常用方法接口。僅提供了接口并無其他配置。

2.核心配置包jeecg-boot-base-core 1.通用類common 1.api

其中為通用接口與通用傳回對象。

1.Result

其中Result為所有類的傳回實體,這樣能夠通過code編碼和message擷取是否成功和成功/失敗的資訊。此類是常用的架構設計

2.aspect

為項目的自定義注解,使用了AOP的切面方式實作,這裡就不詳細說了,比較簡單都可以看懂。

3.constant

存放着枚舉類與常量池,這裡不多說了。

4.es

為操作es的通用類,主要是配置es連接配接和查詢時動态拼接and/or的方法。

5.exception

exception為自定義的異常類。

1.JeecgBootExceptionHandler

這裡詳細說一下JeecgBootExceptionHandler,該類也是常見的架構設計之一,核心為@RestControllerAdvice、@ExceptionHandler。當業務代碼中沒有對異常攔截時,該類會自動攔截異常,并資料log日志。是以某些日志在該類配置後,就不需要在每個接口中都捕獲這個異常了。

6.handler

為下文規範提供了接口類。沒有其他特别說明。

7.system類

這裡主要說controller、entity、service等業務代碼的父類

1.JeecgController

是以controller的父類,提供了導入導出的功能。還可以在裡面擴充分頁、排序、常用調用方法等,這樣就可以避免相同的代碼多次添加。這也是架構設計中常用的技巧。

2.JeecgEntity

将通用字段如id、建立人、修改人、建立時間、修改時間等字段統一封裝在一個實體中,使用其他實體繼承。這也是架構設計中常用的技巧。

3.service

主要提供Mybatis-plus提供的curd方法。

8.utli

提供了一大波的工具類,如果在工作中需要,直接複制使用。

2.通用配置類config 1.mybatis

1.MybatisInterceptor

MybatisInterceptor這裡主要說MybatisInterceptor,該類負責在mybatis執行語句前,攔截并擷取參數,将建立人、建立時間等字元動态插入。這裡上部分核心代碼。

MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];

String sqlId = mappedStatement.getId();

log.debug("------sqlId------" + sqlId);

//擷取sql類型是插入還是修改

SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();

//擷取插入參數

Object parameter = invocation.getArgs()[1];

if (parameter == null) {

return invocation.proceed();

}

if (SqlCommandType.INSERT == sqlCommandType) {

LoginUser sysUser = this.getLoginUser();

//通過反射擷取入參的類

Field[] fields = oConvertUtils.getAllFields(parameter);

for (Field field : fields) {

log.debug("------field.name------" + field.getName());

try {

//将建立人資訊動态加入

if ("createBy".equals(field.getName())) {

field.setAccessible(true);

Object local_createBy = field.get(parameter);

field.setAccessible(false);

if (local_createBy == null || local_createBy.equals("")) {

if (sysUser != null) {

// 登入人賬号

field.setAccessible(true);

field.set(parameter, sysUser.getUsername());

field.setAccessible(false);

}

}

}

}

2.MybatisPlusSaasConfig

該類主要負責多租戶,什麼是多租戶呢?

多租戶:就是多個公司/客戶公用一套系統/資料庫,這就需要保證資料的權限。

該場景比較少不詳細說明。

2.oss

主要從application-dev.yml擷取到上傳的路徑與配置。

3.shiro

安全架構主要有兩個目标:認證與鑒權。

認證:判斷使用者名密碼是否正确。

鑒權:判斷使用者是否有權限通路該接口。

這裡本文着重講解,如果遇到shiro相關應用,可以項目直接移植使用。

1.CustomShiroFilterFactoryBean

該類主要負責解決資源中文路徑問題。這裡有個通用的解決方式。

建立類內建ShiroFilterFactoryBean方法,并重寫核心方法createInstance(),并在注入時,注入建立的類CustomShiroFilterFactoryBean,這樣就達到的以往重新源碼的功能。因為spring提供的功能都是用該思想,是以修改源碼的地方就原來越少了,都可以使用該方式實作。

2.JwtFilter

同上文,複寫BasicHttpAuthenticationFilter的驗證登入使用者的方法,在執行登入接口後判斷使用者是否正确。

3.ResourceCheckFilter

負責鑒權使用,判斷目前使用者是否有權限通路。

//表示是否允許通路 ,如果允許通路傳回true,否則false;

@Override

protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {

Subject subject = getSubject(servletRequest, servletResponse);

//擷取目前url

String url = getPathWithinApplication(servletRequest);

log.info("目前使用者正在通路的 url => " + url);

return subject.isPermitted(url);

}

//onAccessDenied:表示當通路拒絕時是否已經處理了; 如果傳回 true 表示需要繼續處理; 如果傳回 false

@Override

protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {

HttpServletRequest request = (HttpServletRequest) servletRequest;

HttpServletResponse response = (HttpServletResponse) servletResponse;

response.sendRedirect(request.getContextPath() + this.errorUrl);

// 傳回 false 表示已經處理,例如頁面跳轉啥的,表示不在走以下的攔截器了(如果還有配置的話)

return false;

}

4.ShiroRealm

主要負責擷取使用者所有的菜單權限,并提供token的一系列方法。

//擷取所有菜單權限集合

@Override

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

}

//驗證使用者輸入的賬号和密碼是否正确

@Override

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {

}

//校驗token的有效性

public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {

}

//重新整理token有效時間

public boolean jwtTokenRefresh(String token, String userName, String passWord) {

}

//清除目前使用者的權限認證緩存

@Override

public void clearCache(PrincipalCollection principals) {

super.clearCache(principals);

}

5.ShiroConfig

此為shiro的核心配置類,大多數寫法都是固定寫法。

public class ShiroConfig {

@Value("${jeecg.shiro.excludeUrls}")

private String excludeUrls;

@Resource

LettuceConnectionFactory lettuceConnectionFactory;

@Autowired

private Environment env;

@Bean("shiroFilter")

public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {

CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();

shiroFilterFactoryBean.setSecurityManager(securityManager);

// 攔截器

Map<string, string=""> filterChainDefinitionMap = new linkedHashMap<string, string="">();

if(oConvertUtils.isNotEmpty(excludeUrls)){

String[] permissionUrl = excludeUrls.split(",");

for(String url : permissionUrl){

filterChainDefinitionMap.put(url,"anon");

}

}

// 配置不會被攔截的連結 順序判斷 也就是不同通過token通路的位址

filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); /

// 添加自己的過濾器并且取名為jwt

Map<string, filter=""> filterMap = new HashMap<string, filter="">(1);

//如果cloudServer為空 則說明是單體 需要加載跨域配置【微服務跨域切換】

Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);

//前文定義的過濾器

filterMap.put("jwt", new JwtFilter(cloudServer==null));

shiroFilterFactoryBean.setFilters(filterMap);

//

ex.printStackTrace();

}

}

}

}

public interface MqListener<t> {

default void handler(T map, Channel channel) {

}

}

</t></string,></string,></string,></string,>

2.RabbitMqClient

主要在隊列初始化時實作隊列的初始化,而是否初始化根據使用時的@RabbitListener、@RabbitComponent判斷。

public interface MqListener<t> {

default void handler(T map, Channel channel) {

}

}

@Bean

public void initQueue() {

//擷取帶RabbitComponent注解的類

Map<string, object=""> beansWithRqbbitComponentMap = this.applicationContext.getBeansWithAnnotation(RabbitComponent.class);

Class<!--? extends Object--> clazz = null;

//循環map

for (Map.Entry<string, object=""> entry : beansWithRqbbitComponentMap.entrySet()) {

log.info("初始化隊列............");

//擷取到執行個體對象的class資訊

clazz = entry.getValue().getClass();

Method[] methods = clazz.getMethods();

//判斷是否有RabbitListener注解

RabbitListener rabbitListener = clazz.getAnnotation(RabbitListener.class);

//類上有注解 就建立隊列

if (ObjectUtil.isNotEmpty(rabbitListener)) {

createQueue(rabbitListener);

}

//方法上有注解 就建立隊列

for (Method method : methods) {

RabbitListener methodRabbitListener = method.getAnnotation(RabbitListener.class);

if (ObjectUtil.isNotEmpty(methodRabbitListener)) {

createQueue(methodRabbitListener);

}

}

}

}

private void createQueue(RabbitListener rabbitListener) {

String[] queues = rabbitListener.queues();

//建立交換機

DirectExchange directExchange = createExchange(DelayExchangeBuilder.DELAY_EXCHANGE);

rabbitAdmin.declareExchange(directExchange);

//建立隊列

if (ObjectUtil.isNotEmpty(queues)) {

for (String queueName : queues) {

Properties result = rabbitAdmin.getQueueProperties(queueName);

if (ObjectUtil.isEmpty(result)) {

Queue queue = new Queue(queueName);

addQueue(queue);

Binding binding = BindingBuilder.bind(queue).to(directExchange).with(queueName);

rabbitAdmin.declareBinding(binding);

log.info("建立隊列:" + queueName);

}else{

log.info("已有隊列:" + queueName);

}

}

}

}

</string,></string,></t>

3.RabbitMqConfig

為消息隊列的常用配置方式。這裡不多描述。

4.event

這個包主要是為使用mq發送消息使用,多類别的消息會實作JeecgBusEventHandler類,而baseApplicationEvent通過消息類型傳入的不同的參數選擇合适的業務類發送消息。

5.DelayExchangeBuilder

為延時隊列的交換機聲明與綁定。

2.jeecg-boot-starter-lock

1.如何使用分布式鎖

使用時有兩種方式,一種是使用注解方式,一種是使用redisson提供的API。

@Scheduled(cron = "0/5 * * * * ?")

@JLock(lockKey = CloudConstant.REDISSON_DEMO_LOCK_KEY1)

public void execute() throws InterruptedException {

log.info("執行execute任務開始,休眠三秒");

Thread.sleep(3000);

System.out.println("=======================業務邏輯1=============================");

Map map = new baseMap();

map.put("orderId", "BJ0001");

rabbitMqClient.sendMessage(CloudConstant.MQ_JEECG_PLACE_ORDER, map);

//延遲10秒發送

map.put("orderId", "NJ0002");

rabbitMqClient.sendMessage(CloudConstant.MQ_JEECG_PLACE_ORDER, map, 10000);

log.info("execute任務結束,休眠三秒");

}

public DemoLockTest() {

}

//@Scheduled(cron = "0/5 * * * * ?")

public void execute2() throws InterruptedException {

if (redissonLock.tryLock(CloudConstant.REDISSON_DEMO_LOCK_KEY2, -1, 6000)) {

log.info("執行任務execute2開始,休眠十秒");

Thread.sleep(10000);

System.out.println("=======================業務邏輯2=============================");

log.info("定時execute2結束,休眠十秒");

redissonLock.unlock(CloudConstant.REDISSON_DEMO_LOCK_KEY2);

} else {

log.info("execute2擷取鎖失敗");

}

}

2.RepeatSubmitAspect

通過公平鎖判斷是否是多次點選按鈕。

@Around("pointCut(jRepeat)")

public Object repeatSubmit(ProceedingJoinPoint joinPoint,JRepeat jRepeat) throws Throwable {

String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());

if (Objects.nonNull(jRepeat)) {

// 擷取參數

Object[] args = joinPoint.getArgs();

// 進行一些參數的處理,比如擷取訂單号,操作人id等

StringBuffer lockKeyBuffer = new StringBuffer();

String key =getValueBySpEL(jRepeat.lockKey(), parameterNames, args,"RepeatSubmit").get(0);

// 公平加鎖,lockTime後鎖自動釋放

boolean isLocked = false;

try {

isLocked = redissonLockClient.fairLock(key, TimeUnit.SECONDS, jRepeat.lockTime());

// 如果成功擷取到鎖就繼續執行

if (isLocked) {

// 執行程序

return joinPoint.proceed();

} else {

// 未擷取到鎖

throw new Exception("請勿重複送出");

}

} finally {

// 如果鎖還存在,在方法執行完成後,釋放鎖

if (isLocked) {

redissonLockClient.unlock(key);

}

}

}

return joinPoint.proceed();

}

3.DistributedLockHandler

該類主要是jLock的切面類,通過jLock注解參數,判斷需要加鎖的類型,同時加鎖的方法也不相同。

//jLock切面,進行加鎖

@SneakyThrows

@Around("@annotation(jLock)")

public Object around(ProceedingJoinPoint joinPoint, JLock jLock) {

Object obj = null;

log.info("進入RedisLock環繞通知...");

RLock rLock = getLock(joinPoint, jLock);

boolean res = false;

//擷取逾時時間

long expireSeconds = jLock.expireSeconds();

//等待多久,n秒内擷取不到鎖,則直接傳回

long waitTime = jLock.waitTime();

//執行aop

if (rLock != null) {

try {

if (waitTime == -1) {

res = true;

//一直等待加鎖

rLock.lock(expireSeconds, TimeUnit.MILLISECONDS);

} else {

res = rLock.tryLock(waitTime, expireSeconds, TimeUnit.MILLISECONDS);

}

if (res) {

obj = joinPoint.proceed();

} else {

log.error("擷取鎖異常");

}

} finally {

if (res) {

rLock.unlock();

}

}

}

log.info("結束RedisLock環繞通知...");

return obj;

}

//通過參數判斷加鎖類型

@SneakyThrows

private RLock getLock(ProceedingJoinPoint joinPoint, JLock jLock) {

//擷取key

String[] keys = jLock.lockKey();

if (keys.length == 0) {

throw new RuntimeException("keys不能為空");

}

//擷取參數

String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());

Object[] args = joinPoint.getArgs();

LockModel lockModel = jLock.lockModel();

if (!lockModel.equals(LockModel.MULTIPLE) && !lockModel.equals(LockModel.REDLOCK) && keys.length > 1) {

throw new RuntimeException("參數有多個,鎖模式為->" + lockModel.name() + ".無法鎖定");

}

RLock rLock = null;

String keyConstant = jLock.keyConstant();

//判斷鎖類型

if (lockModel.equals(LockModel.AUTO)) {

if (keys.length > 1) {

lockModel = LockModel.REDLOCK;

} else {

lockModel = LockModel.REENTRANT;

}

}

//根據不同的鎖類型執行不同的加鎖方式

switch (lockModel) {

case FAIR:

rLock = redissonClient.getFairLock(getValueBySpEL(keys[0], parameterNames, args, keyConstant).get(0));

break;

case REDLOCK:

List<rlock> rLocks = new ArrayList<>();

for (String key : keys) {

List<string> valueBySpEL = getValueBySpEL(key, parameterNames, args, keyConstant);

for (String s : valueBySpEL) {

rLocks.add(redissonClient.getLock(s));

}

}

RLock[] locks = new RLock[rLocks.size()];

int index = 0;

for (RLock r : rLocks) {

locks[index++] = r;

}

rLock = new RedissonRedLock(locks);

break;

case MULTIPLE:

rLocks = new ArrayList<>();

for (String key : keys) {

List<string> valueBySpEL = getValueBySpEL(key, parameterNames, args, keyConstant);

for (String s : valueBySpEL) {

rLocks.add(redissonClient.getLock(s));

}

}

locks = new RLock[rLocks.size()];

index = 0;

for (RLock r : rLocks) {

locks[index++] = r;

}

rLock = new RedissonMultiLock(locks);

break;

case REENTRANT:

List<string> valueBySpEL = getValueBySpEL(keys[0], parameterNames, args, keyConstant);

//如果spel表達式是數組或者LIST 則使用紅鎖

if (valueBySpEL.size() == 1) {

rLock = redissonClient.getLock(valueBySpEL.get(0));

} else {

locks = new RLock[valueBySpEL.size()];

index = 0;

for (String s : valueBySpEL) {

locks[index++] = redissonClient.getLock(s);

}

rLock = new RedissonRedLock(locks);

}

break;

case READ:

rLock = redissonClient.getReadWriteLock(getValueBySpEL(keys[0], parameterNames, args, keyConstant).get(0)).readLock();

break;

case WRITE:

rLock = redissonClient.getReadWriteLock(getValueBySpEL(keys[0], parameterNames, args, keyConstant).get(0)).writeLock();

break;

}

return rLock;

}

</string></string></string></rlock>

4.RedissonLockClient

redisson用戶端,提供了一大波方法,請自行檢視。

public class RedissonLockClient {

@Autowired

private RedissonClient redissonClient;

@Autowired

private RedisTemplate<string, object=""> redisTemplate;

public RLock getLock(String lockKey) {

return redissonClient.getLock(lockKey);

}

public boolean tryLock(String lockName, long expireSeconds) {

return tryLock(lockName, 0, expireSeconds);

}

</string,>

5.core包

主要通過application.yml配置檔案擷取redis連接配接類型,通過根據該參數動态的選擇政策類,連接配接redis。

public class RedissonManager {

public Redisson getRedisson() {

return redisson;

}

//Redisson連接配接方式配置工廠

static class RedissonConfigFactory {

private RedissonConfigFactory() {

}

private static volatile RedissonConfigFactory factory = null;

public static RedissonConfigFactory getInstance() {

if (factory == null) {

synchronized (Object.class) {

if (factory == null) {

factory = new RedissonConfigFactory();

}

}

}

return factory;

}

//根據連接配接類型創建連接配接方式的配置

Config createConfig(RedissonProperties redissonProperties) {

Preconditions.checkNotNull(redissonProperties);

Preconditions.checkNotNull(redissonProperties.getAddress(), "redis位址未配置");

RedisConnectionType connectionType = redissonProperties.getType();

// 聲明連接配接方式

RedissonConfigStrategy redissonConfigStrategy;

if (connectionType.equals(RedisConnectionType.SENTINEL)) {

redissonConfigStrategy = new SentinelRedissonConfigStrategyImpl();

} else if (connectionType.equals(RedisConnectionType.CLUSTER)) {

redissonConfigStrategy = new ClusterRedissonConfigStrategyImpl();

} else if (connectionType.equals(RedisConnectionType.MASTERSLAVE)) {

redissonConfigStrategy = new MasterslaveRedissonConfigStrategyImpl();

} else {

redissonConfigStrategy = new StandaloneRedissonConfigStrategyImpl();

}

Preconditions.checkNotNull(redissonConfigStrategy, "連接配接方式建立異常");

return redissonConfigStrategy.createRedissonConfig(redissonProperties);

}

}

}

//政策實作,此類是指定redis的連接配接方式是哨兵。

public class SentinelRedissonConfigStrategyImpl implements RedissonConfigStrategy {

@Override

public Config createRedissonConfig(RedissonProperties redissonProperties) {

Config config = new Config();

try {

String address = redissonProperties.getAddress();

String password = redissonProperties.getPassword();

int database = redissonProperties.getDatabase();

String[] addrTokens = address.split(",");

String sentinelAliasName = addrTokens[0];

// 設定redis配置檔案sentinel.conf配置的sentinel别名

config.useSentinelServers().setMasterName(sentinelAliasName);

config.useSentinelServers().setDatabase(database);

if (StringUtils.isNotBlank(password)) {

config.useSentinelServers().setPassword(password);

}

// 設定哨兵節點的服務IP和端口

for (int i = 1; i < addrTokens.length; i++) {

config.useSentinelServers().addSentinelAddress(GlobalConstant.REDIS_CONNECTION_PREFIX+ addrTokens[i]);

}

log.info("初始化哨兵方式Config,redisAddress:" + address);

} catch (Exception e) {

log.error("哨兵Redisson初始化錯誤", e);

e.printStackTrace();

}

return config;

}

}

6.jeecg-cloud-module

這裡詳細的說一下jeecg-cloud-gateway,因為其他的都是開源項目沒下載下傳即用。

jeecg-cloud-system-start為封裝start的使用方法,上文已經介紹了。

1.jeecg-cloud-gateway

1.GatewayRoutersConfiguration

當固定的幾個路由,有特殊化的執行方法。

2.RateLimiterConfiguration

主要配置限流,與application.yml一起使用,下文配置含義是,發送過來的請求隻能容納redis-rate-limiter.burstCapacity的配置(3次)多餘的會全部丢棄(限流),每秒消費redis-rate-limiter.replenishRate(1次)。

3.FallbackController

熔斷的執行方法。

4.GlobalAccessTokenFilter

全局攔截器,在調用其他服務時,将使用者資訊放在請求頭中。

5.SentinelFilterContextConfig

使Sentinel鍊路流控模式生效,固定寫法。

6.HystrixFallbackHandler、SentinelBlockRequestHandler

在降級/限流時,将異常資訊轉換成json傳回給前台。

7.LoderRouderHandler

動态重新整理路由。

8.MySwaggerResourceProvider、SwaggerResourceController

将swagger位址統一管理起來

9.DynamicRouteLoader、DynamicRouteService

DynamicRouteLoader:通過application.yml判斷從nacos/redis中擷取路由資訊,并實作動态的加載。

DynamicRouteService:為底層處理路由的API。

四.總體感想

文章到這裡差不多就接近尾聲了,大多數功能附帶着代碼都講述了一遍。在功能上來說,jeecg提供了很多常用功能,如rabbitMq封裝、積木報表、代碼生成器等。這些在日常工作中有很大的機率碰上,如果有以上需求,可以來架構中直接複制粘貼即可。

但是在格式規範上,如出入參的規範,代碼的寫法,代碼的格式化等方面,并不是特别統一,且沒有嚴格規範。總體來說非常适合做私活與畢業設計,同時也是最早一批開源的前後端項目腳手架,爆贊。