关于SpringBoot启动提示Invalid value type for attribute ‘factoryBeanObjectType’: java.lang.String

最近在用最新的版本SpringBoot(3.3.1)搭建一个项目,依赖使用以前的maven依赖。然后启动结果提示

java.lang.IllegalArgumentException: Invalid value type for attribute 'factoryBeanObjectType': java.lang.String

一开始是因为IDEA的问题,经过一步一步的debug之后,发现是mybatis-plus依赖问题。

以前的依赖是使用了官方的:

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.5</version>
        </dependency>

后面官方针对SpringBoot3.x版本升级了,

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.6</version>
        </dependency>

如果使用的Springboot是3.x,必须使用mybatis-plus-spring-boot3-starter 否则就会提示错误。

SpringBoot项目打包出现Unable to find main class

如果你在尝试单独打包一个Spring Boot模块时遇到“Unable to find main class”的错误,这通常意味着Maven或Gradle在构建过程中没有找到带有@SpringBootApplication注解的主类,即程序的入口点。以下是几个可能的原因及相应的解决办法:


原因与解决方法:

方法一、缺少主启动类

  • 检查模块内是否确实存在一个带有@SpringBootApplication注解的类。
  • 确认该类包含一个公有的static void main(String[] args)方法。

方法二、主启动类命名或位置错误

  • 确认主启动类的名称和包路径是否与你在构建配置中指定的一致
  • 主启动类应位于模块的合理包路径下,通常直接在模块的主要源代码目录中。

方法三、构建插件配置问题

在模块的pom.xml或build.gradle中检查spring-boot-maven-plugin或bootJar的配置。

确保正确设置了mainClass属性,指向你的主启动类的全限定名,例如

       <build>
           <plugins>
               <plugin>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot-maven-plugin</artifactId>
                   <configuration>
                       <mainClass>com.example.module.YourMainApplication</mainClass>
                   </configuration>
               </plugin>
           </plugins>
       </build>
       
bootJar {
   mainClassName = 'com.example.module.YourMainApplication'
}

方法四、模块依赖问题:

  • 检查是否有未解决的依赖问题,特别是那些可能影响到主启动类加载的依赖。
  • 确保所有必要的依赖都已正确声明并可用。

方法五、清理与重建:

  • 执行mvn clean install或gradlew clean build来清除旧的构建结果并重新构建。
  • 有时旧的构建工件或缓存数据可能会干扰新的构建过程。

通过以上步骤,应该能够定位并解决找不到主类的问题,从而成功地单独打包你的Spring Boot模块。

关于SpringBoot 多模块 无法自动装配Bean类型

1、在被引用模块中添加exec

2、增加扫描范围

@SpringBootApplication(scanBasePackages = {"com.xixi.web","com.xixi.dao"})
public class WebApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }

}

web模块引用dao模块,在web模块的时候就增加扫描范围到web和dao模块。

3、被引用模块需要Application启动入口

dao模块是被引用的,作为SpringBoot项目,入口是不能删除的。

关于Chrome Extensions 消息通讯

有些功能需要用到消息通讯,比如我们需要获取某个网站的cookie发送到后台服务器,那么我们手动点击之后,需要获取cookie,然后发送。

一、设置服务脚本

定义接收通讯消息的处理脚本。

{ 
 "background": {
    "service_worker": "background/service-worker.js",
    "type": "module"
  },
  "host_permissions": [
    "https://*/*",
    "http://*/*"
  ],
  "permissions": [
    "cookies", 
    "declarativeNetRequestFeedback",
    "declarativeNetRequestWithHostAccess",
    "declarativeNetRequest",
    "webRequest"
  ]
}

二、在前台发送消息给服务脚本

通过chrome的api发送“chrome.runtime.sendMessage”

<template>
 <button type="primary" style="width: 100%;" @click="onSendMessage">发送消息</el-button>
</template>

<script>
const onSendMessage= async () => {
  const res = await chrome.runtime.sendMessage({
    diyData: dataBody
  });
}
<script>

三、接收

chrome.runtime.onMessage.addListener(
    function (request, sender, sendResponse) {
        console.log(request)
        console.log(sender)
        console.log(sendResponse)
        const data = request.diyData;

   chrome.cookies.getAll({
            domain: "taobao.com"
        }, function (cookies) {
            let cookieString = ""
            cookies.forEach(function (cookie) {
                 cookieString += cookie.name + "=" + cookie.value + ";"
            }
            fetch( "http://www.xx.com/api/data",
            {
                method: "post",
                headers: {
                   'Content-Type': 'application/json'
                },
                body: JSON.stringify(data),
            }
            ).then(
                res => {
                    console.log(res);
                    let reader = res.body.pipeThrough(new TextDecoderStream())
                        .getReader();
                    reader.read().then(rs => {
                        const data = JSON.parse(rs.value)
                        sendResponse(data); //发送给前端。
                    })
                }
            ).catch(
                err => {
                    console.log(err)
                }
            )
        });
    }
)

如果出现问题了,记得看看自己在清单中是否申请了权限。

Hilt依赖注入DataStore代码段

DataStore 代码段

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

class DataStoreManager @Inject constructor(@ApplicationContext private val context: Context) {

    val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

    private val nameKey = stringPreferencesKey("name")
    private val emailKey = stringPreferencesKey("email")

    suspend fun saveName(userModel: UserModel) {
      context.dataStore.edit { preferences ->
            preferences[nameKey] = userModel.name
            preferences[emailKey] = userModel.email
      }
    }
}

Activity中使用

@AndroidEntryPoint
class MainActivity : ComponentActivity() {


    @Inject
    lateinit var dataStoreManager: DataStoreManager

    //其他代码......

}

Android下载网络图片到本地

第一步,需要添加授权

 <uses-permission android:name="android.permission.INTERNET" />
 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />
 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="32"
        tools:ignore="ScopedStorage" />

第二步:网络请求网络图片地址

   val imageUrl = "https://static.jikecrm.com/2018-06-10/152862233429525.jpg"
               val client = OkHttpClient()
               val request = Request.Builder()
                   .url(imageUrl)
                   .build()
               val call = client.newCall(request)

               call.enqueue(object: Callback {
                   override fun onFailure(call: Call, e: IOException) {
                       Log.e("TAG", "onFailure: 下载失败:${e.message}", )
                   }

                   override fun onResponse(call: Call, response: Response) {
                       if(response.isSuccessful){
                           val inputStream = response.body?.byteStream() ?: return
                           DownLoadImage().saveImage(inputStream,context)
                       }else{
                           Log.e("TAG", "onResponse: 下载失败~~~~~~~", )
                       }
                   }
               })

在协程中去请求,不要在UI主线程请求。

第三步保存图片到本地

    fun saveImage(inputStream: InputStream, context: Context) {

        val values = ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, "tj_${System.currentTimeMillis()}.jpg") // 图片文件名
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") // 图片MIME类型
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) // 存储目录,此处为Pictures
        }

        val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        if (uri == null) {
            // 创建文件失败,处理错误
            Log.e("DownLoad", "saveImage: 创建文件失败")
            return
        }

        try {
            val fos = context.contentResolver.openOutputStream(uri, "w") ?: return
            val buffer = ByteArray(4 * 1024)
            var read: Int
            while (inputStream.read(buffer).also { read = it } != -1) {
                fos.write(buffer, 0, read)
            }
            fos.close()
            inputStream.close()

            // 更新媒体数据库,使图片在系统相册中可见
            MediaScannerConnection.scanFile(
                context,
                arrayOf(Environment.DIRECTORY_PICTURES),
                null,
                null
            )
            Log.i("DownLoad", "saveImage: 保存成功~~~")
        } catch (e: Exception) {
            context.contentResolver.delete(uri, null, null)
            Log.e("DownLoad", "saveImage: 保存失败:${e.message}")
        }
    }

如果是同一张网络图片,系统默认是覆盖的。

Android Jetpack Room使用的小结。

添加依赖:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt' // 如果使用Kotlin,需要kapt编译器插件
    id 'dagger.hilt.android.plugin' // Hilt插件
}

dependencies {
    implementation 'com.google.dagger:hilt-android:2.40.5' // Hilt核心库
    kapt 'com.google.dagger:hilt-compiler:2.40.5' // 如果使用Kotlin,需要kapt编译器依赖
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0' // Hilt与ViewModel的集成库
    implementation 'androidx.room:room-runtime:2.4.3' // Room运行时库
    kapt 'androidx.room:room-compiler:2.4.3' // 如果使用Kotlin,需要kapt编译器依赖
}

使用Room的三个步骤:

第一,定义数据库(AppDatabase是自定义的数据库类名称)

定义一个Room数据库实体(例如UserEntity)以及对应的Room数据库抽象类(例如AppDatabase),并使用@Database注解指定包含的实体类与版本信息。

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

第二,创建数据的实体类Entity

@Entity:注解必须存在,这是实体类的标志,tableName 表名
@PrimaryKey:注解表示这个字段是主键,autoGenerate表示是自动增加的
@ColumnInfo:注解这个字段的别名,我们在其他地方就用这个别名。

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val name: String,
    val email: String
)

第三,定义一个Data Access Object (DAO) 接口(例如UserDao),包含用于更新数据的方法等

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update

@Dao
interface UserDao {
    @Update
    suspend fun updateUser(user: UserEntity)


    @Insert
    suspend fun insertUser(user: UserEntity)


    @Query("SELECT * FROM users WHERE id=:id ")
    suspend fun getUserById(id: Int): UserEntity?

}

第四创建Repository

创建一个Repository类(例如UserRepository),它将作为ViewModel与Room数据库之间的中介。在这个类中,注入UserDao并通过其方法执行数据更新操作。

class UserRepository @Inject constructor(private val userDao: UserDao) {

    suspend fun updateUser(user: UserEntity) {
        val data = userDao.getUserById(user.id!!)
        if (data == null) {
            userDao.insertUser(user)
        } else {
            userDao.updateUser(user)
        }

    }
}

第五:配置Hilt Modules

创建一个Hilt模块(例如DatabaseModule),用于提供AppDatabase实例。另外,可以创建另一个模块(例如RepositoryModule)来提供UserRepository实例。

// DatabaseModule.kt
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Singleton
    @Provides
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(context, AppDatabase::class.java, "app_database.db")
            .build()
    }

    @Singleton
    @Provides
    fun provideUserDao(database: AppDatabase): UserDao {
        return database.userDao()
    }
}

// RepositoryModule.kt
@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {

    @Singleton
    @Provides
    fun provideUserRepository(userDao: UserDao): UserRepository {
        return UserRepository(userDao)
    }
}

六:Hilt化ViewModel

使用@HiltViewModel注解标记ViewModel类(例如UserViewModel),并在其中注入所需的UserRepository

// UserViewModel.kt
@HiltViewModel
class UserViewModel @Inject constructor(private val userRepository: UserRepository) : ViewModel() {

    fun updateUser(user: UserEntity) {
        viewModelScope.launch {
            userRepository.updateUser(user)
            // 更新成功后,可以在此处触发LiveData或Flow等通知UI更新
        }
    }
}

七:在Activity/Fragment/Compose中使用ViewModel

在使用Hilt的Activity或Fragment或者Compse中,通过by viewModels()委托属性获取注入的UserViewModel实例,并调用其updateUser方法来更新Room中的数据。

class MainActivity : ComponentActivity() {

    private val userViewModel: UserViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Index(userViewModel)
            }
        }
    }
}
@Composable
fun Index(userViewModel: UserViewModel) {
    var username: String by remember { mutableStateOf("") }
    var email: String by remember { mutableStateOf("") }

    Column {
        OutlinedTextField(value = username, onValueChange = {
            username = it
            userViewModel.updateUser(UserEntity(1, username, email))
        })
        OutlinedTextField(value = email, onValueChange = {
            email = it
            userViewModel.updateUser(UserEntity(1, username, email))
        })
    }
}

至此,您已经完成了在Android应用中使用Hilt依赖注入来更新Room数据库中数据的所有步骤。当updateUser方法在ViewModel中被调用时,Hilt会确保所有依赖(如UserRepositoryUserDao)已正确注入,并通过它们与Room数据库进行交互。

Jetpack Compose 上传相册图片到服务器

一、定义组件

定义一个Composable组件,当点击Composable组件中的按钮时候,就跳转到相册选择,然后选中图片上传。

import android.net.Uri
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import coil.compose.rememberAsyncImagePainter
import com.google.gson.JsonObject
import com.xiangzhenming.taojin.network.ImageUploadNetwork
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.Call
import retrofit2.Response
	
	
	
	@Composable
    fun UploadImageView() {
		val context = LocalContext.current
        val contentResolver = context.contentResolver
		var imageUri by remember { mutableStateOf<Uri?>(null) }
		val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
		  if (uri != null) {
            imageUri = uri
			contentResolver.openInputStream(uri).use { inputStream ->
			
			
				// 创建RequestBody对象
				val requestBody = inputStream?.let {
					it.readBytes().toRequestBody("image/jpeg".toMediaTypeOrNull())
				}
				
				 // 创建MultipartBody.Part对象
				val part = requestBody?.let {
					MultipartBody.Part.createFormData(
						"file", // API接口要求的参数名
						uri.lastPathSegment ?: "${System.currentTimeMillis()}.jpg", // 文件名,默认使用URI的最后一段作为文件名
						it
					)
				}
				
				
				// 假设apiService是你的Retrofit服务实例
				if (part != null) {
					ImageUploadNetwork("").apiService.uploadImage(part)
						.enqueue(object : retrofit2.Callback<JsonObject> {

							override fun onResponse(call: Call<JsonObject>, response: Response<JsonObject>) {
								Log.e("UploadImage", "onResponse: ${response.body().toString()}:")
								if (response.isSuccessful) {
									Log.e("UploadImage", "onResponse: success:")
								} else {
									Log.e("UploadImage", "onResponse: fail${response.code()}")
								}
							}

							override fun onFailure(call: Call<JsonObject>, t: Throwable) {
								Log.e("UploadImage", "onFailure: ${t.message}")
							}
						}
					)
				}
					
				
			}
		  }
		}
		
		
		
		Column(
            Modifier
                .fillMaxSize()
                .background(Color.White),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        )
        {
            Button(onClick = {
                launcher.launch("image/*")
            }) {
                Text(text = "选择照片")
            }
            Image(painter = rememberAsyncImagePainter(model = imageUri), contentDescription = null)
        }
		
		
}

上传接口

interface ImageUploadService {

    companion object {
        const val BASE_URL = "https://www.jishuge.cn/"
        const val PREFIX_URL = "/upload/android"
    }

    @Multipart
    @POST("${PREFIX_URL}/reward/upload/image")
    fun uploadImage( @Part file: MultipartBody.Part): Call<JsonObject>


}
class ImageUploadNetwork {


    val apiService: ImageUploadService by lazy {
        initRetrofit()
    }

    private val gson: Gson = GsonBuilder().setLenient().create()

    private fun initRetrofit(): ImageUploadService {

        val okHttpClient = OkHttpClient.Builder()
            .addInterceptor(LoggingInterceptor())
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .retryOnConnectionFailure(false)
            .build()

        val retrofit = Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create(gson))
            .baseUrl(ImageUploadService.BASE_URL)
            .client(okHttpClient)
            .build()

        return retrofit.create(ImageUploadService::class.java)
    }
}
class LoggingInterceptor : Interceptor {
    @Volatile
    var level: Level = Level.BASIC // 设置默认的日志级别

    enum class Level {
        BASIC, // 只打印基本信息
        HEADERS, // 打印基本信息以及请求/响应头信息
        BODY // 打印所有信息(基本信息、请求/响应头及请求/响应体)
    }

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()

        when (level) {
            Level.BASIC -> logBasic(request)
            Level.HEADERS -> logHeaders(request)
            Level.BODY -> logBodyAndHeaders(request, chain)
        }

        val response = chain.proceed(request)

        when (level) {
            Level.BASIC -> logBasic(response)
            Level.HEADERS -> logHeaders(response)
            Level.BODY -> logBodyAndHeaders(response)
        }

        return response
    }

    private fun logBasic(request: Request) {
        Log.d("LoggingInterceptor", "Sending request ${request.url}")
    }

    private fun logHeaders(request: Request) {
        Log.d("LoggingInterceptor", "Sending request ${request.url}")
        Log.d("LoggingInterceptor", "Request Headers: ${request.headers}")
    }

    private fun logBodyAndHeaders(request: Request, chain: Interceptor.Chain) {
        logHeaders(request)
        val requestBody = request.body ?: return
        val bodyString = requestBody.toString()
        Log.d("LoggingInterceptor", "Request Body: $bodyString")
    }

    private fun logBasic(response: Response) {
        Log.d("LoggingInterceptor", "Received response for ${response.request.url} with code ${response.code}")
    }

    private fun logHeaders(response: Response) {
        Log.d("LoggingInterceptor", "Received response for ${response.request.url} with code ${response.code}")
        Log.d("LoggingInterceptor", "Response Headers: ${response.headers}")
    }

    private fun logBodyAndHeaders(response: Response) {
        logHeaders(response)
        if (response.body != null) {
            val bodyString = response.body?.string()
            Log.d("LoggingInterceptor", "Response Body: $bodyString")
        }
    }
}

服务器端返回的值格式:

{
    "code": 0,
    "msg": "ok",
    "data": "https://www.jishuge.cn/image/2024-03-13/b69935ce-912e-4d31-b1e5-a15a3da85f82-1710318431942.jpg",
    "count": 0,
    "version": "1.0.0",
    "timestamp": 1710318433
}

监听ViewModel中的LiveData数据提示Live data observe in compose。

原因是因为我们的依赖没有引入,只要引入依赖就可以解决该问题:

implementation("androidx.compose.runtime:runtime-livedata:1.5.4")

然后在Activity或者Compose中使用。

var titleValue: String by remember {
	mutableStateOf(myModel.titleValue.value ?: "")
}

myModel.titleValue.observeAsState().value?.let { newData ->
	titleValue= newData
}

AndroidView网页加载

    @Composable
    fun SecondOnePage(): Unit {
        var progression by remember {
            mutableFloatStateOf(0f)
        }
        Surface {
            Column {
                //进度组件
                LinearProgressIndicator(
                    progress = progression / 100,
                    modifier = Modifier.fillMaxWidth(),
                    color = Color.Red
                )
                //利用AndroidView加载网页
                AndroidView(
                    factory = { context ->
                        var webView = WebView(context)
                        webView.apply {
                            settings.javaScriptEnabled = true
                            webViewClient = object : WebViewClient() {}
                            loadUrl("https://m.jd.com/")
                        }

                    },
                    modifier = Modifier.fillMaxSize(),
                    update = { webview ->
                        //监听加载进度。
                        webview.webChromeClient = object :WebChromeClient(){
                            override fun onProgressChanged(view: WebView?, newProgress: Int) {
                                progression = newProgress.toFloat()
                                super.onProgressChanged(view, newProgress)
                            }
                        }
                    }
                )
            }
        }
    }